注册
达梦数据库执行计划缓存机制解析与实践
专栏/技术分享/ 文章详情 /

达梦数据库执行计划缓存机制解析与实践

秧秧 2026/10/16 227 0 0
摘要

达梦数据库执行计划缓存机制解析与实践

一、背景

在数据库性能分析过程中,有一类现象比较容易让人产生疑惑:同一条 SQL,在数据库客户端中查看执行计划时看起来没有问题,但应用实际执行时却很慢。

刚开始接触这类问题时,比较容易把注意力放在 SQL 本身、索引是否存在、统计信息是否最新等方面。但在实际测试过程中,我发现还需要进一步区分两个问题:

  • 当前统计信息下,优化器"认为"应该使用什么执行计划?
  • 当前这条 SQL 实际执行时,数据库"到底"使用了什么执行计划?

这两个计划在某些情况下并不一定相同。本次实验没有直接从理论出发,而是通过人为改变数据分布,观察以下内容:

  • 数据分布发生变化后,统计信息是否发生变化;
  • EXPLAIN 生成的执行计划是否发生变化;
  • V$CACHEPLN 中缓存的执行计划是否发生变化;
  • 实际执行耗时是否发生变化;
  • 清理计划缓存后,实际执行是否恢复。

在此基础上,进一步补充了"如何直接观察实际执行计划(AUTOTRACE / ET)“以及"触发计划失效的几种方式(DDL、增删索引、NO_INVALIDATE、INJECT HINT)”,最终通过多组实测结果建立了一条比较完整的证据链。

二、为什么 EXPLAIN 和实际执行会有差异

2.1 达梦的计划缓存机制

计划缓存是达梦数据库提升重复 SQL 执行性能的重要机制:为了避免每次执行 SQL 都重复做词法分析、语法分析、生成执行计划这些耗时步骤,数据库会把解析后的执行计划缓存在 SQL 缓冲区(SQL Cache Pool)中。后续执行相同 SQL 时,通过 HASH 值比对,若匹配则直接重用缓存计划,不再重新解析。这个机制由 USE_PLN_POOL 参数控制,默认为 1,即启用计划重用。

2.2 关键认知:EXPLAIN 看到的 ≠ 实际执行的

这里有一个很多人忽略的事实:

  • EXPLAIN:让优化器根据当前的表结构、索引、统计信息,生成一份"预估"执行计划。它不会真正执行 SQL,也不会读取内存中已缓存的计划。
  • 内存中的计划:是 SQL 第一次执行(硬解析)时生成的,被缓存下来供后续复用。后续执行时,只要 SQL 文本的 HASH 值不变,就直接复用这个缓存计划,不会再经过优化器。

所以,当统计信息更新了、数据分布变了、索引增删了,但 SQL 文本没变,优化器"想"生成新计划,但计划缓存"说"——不用,我这儿有现成的。结果就是:EXPLAIN 显示的计划,与实际执行使用的计划可能不同。

三、环境准备

以下所有实验均在虚拟机上的达梦 DM8 中完整复现,环境信息如下:

-- 版本信息 SELECT * FROM V$VERSION; -- 结果:DM Database Server 64 V8,DB Version: 0x7000d -- 关键参数 SELECT PARA_NAME, PARA_VALUE FROM V$DM_INI WHERE PARA_NAME IN ('USE_PLN_POOL','CACHE_POOL_SIZE','MONITOR_SQL_EXEC','ENABLE_INJECT_HINT'); -- USE_PLN_POOL = 1 (计划重用已开启) -- CACHE_POOL_SIZE = 200 (SQL 缓冲区大小,MB) -- MONITOR_SQL_EXEC = 0 (SQL 执行监控,默认关闭) -- ENABLE_INJECT_HINT = 0 (hint 注入,默认关闭)

