注册
11 个标量子查询合并为 GROUP BY 聚合 + 外连接
专栏/技术分享/ 文章详情 /

11 个标量子查询合并为 GROUP BY 聚合 + 外连接

Gyyyyy 2026/10/23 53 0 0
摘要

达梦 SQL 优化:11 个标量子查询合并为 GROUP BY 聚合 + 外连接

导读:一条 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 次外连接」:

  1. WHERE 条件收口成一次过滤;
  2. GROUP BY employee_id + MIN(N 列) 一次取齐所有字段;
  3. (+) 外连接保住「无匹配返回 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,行保留 ✅

五、性能收益

  • 改写前:账户表被逐行 × 11 次探测(nested-loop 式),redo 大;
  • 改写后:账户表读 1 次 → 聚合 1 次 → 外连接 1 次,全集合化;
  • 实测:改写后 redo 5.8M / 执行 758ms,整个查询进毫秒级。表越大、字段子查询越多,收益越明显(约 N 倍)。

六、总结

  1. 「多个同构标量子查询」是高频慢 SQL 形态:WHERE 条件相同、只取列不同,导致同一张表被重复探测 N 次。
  2. 优化范式:合并成 GROUP BY 聚合视图 + 外连接,把「逐行探测 ×N」变成「一次聚合 + 一次连接」。
  3. 务必用外连接:标量子查询的「查不到返回 NULL、行保留」语义,只有外连接能完全承接,内连接会丢行。

评论
后发表回复

作者

文章

阅读量

获赞

扫一扫
联系客服