InnoDB的COUNT(*)必须逐行扫描并判断MVCC可见性,因其无全局行数缓存,不同事务视角下可见行数不同,故无法预存精确值;该操作本质是轻量级快照读,时间复杂度O(n),且受长事务、undo log、隔离级别等多重影响。

因为InnoDB不维护精确行数,COUNT(*)会触发全表扫描,且结果受事务隔离级别影响——它不是慢那么简单,而是“不准+不可控+不可预估”三重风险。
为什么COUNT(*)在InnoDB里必须扫表?
InnoDB没有像MyISAM那样把总行数存在磁盘上。它只在每个事务视角下动态统计“当前可见的行”,所以COUNT(*)必须遍历数据(哪怕走最小索引),逐行判断是否对当前事务可见。这不是性能差的问题,是设计使然:并发事务可能看到不同数量的行,数据库无法给出一个“全局唯一正确”的数字。
常见误解是“加个索引就能快”,但即使有联合索引,只要没WHERE条件,优化器仍会选择扫描代价最低的索引树——如果表只有主键索引,那就等于全表扫描;如果有多个二级索引,它挑最窄的那个扫,但仍是O(n)复杂度。
不带WHERE的COUNT(*)在高并发下更危险
当多个长事务同时运行时,COUNT(*)会拉长一致性读(consistent read)的版本链遍历时间,容易引发:
- 大量
rows_examined_per_scan堆积,拖慢其他查询(尤其在performance_schema开启后可实测) - undo log膨胀,导致purge线程压力陡增,甚至触发
ERROR 1205 (Deadlock) - 在RR隔离级别下,若事务A执行了
COUNT(*)后事务B插入并提交,事务A再次执行仍返回旧值——这不是bug,但业务监控误判率极高
替代方案不是“换写法”,而是“换思路”
真实生产中,几乎不存在必须强一致、毫秒级返回的全表计数需求。更可行的做法是:
- 用近似值:
SELECT table_rows FROM information_schema.tables WHERE table_name = 'users' AND table_schema = 'mydb'(注意:该值来自InnoDB统计采样,误差通常 - 异步更新计数表:每次INSERT/DELETE后用
INSERT ... ON DUPLICATE KEY UPDATE维护一张table_counts,避免实时计算 - 业务兜底:前端分页改用游标(cursor-based pagination),彻底避开
COUNT(*)+LIMIT OFFSET组合
特别提醒:别信COUNT(1)比COUNT(*)快这种过时机理——MySQL 8.0+已对二者做等价优化,差异可忽略;真正卡住你的,永远是“要不要扫”这件事本身,而不是“扫的时候填1还是填*”。


















