统计信息不准会导致优化器误判子查询代价,影响物化、索引选择及半连接重写;需通过analyze及时更新单表、多列或直方图统计,并验证cardinality与实际分布是否一致。

统计信息不准时,优化器会误判子查询执行代价
关联子查询(correlated subquery)是否被物化、是否走索引、甚至是否被重写为半连接(semi-join),全靠优化器对“子查询结果集大小”和“外层行数”的预估。一旦统计信息过期或采样不足,比如 customers 表实际有 50 万行但优化器认为只有 5 千,它就可能错误地选择嵌套循环(Nested Loop)而非哈希连接(Hash Join),导致子查询被重复执行数十万次。
常见现象:EXPLAIN 显示 type=ALL 或 Extra 列出现 Using where; Using join buffer,说明优化器放弃了索引下推,转而用暴力匹配。
- MySQL 中可通过
ANALYZE TABLE customers强制刷新单表统计 - PostgreSQL 推荐用
ANALYZE customers (registration_date, status)对高频过滤列做多列统计 - 避免依赖自动更新——某些版本在大表上默认只采样 10%,根本不足以反映数据倾斜
子查询中 WHERE 条件字段缺失统计,索引可能被无视
比如写 WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.status = 'paid'),若 orders(user_id, status) 有联合索引,但优化器不知道 status = 'paid' 只占 0.2% 的行,它就会低估索引效率,宁可全表扫描 orders 也不走索引。
实操建议:
- 对子查询中频繁用于过滤的字段(如
status、created_at),单独建直方图统计:ANALYZE TABLE orders WITH HISTOGRAM ON status(MySQL 8.0+) - 检查
SHOW INDEX FROM orders确认联合索引顺序:等值条件字段(user_id)必须在前,范围条件(status)在后 - 不要假设
status列基数高——如果 99% 都是'pending',那这个字段根本不适合作为索引前导列
相关子查询无法物化,本质是优化器不敢信你的数据分布
MySQL 8.0 默认开启 subquery_materialization_cost_based,但它只在确信“子查询结果集小且稳定”时才物化。统计不准 → 优化器认为子查询可能返回百万行 → 宁可每次重算也不建临时表 → DEPENDENT SUBQUERY 持续出现。
这时硬加 /*+ MATERIALIZE */ 提示可能反而更慢,因为物化本身也有开销。真正该做的,是让统计信息回归真实:
- 对子查询涉及的表,用
INFORMATION_SCHEMA.STATISTICS查看cardinality是否与COUNT(DISTINCT)接近 - 发现偏差超过 3 倍?立刻
ANALYZE,别等自动触发 - 分区表注意:统计信息默认不跨分区聚合,需显式
ANALYZE PARTITION p2024, p2025
JOIN 替代方案失效,往往卡在统计误导的语义转换上
把 WHERE id IN (SELECT user_id FROM logs WHERE type = 'login') 改成 INNER JOIN logs ON ... 本应提速,但如果优化器误判 logs 中 type = 'login' 的行数极少,它可能选错驱动表——让小表 users 当驱动表,却对大表 logs 做全索引扫描,性能比原子查询还差。
关键点:
- 改写前先用
EXPLAIN FORMAT=JSON对比两者的rows_examined_per_scan和filtered字段 -
filtered值低于 10%?说明统计严重失真,优先修统计,别急着改写 - JOIN 后加
DISTINCT是危险信号——意味着你没意识到原IN语义是去重的,而 JOIN 天然放大行数
ANALYZE。











