标量子查询在Oracle 19c中并非自动高效,主表返回N行即执行N次,导致性能断崖下跌;必须通过EXPLAIN确认FILTER与STARTS值,改写为LEFT JOIN时须先内层GROUP BY再关联,并严格对齐外层SELECT与GROUP BY字段,避免数据膨胀或ORA-00937错误。

标量子查询在Oracle 19c里不是“写出来就自动快”,尤其当它被外层主表驱动多次执行时,性能会断崖式下跌——这不是语法问题,而是执行计划失控。直接重写为JOIN或CTE才是可靠解法。
标量子查询重复执行:先确认是不是真在反复跑
别猜,看执行计划里的filter操作和Rows预估。如果主表返回10,000行,而子查询旁标注SELECT MAX(sal) FROM emp WHERE deptno = :B1且没物化,那它就被调用了10,000次。
- 用
EXPLAIN PLAN FOR后查DBMS_XPLAN.DISPLAY,重点盯Operation列是否含FILTER、NESTED LOOPS内层是否走TABLE ACCESS FULL - 检查
OTHER_XML字段是否有dynamic_sampling——动态采样会让预估失真,掩盖真实行数 - 执行
SELECT * FROM V$SQL_PLAN_STATISTICS_ALL WHERE SQL_ID = 'xxx' AND OPERATION = 'FILTER',确认STARTS值是否等于主表输出行数
改写为LEFT JOIN:必须补内层GROUP BY,否则数据爆炸
把(SELECT MAX(e.sal) FROM emp e WHERE e.deptno = d.deptno)直接换成LEFT JOIN emp e ON d.deptno = e.deptno,不加GROUP BY就会让部门对员工产生笛卡尔积,结果行数翻N倍。
- 正确步骤是先聚合再关联:
(SELECT deptno, MAX(sal) AS max_sal FROM emp GROUP BY deptno)作为内联视图,再LEFT JOIN到主表 - 外层
SELECT若选了d.dname,GROUP BY里必须包含d.dname,否则报ORA-00937 - 如果主表
dept有100行,而emp里某部门为空,内联视图缺该deptno记录,LEFT JOIN后仍为NULL——语义和原标量子查询一致;但若你在内联视图里写NVL(MAX(sal), 0),空部门就变成0,业务逻辑就偏了
用WITH + MATERIALIZE强制物化中间结果
当子查询逻辑复杂、含多个过滤条件或被外层多次引用(比如同时用于WHERE和SELECT),优化器大概率不会自动物化它,此时显式控制更稳。
- Oracle中加提示:
WITH dept_stats AS (SELECT /*+ MATERIALIZE */ deptno, COUNT(*), AVG(sal) FROM emp GROUP BY deptno) SELECT ... - 避免CTE被“内联展开”:不加
MATERIALIZED时,Oracle可能把CTE内容复制进每个引用位置,导致同一聚合算多遍 - MySQL 8.0.23+可用
/*+ MATERIALIZE */提示,PostgreSQL则靠MATERIALIZED关键字;老版本MySQL只能用CREATE TEMPORARY TABLE并手动建索引
分区表上标量子查询特别危险:NL连接+索引错位=全分区扫描
如果主表按sale_date范围分区,子查询又常按customer_id查聚合值,直接写(SELECT SUM(amount) FROM sales s WHERE s.customer_id = c.id),很可能触发PARTITION LIST ALL——每个客户都扫一遍所有分区。
- 被驱动表
sales的本地索引必须是前缀索引:CREATE INDEX idx_sales_cust ON sales(customer_id, sale_date) LOCAL,其中sale_date是分区键且放第二位 - 单写
/*+ USE_NL(c s) */没用,必须配LEADING(c)确保customers是驱动表,并用INDEX(s idx_sales_cust)锁定访问路径 - 关掉自适应计划:
/*+ OPT_PARAM('_optimizer_adaptive_plans' 'false') */,防止运行时切走Hash Join打乱NL设计
真正卡住性能的,往往不是子查询本身有多复杂,而是它在执行计划里被当作“黑盒”反复调用,或者改写时漏掉GROUP BY、索引没对齐分区键、物化没强制——这些细节一错,速度就掉一个数量级。


















