EXPLAIN的rows是预估而非真实扫描数,依赖过期统计信息导致误差可达百倍;应以EXPLAIN ANALYZE的actual rows和Handler_read_*状态变量为准验证真实执行行为。

EXPLAIN的rows只是预估,不是真实扫描数
MySQL优化器根本不会真的去扫表,它靠的是采样统计信息来猜“大概要扫多少行”。这个预估值写在rows字段里,但实际执行时可能差十倍、百倍。比如rows=120,真实扫描了9800行——这不叫bug,是设计如此。
原因很实在:innodb_stats_auto_recalc默认只在数据变更超10%后才触发重新采样,且有延迟;刚批量UPDATE完状态字段,统计还停留在旧分布上,优化器就按“status='completed'只占0.5%”来算,结果扫了全表10%才凑够几条。
- 刚导入/删除大量数据后没手动跑
ANALYZE TABLE,rows必然失真 - 表里某个字段95%是NULL,采样恰好漏掉非NULL块,基数估算直接崩
- MySQL 8.0+默认不建直方图,对
status IN (0,1)这种极不均匀分布完全无感
真正该看的是EXPLAIN ANALYZE的actual rows
EXPLAIN只画饼,EXPLAIN ANALYZE才动真格。它会真实执行SQL,并在输出里补上(actual rows=xxx, loops=yyy)。这才是你该盯住的数字。
比如这条语句:
EXPLAIN ANALYZE SELECT id, amount FROM orders WHERE status = 'completed' ORDER BY amount DESC LIMIT 100;
如果看到某节点rows=1000但actual rows=99000,基本能断定统计信息过期。再查mysql.innodb_table_stats里的last_update时间,若早于你上次批量操作,就坐实了。
-
loops > 1特别危险:rows=10, loops=100意味着内部嵌套循环处理了1000行 -
actual time前后值差距大,说明单步耗时高,可能是I/O或CPU瓶颈 - 别在开发环境跑
EXPLAIN ANALYZE——它真执行,会锁表、改数据
哪些写法会让rows预估彻底失效
即使统计信息最新,某些SQL写法也会让优化器的估算逻辑崩盘。这不是数据问题,是语法陷阱。
安装、修复、更新或打包一个基于 Python 和 PyMySQL 实现的 Windows `mysql` 命令。适用于用户希望在 PowerShell/CMD 中输入 `mysql` 时重定向到 Python 实现、需要交互式输入 MySQL 连接信息、需要同步到当前用户本地 bin 目录,或需要将该能力封装成可公开复用的 Codex skill。
典型例子:
-
WHERE YEAR(created_at) = 2024→ 函数导致索引失效,优化器只能按全表扫预估 -
WHERE user_id = '123'(user_id是INT)→ 隐式转换让索引基数估算归零 -
WHERE a = 1 OR b = 2→ 优化器分别估算再相乘,实际交集可能只有3行 -
WHERE name LIKE '%abc'→ 左模糊强制放弃索引,预估直接退化为全表
这些情况下的rows值,连参考价值都谈不上。得先重写SQL,再谈优化。
Handler_read_*才是真正的扫描证据
比EXPLAIN ANALYZE更底层的验证方式,是查Handler_read_*状态变量。它记录引擎层真实读取行为,不受优化器干扰。
执行查询前后,运行:
SHOW STATUS LIKE 'Handler_read%';
重点关注Handler_read_next(索引遍历)、Handler_read_rnd_next(回表读行)。如果Handler_read_rnd_next远高于Handler_read_next,说明大量回表——哪怕rows看着不大,I/O压力也已经拉满。
-
Handler_read_first高 → 频繁全索引扫描 -
Handler_read_key低但Handler_read_next高 → 索引过滤率差,靠遍历硬扛 - 这些值在慢查询日志里不会体现,必须主动抓取
最常被忽略的一点:rows不准本身不是问题,问题是很多人拿它当事实去调优。真正该对比的,是同一SQL在不同索引下EXPLAIN ANALYZE的实际actual rows,以及Handler_read_*的绝对数值。统计信息可以更新,SQL写法可以改,但引擎层的真实读取次数骗不了人。

















