MyISAM 的 COUNT(*) 快是因为内存中缓存行数,增删只更新计数器,不扫描数据页;InnoDB 则需遍历聚簇索引判断可见性,加非空二级索引可优化为扫描更小索引。

MyISAM 的 COUNT(*) 为什么快?因为它不查数据
MyISAM 表的 COUNT(*) 几乎是常量时间,因为引擎在内存里缓存了行数,每次增删只更新这个计数器。它不扫描数据页,也不校验行是否被逻辑删除(MyISAM 没有 MVCC 或事务回滚段)。但注意:这个值仅在表没有崩溃、且未被其他会话并发修改时才准确;如果执行过 OPTIMIZE TABLE 或服务器重启后首次访问,可能触发一次全表扫描来重建计数。
- 仅对无
WHERE条件的COUNT(*)生效 - 一旦加了
WHERE(哪怕只是WHERE 1),就退化为实际扫描 - 如果表被
ALTER TABLE重建过,计数器重置,首次COUNT(*)会慢一次
InnoDB 的 COUNT(*) 默认要扫聚簇索引
InnoDB 没有全局行数缓存,因为要考虑 MVCC —— 同一时刻不同事务看到的“可见行数”可能不同。COUNT(*) 必须遍历聚簇索引(主键 B+ 树)叶子节点,逐行判断可见性。即使表空,也要走一遍索引结构;数据量大时 I/O 和 CPU 开销明显。
- 不走二级索引,哪怕有非空唯一索引 —— 因为聚簇索引包含所有行,能直接判断可见性
-
EXPLAIN显示type: index,说明走了索引全扫描,不是全表扫描,但仍需读取每条记录 - 如果主键很长(比如用 UUID),聚簇索引体积大,扫描更慢
InnoDB 加覆盖索引让 COUNT(*) 变快的原理
关键在于让优化器选择一个**更小、更紧凑的二级索引**来扫描,而不是默认的聚簇索引。只要该索引的所有列都非 NULL(通常主键或唯一 NOT NULL 索引满足),优化器就可能改用它 —— 因为只需读取索引页,不回表,且索引越窄,I/O 越少。
- 建一个单列
NOT NULL索引,例如:ALTER TABLE t ADD INDEX idx_counter (id);
(假设id是主键且非空) - 确保查询不含
WHERE,否则覆盖索引失效;若必须带条件,索引需匹配过滤字段 - 用
EXPLAIN验证是否用了该索引:key字段应显示索引名,rows应显著小于聚簇索引的总行数估算 - 注意:MySQL 8.0+ 对覆盖索引选型更激进,5.7 可能仍倾向聚簇索引,必要时加
USE INDEX
真正要注意的边界情况
覆盖索引提速不是万能的。如果表有频繁写入,二级索引会增加维护开销;如果只读场景极少用 COUNT(*),加索引反而浪费空间。更隐蔽的问题是:某些 ORM 或监控脚本会自动拼 COUNT(1) 或 COUNT(pk),这会让优化器放弃覆盖索引策略 —— 因为它们不再是语义等价的 COUNT(*)。
-
COUNT(1)和COUNT(*)在语义上相同,但 MySQL 5.7 有时不等价优化,建议统一用COUNT(*) - 如果用了
SQL_CALC_FOUND_ROWS,覆盖索引无效,它强制走聚簇索引 - 分区表下,每个分区单独统计再合并,覆盖索引效果取决于分区剪枝是否生效


