本文使用的测试表均为新建表(T_STAT_TEST、T_DDL_TEST、T_IDX_TEST、T_NOINV_TEST、T_HINT_TEST),数据量约 50 万行。文中 CACHE_ITEM 的值与执行耗时均为本机实测结果,因环境而异,仅供参考。

四、实验一:统计信息更新后,计划缓存保持旧计划

这是生产环境中最常见的一类情况:收集统计信息后,优化器本应基于新数据分布生成更优计划,但已缓存的旧计划仍然被复用。

4.1 为什么这样设计测试数据

本次实验刻意将数据分布设计成"少数值 + 大多数值"的形式,制造出非常明显的"旧计划"与"新计划"选择差异,避免因为数据量太小导致两种执行方式耗时接近而难以判断。

初始状态:STATUS=0 → 1000 行(0.2%),STATUS=1 → 499000 行(99.8%)。此时查询 STATUS=0 更适合走索引。随后反转数据,让 STATUS=0 → 499000 行,此时更适合全表扫描。

4.2 建表并构造 50 万行数据

DROP TABLE IF EXISTS T_STAT_TEST; CREATE TABLE T_STAT_TEST (ID INT, NAME VARCHAR(100), STATUS INT); INSERT INTO T_STAT_TEST SELECT LEVEL, 'NAME_'||LEVEL, CASE WHEN LEVEL <= 1000 THEN 0 ELSE 1 END FROM DUAL CONNECT BY LEVEL <= 500000; COMMIT; SELECT STATUS, COUNT(*) AS CNT FROM T_STAT_TEST GROUP BY STATUS ORDER BY STATUS; -- STATUS=0 -> 1000 行;STATUS=1 -> 499000 行

4.3 创建索引并收集统计信息

CREATE INDEX IDX_STATUS ON T_STAT_TEST(STATUS); CALL DBMS_STATS.GATHER_TABLE_STATS('SYSDBA','T_STAT_TEST',NULL,100,TRUE,'FOR ALL COLUMNS SIZE AUTO'); CALL DBMS_STATS.GATHER_INDEX_STATS('SYSDBA','IDX_STATUS');

4.4 执行查询,缓存"索引回表"计划

SELECT COUNT(NAME) FROM T_STAT_TEST WHERE STATUS = 0; -- 返回 1000 行,已用时间 3.809 毫秒 EXPLAIN SELECT COUNT(NAME) FROM T_STAT_TEST WHERE STATUS = 0;
1   #NSET2: [1, 1, 52]
2     #PRJT2: [1, 1, 52]; exp_num(1), is_atom(FALSE)
3       #AAGR2: [1, 1, 52]; grp_num(0), sfun_num(1), distinct_flag[0]; slave_empty(0)
4         #BLKUP2: [1, 1000, 52]; IDX_STATUS(T_STAT_TEST)
5           #SSEK2: [1, 1000, 52]; scan_type(ASC), IDX_STATUS(T_STAT_TEST), scan_range[0,0], is_global(0)

计划解读:优化器估算返回 1000 行,选择了"索引范围扫描 + 回表"(SSEK2 + BLKUP2),代价 [1, 1, 52]。

这里用 COUNT(NAME) 而不是 COUNT(*),是因为 NAME 列不在索引中,必须回表,这样才能放大索引扫描与全表扫描的代价差异。

4.5 确认内存中的计划

SELECT CACHE_ITEM, SQLSTR FROM V$CACHEPLN WHERE SQLSTR LIKE '%COUNT(NAME)%'; -- CACHE_ITEM = 140487481023296 -- 用 PLNDUMP 把内存中的计划导出到 trace 文件 ALTER SESSION SET EVENTS 'IMMEDIATE TRACE NAME PLNDUMP, LEVEL 140487481023296';

trace 文件中的关键内容:

SQL_STR:
SELECT COUNT(NAME) FROM T_STAT_TEST WHERE STATUS = 0;

