执行计划一夜之间变了?别查代码了,是统计信息在"说谎"

简介: 昨天还跑得飞快的SQL,今天突然慢到怀疑人生。代码没改、索引没动、数据量也没暴涨——罪魁祸首是统计信息过期导致的执行计划突变。本文从优化器原理出发,深度解析为什么执行计划会"背叛"你,以及如何建立统计信息监控机制,让慢SQL扼杀在摇篮里。

大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!

有个经典的"凌晨惊魂"场景:某条核心SQL跑了半年都没问题,每天几十万次执行,响应时间稳定在5毫秒以内。某天凌晨三点,监控告警疯狂弹窗——这条SQL突然飙到5秒,CPU打满,整个系统雪崩。

DBA赶到现场,第一反应是看代码有没有人改。没有。看索引有没有人删。没有。看数据量有没有暴涨。也没有。

最后查下来,原因让人无语:统计信息过期了。数据库优化器拿着一份"过时的情报",给这条SQL选了一条错误的执行计划。

今天把执行计划突变的底层原理、排查手段和预防机制,一次讲清楚。


一、先搞懂几个概念

执行计划(Execution Plan):数据库执行一条SQL的具体步骤。先走哪个索引、先JOIN哪张表、用什么JOIN方式(Nested Loop、Hash Join、Merge Join),这些决策组合起来就是执行计划。

优化器(Optimizer):数据库里的"决策引擎",负责为每条SQL选择最优的执行计划。它不跑SQL,只"猜"哪种执行方式最快。

统计信息(Statistics):优化器做决策的依据。包括表的总行数、每列的数据分布(最大值、最小值、NULL占比、直方图)、索引的选择性(不同值的数量)等。本质上就是优化器眼中的"数据库快照"。

基数估计(Cardinality Estimation):优化器预估每一步会返回多少行数据。预估准了,执行计划就优;预估偏了,就可能选错索引、选错JOIN顺序。

CBO(Cost-Based Optimizer):基于成本的优化器。优化器根据统计信息计算每种执行计划的"成本"(CPU消耗、IO次数、内存占用),选成本最低的那个。

理解了这些概念,就能回答一个核心问题:为什么执行计划会突然变?


二、执行计划为什么会"背叛"你

统计信息过期:优化器拿到的是"过期情报"

这是最常见的原因。统计信息不是实时更新的,大多数数据库是定期收集或手动触发。

假设你有一张订单表,平时100万行,统计信息也是按这个量级收集的。某天大促,数据量涨到500万,但统计信息还没更新。优化器依然认为表里只有100万行——于是选择了全表扫描(因为它觉得100万行全表扫比走索引快)。实际上500万行全表扫描直接卡死。

统计信息 ≠ 实时数据,它是一份"延迟的快照"。

数据倾斜:平均值骗了优化器

即使统计信息是新的,也可能因为数据分布不均匀而误导优化器。

比如一个订单表的status列,99%的数据是COMPLETED,1%是PENDING。如果统计信息只记录了平均分布(没有收集直方图),优化器会认为每个状态的占比差不多。当你查询status = 'COMPLETED'时,优化器预估返回1000行(总行数10万的1/100),实际返回99000行——走索引反而比全表扫慢几十倍。

索引变化:新增索引不一定是好事

开发同学看到慢SQL,第一反应是加索引。加完索引后统计信息更新,优化器重新评估所有可用索引,可能选出一个更差的执行计划。

加索引 ≠ SQL变快,它只是给优化器多了一个选择,而这个选择可能是错的。

参数变更:看似无关的配置调整

  • optimizer_mode 从 ALL_ROWS 改为 FIRST_ROWS
  • optimizer_features_enable 版本升级后行为变化
  • statistics_level 从 TYPICAL 改为 BASIC(停止收集部分统计信息)

这些参数调整不会立刻生效,但下一次硬解析时,优化器的决策逻辑可能完全改变。


三、执行计划突变的排查步骤

第一步:确认是不是执行计划变了

-- Oracle
SELECT * FROM v$sql_plan WHERE sql_id = 'your_sql_id';

-- MySQL (8.0+)
EXPLAIN FORMAT=TREE SELECT ...;

-- PostgreSQL
EXPLAIN (ANALYZE, BUFFERS) SELECT ...;

-- 对比历史执行计划
-- Oracle: DBMS_XPLAN.DISPLAY_AWR('your_sql_id')

重点对比:访问路径(全表扫 vs 索引扫描)、JOIN顺序、JOIN方式、预估行数 vs 实际行数。

第二步:检查统计信息是否过期

-- Oracle:查看表的统计信息收集时间
SELECT table_name, last_analyzed, num_rows, blocks
FROM user_tables
WHERE table_name = 'YOUR_TABLE';

-- MySQL:查看InnoDB表统计信息
SHOW TABLE STATUS LIKE 'your_table';

-- PostgreSQL:查看统计信息
SELECT last_analyze, last_autoanalyze, n_live_tup, n_dead_tup
FROM pg_stat_user_tables
WHERE relname = 'your_table';

