在开发系统的时候,经常需要计算一个表的行数,使用select count(*) from t就可以了,但是随着系统中记录数越来越多,这条语句也会执行得越来越慢。
接下来就学习count(*)是如何是实现的,以及mysql这么实现的原因和应用中存在这种频繁变更并需要统计表行数的需求,业务设计上怎么做。
count(*)的实现方式
首先需要知道,在不同的Mysql引擎上,count(*)有不同的实现方式。
- MyISAM引擎把一个表的总行数存在了磁盘上,因此执行count(*)的时候只要执行返回这个数就行了,效率很高。
- InnoDb引擎就麻烦, 它执行count(*)的时候,需要把数据一行一行地取出,然后累计计数。 不过需要注意,这里说的是没有过来条件地count(*),如果加了where条件,MyISAM也不能返回那么快。
InnoDB作为我们最常用的引擎,为什么不跟MyISAM一样,也将数字存起来呢? 最主要的原因就是,InnoDB是支持事务的引擎,在同一时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB表应该返回多少行是不确定的。
有下图这样的一组事务:
对于这三个事务最后进行的总函数的查询来说,每一行记录都需要判断自己是否对这个会话可见,因此对于count(*)来说,InnoDB只好把数据一行行读取出来依次判断。
不过==InnoDB还是对count(*)进行了优化的,优化器会选择找到最小的那颗树进行遍历==,在保证逻辑正确的前提下,尽量减少扫描的数据量,是数据库系统设计的通用法则之一。
到了这里小结一下:
- MyISAM表虽然count(*)很快,但是不支持事务
- show table status命令虽然很快,但是不准确
- InnoDB进行count(*)会遍历全表,虽然结果准确,但是会导致性能问题。
那么,现在有一个页面要经常显示交易系统的操作记录总数,那么就需要我们自己计数。
用缓存系统保存计数
将表的行数使用redis进行存储,每增加一行redis计数就加1,删除一行计数就减1。 虽然这样读和写都很快,但是会存在一些问题。首先便是缓存系统会丢失更新,试想如果刚刚在数据表中插入了一行,Redis 中保存的值也加了 1,然后 Redis 异常重启了,重启后你要从存储 redis 数据的地方把这个值读回来,而刚刚加 1 的这个计数操作却丢失了。 这个问题可以在redis重启之后从mysql读取一次行数来解决。
但是因为更新redis中的计数和mysql记录并不是一个原子操作,所以会导致数据不一致的问题。

所以使用这种方法,计数值在逻辑上还是不精确的。
在数据库中保存计数
将计数直接放到数据库里单独的一张计数表中。 首先解决了崩溃丢失的问题,innodb是支持崩溃恢复不丢数据的。
直接缓存系统存在的数据不一致的问题,可以通过事务来解决。

虽然会话B的读操作仍然是在T3执行的,但是这时候更新事务还没有提交,所以计数值加1这个操作会话B还不可见。
逻辑会话B看到的结果里,查询计数值和“最近100条记录”看到的结果在逻辑上是一致的。
不同的count用法
获取计数除了count(*)之外,还有count(主键id)、count(字段)、count(1)等不同用法。
这些来便了解这些不同用法的性能。 首先要知道count是一个聚合函数,对于返回的结果集,一行行地判断,如果count函数的参数不是NULL,累计值就加1,否则不加,最后返回累计值。
- count(主键id):InnoDB引擎会遍历整张表,把每一行的id值都取出来,返回给server层。server层拿到id后,判断不可能为空,就按行累加。
- count(1):InnoDB引擎遍历整张表,但不取值。server层对返回的每一行放一个数字1进去,判断不可能为空,按行累加。
从引擎返回id会涉及到解析数据行,以及拷贝字段值的操作。所以count(1)要比count(主键id)要快。
- count(字段):一行行地从记录中读取这个字段,判断不能为null,按行累加。
- count(*):这里专门做了优化,不取值。count(*)肯定不是null,按行累加。
所以按照效率排序,==count(字段) < count(主键id) < count(1) ≈ count(*),所以尽量使用count(*)==。