sqlnode[0]::::
1   #NSET2: [1, 1, 52]
2     #PRJT2: [1, 1, 52]; exp_num(1), is_atom(FALSE)
3       #AAGR2: [1, 1, 52]; grp_num(0), sfun_num(1), distinct_flag[0]; slave_empty(0)
4         #BLKUP2: [1, 1000, 52]; IDX_STATUS(T_STAT_TEST)
5           #SSEK2: [1, 1000, 52]; scan_type(ASC), IDX_STATUS(T_STAT_TEST), is_global(0), scan_range[0,0]

内存中的计划与 EXPLAIN 生成的计划一致。

4.6 反转数据,重新收集统计信息

UPDATE T_STAT_TEST SET STATUS = CASE WHEN STATUS = 0 THEN 1 ELSE 0 END; COMMIT; SELECT STATUS, COUNT(*) AS CNT FROM T_STAT_TEST GROUP BY STATUS ORDER BY STATUS; -- STATUS=0 -> 499000 行;STATUS=1 -> 1000 行 CALL DBMS_STATS.GATHER_TABLE_STATS('SYSDBA','T_STAT_TEST',NULL,100,TRUE,'FOR ALL COLUMNS SIZE AUTO');

4.7 对比 EXPLAIN 与实际执行

EXPLAIN SELECT COUNT(NAME) FROM T_STAT_TEST WHERE STATUS = 0;
1   #NSET2: [62, 1, 52]
2     #PRJT2: [62, 1, 52]; exp_num(1), is_atom(FALSE)
3       #AAGR2: [62, 1, 52]; grp_num(0), sfun_num(1), distinct_flag[0]; slave_empty(0)
4         #SLCT2: [62, 499000, 52]; T_STAT_TEST.STATUS = 0
5           #CSCN2: [62, 500000, 52]; INDEX33555473(T_STAT_TEST); btr_scan(1)

EXPLAIN 显示:优化器估算返回 499000 行,改选全表扫描(CSCN2)。这说明优化器已经感知到了数据分布的变化。

-- 检查 CACHE_ITEM 是否变化 SELECT CACHE_ITEM, SQLSTR FROM V$CACHEPLN WHERE SQLSTR LIKE '%COUNT(NAME)%'; -- 该 SQL 的 CACHE_ITEM 仍然不变 -- 实际执行 SELECT COUNT(NAME) FROM T_STAT_TEST WHERE STATUS = 0; -- 返回 499000 行,已用时间 572.847 毫秒

结果:CACHE_ITEM 未变,SQL 文本未变,数据库直接复用了为"1000 行"设计的索引回表计划,实际却要回表 49.9 万次,执行耗时明显增加。

4.8 结论

  • GATHER_TABLE_STATS 默认不会使计划缓存失效:收集统计信息后,旧计划依然留在内存中,CACHE_ITEM 未变化。
  • SQL 文本不变 → HASH 值匹配 → 旧计划被持续复用,优化器根本没有机会基于新统计信息生成新计划。
  • 耗时相差约 25 倍(572ms vs 22ms),根源是旧计划基于变化前的数据分布(估算 1000 行),而实际数据分布已经变化(实际 499000 行)。

五、实验二:如何直接观察"实际执行"的计划

上一节是通过"加空格变快 + 清理缓存后恢复"来间接推断"实际执行用的是旧计划"。这种推断还不够严谨,本节补充两种直接观测实际执行计划与耗时点的手段。

5.1 SET AUTOTRACE TRACE(可看到实际扫描行数)

AUTOTRACE TRACE 会在 SQL 执行后打印"实际执行的计划"(可能是重用了计划缓存中的计划,也可能是新生成的计划),并展示执行过程中的监控信息。它与 EXPLAIN 的区别在于:EXPLAIN 只生成计划而不真正执行,因此计划可能不准;而 AUTOTRACE TRACE 展示的是服务器实际执行的计划。

说明:使用 AUTOTRACE TRACE 需要开启监控参数 ENABLE_MONITOR 和 MONITOR_SQL_EXEC。本机 ENABLE_MONITOR 已默认开启,只需再开启 MONITOR_SQL_EXEC 即可看到实际扫描行数。

