mysql count(*)在innodb中必须逐行扫描,根本原因是mvcc机制导致不同事务的readview不同,无法预存全局统一行数;只能按当前事务可见性逐行判断并累加,优化器会选最小索引树扫描以减少i/o。

MySQL COUNT(*) 为什么必须逐行扫描
InnoDB 不像 MyISAM 那样在元数据里存一个固定总行数,根本原因是 MVCC。同一时刻,不同事务看到的“可见行”可能完全不同——比如事务 A 插入但未提交的行,对事务 B 不可见;事务 C 已提交的新行,对事务 A 的快照又不可见。所以 MySQL 无法返回一个“全局统一”的总数,只能按当前事务的 ReadView,把每行拉出来判断是否可见,再累加。这个过程就是 COUNT(*) 实际做的:不是读磁盘计数器,而是遍历索引树、逐行检查可见性。
为什么加了索引,COUNT(*) 还是慢
常见误解是“加了索引就一定快”,但关键看加的是什么索引、怎么用:
-
COUNT(*)不需要取字段值,优化器会选“最小的索引树”来遍历——比如一个只含id的二级索引,比主键聚簇索引小得多,扫描更快 - 如果表只有主键索引(比如
PRIMARY KEY(id)),而没建其他二级索引,InnoDB 就只能扫主键索引,等价于扫全表数据(因为聚簇索引叶子节点存整行) - 如果现有索引包含可空字段(如
KEY idx_status(status)),而status允许为 NULL,COUNT(*)仍不能用它做覆盖扫描(优化器可能弃用),必须确保索引列定义为NOT NULL
EXPLAIN 能告诉你 COUNT 到底在扫什么
别猜,直接看执行计划:
- 执行
EXPLAIN SELECT COUNT(*) FROM user;,重点看key列:它显示实际走哪个索引;若为NULL,说明在全表扫描 - 看
rows值:这是优化器预估扫描行数,接近真实数据量,说明没走有效索引 - 看
type:如果是ALL,就是全表扫描;如果是index,说明走了某个索引的全扫描(比ALL快,但仍是 O(n)) - 如果
Extra出现Using index,恭喜——这次COUNT(*)真的只读索引没读数据行,是最优路径
count(1) 和 count(*) 真有区别吗
没有实质区别,别花时间纠结写法:
-
COUNT(*)是 MySQL 专门优化过的语法,server 层不取值、直接按行计数,性能最高 -
COUNT(1)效果几乎一致,优化器会做等价转换,实测差异在毫秒级以下 -
COUNT(id)或COUNT(name)反而更慢:要取出字段值判断是否为 NULL,多一次解析开销 - 真正影响性能的是底层扫描范围(索引选择 + MVCC 可见性判断),不是括号里填啥
COUNT(*) 本质是一次带事务语义的“轻量级全量读取”。它不缓存、不跳过、不投机——只要求结果对当前事务精确。这点在千万级表上暴露得最赤裸。











