根本原因是统计信息过期,优化器仍按旧行数估算执行计划;空表不触发自动更新,需手动analyze table刷新n_rows并确认innodb_stats_persistent=on。

EXPLAIN 显示全表扫描,但表明明是空的
空表上 SELECT * FROM t1 JOIN t2 ON t1.id = t2.t1_id 却要耗几百毫秒,EXPLAIN 却显示 type=ALL、rows 估数几万——这说明优化器没信“空”这个事实。根本原因常是统计信息过期:INFORMATION_SCHEMA.STATISTICS 或 mysql.innodb_table_stats 里存的行数、索引基数仍是旧值,哪怕你刚 TRUNCATE TABLE 过。
MySQL 8.0+ 中统计信息自动更新不可靠的场景
MySQL 默认开启 innodb_stats_auto_recalc=ON,但只在表数据变更超 10% 且行数 ≥ 25 非临时表时触发。空表(0 行)永远不满足“变更 10%”,所以统计信息卡在 truncate 前的状态;同样,小表(比如只有 20 行)删掉 15 行后也大概率不刷新。结果就是优化器按“老胖子”估算执行计划,选错驱动表、误判索引有效性。
- 用
SHOW TABLE STATUS LIKE 't2'查Rows字段,若远大于 0,基本可判定统计信息滞后 -
ANALYZE TABLE t2是最直接的修复手段,强制重采样(注意:会加 MDL 读锁,大表慎用) - 对频繁清空又重载的小表,建议在
TRUNCATE后紧跟ANALYZE TABLE,别依赖自动
JOIN 顺序被错误估算导致空表也慢
当优化器以为 t2 有 50000 行,就会把它当小表去驱动 t1,实际却因 t2 为空,整个嵌套循环一次都不进——但代价已经付出了:打开表、初始化 join buffer、尝试读取第一行……这些开销叠加起来,比预期高得多。更麻烦的是,如果 t2 上有低选择性索引(比如 status 列只有 2 个值),优化器还可能放弃走索引而回退到全表扫描逻辑。
- 用
EXPLAIN FORMAT=TREE(MySQL 8.0.16+)看实际选择的驱动表和访问方法 - 显式用
STRAIGHT_JOIN强制驱动顺序,比如SELECT STRAIGHT_JOIN ... FROM t1 JOIN t2,让t1当驱动表(前提是t1确实非空且有合适索引) - 检查
t2.t1_id是否有索引:没索引时,即使t2为空,MySQL 仍需打开并检查结构,延迟更明显
为什么 ANALYZE TABLE 有时也不管用
ANALYZE TABLE 在某些配置下会失效:比如 innodb_stats_persistent=OFF(老版本默认),此时统计信息只存在内存,重启就丢;或者 innodb_stats_method=nulls_unequal 导致对含大量 NULL 的列估算严重失真。另外,分区表、临时表、或使用了 CREATE TABLE ... SELECT 创建的表,其统计信息更新行为也和普通表不同。
- 确认持久化开关:
SELECT @@innodb_stats_persistent,应为ON - 查当前采样页数:
SELECT * FROM mysql.innodb_table_stats WHERE database_name='db' AND table_name='t2',关注n_rows和last_update - 极端情况可手动更新:
UPDATE mysql.innodb_table_stats SET n_rows=0, last_update=NOW() WHERE ...,但需FLUSH TABLES生效,且仅限紧急排查
真实慢的原因往往不是“空”,而是“优化器不信它是空”。统计信息滞后是静默型故障,不报错、不告警,只悄悄拖慢查询。只要涉及定期清空+重载的表,就把 ANALYZE TABLE 当成清理流程的最后一步,而不是可选项。