ALTER SESSION SET 'MONITOR_SQL_EXEC' = 1; SET AUTOTRACE TRACE; SELECT COUNT(NAME) FROM T_STAT_TEST WHERE STATUS = 0; SET AUTOTRACE OFF;

输出(关键部分,注意计划中的 “估算->实际” 行数):

1   #NSET2: [1, 1->1, 52]
2     #PRJT2: [1, 1->1, 52]; exp_num(1), is_atom(FALSE)
3       #AAGR2: [1, 1->1, 52]; grp_num(0), sfun_num(1), distinct_flag[0]; slave_empty(0)
4         #BLKUP2: [1, 1000->499000, 52]; IDX_STATUS(T_STAT_TEST)
5           #SSEK2: [1, 1000->499000, 52]; scan_type(ASC), IDX_STATUS(T_STAT_TEST), is_global(0), scan_range[0,0]

Statistics
    998324  logical reads
    630     exec time(ms)

重点看 BLKUP2、SSEK2 两行的 “1000->499000”:前面的 1000 是优化器估算的行数,箭头后面的 499000 是实际扫描的行数。也就是说,这条 SQL 实际执行的仍是 SSEK2 + BLKUP2(索引回表)计划,但估算只扫描 1000 行,实际却扫描了 499000 行,逻辑读高达 998324,执行耗时 630ms。这就是"EXPLAIN 看到的 ≠ 实际执行的"最直接的证据。

5.2 MONITOR_SQL_EXEC + ET

开启 SQL 执行监控后,用 ET(事件跟踪)可以输出该 SQL 的执行号以及每个算子的实际耗时,直接定位性能热点。

ALTER SESSION SET 'MONITOR_SQL_EXEC' = 1; SELECT COUNT(NAME) FROM T_STAT_TEST WHERE STATUS = 0; ET(执行号); -- 执行号在上一句输出的"执行号:xxxx"中

ET 输出(关键算子耗时):

OP       TIME(US)   PERCENT   N_ENTER
PRJT2    5          0%        4
DLCK     8          0%        2
NSET2    223        0.04%     3
AAGR2    2060       0.33%     1667
SSEK2    12684      2.04%     1665
BLKUP2   606307     97.59%    3330

ET 输出直接证明:BLKUP2(回表)算子耗时 606307 微秒,占总耗时 97.59%。这正是"用索引回表计划扫描 49.9 万行"的铁证。

5.3 三种"计划"的定位区分

手段 看到的是什么 用途
EXPLAIN 优化器预估计划(不执行、不读缓存) 判断"应该"用什么计划
PLNDUMP / V$CACHEPLN 缓存中保存的计划 判断"准备复用"什么计划
AUTOTRACE / ET 实际执行使用的计划与真实耗时 判断"实际用了"什么计划

六、实验三:触发计划失效/淘汰的几种方式

上一节证明了"统计信息更新默认不会使计划失效"。那么,到底哪些操作会让计划缓存失效?本节逐一验证几种常见的触发方式。

6.1 表定义发生变化(DDL)

原理:DDL 会改变表结构,依赖该表的缓存计划失去有效性,数据库会将其失效;下一次执行时重新硬解析。

