目录
[典型业务SQL 示例](#典型业务SQL 示例)
[阶段 1:等价性判定 ------ 只做安全优化](#阶段 1:等价性判定 —— 只做安全优化)
[阶段 2:外连接改写 ------ 子查询变连接](#阶段 2:外连接改写 —— 子查询变连接)
[阶段 3:相似子查询合并 ------ 减少重复扫描](#阶段 3:相似子查询合并 —— 减少重复扫描)
在日常业务开发中,随着业务逻辑不断叠加,SQL 的编写难度也在持续上升。目前绝大多数业务系统都会用到 CTE、多层嵌套子查询、窗口函数以及聚合运算来梳理业务逻辑。这类写法虽然方便开发人员阅读、上手简单,但对数据库优化器并不友好,尤其是包含大量子查询的复杂 SQL,很容易出现性能拖慢的问题。本文结合金仓数据库 V009R002C014 新版本能力,着重讲解标量子查询消除功能的实现思路、技术难点以及实测优化效果,给大家分享一下数据库内核层面的优化实践。
一、标量子查询:常见业务写法,隐藏性能隐患
在实际开发工作里,我经常看到一类典型 SQL:开发者会在 SELECT 后拼接多个标量子查询,用来补充主表的数据计算。这类子查询结构相似度很高,往往只是查询字段不同。从业务角度来看,这种写法直观易懂,适配日常开发,但是在实际执行过程中,隐藏着很大的性能问题。
典型业务SQL 示例
sql
SELECT
s11.id1,
-- 子查询1:按id3分组求和id1
(SELECT sum(s22.id1) FROM s22 WHERE s22.id3 = s11.id3),
-- 子查询2:按id3分组求和id2
(SELECT sum(s22.id2) FROM s22 WHERE s22.id3 = s11.id3)
FROM s11;
上面这条 SQL 语法没有问题,业务逻辑也十分清晰,可是放到数据库中执行,就能明显发现两处不合理的执行缺陷:
-
循环执行,算力浪费:数据库会遍历 s11 表的每一条数据,每读取一行,就要完整执行一次子查询。一旦 s11 数据量上涨,子查询执行次数会同步增加,查询延迟会越来越高。
-
相似逻辑,重复计算:示例中两条子查询的基表、关联条件完全一致,仅仅聚合字段不同。但传统优化器无法识别这类相似逻辑,只能分开执行,造成大量不必要的资源消耗。
结合实际排查经验来看,标量子查询之所以拖慢 SQL 执行速度,问题并不出在语法本身,根本原因是子查询被无意义地反复执行。
二、技术难点:标量子查询消除的等价性挑战
想要优化这类 SQL,最直接的思路就是把标量子查询改写为连接方式执行。但改写操作在内核层面限制很多,首要前提就是保证优化前后语义完全一致,一旦改写出错,数据结果就会错乱,这也是行业内优化标量子查询普遍遇到的难点。结合数据库优化规则,主要存在两处语义风险:
两大核心等价性风险
-
返回值非标量风险:标量子查询定义为单行单列返回结果,若强行改写为连接查询,一旦子查询产生多条数据,原本应该报错的语句会直接返回多行数据,最终导致优化前后结果不一致。
-
聚合函数返回值差异:不同聚合函数在无匹配数据时的返回规则不一样: 如果不做判断直接改写,会把 count 统计的 0 错误补为 NULL,造成业务数据偏差。
-
COUNT无匹配时返回0; -
SUM/MAX/MIN/AVG无匹配时返回NULL。
-
因此,标量子查询消除不能盲目执行,必须制定严格的等价判定标准,只针对安全、不会改变语义的子查询做优化,规避数据错乱风险。
三、金仓数据库标量子查询消除设计:三阶段全链路优化
针对标量子查询的行业通病,金仓数据库在新版本中设计了一套完整优化逻辑,整体分为三步:等价性判定、外连接改写、相似子查询合并。整套流程优先保障数据准确性,再去提升执行性能,兼顾安全性和优化效果。
阶段 1:等价性判定 ------ 只做安全优化
优化器不会一味追求消除子查询,而是先做合规校验,判断当前子查询是否具备优化条件,避免误改出错。主要校验维度如下:
-
拆解子查询结构,核对语义等价基础条件;
-
对包含聚集、窗口、UNION 的复杂子查询进行约束判定;
-
单独甄别 COUNT 函数,防止返回值转换异常;
-
剔除逻辑复杂、无法保障等价性的特殊子查询。
这一步的核心目的就是筛选可优化对象,确保优化后 SQL 的查询结果和原始语句完全一致,从源头规避数据错误。
阶段 2:外连接改写 ------ 子查询变连接
通过等价校验后,优化器会把 SELECT 列表中标量子查询转换成内联视图,再和外层数据表做左外连接。这种改写方式可以彻底规避逐行重复执行子查询的问题,把循环执行改为单次执行。
改写逻辑示例
原始子查询:
sql
SELECT sum(id) FROM t2 WHERE t1.id = t2.id
改写为内联视图:
sql
SELECT id, sum(id) AS sum_id FROM t2 GROUP BY id
主查询与内联视图左外连接:
sql
SELECT t1.id, temp.sum_id
FROM t1
LEFT JOIN (SELECT id, sum(id) AS sum_id FROM t2 GROUP BY id) temp
ON t1.id = temp.id;
改写完成后,原本需要循环执行的子查询,现在只会执行一次,从根本上解决了逐行扫描带来的性能损耗。
阶段 3:相似子查询合并 ------ 减少重复扫描
实际业务中,一条 SQL 往往会存在多个结构相近的标量子查询。对此,金仓优化器支持相似子查询合并,将多条同类子查询整合为一个内联视图,仅扫描一次数据表,减少 I/O 资源消耗。
合并规则
-
多条子查询引用同一张数据表;
-
和主表的关联条件完全相同;
-
分组依据保持一致;
-
过滤条件无差异。
合并之后,多个聚合计算在同一视图内完成,不用反复扫描同一张表,进一步压缩执行耗时。
四、实测效果:性能提升数百倍
为直观验证优化能力,我们搭建了简单测试环境,模拟中等数据量场景,对比优化前后的执行效率,测试数据和语句均参考真实业务排查案例。
测试环境
-
测试表:
t1(1 万条记录)、t2(1 万条记录) -
建表及插入数据SQL:
sql
create table t1(id numeric(10,1));
create table t2(id numeric(10,1));
insert into t1 values(generate_series(1,10000));
insert into t2 values(generate_series(1,10000));
-- 查询t1每条id对应的t2表id之和
select (select sum(id) from t2 where t1.id=t2.id) from t1;
测试结果对比
|------------|---------------------------|-------|--------------|
| 优化状态 | 执行逻辑 | 耗时 | 性能提升 |
| 未消除子查询 | 对 t1 每条记录,全表扫描 t2(共 1 万次) | 32 秒 | 基准 |
| 消除子查询后 | 仅全表扫描 t2 一次,左外连接计算 | 24 毫秒 | 约 1333 倍 |
从测试数据能够明显看出,未优化前语句执行耗时高达32秒,优化后仅需24毫秒。这项优化改动不需要人工改写业务SQL,仅靠数据库内核自动处理,优化效果十分可观。
五、总结
在日常运维和SQL优化工作中,标量子查询是非常常见的写法,简单易用却极易造成性能隐患。金仓数据库推出的标量子查询消除功能,很好地解决了这一痛点,整套优化逻辑简单清晰:
-
以等价性判定为前提,保证业务数据零偏差;
-
把循环执行的子查询改为单次执行,减少算力消耗;
-
合并同类子查询,避免重复扫描数据表。
对于企业而言,该优化无需改动业务代码,依靠数据库原生能力完成SQL优化,能够有效降低复杂查询的执行延迟,适配中大数据量、多聚合查询的业务场景,为系统稳定运行提供可靠支撑。