注册
如何更新统计信息后,同时清掉SQL缓存
专栏/Database Thinking/ 文章详情 /

如何更新统计信息后,同时清掉SQL缓存

胡li 2026/09/17 253 2 0
摘要 SQL优化时,发现预估行数与实际行数对不上,更新统计信息后,仍然走的不对的执行计划,这个时候我们就需要在DBMS_STATS.GATHER_TABLE_STATS方法中加一个参数NO_INVALIDATE => FALSE

一、复现思路及解决方案

① 阶段一的计划确实是好计划、
② 阶段二它确实变成了坏计划、
③ 结果集大小始终不变(排除"数据变多所以本来就慢"的干扰)。

阶段 数据 优化器会选 实际耗时
阶段一 100 万行,C1=100 只有 10 行 索引扫描 + 回表 10 次 ✅ 毫秒级
阶段二 再灌 400 万行,C1 全是 100(占比 80%) 应该是全表扫描 ✅ 用缓存旧计划 → 秒级/几十秒 ❌

解决方案

执行以下SQL更新统计信息并清除缓存旧计划

  DBMS_STATS.GATHER_TABLE_STATS(
    OWNNAME          => 'SYSDBA',
    TABNAME          => 'T_PLAN_DEMO',
    ESTIMATE_PERCENT => 100,
    BLOCK_SAMPLE     => TRUE,
    METHOD_OPT       => 'FOR ALL COLUMNS SIZE AUTO',
    DEGREE           => 2,
    GRANULARITY      => 'ALL',
    CASCADE          => TRUE,
    NO_INVALIDATE    => FALSE
  );

二、演示步骤

创建示例表和生成数据

DROP TABLE IF EXISTS SYSDBA.T_PLAN_DEMO;

CREATE TABLE SYSDBA.T_PLAN_DEMO
(
    ID   INT,
    C1   INT,            -- 演示用的关键列:数据分布会由稀疏变极度倾斜
    C2   INT,
    PAD  VARCHAR(200)    -- 填充列:让表足够大,使回表代价明显
);

CREATE INDEX IDX_PLAN_DEMO_C1 ON SYSDBA.T_PLAN_DEMO (C1);


/* ================== 步骤 2:造「阶段一」数据 —— 100 万行,C1 高度稀疏 ==================
 * C1 = 100 的行只有 10 行,其余各行 C1 值互不相同 → 选择性极佳
 * ==================================================================================== */
DECLARE
    v_rows CONSTANT INT := 200000;
    v_base INT;
BEGIN
    FOR i IN 0 .. 4 LOOP                       -- 5 批 × 20 万 = 100 万行
        v_base := i * v_rows;

        INSERT INTO SYSDBA.T_PLAN_DEMO (ID, C1, C2, PAD)
        SELECT G,
               CASE WHEN G <= 10 THEN 100 ELSE 1000000 + G END,
               MOD(G * 7, 1000),
               RPAD('A', 150, 'B')
          FROM (SELECT v_base + LEVEL AS G FROM DUAL CONNECT BY LEVEL <= v_rows);

        COMMIT;
    END LOOP;
    PRINT '阶段一数据完成:100 万行,其中 C1 = 100 的仅 10 行';
END;
/

确认分布(应看到 C1=100 的行数为 10)

SELECT C1, COUNT(*) AS CNT FROM SYSDBA.T_PLAN_DEMO WHERE C1 = 100 GROUP BY C1;

结果集截图如下
image.png

查看表的统计信息收集信息

SELECT TABLE_NAME, NUM_ROWS, LAST_ANALYZED FROM DBA_TABLES
 WHERE OWNER = 'SYSDBA' AND TABLE_NAME = 'T_PLAN_DEMO';

截图如下:
image.png

使用disql登录

[dmdba@localhost.localdomain:/dm62/bin]$ ./disql SYSDBA/SYSDBA:5237

服务器[LOCALHOST:5237]:处于主库打开状态
登录使用时间 : 2.500(ms)
密钥过期时间:2026-09-23
disql V8
15:16:46 SYSDBA@LOCALHOST:5237 SQL>

