根本原因是优化器基于过期统计信息误判索引成本;需在批量导入、大删改、表结构变更后手动analyze更新统计,否则即使有索引也会全表扫描。

为什么 EXPLAIN 显示关联字段走了全表扫描,而不是走索引?
根本原因往往不是 SQL 写得有问题,而是优化器“以为”走索引不划算——而这个“以为”,依赖的是过期的统计信息。MySQL / PostgreSQL / Oracle 都会基于表的行数、列的数据分布(如 cardinality)、索引页深度等估算成本,一旦这些数据没更新,优化器就可能选错执行路径。
典型表现:明明 JOIN 字段上有索引,EXPLAIN 却显示 type: ALL(MySQL)或 Seq Scan(PostgreSQL),且 rows 估算值严重偏离实际(比如估算 100 行,实际 50 万行)。
- MySQL 中,
ANALYZE TABLE t1会重新采样并更新information_schema.STATISTICS和mysql.innodb_table_stats - PostgreSQL 中,
ANALYZE t1更新pg_statistic,注意它默认只分析“修改超过阈值”的表,大表可能长期不触发 - Oracle 的
DBMS_STATS.GATHER_TABLE_STATS需显式调用,且ESTIMATE_PERCENT设为DBMS_STATS.AUTO_SAMPLE_SIZE更可靠
哪些操作后必须手动 ANALYZE?
统计信息不会自动刷新到毫秒级,尤其在批量导入、归档删除、大范围 UPDATE 后,旧统计会立刻失效。以下场景几乎必然导致执行计划劣化:
- 执行了
TRUNCATE或大批量DELETE(比如删掉 80% 的数据),但没跟ANALYZE - 用
LOAD DATA INFILE或COPY导入百万级以上新数据 - 对某列做了大量倾斜写入(例如新增 10 万条
status = 'pending',但统计里还记着各值均匀分布) - 升级数据库小版本后首次启动(部分版本重置统计缓存)
JOIN 字段有索引却未被选用:检查三件事
别急着删索引或改 SQL,先确认是不是统计误导了优化器:
- 查真实行数:
SELECT COUNT(*) FROM t1 WHERE join_col = 'x',再对比EXPLAIN里的rows值——如果差 10 倍以上,统计大概率不准 - 看索引选择性:
SELECT COUNT(DISTINCT join_col) / COUNT(*) FROM t1,若低于 0.01(即 99% 值重复),即使有索引,优化器也可能弃用(这是合理判断,不是 bug) - 确认索引是否覆盖关联条件:
WHERE t1.a = t2.b时,t1 上要有INDEX(a),t2 上要有INDEX(b);少一个,另一张表就容易被迫全扫
生产环境 ANALYZE 的风险与折中
全量 ANALYZE 在大表上会锁表(MySQL 5.7+ 多数情况不锁,但会加读锁)、消耗 I/O,不能在高峰期跑。更稳妥的做法是:
- 对核心大表,用采样分析:
ANALYZE TABLE t1 UPDATE HISTOGRAM ON join_col WITH 16 BUCKETS(MySQL 8.0+),比全量快 5–10 倍 - PostgreSQL 可设
default_statistics_target = 200(默认 100),提升关键列统计精度,再配合定期ANALYZE - 避免在从库执行
ANALYZE(MySQL 从库默认关闭收集,PostgreSQL 从库禁止写),所有统计操作应在主库完成
最常被忽略的一点:有些 ORM 框架(如 Django 的 bulk_create)或 ETL 工具插入数据后,完全不触发统计更新——这得靠运维侧建立配套的 ANALYZE 任务,不能指望开发记得补。










