文章目录
-
- 一、前言
- 二、测试索引长度和索引基数对count(*)查询的影响
-
- [1、总数据量1100W+ 表的速度](#1、总数据量1100W+ 表的速度 "#11100W__37")
- 2、默认使用的索引
- 3、查看该表所有索引信息
- 4、强制选择基数最小的country字段
- 5、强制使用基数最小索引的聚合查询速度
- [三、两千万的大表count(*) 优化](#三、两千万的大表count(*) 优化 "#count__109")
- 四、索引基数一致的情况下,会选择使用哪个索引呢
- 五、为什么count(*)不用主键,或者count(主键)速度并不快
-
- 1、部分解释
- [2、有人可能会觉得,如果辅助索引长度和主键索引长度一样,那么count(*) 会不会使用PRIMARY索引呢?](#2、有人可能会觉得,如果辅助索引长度和主键索引长度一样,那么count(*) 会不会使用PRIMARY索引呢? "#2count_PRIMARY_222")
- 六、总结
一、前言
博主今天在对一个千万级表进行count(*)
查询的时候,发现速度有点慢,达到了9s
,这对于程序来说是不可承受的,因此萌生了优化count(*)
查询的想法,这里记录一下。
1、网上的主要两种说法
php
(1)count(*) 函数会选择索引长度最短的字段
ps:索引长度指的是执行计划explain里面的key_len长度。
(2)count(*)函数会选择索引基数最小的字段
PS:索引基数其实就是说该字段在表中对应的不重复的记录值。
查询方式:
select count(distinct xxx) from xxx;
咱们这里主要就来讨论测试下这两种说法,并且找到适合千万级数据库查询的优化方式。
2、不贴出mysql版本的测试都是耍流氓~
php
mysql> select count(*) from test;
+----------+
| count(*) |
+----------+
| 11793920 |
+----------+
1 row in set (9.10 sec)
3、mysql的count(*)和count(1)
有很多文章都在说count(*)
和count(1)
的区别,还有文章说count(1)
比count(*)
速度快之类的,这里郑重说一下,对于目前的的mysql(>=5.6)
对于count(1)
和count(*)
,MySQL
的优化是完全一样的,根本不存在谁更快,而且count(*)
是SQL92
定义的标准统计行数的语法,使用count(*)准没错。(PS:更早的版本可能会有一些区别)
二、测试索引长度和索引基数对count(*)查询的影响
1、总数据量1100W+ 表的速度
php
mysql> select count(*) from test;
+----------+
| count(*) |
+----------+
| 11793920 |
+----------+
1 row in set (9.10 sec)
查看count(*)
速度并不是很理想
2、默认使用的索引
php
mysql> explain select count(*) from test;
+------+-------------+-------------+-------+---------------+-------------+---------+------+----------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+------+-------------+-------------+-------+---------------+-------------+---------+------+----------+-------------+
| 1 | SIMPLE | test | index | NULL | idx_install | 4 | NULL | 10795387 | Using index |
+------+-------------+-------------+-------+---------------+-------------+---------+------+----------+-------------+
这里可以看到选用的索引长度很短,是个int
类型的时间戳。那么不同索引字段的基数如何呢?
3、查看该表所有索引信息
php
mysql> show index from test;
+-------------+------------+---------------------------+--------------+-----------------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-------------+------------+---------------------------+--------------+-----------------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
|test | 0 | PRIMARY | 1 | id | A | 10795387 | NULL | NULL | | BTREE | | |
| test | 1 | idx_install_tracker | 1 | install_tracker | A | 981398 | NULL | NULL | | BTREE | | |
|test | 1 | idx_user_id | 1 | user_id | A | 10795387 | NULL | NULL | | BTREE | | |
| test | 1 | idx_country | 1 | country | A | 21334 | NULL | NULL | | BTREE | | |
| test | 1 | idx_install | 1 | installed_at | A | 3598462 | NULL | NULL | | BTREE | | |
+-------------+------------+---------------------------+--------------+-----------------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
可以看到,默认使用的idx_install
索引基数并不是最小的,但是为何还是选择它了呢?明明country
字段的基数是最小的才对。
4、强制选择基数最小的country字段
php
mysql> explain select count(*) from test force index (idx_country);
+------+-------------+-------------+-------+---------------+-------------+---------+------+----------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+------+-------------+-------------+-------+---------------+-------------+---------+------+----------+-------------+
| 1 | SIMPLE | test | index | NULL | idx_country | 62 | NULL | 10795387 | Using index |
+------+-------------+-------------+-------+---------------+-------------+---------+------+----------+-------------+
可以看到,虽然扫描行数差不多,但是这个索引的长度太长了,,达到了62
字节。那么速度呢,会加快吗
5、强制使用基数最小索引的聚合查询速度
php
mysql> select count(*) from test force index (idx_country);
+----------+
| count(*) |
+----------+
| 11793920 |
+----------+
1 row in set (34.33 sec)
可以看到,比使用默认的idx_install
索引速度要慢很多。这就是为什么很多文章都说目前的count(*)
是经过优化的原因,mysql
对于count(*)
的优化确实是下过功夫的,默认选择的就是最优解。
按理说此时应该新建个索引长度最小的标识字段,来测试下实际的count(*)
优化速度,奈何没有数据库权限,还需要找dba
,所以暂时就这么着了,只要懂原理了就行。不过虽然这个表没有权限,那么我们可以看看其他的表是否有类似的优化。
三、两千万的大表count(*) 优化
这里重新选择了user
表,主要对比的就是,在有int
类型的索引时,count(*)
选择的是哪个索引,速度是否有加快?以及对于2000W
的大表来说,如何优化count(*)
查询。
1、user表的数量级以及默认count(*)的速度
php
mysql> select count(*) from user;
+----------+
| count(*) |
+----------+
| 20190648 |
+----------+
1 row in set (2.33 sec)
这里可以看到,2000W
的大表,速度竟然比上面1000W
的表快很对!经过博主的比对,原来这个表不经意间已经有了优化的字段,怪不得这么快。不过下面的测试还是需要的。
2、查看表索引
php
mysql> show index from user;
+-------+------------+--------------------+--------------+-----------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-------+------------+--------------------+--------------+-----------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| user | 0 | PRIMARY | 1 | uin | A | 14607030 | NULL | NULL | | BTREE | | |
| user | 1 | last_login_ip | 1 | last_login_ip | A | 14607030 | NULL | NULL | | BTREE | | |
| user | 1 | first_login_time | 1 | reg_time | A | 14607030 | NULL | NULL | | BTREE | | |
| user | 1 | last_login_time | 1 | last_login_time | A | 14607030 | NULL | NULL | | BTREE | | |
| user | 1 | email | 1 | email | A | 2 | NULL | NULL | | BTREE | |
| user | 1 | email_state | 1 | email_state | A | 2 | NULL | NULL | | BTREE | | |
+-------+------------+--------------------+--------------+-----------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
可以看到,first_login_time
是int
类型的时间戳,索引长度为4
,但是我们用到的索引是它吗?
3、实际使用的索引
php
mysql> explain select count(*) from user;
+----+-------------+-------+-------+---------------+-------------+---------+------+----------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+-------------+---------+------+----------+-------------+
| 1 | SIMPLE | user | index | NULL | email_state | 1 | NULL | 14607063 | Using index |
+----+-------------+-------+-------+---------------+-------------+---------+------+----------+-------------+
email_state
这个索引,索引长度仅为1
,符合我们上面说的优化手段,即基数很小。
四、索引基数一致的情况下,会选择使用哪个索引呢
这里继续引用user
表作为比对表。
1、索引基数一致
php
mysql> select count(distinct email) from user;
+-----------------------+
| count(distinct email) |
+-----------------------+
| 1 |
+-----------------------+
mysql> select count(distinct email_state) from user;
+-----------------------------+
| count(distinct email_state) |
+-----------------------------+
| 1 |
+-----------------------------+
可以看到在索引列表里面,有个email
字段,基数也很小,那么为什么不选择它呢?
2、查看对应的索引长度
php
mysql> explain select count(*) from user force index (email);
+----+-------------+-------+-------+---------------+-------+---------+------+----------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+-------+---------+------+----------+-------------+
| 1 | SIMPLE | user | index | NULL | email | 194 | NULL | 14608467 | Using index |
+----+-------------+-------+-------+---------------+-------+---------+------+----------+-------------+
发现email
索引长度194
,是远远大于email_state
的索引长度1的。
3、实际的优化手段
从上面对比,我们可以发现,mysql
对于count(*)
部分的优化是偏向于选择索引长度短 ,字段重复值比较多的字段。
而索引字段最短也得是1
吧,对应目前数据库常见的类型就是tinyint
。字段重复值最多的情况就是某个字段的值完全一样,不过这种字段一般不会出现在正式的表里面,不过我们经常用到的标识字段倒是可以胜任。
php
tinyint(1) 和 tinyint(3) 没什么区别,占用字节都是一位,存储范围都是一样的。
tinyint一个字节 smallint 两个字节 MEDIUMINT三个字节 int 4个字节 BIGINT 8个字节。
综上所述:
当对于一个千万级的大表进行count(*)
的时候,速度耗时是比较慢的,此时我们可以考虑到,给大表的一些标识字段,比如is_del tinyint(1)
这种加索引,因为tinyint
类型占字节最少,因此count(*)
的时候会自动找长度最短的索引使用,可以有效加速查询速度。
上面2000W
的user
表就是最好的例子,email_state
确实是个标识字段,类似于bool
值的存在,完美符合优化规则。
五、为什么count(*)不用主键,或者count(主键)速度并不快
这里直接说一下结论,还有一些例子:
1、部分解释
(1) 主键虽然一般是int
类型,索引长度比较短,但是主键的索引是比较大的,存储了整行数据,而辅助索引只存储了主键,使用辅助索引更快一些
(2) 从基数上来讲,主键的基数当然是很大的,使用主键确实不符合优化原则
2、有人可能会觉得,如果辅助索引长度和主键索引长度一样,那么count(*) 会不会使用PRIMARY索引呢?
这里选用了另一个比较小的服务器表,直接对比两个执行计划,大家注意选择的key
以及长度:
php
mysql> explain select count(*) from server_list;
+----+-------------+-------------+-------+---------------+---------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------------+-------+---------------+---------+---------+------+------+-------------+
| 1 | SIMPLE | server_list | index | NULL | game_id | 4 | NULL | 157 | Using index |
+----+-------------+-------------+-------+---------------+---------+---------+------+------+-------------+
1 row in set (0.00 sec)
mysql> explain select count(*) from server_list force index (PRIMARY);
+----+-------------+-------------+-------+---------------+---------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------------+-------+---------------+---------+---------+------+------+-------------+
| 1 | SIMPLE | server_list | index | NULL | PRIMARY | 4 | NULL | 157 | Using index |
+----+-------------+-------------+-------+---------------+---------+---------+------+------+-------------+
1 row in set (0.00 sec)
结果很明显了,索引长度一致的情况下,优先使用辅助索引的。
六、总结
1、结论
scss
(1)索引长度最小的字段会优先被count(*)选择,一般是int类型的
(2)如果索引长度一致,那么选择基数最小的(这部分是猜测,但是综合各种文章,感觉还是有可信度的)
(3)如果索引基数一致,选择索引长度最小的
(4)大表的count()查询优化手段就是新增tinyint类型的标识字段,速度可以得到有效提升
2、其他
(1)这些sql都是建立在没有where条件的基础上。
如果有where
条件,那么就会使用where
条件中的索引,这样的话,count
查询的速度是不能保证的。目前没什么好办法,除非你的where
条件用到的索引刚好符合咱们上面说的,基数小,索引长度小。
(2)如果只是要手动统计一个大表有多少数据,可以采用另一种方式:
sql
SELECT TABLE_ROWS FROM `information_schema`.tables WHERE table_name='xxx'
缺点: 不够实时,这个类似于定时统计表条数写入的关系,如果对数据要求不是很精准的话,可以用这种方式
参考链接:
mysql的count(*)的优化,获取千万级数据表的总行数
end,好久没写了,奥利给!!!