执行以下信息,查看SQL执行耗时和计划

call sf_set_session_para_value('MONITOR_SQL_EXEC',1);
set autotrace trace
SELECT COUNT(PAD) FROM SYSDBA.T_PLAN_DEMO WHERE C1 = 100;

image.png
从上图可以看出SELECT COUNT(PAD) SQL执行耗时1ms, 从25000行中筛选出10行

再次创建数据

阶段二数据生成:新增 400 万行(C1 全为 100),总计约 500 万行

DECLARE
    v_rows CONSTANT INT := 200000;
    v_base INT;
BEGIN
    FOR i IN 0 .. 19 LOOP                      -- 20 批 × 20 万 = 400 万行
        v_base := 1000000 + i * v_rows;

        INSERT INTO SYSDBA.T_PLAN_DEMO (ID, C1, C2, PAD)
        SELECT G, 100, MOD(G * 7, 1000), RPAD('A', 150, 'B')
          FROM (SELECT v_base + LEVEL AS G FROM DUAL CONNECT BY LEVEL <= v_rows);

        COMMIT;
    END LOOP;
    PRINT '阶段二数据完成:新增 400 万行(C1 全为 100),总计约 500 万行';
END;
/

再次查询SQL

SELECT COUNT(PAD) FROM SYSDBA.T_PLAN_DEMO WHERE C1 = 100;

image.png
从上图中可以看出,4000010的结果集走了回表,耗时8s,计划应该走全表扫描

更新表统计信息

不带参数 NO_INVALIDATE ,默认为true,保留SQL执行计划缓存

  DBMS_STATS.GATHER_TABLE_STATS(
    OWNNAME          => 'SYSDBA',
    TABNAME          => 'T_PLAN_DEMO',
    ESTIMATE_PERCENT => 100,
    BLOCK_SAMPLE     => TRUE,
    METHOD_OPT       => 'FOR ALL COLUMNS SIZE AUTO',
    DEGREE           => 2,
    GRANULARITY      => 'ALL',
    CASCADE          => TRUE
  );

更新条件列统计信息

stat 100 on T_PLAN_DEMO(C1)

再次查询表统计信息

SELECT TABLE_NAME, NUM_ROWS, LAST_ANALYZED FROM DBA_TABLES
 WHERE OWNER = 'SYSDBA' AND TABLE_NAME = 'T_PLAN_DEMO';

image.png

查询SQL,耗时和计划没有变化

SELECT COUNT(PAD) FROM SYSDBA.T_PLAN_DEMO WHERE C1 = 100;

image.png

两种解决方案

1、手动清掉缓存

SELECT CACHE_ITEM, SQLSTR FROM V$CACHEPLN WHERE SQLSTR LIKE '%T_PLAN_DEMO%';

image.png

手动将相关SQL的清掉缓存

SP_CLEAR_PLAN_CACHE(281470045964424);
SP_CLEAR_PLAN_CACHE(281470045923464);
SP_CLEAR_PLAN_CACHE(281470045513864);

image.png

或者将缓存全部清除(不建议)

SP_CLEAR_PLAN_CACHE();

2、收集统计信息加上 NO_INVALIDATE => FALSE

  DBMS_STATS.GATHER_TABLE_STATS(
    OWNNAME          => 'SYSDBA',
    TABNAME          => 'T_PLAN_DEMO',
    ESTIMATE_PERCENT => 100,
    BLOCK_SAMPLE     => TRUE,
    METHOD_OPT       => 'FOR ALL COLUMNS SIZE AUTO',
    DEGREE           => 2,
    GRANULARITY      => 'ALL',
    CASCADE          => TRUE,
    NO_INVALIDATE    => FALSE
  );

再次执行SQL

SELECT COUNT(PAD) FROM SYSDBA.T_PLAN_DEMO WHERE C1 = 100;

image.png
执行耗时719ms,走全表扫描

评论
后发表回复

作者

文章

阅读量

获赞

扫一扫
联系客服