innodb的count(*)慢是因为需按事务快照逐行扫描并判断可见性,无法缓存全局行数;低版本(如5.6)还不支持索引覆盖优化,常被迫扫描整行的聚簇索引,i/o和cpu开销大。

MySQL 5.6 及更早版本的 InnoDB 没有“索引覆盖计数”优化
低版本(如 MySQL 5.6、5.5)的 InnoDB 在执行 COUNT(*) 时,即使表上有二级索引,优化器也大概率不会选它来遍历——而是直接扫主键索引(聚簇索引)。因为主键索引叶子节点存的是整行数据,I/O 成本高、CPU 解析开销大。而 MyISAM 不管有没有索引,都只读元数据里的 rec_count,不碰数据文件。
常见错误现象:EXPLAIN SELECT COUNT(*) FROM orders; 显示 type: ALL 且 key: NULL,说明连最小索引都没用上。
- MySQL 5.7 起才开始更积极地选择非空二级索引(比如只有
idINT 的索引)来减少扫描体积 - MySQL 8.0 进一步强化了对
NOT NULL二级索引的识别,能跳过主键回表,显著降低 I/O - 如果你用的是 5.6 或更老版本,加一个
KEY idx_dummy (id)(id是主键且NOT NULL)也不一定被采纳,得靠FORCE INDEX强制,但不推荐线上滥用
MyISAM 的 rec_count 是磁盘级原子写,InnoDB 在低版本无等效机制
MyISAM 把行数存在 .MYI 文件头,每次 INSERT/DELETE 后由引擎原子更新这个整数。而低版本 InnoDB 根本不维护任何行数缓存,也没有后台线程做采样估算——SHOW TABLE STATUS 返回的 Rows 字段在 5.6 中误差更大(常达 ±60%),且不随 ANALYZE TABLE 实时刷新。
使用场景:你查 SHOW TABLE STATUS LIKE 'orders' 看到 Rows: 1234567,在 5.6 上基本不能信;但在 MyISAM 表里,这数字就是刚写入磁盘的精确值。
- MyISAM 的
rec_count更新不走事务日志,所以快,但也意味着崩溃后可能不准(需REPAIR TABLE) - InnoDB 低版本若想“模拟”类似行为,只能靠应用层双写或触发器维护计数表,但会引入死锁和一致性风险
- 别依赖
information_schema.TABLES.TABLE_ROWS做分页总数——它底层就是调SHOW TABLE STATUS
COUNT(*) 在低版本 InnoDB 中无法利用“只读事务优化”
MySQL 5.7.20+ 加入了 read_only 事务下对 COUNT(*) 的轻量级快照优化(绕过部分 MVCC 判断逻辑),而 5.6 及之前所有事务都严格走完整 Read View 构建 + 行可见性逐条判定流程。哪怕你只是 SET SESSION tx_isolation='READ-COMMITTED',也得为每一行检查 trx_id 和 roll_ptr。
性能影响:同样千万级表,在 5.6 上 COUNT(*) 可能耗时 8–12 秒;在 8.0 上开启 innodb_stats_persistent=ON 并配合合适索引,可压到 1–3 秒。
- 低版本没有
innodb_stats_persistent_sample_pages这类可控采样参数,统计信息老化后,优化器更容易误判索引可用性 -
SELECT COUNT(*)在低版本无法被查询缓存(Query Cache)有效命中,因为表变更会清空整个 cache key,而高并发写入下 cache 命中率趋近于 0 - 别试图用
SQL_NO_CACHE COUNT(*)来“绕过缓存”,Query Cache 本身在 8.0 已被移除,低版本用它反而增加解析开销
为什么升级不是万能解药:MVCC 本质矛盾仍在
哪怕升到 MySQL 8.0,COUNT(*) 依然要现场判断可见性——这是 MVCC 的代价,不是版本 bug。你看到的“变快”,其实是优化器更聪明地挑了小索引、采样更准、I/O 更少,而非跳过了行扫描本身。
最容易被忽略的一点:很多业务接口根本不需要精确总数。比如“是否有下一页”,用 SELECT id FROM orders ORDER BY id LIMIT 11 查 11 条,够 11 条就显示下一页,比 COUNT(*) 快两个数量级,且完全规避了版本差异问题。











