count(*) 慢的根本原因
count(*) 的实现方式
MyISAM 引擎会把一个表的总行数存在了磁盘上 InnoDB 引擎需要把数据一行行读出,累计计数
为什么 InnoDB 不跟 MyISAM 一样,也把数字存起来呢
由于多版本并发控制的原因(和快照读有关系),即同一时刻存在多个查询,InnoDB 表返回多少行不确定 例如三个用户对 A、B、C 并行开启事务会话,结构都不一样
会话 A 先启动事务并查询一次表的总行数 会话 B 启动事务,插入一行后记录后,查询表的总行数 会话 C 先启动一个单独的语句,插入一行记录后,查询表的总行数
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 中 如果有时序问题,就用事务来查表