MyISAM的COUNT(*)快是因为直接读取.MYI文件头中预存的rec_count元数据,时间复杂度O(1);而InnoDB必须按MVCC可见性逐行判断并累加,本质是带过滤的全索引扫描,无法预存精确行数。

MyISAM 的 COUNT(*) 快,是因为它根本没查数据——只读磁盘上一个固定整数;InnoDB 每次都得按当前事务的可见性规则,一行行判断、累加,绕不开全索引扫描。
MyISAM 的 COUNT(*) 本质是读元数据
MyISAM 在 .MYI 文件头固定偏移处存了一个叫 rec_count 的 4 字节整数,每次 INSERT 或 DELETE 都原子更新它。执行无 WHERE 的 COUNT(*) 时,MySQL 直接取这个值,不读数据页、不走索引树、不判断可见性。
- 时间复杂度是
O(1),和表大小无关,10 行和 10 亿行耗时几乎一样 - 这个优化**仅对无条件
COUNT(*)生效**;一旦加了WHERE status = 1,立刻退化为实际扫描 -
COUNT(1)、COUNT(pk)、COUNT(*)在 MyISAM 下执行计划和性能完全一致,都是读同一个元数据 - 如果表刚被
OPTIMIZE TABLE重建或 MySQL 刚启动,首次COUNT(*)可能触发一次全表扫描来重置计数器
InnoDB 的 COUNT(*) 必须逐行验证 MVCC 可见性
InnoDB 没有“全局行数”这个概念——同一时刻,不同事务看到的行数天然不同。它必须按当前事务的 ReadView,调用 row_search_mvcc 逐行判断是否可见,再累加。这本质上是「带可见性过滤的全索引扫描」。
- 优化器会自动选最小的可用索引(比如
INDEX idx_status (status))来遍历,叶子节点只存索引列 + 主键,比扫聚簇索引轻得多 - 如果表没有二级索引,
EXPLAIN SELECT COUNT(*) FROM t的key列会显示NULL,说明只能扫主键索引(即整行),I/O 爆炸 - 即使建了单字段索引(如
INDEX idx_id (id)),也只是降低 I/O 开销,无法改变必须逐行判断的本质 - MySQL 8.0.18 存在已知 bug:当
buffer_pool紧张时,物理读激增,COUNT(*)更容易超时
SHOW TABLE STATUS 的 Rows 字段为什么不能信
SHOW TABLE STATUS LIKE 't' 返回快,但 Rows 是 InnoDB 的采样估算值,不是精确结果。官方文档明确说明误差可达 ±40%~50%。
- InnoDB 默认随机采样约 10 个数据页,根据平均行密度推算总数
- 数据分布倾斜(比如大量删除后未
OPTIMIZE TABLE)时,偏差更离谱 - 这个值不参与 MVCC 判断,也不保证事务一致性,和
COUNT(*)解决的是两类问题 - 能用的场景只有两类:运维监控趋势(如发现突降 30%,提示误删);后台报表类需求(用户看到“约 2.3 万条”就足够)
- 绝不能用于分页总数、库存校验、审计统计等需要强一致性的逻辑
别为了 COUNT(*) 换引擎,代价远大于收益
MyISAM 的“快”只存在于无条件全表统计这一极其狭窄的场景;一旦业务需要事务、写一致性、任意过滤条件,这个优势就归零,而代价早已埋在引擎选型里。
- MyISAM 不支持事务、行锁、崩溃恢复、外键——线上业务基本不用,只适合只读报表或日志归档
- MyISAM 表级锁:一个
UPDATE正在跑,所有后续读写全排队,QPS 断崖下跌 - MyISAM 回表要两次 I/O(先查索引得地址,再按地址读磁盘),InnoDB 聚集索引一次定位即得整行
- 超过 5000 万行后,
.MYI索引文件容易碎片化,OPTIMIZE TABLE会锁表数小时 - 真实案例:11 亿记录的订单表,换引擎测试发现 InnoDB 的
WHERE id BETWEEN ? AND ?查询比 MyISAM 快 4 倍以上——瓶颈不在 COUNT,而在并发读写能力
真正容易被忽略的是:InnoDB 的 COUNT(*) 慢,不是实现偷懒,而是 MVCC 和事务隔离的必然代价;想让它变快,就得接受近似值、预计算或应用层缓存——没有银弹,只有取舍。


















