explain的rows只是预估,非实际扫描行数;它基于过时统计信息估算,不执行语句、不读真实数据页,真正反映实际扫描量的是explain analyze的actual rows或handler_read_next等运行时指标。

EXPLAIN的rows本来就不代表实际扫描行数
rows是优化器基于采样统计做的成本估算值,不是执行时真实读取的行数,也不是结果集大小。哪怕你刚删掉90%数据,只要没触发重采样(默认变更约10%才触发),rows仍可能显示“扫描10万行”,而实际只返回3条。
它不执行语句、不读真实数据页,只依赖INFORMATION_SCHEMA.STATISTICS里的索引基数(cardinality)和表级粗略行数。所以rows=1但慢查询日志里Rows_examined=500000,完全正常——这不是bug,是设计如此。
哪些写法会让rows彻底失真
这些情况会让优化器直接放弃使用索引统计,退化为粗略估算(比如全表行数除以2),此时调参毫无意义:
-
WHERE YEAR(create_time) = 2024或WHERE UPPER(name) = 'A':索引字段被函数包裹,统计失效 -
WHERE id = '123'(id是INT)或WHERE code = 123(code是VARCHAR):隐式类型转换导致索引无法匹配 - 联合索引
(a,b,c),却写WHERE b = 1:缺失最左前缀,该查询路径无对应统计 -
LEFT JOIN ... WHERE right_table.status = 'paid':把LEFT JOIN逻辑转成了INNER JOIN,还干扰选择率估算
怎么验证rows到底准不准
唯一可靠方式是用EXPLAIN ANALYZE(MySQL ≥8.0.18,且用户有PROCESS权限):
它强制真实执行一次查询,返回actual rows字段——这才是真正扫描的索引行数。例如输出中这一行:
-> Index range scan on orders using idx_user_status (user_id=1001, status IN (1,2,5)) (cost=12.50 rows=85) (actual time=0.042..1.28 rows=72 loops=1)
这里的rows=72才是真实值,rows=85只是预估。
如果不能用EXPLAIN ANALYZE,就看运行后状态变量:SHOW SESSION STATUS LIKE 'Handler_read_%',重点关注Handler_read_next(索引遍历次数)和Handler_read_rnd_next(回表次数),它们比rows更接近真实IO量。
ANALYZE TABLE能做什么、不能做什么
ANALYZE TABLE能刷新索引统计信息,让rows估算更贴近现实,但它不修复SQL写法问题,也不改变执行计划本身:
- 对刚导入/大批量变更后的表,执行
ANALYZE TABLE your_table是最快速补救 - 若数据倾斜严重(如
status字段95%是'done'),可先设SET SESSION innodb_stats_persistent_sample_pages = 100再ANALYZE - 必须确认
innodb_stats_persistent = ON,否则统计不落盘,重启即丢 - 即使分析了,只要
WHERE含函数、非最左前缀或跨分区,rows仍可能严重偏离
真正影响性能的,往往藏在Rows_examined远大于Rows_sent、tmp_disk_tables > 0这类运行时指标里,而不是盯着rows本身。