-- 建表并缓存计划 CREATE TABLE T_DDL_TEST (ID INT, NAME VARCHAR(100), STATUS INT); INSERT INTO T_DDL_TEST SELECT LEVEL,'NAME_'||LEVEL,CASE WHEN LEVEL<=1000 THEN 0 ELSE 1 END FROM DUAL CONNECT BY LEVEL <= 500000; COMMIT; CREATE INDEX IDX_DDL_STATUS ON T_DDL_TEST(STATUS); CALL DBMS_STATS.GATHER_TABLE_STATS('SYSDBA','T_DDL_TEST',NULL,100,TRUE,'FOR ALL COLUMNS SIZE AUTO'); SELECT COUNT(NAME) FROM T_DDL_TEST WHERE STATUS = 0; SELECT CACHE_ITEM, SQLSTR FROM V$CACHEPLN WHERE SQLSTR LIKE 'SELECT COUNT(NAME) FROM T_DDL_TEST%'; -- CACHE_ITEM = 140487490055904 -- 修改表定义 ALTER TABLE T_DDL_TEST ADD COLUMN REMARK VARCHAR(50); -- 再次检查:原计划已从 V$CACHEPLN 消失(未选定行) -- 重新执行,触发硬解析,产生新的 CACHE_ITEM SELECT COUNT(NAME) FROM T_DDL_TEST WHERE STATUS = 0; SELECT CACHE_ITEM, SQLSTR FROM V$CACHEPLN WHERE SQLSTR LIKE 'SELECT COUNT(NAME) FROM T_DDL_TEST%'; -- 新的 CACHE_ITEM = 140487489925216(与之前不同)

结论:ALTER TABLE 会使依赖该表的计划失效,原 CACHE_ITEM 从 V$CACHEPLN 中消失。

6.2 增删索引

原理:索引增删同样改变表的物理访问路径,也会使相关计划失效。本节实测验证。

-- 建表并缓存计划 CREATE TABLE T_IDX_TEST (ID INT, NAME VARCHAR(100), STATUS INT); INSERT INTO T_IDX_TEST SELECT LEVEL,'NAME_'||LEVEL,CASE WHEN LEVEL<=1000 THEN 0 ELSE 1 END FROM DUAL CONNECT BY LEVEL <= 500000; COMMIT; CREATE INDEX IDX_TI_STATUS ON T_IDX_TEST(STATUS); CALL DBMS_STATS.GATHER_TABLE_STATS('SYSDBA','T_IDX_TEST',NULL,100,TRUE,'FOR ALL COLUMNS SIZE AUTO'); SELECT COUNT(NAME) FROM T_IDX_TEST WHERE STATUS = 0; SELECT CACHE_ITEM FROM V$CACHEPLN WHERE SQLSTR LIKE 'SELECT COUNT(NAME) FROM T_IDX_TEST%'; -- CACHE_ITEM = 140487490153632 -- 新增索引 CREATE INDEX IDX_TI_STATUS_NAME ON T_IDX_TEST(STATUS, NAME); -- 检查:原计划已消失 -- 重新执行,产生新计划 SELECT COUNT(NAME) FROM T_IDX_TEST WHERE STATUS = 0; -- 新 CACHE_ITEM = 140487490545888 -- 删除索引 DROP INDEX IDX_TI_STATUS; -- 检查:计划再次消失

结论:CREATE INDEX 和 DROP INDEX 都会使相关计划失效。

6.3 收集统计信息指定 NO_INVALIDATE

上一节观察到"GATHER_TABLE_STATS 默认不失效",但该过程其实提供了 NO_INVALIDATE 参数(第 13 个参数,BOOLEAN 类型)来主动控制是否使计划失效。

说明:本机版本中 GATHER_TABLE_STATS 的参数顺序为:OWNNAME, TABNAME, PARTNAME, ESTIMATE_PERCENT, BLOCK_SAMPLE, METHOD_OPT, DEGREE, GRANULARITY, CASCADE, STATTAB, STATID, STATOWN, NO_INVALIDATE, FORCE, MERGE_STAT。

