MySQL的count
本文为极客时间专栏《MySQL实战45讲》笔记,文中部分图文来自该专栏。
count(*)语句应该是我们开发中很经常用到的,n那么你有仔细研究过吗?
实现方式
MyISAM引擎把一个表的总行数存在了磁盘上,因此执行count()的时候会直接返回这个数,效率很高;如果加了where 条件的话,MyISAM表也是不能返回得这么快的。而InnoDB引擎就麻烦了,它执行count()的时候,需要把数据一行一行地从引擎里面读出来,然后判断not null累积计数。
那你就问了,为什么InnoDB不跟MyISAM一样,也把数字存起来呢?
这是因为即使是在同一个时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB表“应该返回多少行”也是不确定的。
假设表t中现在有10000条记录,我们设计了三个用户并行的会话。
- 会话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读到的数据很可能是不一致的,我们可以看看图片,就不展开说明了。
当然了,在某些场景下,我们可以这么做,因为业务并不要保证数据每时每刻都是精确的,那就无需考虑这些问题,Redis异常后我们可以从计算表行数去更新。
数据库计数
我们也可以用MySQl新建一张表去计数。那么针对缓存计数的两个问题,我们来分析下:
首先,这解决了崩溃丢失的问题,InnoDB是支持崩溃恢复不丢数据的。
然后,我们再看看能不能解决计数不精确的问题。当然,我们有“事务”这个大杀器,可以保证数据一致性。
不同的count用法对比
count(主键id)
:InnoDB引擎会遍历整张表,把每一行的id值都取出来,返回给server层。server层拿到id后,判断是不可能为null,就按行累加。count(1)
:InnoDB引擎遍历整张表,但不取值。server层对于返回的每一行,放一个数字“1”进去,判断是不可能为null,按行累加,显然这比上面的效率要高一些。count(字段)
:如果这个“字段”是定义为not null的话,一行行地从记录里面读出这个字段,判断不能为null,按行累加;如果这个“字段”定义允许为null,那么执行的时候,判断到有可能是null,还要把值取出来再判断一下,不是null才累加。count():是例外,并不会把全部字段取出来,而是专门做了优化,不取值。count()肯定不是null,按行累加。
按照效率排序的话,
count(字段)<count(主键id)<count(1)≈count(*)
,所以我建议你,尽量使用count(*)
。