count(*) 慢的根本原因
count(*) 的实现方式
- MyISAM 引擎会把一个表的总行数存在了磁盘上
- InnoDB 引擎需要把数据一行行读出,累计计数
为什么 InnoDB 不跟 MyISAM 一样,也把数字存起来呢
- 由于多版本并发控制的原因(和快照读有关系),即同一时刻存在多个查询,InnoDB 表返回多少行不确定
- 例如三个用户对 A、B、C 并行开启事务会话,结构都不一样
- 会话 A 先启动事务并查询一次表的总行数
- 会话 B 启动事务,插入一行后记录后,查询表的总行数
- 会话 C 先启动一个单独的语句,插入一行记录后,查询表的总行数
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/b1ce71fcf47a0dea1d03ff1a273f13e4.png)
- MYSQL 对 count(*) 操作,仍然做了优化
- MySQL 优化器会找数据量少的索引树(包括主键、普通索引树比较)来遍历获取结果
- 不要使用下面命令来获取行数,其中的 TABLE_ROWS 字段是统计索引值,不准确
show table status
不同的 count 用法
- 首先明确,count() 是一个聚合函数,对于返回的结果集,一行行地判断,如果 count 函数的参数不是 NULL,累计值就加 1,否则不加
- 对于 count(主键 id) 、 count(1) 、 count(字段)、count(*) 的性能分析
- count(主键 id)
- 会遍历整张表,把每一行的 id 值都取出来,返回给 server 层。server 层拿到 id 后,判断是不可能为空的,就按行累加
- count(1)
- 遍历整张表,但不取值。server 层对于返回的每一行,放一个数字 “1” 进去,判断是不可能为空的,按行累加
- count(字段)
- 如果这个 “字段” 是定义为 not null 的话,一行行地从记录里面读出这个字段,判断不能为 null,按行累加
- 如果这个 “字段” 定义允许为 null,需要判断累加
- count(*)
- 做了专门优化,不取值。count(*) 肯定不是 null,按行累加
- 效率排序
count(字段)<count(主键 id)<count(1)≈count(*)
解决方案
- 用缓存系统保存计数
- 使用 Redis 服务保存这个表的总行数
- 这个表每被插入一行 Redis 计数就加 1,每被除一行 Redis 计数就减 1
- 然后把总行数定期地持久化存储起来
- 如果 Redis 异常重启以后,则到数据库里面单独执行一次 count(*) 获取真实的行数,然后把值写回到 Redis 里
- 注意: 将计数保存在缓存系统中的方式,还不只是丢失更新的问题。即使 Redis 正常工作,这个值还是逻辑上不精确的
- 在数据库保存计数
- 计数直接放到数据库里单独的一张计数表 C 中
- 如果有时序问题,就用事务来查表