mysql的count()函数如何选择索引,千万级表的count()查询优化实例

文章目录

一、前言

博主今天在对一个千万级表进行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_timeint类型的时间戳,索引长度为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(*)的时候会自动找长度最短的索引使用,可以有效加速查询速度。

上面2000Wuser表就是最好的例子,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(*)的优化,获取千万级数据表的总行数

mysql count(*) 会选哪个索引?

end,好久没写了,奥利给!!!

相关推荐
BlockChain8886 小时前
Solidity 实战【二】:手写一个「链上资金托管合约」
go·区块链
BlockChain88814 小时前
Solidity 实战【三】:重入攻击与防御(从 0 到 1 看懂 DAO 事件)
go·区块链
剩下了什么19 小时前
Gf命令行工具下载
go
地球没有花20 小时前
tw引发的对redis的深入了解
数据库·redis·缓存·go
BlockChain8881 天前
字符串最后一个单词的长度
算法·go
龙井茶Sky1 天前
通过higress AI统计插件学gjson表达式的分享
go·gjson·higress插件
宇宙帅猴2 天前
【Ubuntu踩坑及解决方案(一)】
linux·运维·ubuntu·go
SomeBottle3 天前
【小记】解决校园网中不同单播互通子网间 LocalSend 的发现问题
计算机网络·go·网络编程·学习笔记·计算机基础
且去填词3 天前
深入理解 GMP 模型:Go 高并发的基石
开发语言·后端·学习·算法·面试·golang·go
大厂技术总监下海4 天前
向量数据库“卷”向何方?从Milvus看“全功能、企业级”的未来
数据库·分布式·go·milvus·增强现实