EXPLAIN 的 rows 值不准是因为它是优化器基于过时或不可用的索引统计信息做的粗略预估,受自动统计更新关闭、非前缀/函数索引、无索引 WHERE 条件及旧版 ANALYZE TABLE 行为影响。

为什么 EXPLAIN 的 rows 值不准?
EXPLAIN 中的 rows 是优化器基于索引统计信息(如 INFORMATION_SCHEMA.STATISTICS 或采样估算)给出的粗略预估,不是真实行数。它在以下情况偏差极大:
- 表刚执行过大量
INSERT/DELETE,但未触发统计信息自动更新(尤其innodb_stats_auto_recalc=OFF或小表) - 使用了非前缀索引、函数索引或虚拟列,导致统计不可用
- 查询带
WHERE条件且条件字段无索引,优化器退化为全表扫描估算,误差常达 10 倍以上 - MySQL 8.0 之前,
ANALYZE TABLE不强制刷新所有统计,需配合innodb_stats_persistent=ON才稳定
什么时候该用 COUNT(*) 而不是 COUNT(主键)?
InnoDB 引擎下,COUNT(*) 和 COUNT(主键) 性能几乎一致——因为两者都走聚集索引(clustered index)遍历,且不校验字段非空。但关键区别在于语义和可维护性:
-
COUNT(*)明确表达“统计行数”,语义清晰;COUNT(id)容易让人误以为在过滤NULL,而 InnoDB 主键不可能为NULL,实际并无差别 - 若未来主键变更(比如改成联合主键或引入可空字段),
COUNT(主键)可能意外变慢或语义漂移,COUNT(*)始终安全 - MySQL 8.0+ 对
COUNT(*)有额外优化:若表无二级索引,可能直接读取TABLE_ROWS(来自information_schema.tables,但仅当innodb_stats_persistent=ON且统计较新时才可信)
百万级以上表,如何避免 COUNT(*) 全表扫描?
真正需要精确值时,全表扫描无法绕过;但多数业务场景其实可以接受近似或延迟更新。实操建议分三层应对:
- **监控/后台任务类**:用
SELECT TABLE_ROWS FROM information_schema.tables WHERE table_schema = 'db' AND table_name = 't'。注意该值是上次ANALYZE TABLE时的快照,误差通常在 ±10% 内,且不锁表 - **实时性要求低的前台展示**(如“约 245 万条”):加缓存层,比如 Redis 存储
count:t,每次写操作(INSERT/DELETE)后用INCR/DECR维护,比查库快两个数量级 - **必须精确且高频查询的场景**:建单独计数表,例如
table_counts(table_name VARCHAR, cnt BIGINT),用事务保证一致性:START TRANSACTION;<br>INSERT INTO t VALUES (...);<br>UPDATE table_counts SET cnt = cnt + 1 WHERE table_name = 't';<br>COMMIT;
避免在大事务中直接COUNT(*)
MyISAM 表的 COUNT(*) 为什么快?能不能迁移到 InnoDB?
MyISAM 把总行数存在磁盘文件头,COUNT(*) 是 O(1) 操作。但这只是幻觉——它不区分事务隔离级别,SELECT COUNT(*) 返回的是“当前物理存储的行数”,对未提交事务、MVCC 可见性一概不感知,结果在并发写入下完全不可靠。
迁移时别留恋这个“快”:
- MyISAM 无行级锁、无崩溃恢复、无外键,现代应用基本不该用
- InnoDB 的
COUNT(*)虽慢,但结果严格符合 ACID 和隔离级别(比如 RR 级下只统计本事务可见行) - 如果真卡在计数性能,优先优化查询模式(如分页改用游标)、加缓存、或接受估算值,而不是倒退到 MyISAM
最常被忽略的一点:很多人把 information_schema.tables.TABLE_ROWS 当成“免费精确值”,但它在 InnoDB 下根本不是实时的,连 ANALYZE TABLE 都不一定触发更新——得看 innodb_stats_on_metadata 是否开启(默认 OFF),以及表大小是否触发自动采样阈值。



