如果last_analyzed是几天甚至几周前,而这段时间数据变化超过10%,基本可以判定统计信息过期。

第三步:对比预估行数和实际行数

这是判断优化器是否"误判"的关键指标。

-- 执行SQL时开启实际执行统计
-- Oracle: EXPLAIN PLAN FOR ... 然后 SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
-- MySQL: EXPLAIN ANALYZE SELECT ...;
-- PostgreSQL: EXPLAIN ANALYZE SELECT ...;

如果某一步的预估行数(estimated rows)和实际行数(actual rows)相差10倍以上,说明基数估计严重失真,执行计划很可能选错了。

第四步:检查是否有绑定变量窥探问题

绑定变量第一次执行时,优化器会"窥探"变量值来生成执行计划。后续执行直接复用这个计划,即使变量值的数据分布差异很大。

比如第一次传的是status = 'PENDING'(只有100行),优化器选了索引扫描。后面传的是status = 'COMPLETED'(99000行),还是走索引——但全表扫描反而更快。


四、预防执行计划突变的4种手段

手段一:合理设置统计信息收集策略

不要完全依赖自动收集,根据业务特点定制:

策略 适用场景 收集频率
自动收集 + 默认阈值 数据变化平稳的普通表 系统自动触发
手动定时收集 数据批量导入/删除的表 每天凌晨或批量操作后
锁定统计信息 历史归档表(数据不变化) 收集一次后锁定
收集直方图 数据分布严重倾斜的列 按需收集
-- Oracle: 手动收集统计信息(含直方图)
EXEC DBMS_STATS.GATHER_TABLE_STATS('SCHEMA', 'TABLE_NAME',
  method_opt => 'FOR COLUMNS SIZE AUTO skewed_column');

-- MySQL: 手动分析表
ANALYZE TABLE your_table;

-- PostgreSQL: 手动分析
ANALYZE your_table;

手段二:使用执行计划基线(Plan Baseline)

Oracle提供了SQL Plan Management(SPM),可以把"好的执行计划"锁定下来,即使统计信息变化也不让优化器切换到更差的计划。

-- Oracle: 创建执行计划基线
DECLARE
  l_plans_loaded  PLS_INTEGER;
BEGIN
  l_plans_loaded := DBMS_SPM.LOAD_PLANS_FROM_CURSOR_CACHE(
    sql_id => 'your_sql_id');
END;

金仓数据库也提供了类似的执行计划管理能力。通过DBMS_SPM兼容包,可以将经过验证的优秀执行计划固定下来,避免因统计信息变化导致的性能波动。同时支持执行计划演化(evolve),在确认新计划更优后才自动切换。

手段三:SQL Profile / Outline

SQL Profile是优化器的"纠正器"。当发现某条SQL的执行计划不理想时,可以创建一个SQL Profile,告诉优化器"这条SQL按这个方式执行"。

-- Oracle: 使用SQL TUNING ADVISOR
DECLARE
  l_tuning_task VARCHAR2(30);
BEGIN
  l_tuning_task := DBMS_SQLTUNE.CREATE_TUNING_TASK(sql_id => 'your_sql_id');
  DBMS_SQLTUNE.EXECUTE_TUNING_TASK(l_tuning_task);
  DBMS_SQLTUNE.ACCEPT_SQL_PROFILE(task_name => l_tuning_task);
END;

手段四:监控统计信息变化

建立监控机制,在统计信息过期前主动预警:

-- 找出统计信息超过7天未更新的表
SELECT table_name, last_analyzed,
  ROUND((SYSDATE - last_analyzed), 1) as days_since_analyze
FROM user_tables
WHERE last_analyzed < SYSDATE - 7
ORDER BY last_analyzed;

建议在监控系统中加入以下告警项:

  • 核心表统计信息超过X天未更新
  • 单表数据变化量超过上次统计的10%
  • 执行计划发生变更(对比AWR报告)

五、总结

执行计划突变的本质,是优化器拿着一份过时的地图,给你指了一条错误的路。

代码没改、索引没动,SQL突然变慢——不要急着翻代码,先查统计信息。

排查执行计划问题,按这个顺序来:

  1. 对比执行计划:确认是不是执行计划变了,不是SQL本身的问题
  2. 检查统计信息:last_analyzed多久了?数据变化量超过10%了吗?
  3. 预估 vs 实际:基数估计偏差超过10倍,优化器就"失明"了
  4. 绑定变量:第一次执行的变量值,可能不适合后续的变量值

预防胜于治疗:统计信息收集策略 + 执行计划基线 + 变化监控,三管齐下,让慢SQL扼杀在摇篮里。


小耶在手,SQL不愁。

还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽……我们下次见~

