导读:一条 SQL 执行 14 秒,执行计划里 PRJT2(投影)占了 52% 的消耗——罪魁祸首是 11 个同构标量子查询。它们的
WHERE条件一模一样,只是SELECT的列不同,导致同一张表被独立探测了 11 次。本文演示如何把这 11 次探测压缩成 1 次 GROUP BY 聚合 + 1 次外连接,并讲清楚为什么用(+)外连接而不是内连接(否则会丢数据)。
现场一条 SQL 执行时长达 14 秒。查看执行计划,定位到具体慢的部分是 PRJT2(投影) 节点,占所有消耗的 52%。
沿着执行计划继续往下看,慢的根源在这些标量子查询上:
SELECT DISTINCT emp.employee_id, emp.employee_code, emp.name,
(SELECT sea.account_number FROM exp_employee_accounts sea
WHERE emp.employee_id=sea.employee_id AND sea.primary_flag='Y' AND sea.enabled_flag='Y' AND ROWNUM=1) account_number,
(SELECT sea.account_name FROM exp_employee_accounts sea
WHERE emp.employee_id=sea.employee_id AND sea.primary_flag='Y' AND sea.enabled_flag='Y' AND ROWNUM=1) account_name,
(SELECT sea.bank_code FROM exp_employee_accounts sea WHERE ...) bank_code,
(SELECT sea.bank_name FROM exp_employee_accounts sea WHERE ...) bank_name,
(SELECT sea.bank_location_code FROM exp_employee_accounts sea WHERE ...) bank_location_code,
(SELECT sea.bank_location_name FROM exp_employee_accounts sea WHERE ...) bank_location_name,
(SELECT sea.province_code FROM exp_employee_accounts sea WHERE ...) province_code,
(SELECT sea.province_name FROM exp_employee_accounts sea WHERE ...) province_name,
(SELECT sea.city_code FROM exp_employee_accounts sea WHERE ...) city_code,
(SELECT sea.city_name FROM exp_employee_accounts sea WHERE ...) city_name
FROM exp_employees emp;
每个子查询的 WHERE / 驱动条件完全一样,只是 SELECT 取的列不同。执行时,exp_employee_accounts 这张表被独立探测了 11 次(每次都要按 employee_id 定位 + ROWNUM=1 取首行),扫描成本 ×11。
标量子查询本质是「逐行相关探测」——主表每行触发 11 次对账户表的定位+过滤+取首行。表一大、行数一多,11 倍重复开销线性放大。
把「N 次同条件子查询探测」压缩成「1 次 GROUP BY 聚合 + 1 次外连接」:
WHERE 条件收口成一次过滤;GROUP BY employee_id + MIN(N 列) 一次取齐所有字段;(+) 外连接保住「无匹配返回 NULL、行保留」的语义。把 11 个子查询并成一次聚合,放进 FROM 的内联视图 x:
SELECT DISTINCT emp.employee_id, emp.employee_code, emp.name,
x.account_number, x.account_name, x.bank_code, x.bank_name,
x.bank_location_code, x.bank_location_name, x.province_code,
x.province_name, x.city_code, x.city_name
FROM exp_employees emp
LEFT JOIN ( -- 关键1:外连接
SELECT employee_id,
MIN(account_number) AS account_number,
MIN(account_name) AS account_name,
MIN(bank_code) AS bank_code,
MIN(bank_name) AS bank_name,
MIN(bank_location_code) AS bank_location_code,
MIN(bank_location_name) AS bank_location_name,
MIN(province_code) AS province_code,
MIN(province_name) AS province_name,
MIN(city_code) AS city_code,
MIN(city_name) AS city_name
FROM exp_employee_accounts
WHERE primary_flag='Y' AND enabled_flag='Y' -- 关键2:条件收口 1 次
GROUP BY employee_id -- 关键3:1 次聚合
) x ON x.employee_id = emp.employee_id;
达梦兼容 Oracle 的
(+)外连接写法:AND x.employee_id(+) = s.employee_id,效果同LEFT JOIN。
| 关键点 | 说明 | 出错后果 |
|---|---|---|
| 条件收口 1 次 | 原 11 个子查询的 WHERE primary='Y' AND enabled='Y' 在视图内过滤 1 遍 |
过滤条件等价,备用账户被滤掉不干扰 |
| 外连接是灵魂 | LEFT JOIN / (+) 保证「无匹配返回 NULL、行保留」 |
误用 INNER JOIN 会丢掉无账户的员工 |
MIN(列) 取确定一条 |
当每个员工在过滤条件下只有一条主账户时,MIN 等价于 ROWNUM=1 |
若可能多条,需改用 ROW_NUMBER() OVER(PARTITION BY ...) 取确定一行 |
用 3 个员工 × 3 个账户的最小数据集,覆盖三种典型场景(有主账户 / 有主+备用 / 无账户),验证改写前后输出逐行完全一致:
EMPLOYEE_ID NAME ACCOUNT_NUMBER ACCOUNT_NAME BANK_CODE PROVINCE_NAME
1 张三 ACCT_1_MAIN 张三主账户 BOC 北京市
2 李四 ACCT_2_MAIN 李四主账户 ICBC 上海市
3 王五 NULL NULL NULL NULL
| 校验点 | 原版 | 改写版 | 结论 |
|---|---|---|---|
| 员工1 取主账户 | ACCT_1_MAIN | ACCT_1_MAIN | ✅ |
| 员工2 只取主账户(滤备用) | ACCT_2_MAIN | ACCT_2_MAIN | ✅ |
| 员工3 无账户 | NULL,行保留 | NULL,行保留 | ✅ |
redo 5.8M / 执行 758ms,整个查询进毫秒级。表越大、字段子查询越多,收益越明显(约 N 倍)。GROUP BY 聚合视图 + 外连接,把「逐行探测 ×N」变成「一次聚合 + 一次连接」。文章
阅读量
获赞
