count(*)必须遍历聚簇索引叶子节点,因innodb不维护精确行数且受mvcc影响,需按当前事务隔离级别逐行判断可见性;它不走覆盖索引,因语义要求确认整行可见而非仅键值,且需读取隐藏事务id等字段完成可见性检查。

为什么COUNT(*)必须遍历聚簇索引叶子节点?
InnoDB 不维护精确行数,COUNT(*) 无法直接查个数字返回——它得真实数一遍。而“数一遍”的最可靠路径,就是顺着主键索引(即聚簇索引)的 B+ 树叶子节点逐行检查。这不是偷懒或设计缺陷,而是 MVCC 和事务可见性共同决定的:同一时刻,不同事务看到的“有效行数”可能完全不同,引擎必须按当前事务的隔离级别,现场判断每行是否可见。
COUNT(*)为什么不走覆盖索引优化?
哪怕表只有 id 和 created_at 两列,COUNT(*) 默认也不会只读主键索引的键值。原因有二:
- 语义上,
*表示“所有行”,引擎需确认整行记录是否对当前事务可见,不能跳过行数据本身的可见性检查(比如该行被其他事务删除但未提交,或插入但未提交) - 优化器不认为仅读主键值就足够;它会加载整行(包括隐藏的事务ID字段),才能执行可见性判断
反过来说,COUNT(id) 在 id 是 NOT NULL 主键时,才有可能触发覆盖扫描(只读主键索引叶子节点的键值),省去回表和数据页加载——但这不是 guaranteed,取决于优化器是否选中该路径。
information_schema.TABLES.TABLE_ROWS 为什么不准?
TABLE_ROWS 是采样估算值,来自 ANALYZE TABLE 时对少量索引页(默认仅 20 页)的随机抽样。它不反映实时、精确、事务一致的行数,误差常达 20%~50%,尤其在以下情况:
- 表刚批量导入百万数据,但没手动
ANALYZE TABLE - 主键是 UUID 或随机字符串,导致采样页严重缺乏代表性
- 存在大量长事务,使历史版本堆积,实际“可见行”远少于物理存储行
别把它当真——它连“快照”都算不上,只是优化器成本估算的一个粗糙输入。
想快一点,只能换思路,而不是换写法
硬要 COUNT(*) 在亿级表上毫秒返回?做不到。InnoDB 的设计决定了它必须做可见性判断,而判断的前提是触达每一行。可行的替代路径只有:
- 业务层自己维护计数(如用 Redis + 原子增减,配合 binlog 或应用逻辑补偿)
- 接受估算,定期
ANALYZE TABLE并信任TABLE_ROWS(但需明确告知前端这是“约等于”) - 改查询意图:比如“是否有新数据?”用
SELECT 1 FROM t WHERE updated_at > ? LIMIT 1,比COUNT(*)快几个数量级
真正容易被忽略的是:这个“慢”,不是 SQL 写错了,也不是索引没建好,而是 InnoDB 为保证 ACID 所付出的必要代价。想绕开它,得从问题本身出发,而不是在 COUNT 的括号里折腾字段名。