相关文章
|
2月前
|
SQL 人工智能 关系型数据库
实测四大AI模型写SQL,表现差距不小
基于2026年8月已公开的主流模型版本(GPT-5.5、Claude Opus 4.7、Qwen3、Kimi k2.6),实测四个真实业务SQL场景。深入分析基准测试与真实场景的鸿沟、SQL幻觉根因,从准确性、可读性、性能三维度给出量化测评。
|
2月前
|
存储 架构师 数据库
从DBA到数据架构师:技术债务管理是分水岭
“架构债”在业务快速迭代中被不断放大,最终成为系统稳定性的定时炸弹。本文从数据架构的视角出发,拆解数据库技术债务的四种典型类型,提供识别、评估和偿还的完整方法论,帮助读者从“数据库管理员”升级为“数据架构师”。
|
2月前
|
SQL 缓存 NoSQL
Redis缓存三大坑:穿透、击穿、雪崩,一次讲透
缓存穿透、击穿、雪崩,名字像兄弟但成因解法完全不同。本文深入讲解三种问题的原理、实现细节与隐藏的坑,覆盖布隆过滤器、互斥锁、逻辑过期、过期随机化等解法。
|
2月前
|
SQL JSON 移动开发
SQL派生表优化实战:从物化机制到LATERAL JOIN的完整进阶
很多人只知道“子查询改JOIN就快了”,但不知道为什么,也不知道什么时候该改、什么时候不该改。本文从派生表的物化机制出发,拆解临时表膨胀、索引失效的根因,通过真实案例对比派生表、CTE、LATERAL JOIN三种写法的性能差异,帮助读者从“知道现象”升级到“理解原理”。
|
2月前
|
存储 固态存储 关系型数据库
从月账单5万到3.5万:云数据库成本优化的完整复盘
上云本应是降本增效,但很多企业上云之后,账单反而越滚越大。实例规格买高了、历史数据堆在SSD上、测试环境没人关、过期快照没清理——每一笔费用都在悄悄累积。本文从云账单的三大“黑洞”出发,拆解云成本失控的根因,给出实例降配、冷热数据分层、僵尸资源清理三条可落地的优化路径,帮助DBA和运维工程师用数据驱动成本优化,让每一分钱都花在刀刃上。
|
2月前
|
SQL 运维 监控
慢查询日志的“高级用法”:从找慢SQL到做容量规划
慢查询日志是DBA最熟悉的工具,但大多数人只用它来找“跑得慢的SQL”。如果只做到这一步,你只用了慢查询日志20%的价值——剩下的80%是建立性能基线、预测容量瓶颈、评估优化效果、发现潜在风险。本文从慢查询日志的进阶用法出发,讲解如何通过持续记录慢查询建立性能基线、如何通过慢查询趋势预测容量瓶颈、如何将慢查询日志从“故障排查工具”升级为“容量规划工具”,帮助读者从“出了问题再查”升级到“看着趋势主动调整”。
|
2月前
|
SQL JSON 数据库
SQL性能调优进阶:从“会看执行计划”到“会诊断整个系统”
一条SQL慢,可能有一百种原因——SQL写法有问题、索引没建对、统计信息过旧、参数没调好、磁盘I/O满了、内存不够、网络抖动……很多DBA的做法是“先查SQL”,但真正的问题往往不在SQL本身。本文从“分层诊断”的思路出发,建立一套从SQL层→数据库层→操作系统层的逐层排查方法论,帮助读者在面对性能问题时不再“眉毛胡子一把抓”。
|
2月前
|
SQL 关系型数据库 MySQL
UNION vs UNION ALL:一个“ALL”字,性能差了一个数量级
UNION和UNION ALL的区别很多人知道,但INTERSECT和EXCEPT的执行机制、性能差异,以及如何用JOIN和子查询替代,很多人并不清楚。本文从集合操作的执行计划出发,拆解UNION去重的“隐形代价”、INTERSECT与INNER JOIN的本质差异、EXCEPT与NOT EXISTS的性能对比,并通过真实案例展示集合操作在业务场景中的正确用法与避坑指南,帮助读者从“会写集合操作”升级到“理解集合操作的底层逻辑”。
|
2月前
|
SQL JSON 算法
SQL执行计划的“成本模型”:读懂cost,理解优化器为什么选这个计划
EXPLAIN能告诉你优化器选了哪个执行计划,但说不出它为什么这么选——明明有索引它却走全表扫描,明明A计划更快它却选了B计划。优化器不靠猜,它靠一套成本模型(Cost Model)做决策。本文从优化器的成本模型出发,拆解cost的构成(IO_cost、CPU_cost、memory_cost),讲解如何通过EXPLAIN FORMAT=JSON和OPTIMIZER_TRACE看到优化器的“思考过程”,并通过真实案例展示优化器“算错账”的根因,帮助读者从“知道选了谁”升级到“理解为什么选它”。
|
2月前
|
SQL 关系型数据库 MySQL
全量迁移时源库还在写,数据一致性怎么保证?
数据库迁移最怕的不是慢,是“搬完了发现数据不对”。全量迁移时源库还在写、增量同步时顺序乱了、异构数据库类型映射丢了精度——这些坑,在POC阶段很难暴露,一上生产就变成事故。本文从三种一致性风险场景出发,拆解全量校验、增量校验、抽样校验的完整方法论,帮助读者在迁移项目中做到“数据搬得对、心里有底”。