在做业务开发的时候,经常会使用 count() 函数对满足条件的结果集统计数量,但执行 select count(*) from t 这种语句的时候会很慢,而 count(*)、count(1)、count(字段)、count(主键id)这几种方式统计有所区别,这篇文章介绍学习一下不同 count() 之间区别。
1.笔记图
2.count(*) 的实现方式
- MySQL 引擎:MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个数,效率很高(如果加了 where 条件的时候也需要扫描结果)
- InnoDB 引擎:InnoDB 执行 count(*) 的时候,需要把数据一行一行地从引擎里面读出来,然后累积计数
- 为什么 InnoDB 不跟 MyISAM 一样,也把数字存起来呢?
- 这是因为即使是在同一个时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB 表 应该返回多少行 也是不确定的
- 可重复读是它默认的隔离级别,在代码上就是 MVCC 来实现的。每一行记录都要判断自己是否对这个会话可见,对于 count(*) 请求来说,可见的行才能够用于计算 基于这个查询 的表的总行数
3.InnoDB count(*) 优化
- 扫描行数选择
- InnoDB 是索引组织表,主键索引树的叶子节点是数据,而普通索引树的叶子节点是主键值。所以,普通索引树比主键索引树小很多
- MySQL 优化器会找到最小的那棵树来遍历,在保证逻辑正确的前提下,尽量减少扫描的数据量,是数据库系统设计的通用法则之一
- TABLE_ROWS 能代替 count(*) 吗?
- 索引扫描行数统计的值是通过采样来估算的,因此它也很不准
- 官方文档说误差可能达到 40% 到 50%。所以,show table status 命令显示的行数也不能直接使用。
- 小结
- MyISAM 表虽然 count(*) 很快,但是不支持事务
- show table status 命令虽然返回很快,但是不准确
- InnoDB 表直接 count(*) 会遍历全表,虽然结果准确,但会导致性能问题
4.count(*) 业务层优化
- 用缓存系统保存计数
- 使用 Redis 记录总数:你可以用一个 Redis 服务来保存这个表的总行数。这个表每被插入一行 Redis 计数就加 1,每被删除一行 Redis 计数就减 1
- 使用 Redis 记录总数带来的问题
- 缓存系统可能会丢失更新
- 试想如果刚刚在数据表中插入了一行,Redis 中保存的值也加了 1,然后 Redis 异常重启了,重启后你要从存储 redis 数据的地方把这个值读回来,而刚刚加 1 的这个计数操作却丢失了
- 将计数保存在缓存系统中的方式,还不只是丢失更新的问题。即使 Redis 正常工作,这个值还是逻辑上不精确的
- 在数据库保存计数:计数直接放到单独的一张计数表中
- 这里要解决的问题是由于 InnoDB 要支持事务,从而导致 InnoDB 表不能把 count(*) 直接存起来,然后查询的时候直接返回形成的
- 以子之矛攻子之盾,利用 事务 这个特性,可以把问题解决掉
5.InnoDB 不同的 count 用法
- count( ) 含义:count() 是一个聚合函数,对于返回的结果集,一行行地判断,如果 count 函数的参数不是 NULL,累计值就加 1,否则不加。最后返回累计值。
- 分析性能差别原则
- server 层要什么就给什么
- InnoDB 只给必要的值
- 优化器只优化了 count(*) 的语义为“取行数”,其他显而易见的优化并没有做
- count(*):并不会把全部字段取出来,而是专门做了优化,不取值。count(*) 肯定不是 null,按行累加
- count(主键 id):InnoDB 引擎会遍历整张表,把每一行的 id 值都取出来,返回给 server 层。server 层拿到 id 后,判断是不为空的,就按行累加
- count(字段)
- 如果这个字段是定义为 not null 的话,一行行地从记录里面读出这个字段,判断不能为 null,按行累加
- 如果这个字段定义允许为 null,那么执行的时候,判断到有可能是 null,还要把值取出来再判断一下,不是 null 才累加
- count(1):InnoDB 引擎遍历整张表,但不取值。server 层对于返回的每一行,放一个数字1进去,判断是不可能为空的,按行累加。
- 按照效率排序:count(字段)<count(主键 id)<count(1)≈count(*),所以建议你,尽量使用 count(*)