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,好久没写了,奥利给!!!

相关推荐
郝同学的测开笔记18 小时前
云原生探索系列(十二):Go 语言接口详解
后端·云原生·go
一点一木1 天前
WebAssembly:Go 如何优化前端性能
前端·go·webassembly
千羽的编程时光2 天前
【CloudWeGo】字节跳动 Golang 微服务框架 Hertz 集成 Gorm-Gen 实战
go
27669582923 天前
阿里1688 阿里滑块 231滑块 x5sec分析
java·python·go·验证码·1688·阿里滑块·231滑块
Moment4 天前
在 NodeJs 中如何通过子进程与 Golang 进行 IPC 通信 🙄🙄🙄
前端·后端·go
唐僧洗头爱飘柔95275 天前
(Go基础)变量与常量?字面量与变量的较量!
开发语言·后端·golang·go·go语言初上手
黑心萝卜三条杠5 天前
【Go语言】深入理解Go语言:并发、内存管理和垃圾回收
google·程序员·go
不喝水的鱼儿5 天前
【LuatOS】基于WebSocket的同步请求框架
网络·websocket·网络协议·go·luatos·lua5.4
微刻时光6 天前
程序员开发速查表
java·开发语言·python·docker·go·php·编程语言
lidenger6 天前
服务认证-来者何人
后端·go