MySQL中count(*)运行缓慢?解决之道寻踪

3周前发布 gsjqwyl
12 0 0

count(*)的施行方式

在不一样的MySQL引擎当中,count(*)存在着各异的施行办法:

  • MyISAM引擎把一个表的所有行数存储在磁盘里,当执行count(*)时能够直接返回总行数,效率颇高;

  • InnoDB引擎得把数据一行一行地从引擎中读出来,随后累积计数。

需要说明的是,本文探讨的是没有过滤条件的count(*),要是加上了where条件,MyISAM也就没那么快了。

那为啥InnoDB不像MyISAM那样把行数存起来呢?

这是由于就算是同一时刻的多个查询,因为MVCC的缘由,返回多少行并非是确定的。假设当下表t中有10000条记录,设定了三个用户并行的会话:

  • 会话A先开启事务并查询一次表的总行数;

  • 会话B开启事务,插入一行记录后,查询表的总行数;

  • 会话C先开启一个单独的语句,插入一行记录后,查询表的总行数。

假定该过程的时间顺序如下:

能够看到,三个会话在同一时刻查询总行数,得到的结果却不一样。至于为啥查询结果会如上述情况,能从MySQL 08那篇文章找到答案。

这是因为InnoDB的默认隔离级别是可重复读,在代码层面通过MVCC来实现。由于每一行记录都得判断自身是否对这个会话可见,所以对于count(*)请求而言,InnoDB得把数据一行一行读出来依次进行判断,只有可见的行才能算入总行数。

不过MySQL还是对count(*)做了一定的优化。因为普通索引树比主键索引树小很多,而count(*)操作遍历哪个索引树得到的结果在逻辑上是一样的,所以优化器会找到最小的索引树来遍历。在保证逻辑正确的前提下,尽可能减少扫描的数据量,这是数据库系统设计的通用准则之一。

有个命令叫show table status,它的输出结果里有一个TABLE_ROWS用来显示这个表当前有多少行。这个命令执行速度比较快,那能不能用它来加速计算行数呢?

不行的,TABLE_ROWS的值是从MySQL 10中提到的采样值估算而来的,所以也很不准确。

那么要是经常需要用到记录总数的值,也就是要加快count(*),那就得自己计数,基本思路是找个地方把记录表的行数存起来

借助缓存系统保存计数

对于更新频繁的库来说,可能会想到用缓存系统来支持。比如用Redis保存总行数,每插入一行Redis的计数就加1,每删除一行Redis计数就减1。

这样的设计速度确实很快,但缓存系统有可能丢失更新。要是想找地方把值持久化存储,仍然可能丢失更新。比如刚在数据表中插入一行,Redis中保存的值也加了1,然后Redis异常重启,而此时加1还没持久化。

当然,异常重启是有办法解决的,可以在异常重启后再执行一次count(*)获取真实的最新值,再写回Redis。异常重启毕竟不常出现,这样偶尔的情况是可以接受的。

不过,就算Redis正常工作,这个值依旧是逻辑上不精确的。

设想有这么一个页面,要显示操作记录的总数,同时还要显示最近操作的100条记录,那么就得先到Redis里取出计数,再到数据表里取记录。

不精确的定义如下:

  • 查到的100条记录里有最新的插入记录,但Redis里计数还没加1;

  • 或者查到的100条记录里没有最新的插入记录,但Redis计数已经加1。

看两个例子:

上面的两个例子,就对应了不精确的两种情况。

因为在并发系统里没法精确控制不同线程的执行时刻,可能出现上述两种情况,所以说就算Redis正常工作,计数值还是逻辑上不精确的。

在数据库保存计数

既然不能用缓存系统保存,那把计数直接放到数据库里单独的一张计数表C中会怎样呢?

首先,解决了崩溃丢失的问题,因为InnoDB支持崩溃恢复。

其次,由于事务的特性,也能解决不精确的问题。

在上图中,会话B在T3执行读操作,但此时更新事务还没提交,所以计数值+1这个操作对会话B不可见,从而会话B查计数值和“最近100条记录”看到的结果在逻辑上一致。

不同的count用法

最后说说count()括号内填不同内容的几种用法。

count()是一个聚合函数,对于返回的结果集,会一行一行地判断,如果count()函数参数对应的值不为NULL,累计值就加1,否则不加,最后返回累计值。

所以,count(*)count(主键id)count(1)都表示返回满足条件的结果集的总行数,而count(字段)表示返回满足条件的数据行中,“字段”这一列的值不为NULL的总个数。

而分析性能差别时,可以记住以下几个原则:

  • Server层要什么就给什么;

  • InnoDB只给必要的值;

  • 优化器只优化了count(*)的语义为“取行数”,其他优化并没有做。

怎么理解呢?具体来看:

  • 对于count(主键id),InnoDB会遍历整张表,取出每一行的id,返回给Server层。Server层拿到id,判断不可能为空,按行累加;

  • 对于count(1),InnoDB会遍历整张表但不取值,Server层对返回的每一行,放一个数字“1”进去并判断不可能为空,按行累加;

  • 对于count(字段)

    • 如果字段定义为not null,Server层能直接判断不能为null,按行累加;

    • 如果字段定义允许null,Server层需要把值取出来再判断一下,不是null才累加;

  • 对于count(*),进行了优化,InnoDB遍历整张表但不取值,Server会判断肯定不是null,按行累加。

所以,如果按照效率排序,count(字段)<count(主键id)<count(1)≈count(*)

© 版权声明

相关文章

没有相关内容!

暂无评论

none
暂无评论...