MySQL的count

本文为极客时间专栏《MySQL实战45讲》笔记,文中部分图文来自该专栏。

count(*)语句应该是我们开发中很经常用到的,n那么你有仔细研究过吗?

实现方式

MyISAM引擎把一个表的总行数存在了磁盘上,因此执行count()的时候会直接返回这个数,效率很高;如果加了where 条件的话,MyISAM表也是不能返回得这么快的。而InnoDB引擎就麻烦了,它执行count()的时候,需要把数据一行一行地从引擎里面读出来,然后判断not null累积计数。

那你就问了,为什么InnoDB不跟MyISAM一样,也把数字存起来呢?

这是因为即使是在同一个时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB表“应该返回多少行”也是不确定的。

假设表t中现在有10000条记录,我们设计了三个用户并行的会话。

  • 会话A先启动事务并查询一次表的总行数;
  • 会话B启动事务,插入一行后记录后,查询表的总行数;
  • 会话C先启动一个单独的语句,插入一行记录后,查询表的总行数。
  • 我们假设从上到下是按照时间顺序执行的,同一行语句是在同一时刻执行的。

图1 会话A、B、C的执行流程

你会看到,在最后一个时刻,三个会话A、B、C会同时查询表t的总行数,但拿到的结果却不同。

这和InnoDB的事务设计有关系,可重复读是它默认的隔离级别,在代码上就是通过多版本并发控制来实现的。每一行记录都要判断自己是否对这个会话可见,因此对于count(*)请求来说,InnoDB只好把数据一行一行地读出依次判断,可见的行才能够用于计算“基于这个查询”的表的总行数。

当然,现在这个看上去笨笨的MySQL,在执行count(*)操作的时候还是做了优化的。

InnoDB支持的是索引组织表,主键索引树的叶子节点是数据,而普通索引树的叶子节点是主键值。所以,普通索引树比主键索引树小很多。对于count(*)这样的操作,遍历哪个索引树得到的结果逻辑上都是一样的。因此,MySQL优化器会找到最小的那棵树来遍历。

在保证逻辑正确的前提下,尽量减少扫描的数据量,是数据库系统设计的通用法则之一。

如果你用过show table status 命令的话,就会发现这个命令的输出结果里面也有一个TABLE_ROWS用于显示这个表当前有多少行,这个命令执行挺快的,那这个TABLE_ROWS能代替count(*)吗?

而索引统计的值是通过采样来估算的,实际上,TABLE_ROWS就是从这个采样估算得来的,因此它也不是很准。有多不准呢,官方文档说误差可能达到40%到50%。所以,show table status命令显示的行数也不能直接使用。

MyISAM表虽然count(*)很快,但是不支持事务;show table status命令虽然返回很快,但是不准确;InnoDB表直接count(*)会遍历全表,虽然结果准确,但会导致性能问题。

那么,如果你现在有一个页面经常要显示记录总数,到底应该怎么办呢?

有时候,我们未必就只能纠结于count,我们可以自己额外计数。

计数方法

缓存计数

对于更新很频繁的库来说,你可能会第一时间想到,用缓存系统来支持,比如Redis。

我们可以用一个Redis服务来保存这个表的总行数,读和更新操作都很快。

当然,使用Redis存储计数是有一些问题的。

首先,无法保证Redis完全可用,假如异常挂掉,我们无法保证MySQL和Redis数据的一致性。

其次,即使Redis正常可用,计数也并不精确,因为MySQL和Redis存储必然有先后之分,在高并发场景下,多个会话从Redis和MySQL读到的数据很可能是不一致的,我们可以看看图片,就不展开说明了。

图2 会话A、B执行时序图

当然了,在某些场景下,我们可以这么做,因为业务并不要保证数据每时每刻都是精确的,那就无需考虑这些问题,Redis异常后我们可以从计算表行数去更新。

数据库计数

我们也可以用MySQl新建一张表去计数。那么针对缓存计数的两个问题,我们来分析下:

首先,这解决了崩溃丢失的问题,InnoDB是支持崩溃恢复不丢数据的。

然后,我们再看看能不能解决计数不精确的问题。当然,我们有“事务”这个大杀器,可以保证数据一致性。

不同的count用法对比

  1. count(主键id):InnoDB引擎会遍历整张表,把每一行的id值都取出来,返回给server层。server层拿到id后,判断是不可能为null,就按行累加。

  2. count(1):InnoDB引擎遍历整张表,但不取值。server层对于返回的每一行,放一个数字“1”进去,判断是不可能为null,按行累加,显然这比上面的效率要高一些。

  3. count(字段):如果这个“字段”是定义为not null的话,一行行地从记录里面读出这个字段,判断不能为null,按行累加;如果这个“字段”定义允许为null,那么执行的时候,判断到有可能是null,还要把值取出来再判断一下,不是null才累加。

  4. count():是例外,并不会把全部字段取出来,而是专门做了优化,不取值。count()肯定不是null,按行累加。

按照效率排序的话,count(字段)<count(主键id)<count(1)≈count(*),所以我建议你,尽量使用count(*)


MySQL的count
https://blog.puresai.com/2020/03/15/MySQL-count/
作者
puresai
许可协议