myisam的count(*)是o(1)操作,因其在.myi文件头固定位置存储并原子更新行数,无需扫描;innodb因mvcc机制下各事务可见行数不同,必须按当前readview逐行判断可见性,只能全索引扫描,时间复杂度o(n),这是事务隔离语义决定的硬约束。

MyISAM 的 COUNT(*) 是 O(1) 操作,InnoDB 必须全索引扫描 —— 这不是配置问题,是 MVCC 和事务语义决定的硬约束。
MyISAM 的 COUNT(*) 为什么是 O(1)
MyISAM 在 .MYI 索引文件头固定偏移处存着一个 4 字节整数,每次 INSERT/DELETE 都原子更新它。执行 SELECT COUNT(*) FROM t 时,MySQL 根本不读数据页,只取这个值。
- 仅对无
WHERE条件生效;加了WHERE status = 1就得走索引扫描,速度立刻掉下来 - 表级锁下,一个长
UPDATE会阻塞所有后续COUNT(*),QPS 可能断崖下跌 - 不支持事务,事务 B 插入未提交时,事务 A 查不到这行 → 计数短暂不一致
InnoDB 的 COUNT(*) 为什么必须逐行判断可见性
InnoDB 没有全局行数缓存,不是“懒得存”,而是 MVCC 让“当前有多少行”本身成了动态问题:同一时刻,不同事务看到的行数可能完全不同。
- 事务 A 启动后,事务 B 插入但未提交 → A 看不到这行
- 事务 C 在 A 之后启动、B 提交后插入并提交 → C 能看到 B 的行,A 仍看不到
- 优化器会选体积最小的索引(比如
INDEX(status))遍历叶子节点,对每条记录回查聚簇索引获取DB_TRX_ID和DB_ROLL_PTR,再比对事务 ID 列表 -
EXPLAIN SELECT COUNT(*) FROM t显示type: index,说明确实在走索引全扫
SHOW TABLE STATUS LIKE 't' 返回的 Rows 能信吗
不能用于业务逻辑。这个值来自 InnoDB 的采样估算(默认扫描约 10 个数据页),官方文档明确说误差可达 ±40%~50%,尤其在大批量 DELETE 后或数据分布严重倾斜时更不准。
- 只适合两类场景:运维监控趋势(比如发现
table_rows突降 30%,提示可能误删)、后台报表类需求(用户看到“约 2.3 万条”就足够) - 别用它做分页总数、库存校验或触发告警阈值 —— 这些地方必须强一致,就得绕开全表扫描
- 即使加了
COUNT(1)或COUNT(id),也不比COUNT(*)快 —— MySQL 优化器早已做了等价处理
真正想让 InnoDB 的 COUNT(*) 快起来,得放弃“实时精确”
没有银弹,只有折中。如果你的应用能接受最终一致性,外部维护计数器是最实用的解法;如果必须强一致,就得在写入路径上加成本。
- 写时维护:建一张
counter_table(table_name VARCHAR(64), cnt BIGINT),在业务代码里或用TRIGGER同步增减;注意触发器里不能写复杂逻辑,否则拖慢主表写入 - 如果只统计带条件的行(如
COUNT(*) WHERE status=1),给加覆盖索引:INDEX(status)或INDEX(status, id),让 InnoDB 只扫二级索引即可 - MyISAM 表级锁和非聚集索引在高并发范围查询中反而成瓶颈;超过 5000 万行后,
.MYI容易碎片化,OPTIMIZE TABLE会锁表数小时
最常被忽略的一点:InnoDB 的“慢”只针对无条件 COUNT(*);一旦加了 WHERE,两者都要走索引或扫描,差距就没了 —— 别因为这个单一指标否定整个引擎选型。