-- 建表并缓存计划 CREATE TABLE T_NOINV_TEST (ID INT, NAME VARCHAR(100), STATUS INT); INSERT INTO T_NOINV_TEST SELECT LEVEL,'NAME_'||LEVEL,CASE WHEN LEVEL<=1000 THEN 0 ELSE 1 END FROM DUAL CONNECT BY LEVEL <= 500000; COMMIT; CREATE INDEX IDX_NOINV_STATUS ON T_NOINV_TEST(STATUS); CALL DBMS_STATS.GATHER_TABLE_STATS('SYSDBA','T_NOINV_TEST',NULL,100,TRUE,'FOR ALL COLUMNS SIZE AUTO'); SELECT COUNT(NAME) FROM T_NOINV_TEST WHERE STATUS = 0; -- CACHE_ITEM = 140487490545888 -- 情况 A:NO_INVALIDATE => TRUE(不失效,保留旧计划) CALL DBMS_STATS.GATHER_TABLE_STATS('SYSDBA','T_NOINV_TEST',NULL,100,TRUE, 'FOR ALL COLUMNS SIZE AUTO',NULL,NULL,FALSE,NULL,NULL,NULL,TRUE); -- 检查:CACHE_ITEM 仍然存在(计划未失效) -- 情况 B:NO_INVALIDATE => FALSE(立即使旧计划失效) CALL DBMS_STATS.GATHER_TABLE_STATS('SYSDBA','T_NOINV_TEST',NULL,100,TRUE, 'FOR ALL COLUMNS SIZE AUTO',NULL,NULL,FALSE,NULL,NULL,NULL,FALSE); -- 检查:CACHE_ITEM 消失(计划已失效)

结论:NO_INVALIDATE=TRUE 时收集统计信息不使计划失效(与默认行为一致);NO_INVALIDATE=FALSE 时主动使计划失效。

6.4 INJECT HINT(hint 注入)

原理:达梦支持通过系统函数 SF_INJECT_HINT 对指定 SQL 注入 hint,从而在不修改 SQL 文本的情况下干预优化器的执行计划选择,规则存储在系统表 SYSINJECTHINT 中。

说明:本机版本中 hint 注入需先通过参数 ENABLE_INJECT_HINT 开启(默认 0);注入函数为内置函数 SF_INJECT_HINT;删除用 SF_DEINJECT_HINT。

-- 1) 开启 hint 注入 SP_SET_PARA_VALUE(1, 'ENABLE_INJECT_HINT', 1); -- 2) 注入 hint:强制该 SQL 不走 IDX_HINT_STATUS 索引(即改走全表扫描) -- SF_INJECT_HINT 的参数依次为:sql_text, hint_text, name, description, -- validate(是否生效), fuzzy(是否模糊匹配), need_clear(是否清空缓存计划) CALL SF_INJECT_HINT('SELECT COUNT(NAME) FROM T_HINT_TEST WHERE STATUS = 0', 'NO_INDEX(T_HINT_TEST, IDX_HINT_STATUS)', 'force_noindex', '强制全表扫描', TRUE, TRUE, TRUE); -- 3) 查看已注入的规则 SELECT NAME, VALIDATE, SQL_TEXT, HINT_TEXT FROM SYSINJECTHINT; -- force_noindex | TRUE | SELECT COUNT(NAME) FROM T_HINT_TEST WHERE STATUS = 0 | NO_INDEX(T_HINT_TEST, IDX_HINT_STATUS) -- 4) 注入前 EXPLAIN 为 SSEK2+BLKUP2(索引回表);注入后 EXPLAIN 变为 CSCN2(全表扫描) EXPLAIN SELECT COUNT(NAME) FROM T_HINT_TEST WHERE STATUS = 0;
1   #NSET2: [62, 1, 52]
2     #PRJT2: [62, 1, 52]; exp_num(1), is_atom(FALSE)
3       #AAGR2: [62, 1, 52]; grp_num(0), sfun_num(1), distinct_flag[0]; slave_empty(0)
4         #SLCT2: [62, 1000, 52]; T_HINT_TEST.STATUS = 0
5           #CSCN2: [62, 500000, 52]; INDEX33555482(T_HINT_TEST); btr_scan(1)
-- 5) 删除注入,并关闭 hint 注入 CALL SF_DEINJECT_HINT('force_noindex'); SP_SET_PARA_VALUE(1, 'ENABLE_INJECT_HINT', 0);

