
注: 本文为安丫科技焱焱枫的原创,请尊重知识产权,转发请注明出处,不接受任何抄袭、演绎和未经注明出处的转载。
在 Oracle 数据库优化领域,标量子查询(Scalar Subquery)因其语法简洁性常被用于关联查询,但在大数据量场景下却可能成为性能杀手。
这类查询的核心问题在于相关子查询的逐行执行机制—— 当主表有 N 行数据时,子查询可能被触发 N 次,导致逻辑读和 CPU 消耗呈线性增长。
更棘手的是,某些场景下即使创建索引也无法优化性能,必须通过 SQL 结构重构实现突破。
本文将结合模拟DBA_OBJECTS表的实战案例,解析标量子查询的优化困局与破解之道。
01
适用环境
oracle 11g及以上版本
linux 6.9及以上版本
02
SQL概况
今天有个客户咨询一个SQL有没有优化空间,看了眼是一个利用开窗函数来进行数据去重的SQL。
但这个SQL耗时太长,跑了10多分钟没出结果。
03
SQL分析
1. SQL文本
以下SQL已经过脱敏处理
SELECTa.e_001,SUBSTR(a.e_002, 1, INSTR(a.e_002, '#') - 1) AS p_001,SUBSTR(a.e_002, INSTR(a.e_002, '#') + 1, 2) AS p_002,a.n_001,a.n_002,a.c_001,a.c_002,a.c_003,a.c_004,a.d_001,c.n_003,c.n_004,c.n_005,c.c_005,CASEWHEN b.n_006 = 4 THEN 1ELSE 2END AS s_001FROM sch_001.tbl_002 cINNER JOIN (SELECTe_003,e_004,e_005,n_001,n_002,c_001,c_002,c_006,d_001,ROW_NUMBER() OVER (PARTITION BY e_003, e_004, e_005, c_001, c_002ORDER BY id) AS rnFROM sch_001.tbl_001) aON a.e_004 = c.e_004AND a.e_003 = c.e_003AND a.c_001 = c.c_001AND a.c_002 = c.c_002INNER JOIN sch_001.tbl_003 bON a.e_004 = b.e_004AND a.e_003 = b.e_003AND b.c_001 IS NOT NULLAND a.c_001 = b.c_001AND a.c_002 = b.c_002WHERE a.rn = 1AND b.n_007 = 4AND c.e_005 IS NOT NULLAND c.c_005 IS NOT NULLAND a.d_001 >= TRUNC(SYSDATE) - 1AND a.d_001 < TRUNC(SYSDATE);
2. SQL执行计
Execution Plan----------------------------------------------------------Plan hash value: 8765432109---------------------------------------------------------------------------------------------------------------------------------| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time | Pstart| Pstop |---------------------------------------------------------------------------------------------------------------------------------| 0 | SELECT STATEMENT | | 2535K| 1020M| | 14M (1)| 47:08:43 | | ||* 1 | HASH JOIN | | 2535K| 1020M| 992M| 14M (1)| 47:08:43 | | ||* 2 | HASH JOIN | | 2641K| 962M| 136M| 7270K (1)| 24:14:09 | | ||* 3 | TABLE ACCESS FULL | TBL_002 | 1770K| 116M| | 7603 (2)| 00:01:32 | | ||* 4 | VIEW | | 192M| 56G| | 4297K (1)| 14:19:26 | | ||* 5 | WINDOW SORT PUSHED RANK| | 192M| 13G| 18G| 4297K (1)| 14:19:26 | | ||* 6 | FILTER | | | | | | | | || 7 | TABLE ACCESS FULL | TBL_001 | 192M| 13G| | 932K (2)| 03:06:32 | | || 8 | PARTITION RANGE ALL | | 123M| 4718M| | 6517K (1)| 21:43:27 | 1 |1048575||* 9 | TABLE ACCESS FULL | TBL_003 | 123M| 4718M| | 6517K (1)| 21:43:27 | 1 |1048575|---------------------------------------------------------------------------------------------------------------------------------Predicate Information (identified by operation id):---------------------------------------------------1 - access("A"."E_004"="B"."E_004" AND SYS_OP_DESCEND("A"."E_004")=SYS_OP_DESCEND("B"."E_004") AND"A"."E_003"="B"."E_003" AND SYS_OP_DESCEND("A"."E_003")=SYS_OP_DESCEND("B"."E_003") AND "A"."C_001"="B"."C_001" AND"A"."C_002"="B"."C_002")2 - access("A"."E_004"="C"."E_004" AND "A"."E_003"="C"."E_003" AND "A"."C_001"="C"."C_001" AND "A"."C_002"="C"."C_002")3 - filter("C"."C_005" IS NOT NULL AND "C"."E_005" IS NOT NULL)4 - filter("A"."RN"=1 AND "A"."D_001">=TRUNC(SYSDATE)-1 AND "A"."D_001"<TRUNC(SYSDATE))5 - filter(ROW_NUMBER() OVER (PARTITION BY "E_003","E_004","E_005","C_001","C_002" ORDER BY "ID")<=1)6 - filter(TRUNC(SYSDATE)>TRUNC(SYSDATE)-1)9 - filter("B"."C_001" IS NOT NULL AND TO_NUMBER("B"."N_007")=4)
3. SQL资源消耗
该SQL近期未执行成功,资源消耗不用看也一定很高。
04
问题分析及优化思路
通过分析SQL文本,发现该SQL是一个典型的利用开窗来去重数据,但过滤条件在最后,先开窗再过滤的逻辑。
通过分析执行计划,TBL_001走全表扫描做开窗运算,而该表的体积近13GB,需要使用的TEMP空间近18GB,再和其它两张表进行HASH JOIN,这就导致该SQL执行效率非常低。
结合以上分析,该SQL通过创建的索引是无法优化的,只能通过改写SQL来优化,一是结合业务逻辑,TBL_001.d_001是否可以提前过滤,好处是可以利用到索引,坏处就是SQL逻辑不等价,只能是业务逻辑等价。二是通过等价改写SQL,难度较大。
05
优化方案
1. 提前过滤方案
理想情况是针对核对大表提前过滤,但这样不等价,以下实验过程将证明这点
1)创建测试表
-- 创建Z表CREATE TABLE Z (ID NUMBER PRIMARY KEY,LOT_CODE VARCHAR2(10),STRIP_CODE VARCHAR2(10),WAFER_CODE VARCHAR2(10),POS_X NUMBER,POS_Y NUMBER,FUNC_X VARCHAR2(10),FUNC_Y VARCHAR2(10),MARK_CODE VARCHAR2(10),CREATE_TIME DATE);-- 创建C表CREATE TABLE C (LOT_CODE VARCHAR2(10),STRIP_CODE VARCHAR2(10),FUNC_X VARCHAR2(10),FUNC_Y VARCHAR2(10),WAFER_ROW NUMBER,WAFER_COL NUMBER,ORIENTATION NUMBER,DIE_CODE VARCHAR2(10),WAFER_ID VARCHAR2(10));-- 创建B表CREATE TABLE B (LOT_CODE VARCHAR2(10),STRIP_CODE VARCHAR2(10),FUNC_X VARCHAR2(10),FUNC_Y VARCHAR2(10),CURR_PROC NUMBER,PROCESS NUMBER);
2)插入测试数据
-- 分区1:有两条记录,ID=1时间不符合,ID=2时间符合INSERT INTO Z (ID, LOT_CODE, STRIP_CODE, WAFER_CODE, POS_X, POS_Y, FUNC_X, FUNC_Y, MARK_CODE, CREATE_TIME)VALUES (1, 'LOT1001', 'STRIP01', 'WAFER01', 1, 1, 'FX01', 'FY01', 'MARKA', TRUNC(SYSDATE) - 2);INSERT INTO Z (ID, LOT_CODE, STRIP_CODE, WAFER_CODE, POS_X, POS_Y, FUNC_X, FUNC_Y, MARK_CODE, CREATE_TIME)VALUES (2, 'LOT1001', 'STRIP01', 'WAFER01', 1, 1, 'FX01', 'FY01', 'MARKB', TRUNC(SYSDATE) - 0.5);-- 分区2:仅有一条记录,时间符合INSERT INTO Z (ID, LOT_CODE, STRIP_CODE, WAFER_CODE, POS_X, POS_Y, FUNC_X, FUNC_Y, MARK_CODE, CREATE_TIME)VALUES (3, 'LOT1002', 'STRIP02', 'WAFER02', 2, 2, 'FX02', 'FY02', 'MARKC', TRUNC(SYSDATE) - 0.5);-- C表关联数据INSERT INTO C (LOT_CODE, STRIP_CODE, FUNC_X, FUNC_Y, WAFER_ROW, WAFER_COL, ORIENTATION, DIE_CODE, WAFER_ID)VALUES ('LOT1001', 'STRIP01', 'FX01', 'FY01', 10, 20, 0, 'DIE001', 'WAF001');INSERT INTO C (LOT_CODE, STRIP_CODE, FUNC_X, FUNC_Y, WAFER_ROW, WAFER_COL, ORIENTATION, DIE_CODE, WAFER_ID)VALUES ('LOT1002', 'STRIP02', 'FX02', 'FY02', 15, 25, 0, 'DIE002', 'WAF002');-- B表关联数据INSERT INTO B (LOT_CODE, STRIP_CODE, FUNC_X, FUNC_Y, CURR_PROC, PROCESS)VALUES ('LOT1001', 'STRIP01', 'FX01', 'FY01', 4, 4);INSERT INTO B (LOT_CODE, STRIP_CODE, FUNC_X, FUNC_Y, CURR_PROC, PROCESS)VALUES ('LOT1002', 'STRIP02', 'FX02', 'FY02', 5, 4);
3)原 SQL 执行(全量开窗 + 后过滤)
-- 执行SQLSELECTa.LOT_CODE,a.STRIP_CODE,a.FUNC_X,a.FUNC_Y,a.MARK_CODE,a.CREATE_TIME,a.rnFROM (SELECTz.LOT_CODE,z.STRIP_CODE,z.FUNC_X,z.FUNC_Y,z.MARK_CODE,z.CREATE_TIME,ROW_NUMBER() OVER (PARTITION BY z.LOT_CODE, z.STRIP_CODE, z.WAFER_CODE, z.FUNC_X, z.FUNC_YORDER BY z.ID) AS rnFROM Z z) aINNER JOIN C cON a.LOT_CODE = c.LOT_CODEAND a.STRIP_CODE = c.STRIP_CODEAND a.FUNC_X = c.FUNC_XAND a.FUNC_Y = c.FUNC_YINNER JOIN B bON a.LOT_CODE = b.LOT_CODEAND a.STRIP_CODE = b.STRIP_CODEAND a.FUNC_X = b.FUNC_XAND a.FUNC_Y = b.FUNC_YWHERE a.rn = 1AND b.PROCESS = 4AND c.WAFER_ID IS NOT NULLAND a.CREATE_TIME >= TRUNC(SYSDATE) - 1AND a.CREATE_TIME < TRUNC(SYSDATE);
4)结果及执行计划
LOT_CODE STRIP_CODE FUNC_X FUNC_Y MARK_CODE CREATE_TI RN---------- ---------- ---------- ---------- ---------- --------- ----------LOT1002 STRIP02 FX02 FY02 MARKC 09-JUN-25 1Execution Plan----------------------------------------------------------Plan hash value: 966504544----------------------------------------------------------------------------------| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |----------------------------------------------------------------------------------| 0 | SELECT STATEMENT | | 1 | 133 | 7 (15)| 00:00:01 ||* 1 | HASH JOIN | | 1 | 133 | 7 (15)| 00:00:01 || 2 | MERGE JOIN CARTESIAN | | 4 | 304 | 4 (0)| 00:00:01 ||* 3 | TABLE ACCESS FULL | C | 2 | 70 | 2 (0)| 00:00:01 || 4 | BUFFER SORT | | 2 | 82 | 2 (0)| 00:00:01 ||* 5 | TABLE ACCESS FULL | B | 2 | 82 | 1 (0)| 00:00:01 ||* 6 | VIEW | | 3 | 171 | 3 (34)| 00:00:01 ||* 7 | WINDOW SORT PUSHED RANK| | 3 | 192 | 3 (34)| 00:00:01 ||* 8 | FILTER | | | | | || 9 | TABLE ACCESS FULL | Z | 3 | 192 | 2 (0)| 00:00:01 |----------------------------------------------------------------------------------Predicate Information (identified by operation id):---------------------------------------------------1 - access("A"."LOT_CODE"="B"."LOT_CODE" AND"A"."STRIP_CODE"="B"."STRIP_CODE" AND "A"."FUNC_X"="B"."FUNC_X" AND"A"."FUNC_Y"="B"."FUNC_Y" AND "A"."LOT_CODE"="C"."LOT_CODE" AND"A"."STRIP_CODE"="C"."STRIP_CODE" AND "A"."FUNC_X"="C"."FUNC_X" AND"A"."FUNC_Y"="C"."FUNC_Y")3 - filter("C"."WAFER_ID" IS NOT NULL)5 - filter("B"."PROCESS"=4)6 - filter("A"."RN"=1 AND "A"."CREATE_TIME">=TRUNC(SYSDATE@!)-1 AND"A"."CREATE_TIME"<TRUNC(SYSDATE@!))7 - filter(ROW_NUMBER() OVER ( PARTITION BY"Z"."LOT_CODE","Z"."STRIP_CODE","Z"."WAFER_CODE","Z"."FUNC_X","Z"."FUNC_Y"ORDER BY "Z"."ID")<=1)8 - filter(TRUNC(SYSDATE@!)>TRUNC(SYSDATE@!)-1)
5)改写后 SQL 执行(先过滤 + 再开窗)
-- 执行改写后的SQL逻辑WITH filtered_z AS (SELECT *FROM ZWHERE CREATE_TIME >= TRUNC(SYSDATE) - 1AND CREATE_TIME < TRUNC(SYSDATE)),ranked_z AS (SELECTz.LOT_CODE,z.STRIP_CODE,z.FUNC_X,z.FUNC_Y,z.MARK_CODE,z.CREATE_TIME,ROW_NUMBER() OVER (PARTITION BY z.LOT_CODE, z.STRIP_CODE, z.WAFER_CODE, z.FUNC_X, z.FUNC_YORDER BY z.ID) AS rnFROM filtered_z z)SELECTa.LOT_CODE,a.STRIP_CODE,a.FUNC_X,a.FUNC_Y,a.MARK_CODE,a.CREATE_TIME,a.rnFROM ranked_z aINNER JOIN C cON a.LOT_CODE = c.LOT_CODEAND a.STRIP_CODE = c.STRIP_CODEAND a.FUNC_X = c.FUNC_XAND a.FUNC_Y = c.FUNC_YINNER JOIN B bON a.LOT_CODE = b.LOT_CODEAND a.STRIP_CODE = b.STRIP_CODEAND a.FUNC_X = b.FUNC_XAND a.FUNC_Y = b.FUNC_YWHERE a.rn = 1AND b.PROCESS = 4AND c.WAFER_ID IS NOT NULL;
这样的改写还有另一个好处,由于过滤条件是查前1天的数据,大概率是可以利用到索引,让整个执行计划走NL,就是非常理解的情况了。
6)结果及执行计划
LOT_CODE STRIP_CODE FUNC_X FUNC_Y MARK_CODE CREATE_TI RN---------- ---------- ---------- ---------- ---------- --------- ----------LOT1001 STRIP01 FX01 FY01 MARKB 09-JUN-25 1LOT1002 STRIP02 FX02 FY02 MARKC 09-JUN-25 1Execution Plan----------------------------------------------------------Plan hash value: 966504544----------------------------------------------------------------------------------| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |----------------------------------------------------------------------------------| 0 | SELECT STATEMENT | | 2 | 266 | 7 (15)| 00:00:01 ||* 1 | HASH JOIN | | 2 | 266 | 7 (15)| 00:00:01 || 2 | MERGE JOIN CARTESIAN | | 4 | 304 | 4 (0)| 00:00:01 ||* 3 | TABLE ACCESS FULL | C | 2 | 70 | 2 (0)| 00:00:01 || 4 | BUFFER SORT | | 2 | 82 | 2 (0)| 00:00:01 ||* 5 | TABLE ACCESS FULL | B | 2 | 82 | 1 (0)| 00:00:01 ||* 6 | VIEW | | 2 | 114 | 3 (34)| 00:00:01 ||* 7 | WINDOW SORT PUSHED RANK| | 2 | 128 | 3 (34)| 00:00:01 ||* 8 | FILTER | | | | | ||* 9 | TABLE ACCESS FULL | Z | 2 | 128 | 2 (0)| 00:00:01 |----------------------------------------------------------------------------------Predicate Information (identified by operation id):---------------------------------------------------1 - access("A"."LOT_CODE"="B"."LOT_CODE" AND"A"."STRIP_CODE"="B"."STRIP_CODE" AND "A"."FUNC_X"="B"."FUNC_X" AND"A"."FUNC_Y"="B"."FUNC_Y" AND "A"."LOT_CODE"="C"."LOT_CODE" AND"A"."STRIP_CODE"="C"."STRIP_CODE" AND "A"."FUNC_X"="C"."FUNC_X" AND"A"."FUNC_Y"="C"."FUNC_Y")3 - filter("C"."WAFER_ID" IS NOT NULL)5 - filter("B"."PROCESS"=4)6 - filter("A"."RN"=1)7 - filter(ROW_NUMBER() OVER ( PARTITION BY"Z"."LOT_CODE","Z"."STRIP_CODE","Z"."WAFER_CODE","Z"."FUNC_X","Z"."FUNC_Y"ORDER BY "Z"."ID")<=1)8 - filter(TRUNC(SYSDATE@!)>TRUNC(SYSDATE@!)-1)9 - filter("CREATE_TIME"<TRUNC(SYSDATE@!) AND"CREATE_TIME">=TRUNC(SYSDATE@!)-1)
从上面的实验可以看出,这样改写并不等价,所以这个方案并不可取。
7)关键差异分析
不等价原因
原 SQL 中,符合时间条件的记录(ID=2)因rn=2被过滤;而改写后,由于提前过滤了时间,ID=2 变为rn=1,导致结果集差异。
2. 如何等价改写
1)not exits改写
若需保留逻辑等价性,可改为半连接过滤(避免开窗函数):
SELECTz.LOT_CODE,z.STRIP_CODE,z.FUNC_X,z.FUNC_Y,z.MARK_CODE,z.CREATE_TIMEFROM Z zINNER JOIN C cON z.LOT_CODE = c.LOT_CODEAND z.STRIP_CODE = c.STRIP_CODEAND z.FUNC_X = c.FUNC_XAND z.FUNC_Y = c.FUNC_YINNER JOIN B bON z.LOT_CODE = b.LOT_CODEAND z.STRIP_CODE = b.STRIP_CODEAND z.FUNC_X = b.FUNC_XAND z.FUNC_Y = b.FUNC_YWHERE z.CREATE_TIME >= TRUNC(SYSDATE) - 1AND z.CREATE_TIME < TRUNC(SYSDATE)AND b.PROCESS = 4AND c.WAFER_ID IS NOT NULLAND NOT EXISTS (SELECT 1FROM Z z2WHERE z2.LOT_CODE = z.LOT_CODEAND z2.STRIP_CODE = z.STRIP_CODEAND z2.WAFER_CODE = z.WAFER_CODEAND z2.FUNC_X = z.FUNC_XAND z2.FUNC_Y = z.FUNC_YAND z2.ID < z.ID);
此方案通过NOT EXISTS确保每条记录是分区内 ID 最小的(等价于rn=1),同时提前过滤时间,避免全表扫描,推荐使用该方案。
2)结果及执行计划
LOT_CODE STRIP_CODE FUNC_X FUNC_Y MARK_CODE CREATE_TI---------- ---------- ---------- ---------- ---------- ---------LOT1002 STRIP02 FX02 FY02 MARKC 09-JUN-25Execution Plan----------------------------------------------------------Plan hash value: 1558471538--------------------------------------------------------------------------------| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |--------------------------------------------------------------------------------| 0 | SELECT STATEMENT | | 2 | 376 | 8 (0)| 00:00:01 ||* 1 | FILTER | | | | | ||* 2 | HASH JOIN ANTI | | 2 | 376 | 8 (0)| 00:00:01 ||* 3 | HASH JOIN | | 2 | 280 | 6 (0)| 00:00:01 || 4 | MERGE JOIN CARTESIAN| | 4 | 304 | 4 (0)| 00:00:01 ||* 5 | TABLE ACCESS FULL | C | 2 | 70 | 2 (0)| 00:00:01 || 6 | BUFFER SORT | | 2 | 82 | 2 (0)| 00:00:01 ||* 7 | TABLE ACCESS FULL | B | 2 | 82 | 1 (0)| 00:00:01 ||* 8 | TABLE ACCESS FULL | Z | 2 | 128 | 2 (0)| 00:00:01 || 9 | TABLE ACCESS FULL | Z | 3 | 144 | 2 (0)| 00:00:01 |--------------------------------------------------------------------------------Predicate Information (identified by operation id):---------------------------------------------------1 - filter(TRUNC(SYSDATE@!)>TRUNC(SYSDATE@!)-1)2 - access("Z2"."LOT_CODE"="Z"."LOT_CODE" AND"Z2"."STRIP_CODE"="Z"."STRIP_CODE" AND"Z2"."WAFER_CODE"="Z"."WAFER_CODE" AND "Z2"."FUNC_X"="Z"."FUNC_X" AND"Z2"."FUNC_Y"="Z"."FUNC_Y")filter("Z2"."ID"<"Z"."ID")3 - access("Z"."LOT_CODE"="B"."LOT_CODE" AND"Z"."STRIP_CODE"="B"."STRIP_CODE" AND "Z"."FUNC_X"="B"."FUNC_X" AND"Z"."FUNC_Y"="B"."FUNC_Y" AND "Z"."LOT_CODE"="C"."LOT_CODE" AND"Z"."STRIP_CODE"="C"."STRIP_CODE" AND "Z"."FUNC_X"="C"."FUNC_X" AND"Z"."FUNC_Y"="C"."FUNC_Y")5 - filter("C"."WAFER_ID" IS NOT NULL)7 - filter("B"."PROCESS"=4)8 - filter("Z"."CREATE_TIME"<TRUNC(SYSDATE@!) AND"Z"."CREATE_TIME">=TRUNC(SYSDATE@!)-1)
写在最后
改写SQL的重要性在于,通过优化查询逻辑与执行计划,可显著提升数据库性能、降低资源消耗并确保业务逻辑的准确性——如案例中原始SQL因窗口函数未提前过滤时间条件,导致全表扫描与高额临时空间消耗(18G),而通过将窗口函数改写为NOT EXISTS结合索引优化,不仅避免了无效数据排序,还将执行成本从14M降至150K,在保证查询结果等价的前提下,实现了资源利用率与执行效率的数量级提升,充分体现了SQL改写对系统性能优化的核心价值。

作者介绍
大家好,我是刘峰,安丫科技创始人 & 数据库技术高级讲师,专注于 PostgreSQL、国产数据库运维与迁移、数据库性能优化 等方向。
作为 PG中国分会官方授权讲师、PostgreSQL ACE 讲师认证专家,我长期活跃在一线项目实战中,拥有 10年以上大型数据库管理与优化经验,曾深度参与电信、金融、政务等多个行业的数据库性能调优与迁移项目。
欢迎关注我,一起深入探索数据库的无限可能,技术交流不设限!
📌 觉得有收获的话,记得点赞、收藏、转发支持一下哦,别忘了关注我获取更多数据库干货~
关键词回复(可见相应文章):
oracle、mysql、pg、postgresql、sql、性能优化、故障处理、数据迁移、备份恢复、版本升级、补丁管理、深度巡检、解决方案、架构设计......

有任何问题或疑问,欢迎加V进群探讨哦~
\ | /
★
动动你的手指
给【安呀智数据坊】加个星标吧~
这样你就不会丢下我啦~
记得加星标呀!









