当前位置: 首页 > YashanDB, 优化 > 正文

我们的文章会在微信公众号 IT民工的龙马人生 和博客网站 ( www.htz.pw )同步更新 ,欢迎关注收藏,也欢迎大家转载,但是请在文章开始地方标注文章出处,谢谢!

由于博客中有大量代码,通过页面浏览效果更佳。

SQL优化改IN躲开全表扫描性能提升53倍

1. 案例摘要

慢因: UPDATE列 = (标量子查询) 驱动目标行时,计划落到 TABLE ACCESS FULL + HASH JOIN,并对外层大量候选行反复执行 EXISTSSUBQUERY(Loops 约 10 万),逻辑读放大。
改法: 将等值标量改为 列 IN (子查询),并加 列 IS NOT NULL,使优化器走 NESTED LOOPS + 索引范围扫描
效果(23.5.2.101): Elapsed 0.210s → 0.004s,提升倍数 = 0.210 / 0.004 = 52.5db block gets 202438 → 155,提升倍数 = 202438 / 155 ≈ 1306.1;影响行数均为 4。同脚本在 23.4.14.100 上 = 写法已走索引,前后差异很小(见第 6 章)。

2. 问题引入

生产引入

最近在某客户现场做 SQL 优化,遇到如下的一个问题。

SQL 为自表 UPDATE:用 BIZ_KEY = (SELECT BIZ_KEY … WHERE DOC_ID = :x AND ROWNUM = 1) 定位业务键,再叠加 STATUS_CD < 3 与相关 EXISTS(时间窗与 SIDE_FLAG 空值交叉)。在 YashanDB 上执行计划出现 TABLE ACCESS FULL + HASH JOINEXISTS 侧为 SUBQUERY + 组合索引探测;库内统计可见单次执行逻辑读约 21.28 万(现场口径),而同形态 SQL 在 Oracle 侧可走 BIZ_KEY 索引。将 = 改为 IN 并补充 BIZ_KEY IS NOT NULL 后,外层改为索引驱动,代价显著下降。

原始 SQL

以下 SQL 在模拟环境中复现原生产侧同类写法(脱敏/仿建对象 DEMO_BIZ_HDR,非生产库原文直贴):

UPDATE demo_biz_hdr t
   SET status_cd = (CASE WHEN SYSDATE > due_tm THEN 3 ELSE 1 END),
       remark_tx  = 'Manual'
 WHERE t.biz_key = (
         SELECT biz_key
           FROM demo_biz_hdr x
          WHERE x.doc_id = 'D000000001' AND ROWNUM = 1
       )
   AND t.status_cd < 3
   AND EXISTS (
         SELECT 1
           FROM demo_biz_hdr a
          WHERE a.biz_key = t.biz_key
            AND a.node_cd = t.node_cd
            AND a.status_cd > 0
            AND a.doc_id <> t.doc_id
            AND (a.side_flag IS NULL AND t.side_flag IS NOT NULL
              OR t.side_flag IS NULL AND a.side_flag IS NOT NULL)
            AND a.event_tm > t.event_tm - (149/2880)
            AND a.event_tm < t.event_tm + (149/2880)
       );

文本说明:

片段 含义
BIZ_KEY = (SELECT … ROWNUM = 1) 用 DOC_ID 取一条业务键的标量等值条件
STATUS_CD < 3 限制待更新状态
EXISTS (… EVENT_TM 时间窗 … SIDE_FLAG 空值交叉) 同业务键/节点下是否存在“对向”明细
CASE WHEN SYSDATE > DUE_TM … 按时刻写回 STATUS_CD / REMARK_TX

3. 分析问题

分析思路:

  1. 如何确认是慢 SQL: 现场该语句逻辑读约二十万量级;lab 23.5 单次 Elapsed≈0.210s,db block gets≈20.2 万,相对改写后同语义语句明显偏高。
  2. 如何定位到慢的地方: 计划外层为 TABLE ACCESS FULL + HASH JOIN INNEREXISTS 对应 SUBQUERY,内层索引 Loops≈100001,慢在“全表过候选 + 反复相关探测”,而不是 CASE/EXISTS 业务语义本身。