结论:hint 注入可以让目标 SQL 在不修改文本的前提下改变执行计划,规则全局生效;这是除 DDL、索引变更、统计信息收集之外的又一种计划变更方式。

关于 need_clear(是否清空缓存计划)参数:

  • need_clear=TRUE:注入时同步清空全库所有缓存的计划,注入的 hint 立即生效;但会导致全库大量硬解析,生产环境需谨慎使用;
  • need_clear=FALSE:注入时不清空缓存计划,模糊匹配时需手动清除对应 SQL 的计划后 hint 才会生效;
  • need_clear 缺省(不指定):默认不清空缓存计划。

建议:无论清计划还是注入 hint,都建议将 need_clear 设为 FALSE(或直接缺省),再通过 SP_CLEAR_PLAN_CACHE 只清理目标 SQL 的计划,避免清空全库计划引发大量硬解析。

6.5 触发方式汇总

触发方式 是否使计划失效 本机验证结果
表定义变化(ALTER TABLE) 失效 CACHE_ITEM 从 V$CACHEPLN 消失
新增索引(CREATE INDEX) 失效 CACHE_ITEM 消失
删除索引(DROP INDEX) 失效 CACHE_ITEM 消失
统计信息 + NO_INVALIDATE=FALSE 失效 CACHE_ITEM 消失
统计信息 + NO_INVALIDATE=TRUE(或默认) 不失效 CACHE_ITEM 保留
INJECT HINT(need_clear=TRUE) 清空全库计划 注入后计划立即改变
SP_CLEAR_PLAN_CACHE 手动清理 CACHE_ITEM 消失

七、总结与收获

7.1 数据分布变化确实会影响优化器选择的执行计划

初始 STATUS=0 只有 1000 行时,优化器选择 SSEK2 + BLKUP2(索引回表);数据反转后 STATUS=0 变成 499000 行,重新基于统计信息生成的计划变成 CSCN2(全表扫描)。说明优化器能感知数据分布变化。

7.2 EXPLAIN 与实际执行需要分开理解

本次实验中,EXPLAIN 已经显示 CSCN2,但原 SQL 实际执行耗时仍达 572ms;通过 AUTOTRACE TRACE(显示 1000->499000 的实际扫描行数)和 ET(BLKUP2 占 97.59%)直接证明实际执行仍在用 SSEK2 + BLKUP2。因此排查实际 SQL 性能问题时,不能只根据 EXPLAIN 判断实际执行计划。

7.3 统计信息更新并不等于缓存计划已同步变化

默认情况下(不指定 NO_INVALIDATE,或 NO_INVALIDATE=TRUE),GATHER_TABLE_STATS 不会使旧计划失效;只有显式指定 NO_INVALIDATE=FALSE,或通过 DDL、索引变更、SP_CLEAR_PLAN_CACHE、重启等方式,才会让旧计划失效或重新生成。

7.4 排查建议

  • 发现"EXPLAIN 正常但实际执行慢"时,优先用 AUTOTRACE TRACE(开启监控参数后)或 MONITOR_SQL_EXEC + ET 确认"实际执行计划"和耗时点,而不是只看 EXPLAIN。
  • 若确认是计划缓存问题,可按需用 SP_CLEAR_PLAN_CACHE 清理单个计划;高并发场景下不要随意批量清理计划缓存,会导致短时间内大量硬解析、CPU 飙升。
  • 收集统计信息时,如需立即生效,可显式指定 NO_INVALIDATE=FALSE。
  • 通过 SF_INJECT_HINT 注入 hint 时,注意 need_clear 参数:TRUE 会清空全库所有计划(需谨慎),建议用 FALSE 或缺省,再通过 SP_CLEAR_PLAN_CACHE 只清理目标 SQL 的计划。
评论
后发表回复

作者

文章

阅读量

获赞

扫一扫
联系客服