优化思路:

标量 = 子查询不利于把 BIZ_KEY 作为索引访问起点;改为 IN(无 AGGR/GROUP BY)更贴近官方可改写的子查询形态,并显式 BIZ_KEY IS NOT NULL,促使优化器以子查询结果驱动 NESTED LOOPS + INDEX RANGE SCAN,避免全表扫描。参见:

本轮在 23.5.2.101 复现“全表 + 高 Loops”;在 23.4.14.100 同脚本下 = 已走索引(见第 5.3、第 6 章双版本对照)。两版本优化器路径不同,改写收益也不同;上线与兼容验证须以目标版本实测为准。YashanDB 优化器迭代较快,稳定发布线内宜优先部署该线上最新小版本,并在该版本复核计划。

4. 环境与数据

为便于复现与对照,在模拟环境中按生产侧同类问题 仿建业务对象并灌入数据(非生产库直接操作)。同一套脚本分别在两套库上执行。

内容
对象 DEMO_BIZ_HDR 约 10 万行(本轮计数 100001)
造数 模拟环境新建;DOC_ID='D000000001' 对应 BIZ_KEY=1,并植入可命中 EXISTS 的配对行
关键索引 IDX_DEMO_DOC_ID(DOC_ID)IDX_DEMO_BIZ_COMP(BIZ_KEY,NODE_CD,EVENT_TM,STATUS_CD,DOC_ID,SIDE_FLAG)IDX_DEMO_BIZ_KEY_ST(BIZ_KEY,STATUS_CD)IDX_DEMO_BIZ_KEY(BIZ_KEY)
统计信息 DBMS_STATS.GATHER_TABLE_STATS 已采集
版本 YashanDB 23.5.2.10123.4.14.100(同主机 lab)

造数要点(可复现): biz_key 约 5000 档循环分配;doc_id 近似唯一;时间与 side_flag 空值比例用于触发 EXISTS 条件。

5. 解决方案

5.1 优化前(现状)

UPDATE demo_biz_hdr t
   SET status_cd = (CASE WHEN SYSDATE > due_tm THEN 3 ELSE 1 END),
       remark_tx  = 'Manual'
 WHERE t.biz_key = (
         SELECT biz_key
           FROM demo_biz_hdr x
          WHERE x.doc_id = 'D000000001' AND ROWNUM = 1
       )
   AND t.status_cd < 3
   AND EXISTS (
         SELECT 1
           FROM demo_biz_hdr a
          WHERE a.biz_key = t.biz_key
            AND a.node_cd = t.node_cd
            AND a.status_cd > 0
            AND a.doc_id <> t.doc_id
            AND (a.side_flag IS NULL AND t.side_flag IS NOT NULL
              OR t.side_flag IS NULL AND a.side_flag IS NOT NULL)
            AND a.event_tm > t.event_tm - (149/2880)
            AND a.event_tm < t.event_tm + (149/2880)
       );

23.5.2.101

  • 影响行数:4(随后 ROLLBACK
  • Elapsed:00:00:00.210
  • SQL hash:736448843

Execution Plan(关键):

----------------------------------------------------------------------------------------
| Id | Operation                        | Name              | A-Rows | A-Time |  Loops |
----------------------------------------------------------------------------------------
|  0 | UPDATE STATEMENT                 |                   |      0 | 204153 |      1 |
|  1 |  SUBQUERY                        | QUERY[2]          |   6961 | 148855 | 100001 |
|  2 |   INDEX RANGE SCAN               | IDX_DEMO_BIZ_COMP |   6961 | 142570 | 100001 |
|  3 |  UPDATE                          | DEMO_BIZ_HDR      |      0 | 204153 |      1 |
|  4 |   HASH JOIN INNER                |                   |      4 | 204011 |      5 |
|  5 |    TABLE ACCESS FULL             | DEMO_BIZ_HDR      |      4 | 203955 |      5 |
|  6 |    VIEW                          |                   |      1 | 10     |      2 |
|  7 |     COUNT STOPKEY                |                   |      1 | 9      |      2 |
|  8 |      TABLE ACCESS BY INDEX ROWID | DEMO_BIZ_HDR      |        |        |        |
|  9 |       INDEX RANGE SCAN           | IDX_DEMO_DOC_ID   |      1 | 8      |      1 |
----------------------------------------------------------------------------------------

关键谓词:外层 EXISTS QUERY[2] AND STATUS_CD < 3HASH JOINT.BIZ_KEY = VSQ$1…BIZ_KEY(含 runtime filter);SUBQUERY 侧按 BIZ_KEY/NODE_CD/EVENT_TM 访问 IDX_DEMO_BIZ_COMP

Statistics: physical reads 0;db block gets 202,438;consistent gets 0;rows processed 4。

5.2 方案:改为 IN + IS NOT NULL

BIZ_KEY = (SELECT …) 改为 BIZ_KEY IN (SELECT …),并增加 BIZ_KEY IS NOT NULL(与现场改写一致):

UPDATE demo_biz_hdr t
   SET status_cd = (CASE WHEN SYSDATE > due_tm THEN 3 ELSE 1 END),
       remark_tx  = 'Manual'
 WHERE t.biz_key IN (
         SELECT biz_key
           FROM demo_biz_hdr x
          WHERE x.doc_id = 'D000000001' AND ROWNUM = 1
       )
   AND t.biz_key IS NOT NULL
   AND t.status_cd < 3
   AND EXISTS (
         SELECT 1
           FROM demo_biz_hdr a
          WHERE a.biz_key = t.biz_key
            AND a.node_cd = t.node_cd
            AND a.status_cd > 0
            AND a.doc_id <> t.doc_id
            AND (a.side_flag IS NULL AND t.side_flag IS NOT NULL
              OR t.side_flag IS NULL AND a.side_flag IS NOT NULL)
            AND a.event_tm > t.event_tm - (149/2880)
            AND a.event_tm < t.event_tm + (149/2880)
       );

23.5.2.101

  • 影响行数:4(与优化前一致;随后 ROLLBACK
  • Elapsed:00:00:00.004
  • SQL hash:1694482287

Execution Plan(关键):

----------------------------------------------------------------------------------------
| Id | Operation                         | Name              | A-Rows | A-Time | Loops |
----------------------------------------------------------------------------------------
|  0 | UPDATE STATEMENT                  |                   |      0 | 268    |     1 |
|  1 |  SUBQUERY                         | QUERY[2]          |      4 | 73     |    21 |
|  2 |   INDEX RANGE SCAN                | IDX_DEMO_BIZ_COMP |      4 | 71     |    21 |
|  3 |  UPDATE                           | DEMO_BIZ_HDR      |      0 | 268    |     1 |
|  4 |   NESTED LOOPS INNER              |                   |      4 | 228    |     5 |
|  5 |    SORT DISTINCT                  |                   |      1 | 10     |     2 |
|  6 |     VIEW                          |                   |      1 | 8      |     2 |
|  7 |      COUNT STOPKEY                |                   |      1 | 7      |     2 |
|  8 |       TABLE ACCESS BY INDEX ROWID | DEMO_BIZ_HDR      |        |        |       |
|  9 |        INDEX RANGE SCAN           | IDX_DEMO_DOC_ID   |      1 | 6      |     1 |
| 10 |    TABLE ACCESS BY INDEX ROWID    | DEMO_BIZ_HDR      |        |        |       |
| 11 |     INDEX RANGE SCAN              | IDX_DEMO_BIZ_KEY  |      4 | 199    |     5 |
----------------------------------------------------------------------------------------

Statistics: physical reads 0;db block gets 155;consistent gets 0;rows processed 4。

5.3 同脚本在 23.4.14.100

同一 Before/After SQL 在 23.4.14.100 上,优化前 = 已走 NESTED LOOPS + IDX_DEMO_BIZ_KEY_ST,未出现全表扫描;改为 IN 后计划形态接近,指标接近。

优化前(23.4): Elapsed 0.003s;db block gets 158;影响行数 4;Plan hash 3044850505。

------------------------------------------------------------------------------------------
| Id | Operation                        | Name                | A-Rows | A-Time | Starts |
------------------------------------------------------------------------------------------
|  0 | UPDATE STATEMENT                 |                     |      0 | 191    |      1 |
|  1 |  SUBQUERY                        | QUERY[2]            |      4 | 63     |     21 |
|  2 |   INDEX RANGE SCAN               | IDX_DEMO_BIZ_COMP   |      4 | 58     |     21 |
|  3 |  UPDATE                          | DEMO_BIZ_HDR        |      0 | 191    |      1 |
|  4 |   NESTED LOOPS INNER             |                     |      4 | 145    |      1 |
|  5 |    VIEW                          |                     |      2 | 11     |      2 |
|  6 |     COUNT STOPKEY                |                     |      2 | 9      |      2 |
|  7 |      TABLE ACCESS BY INDEX ROWID | DEMO_BIZ_HDR        |        |        |        |
|  8 |       INDEX RANGE SCAN           | IDX_DEMO_DOC_ID     |      2 | 7      |      2 |
|  9 |    TABLE ACCESS BY INDEX ROWID   | DEMO_BIZ_HDR        |        |        |        |
| 10 |     INDEX RANGE SCAN             | IDX_DEMO_BIZ_KEY_ST |      4 | 114    |      1 |
------------------------------------------------------------------------------------------

优化后(23.4): Elapsed 0.002s;db block gets 155;影响行数 4;Plan hash 1256705303。

-------------------------------------------------------------------------------------------
| Id | Operation                         | Name                | A-Rows | A-Time | Starts |
-------------------------------------------------------------------------------------------
|  0 | UPDATE STATEMENT                  |                     |      0 | 175    |      1 |
|  1 |  SUBQUERY                         | QUERY[2]            |      4 | 58     |     21 |
|  2 |   INDEX RANGE SCAN                | IDX_DEMO_BIZ_COMP   |      4 | 56     |     21 |
|  3 |  UPDATE                           | DEMO_BIZ_HDR        |      0 | 175    |      1 |
|  4 |   NESTED LOOPS INNER              |                     |      4 | 143    |      1 |
|  5 |    SORT DISTINCT                  |                     |      1 | 13     |      1 |
|  6 |     VIEW                          |                     |      1 | 8      |      1 |
|  7 |      COUNT STOPKEY                |                     |      1 | 6      |      1 |
|  8 |       TABLE ACCESS BY INDEX ROWID | DEMO_BIZ_HDR        |        |        |        |
|  9 |        INDEX RANGE SCAN           | IDX_DEMO_DOC_ID     |      1 | 5      |      1 |
| 10 |    TABLE ACCESS BY INDEX ROWID    | DEMO_BIZ_HDR        |        |        |        |
| 11 |     INDEX RANGE SCAN              | IDX_DEMO_BIZ_KEY_ST |      4 | 112    |      1 |
-------------------------------------------------------------------------------------------

6. 对比总表与提升倍数

指标 23.5 Before 23.5 After 23.4 Before 23.4 After 来源
影响行数 4 4 4 4 UPDATE
Elapsed 0.210 s 0.004 s 0.003 s 0.002 s autotrace Timing
关键外层路径 HASH JOIN + FULL SCAN NL + INDEX NL + INDEX NL + INDEX autotrace Plan
EXISTS Loops/Starts 100001 21 21 21 Plan
db block gets 202,438 155 158 155 autotrace Statistics
  • 23.5 耗时提升倍数: 0.210 / 0.004 = 52.5
  • 23.5 逻辑读提升倍数: 202438 / 155 ≈ 1306.1
  • 23.4 耗时提升倍数: 0.003 / 0.002 = 1.5= 已优,改写收益有限)
  • 23.4 逻辑读提升倍数: 158 / 155 ≈ 1.0

(公式:提升倍数 = 优化前指标 / 优化后指标。)

双版本差异说明

同一套脱敏 SQL、同一造数与索引,在两个版本上的表现 不一致

维度 23.5.2.101 23.4.14.100
优化前 列 = (标量子查询) HASH JOIN + 全表扫描;EXISTS Loops≈10 万;逻辑读约 20 万 已是 NESTED LOOPS + 索引;Starts≈21;逻辑读约 158
改为 IN + IS NOT NULL 外层改为 NL + 索引,耗时/逻辑读大幅下降 计划仍为 NL + 索引,前后几乎持平
本案例改写的“可见收益” (约 52.5× / 1306×) (约 1.5× / 1.0×)

因此:不能把某一版本上的计划或加速比直接套到另一版本;现场必须以 目标版本 实测为准。本轮差异说明的是「同形态 SQL 在不同发行版上的优化器路径选择不同」,并不否定任一侧的正确性。

7. 根因与改写说明

  1. 为何慢(23.5 / 与现场同类): 列 = (标量子查询) 路径下,优化器用全表扫描产生更新候选,再与标量子查询结果做 HASH JOINEXISTS 仍以 SUBQUERY 形式按外层行反复探测(Loops≈表规模),逻辑读与 A-Time 集中在 FULL SCAN 与 SUBQUERY。
  2. 方案做了什么: IN + IS NOT NULL 后,外层变为以子查询结果驱动的 NESTED LOOPS + INDEX RANGE SCANEXISTS 探测次数从约 10 万降至数十。这与文档中「IN 子查询在无 AGGR 等限制时可改写为更优连接形态」的方向一致。
  3. 语义: ROWNUM = 1 的标量子查询在单值语义下,=IN 对命中行等价;本轮两边影响行数均为 4。
  4. 版本差异: 23.4.14.100 上同 SQL 的 = 已选索引嵌套循环,不复现全表问题;23.5.2.101 上则需改写才能避开全表路径。不能假设所有版本行为相同。

8. 结论与建议

  • UPDATE/SELECT标量等值子查询 绑驱动键时,若计划出现 FULL SCAN + 高 Loops 的 SUBQUERY,优先尝试改为 IN(或半连接友好写法),并视情况补 IS NOT NULL
  • 可推广: 按业务键定位再更新/过滤的语句;同类 列 = (SELECT … ROWNUM=1) 自表更新语句。
  • 版本与部署: YashanDB 优化器处于快速迭代中,每一发行版都可能引入新的改写规则、代价模型或执行路径;同一 SQL 在 23.4 与 23.5 上计划不同,属于正常现象。兼容与上线时,宜在既定的 稳定大版本/发布线内,优先部署该线上的最新小版本(含补丁),并在目标版本上复核关键 SQL 的计划与耗时,避免用旧版 lab 结论替代生产版本行为。
  • 局限: 本结果在 lab 约 10 万行、热缓存(physical reads=0)下测得;生产分布、并发与索引集可能改变代价。
  • 本案例未做: 改索引结构、改优化器参数、绑定变量窥视;上线前请在目标版本用同等口径复核。

9. 附录

DDL 骨架(示意):

CREATE TABLE demo_biz_hdr (
  biz_key  NUMBER(18) NOT NULL,
  doc_id       VARCHAR2(32) NOT NULL,
  due_tm      DATE,
  status_cd  NUMBER(18) NOT NULL,
  remark_tx   VARCHAR2(64),
  node_cd VARCHAR2(32),
  side_flag       VARCHAR2(64),
  event_tm      DATE,
  c1        VARCHAR2(64),
  c2        VARCHAR2(64)
);

CREATE INDEX idx_demo_doc_id ON demo_biz_hdr (doc_id);
CREATE INDEX idx_demo_biz_comp ON demo_biz_hdr
  (biz_key, node_cd, event_tm, status_cd, doc_id, side_flag);
CREATE INDEX idx_demo_biz_key_st ON demo_biz_hdr (biz_key, status_cd);
CREATE INDEX idx_demo_biz_key ON demo_biz_hdr (biz_key);
-- 再按约 1e5 行灌数并收集统计信息

参考文档:

SQL优化改IN躲开全表扫描性能提升53倍:等您坐沙发呢!

发表评论

gravatar

? razz sad evil ! smile oops grin eek shock ??? cool lol mad twisted roll wink idea arrow neutral cry mrgreen

快捷键:Ctrl+Enter