在实际的业务系统中,SQL 语句的复杂程度往往远超预期。随着业务逻辑的不断累积,CTE、多层子查询、窗口函数、聚集计算被大量用于组织查询逻辑,由此带来了可读性的同时,也给查询优化器带来了巨大的挑战。其中,标量子查询(Scalar Subquery)导致的性能瓶颈是客户场景中出现频率最高、影响最为突出的问题之一。本文将围绕这一问题,从背景、技术难点、优化器设计到实测效果,进行系统性的介绍。
一、问题的根源:子查询的"逐行执行"陷阱
在报表类 SQL(OLAP 场景)中,一种极为常见的写法是在 SELECT 子句之后挂载多个标量子查询,每个子查询对主查询的每一行返回一个聚合值,例如:
sql
SELECT s11.id1
, (SELECT sum(s22.id1) FROM s22 WHERE s22.id3 = s11.id3)
, (SELECT sum(s22.id2) FROM s22 WHERE s22.id3 = s11.id3)
FROM s11;
从业务语义上看,这条 SQL 非常直观,也方便维护;但从执行角度看,却隐藏着严重的性能陷阱。传统优化器对此类语句的处理策略是:完整执行最外层查询,然后对外层结果的每一行分别执行一遍子查询;如果存在多个子查询,则分别独立执行。这意味着,如果外层表有 1 万行记录,子查询就会被执行 1 万次,且每次都要访问相同的底层数据。多个结构相似的子查询更是会造成成倍的资源浪费,根本问题不在子查询本身,而在于它被不断重复地执行。
这种写法在 OLAP 报表场景中几乎无处不在,而在 2026 年 HTAP(混合事务/分析处理)已成为主流架构的今天,同一套数据库系统往往需要同时承载交易型写入和分析型查询。如果分析侧的标量子查询不能被有效优化,其产生的大量重复扫描将直接影响事务侧的响应延迟,成为 HTAP 架构落地的隐形瓶颈。
二、技术难点:消除不是改写,是一场语义手术
将 SELECT 子句中的标量子查询消除,表面上看是一个直观有效的优化方向,但在数据库内核层面,这个问题远没有想象中简单。消除的本质是将子查询改写为连接操作,而这一改写极易破坏原始 SQL 的语义等价性(Equivalence),主要体现在以下两类场景:
第一,当子查询实际上返回的不是标量(即多于一行)时,原始语句会报错,这是数据库的语义约束;而如果将其改写为连接操作,查询不仅不报错,还会返回多条记录,造成优化前后结果集完全不同。
第二,当子查询中使用的是 COUNT 聚合函数时,情况更为微妙。SUM、MAX、MIN、AVG 在没有匹配记录时返回 NULL,而 COUNT 在同样情况下返回 0。如果将此类标量子查询直接改写为左外连接,无匹配时会补 NULL,与原始结果 0 不一致,引入了难以察觉的数据错误。
因此,不是所有的标量子查询都可以消除,优化器必须建立严格的等价性判定机制,先确认"消除之后结果会不会变",再决定是否动手。
三、优化器的设计:三步走的消除机制
针对上述问题,在最新版本的数据库内核中,引入了一套完整的标量子查询消除机制,整体思路可以概括为三步。
第一步,等价性判定。 优化器的目标不是"尽可能多地消除子查询",而是只识别绝对安全的优化机会。这一阶段会分析子查询的结构,判断是否满足语义等价条件,并对含有聚集、窗口函数、UNION 等复杂结构的子查询进行约束性判定,确保消除操作不会改变任何一行的计算结果。通过这一关才允许进入下一阶段。
第二步,子查询转换为左外连接。 通过等价性校验后,优化器将目标列中满足条件的相关标量子查询提取出来,转换为内联视图,再与外部的相关表进行左外连接。这一改写将原本需要逐行触发的子查询执行,变为一次性的连接操作,从根本上消除了重复扫描的问题,后续的连接优化策略(如 Hash Join、索引利用等)也得以在此基础上发挥作用。
第三步,相似子查询合并。 如果目标列中存在多个结构相似、仅输出字段不同的标量子查询,优化器会进一步将其合并为一个内联视图,再统一与外部查询进行连接。这一步针对的正是开篇示例中"两个子查询结构相同只是 sum 字段不同"的典型模式,避免了即使消除之后仍然重复扫描底层表的情况。
三步走下来,完整的执行流程如下:收到查询后,从目标列中收集满足消除条件的标量子查询及其所涉及的表和相关条件;从目标列中移除这些子查询;检查是否存在可合并的子查询并进行合并;最后针对每一个要消除的子查询,转换为和外部查询的左连接,返回改写后的查询。
四、实测效果:32 秒到 24 毫秒
用一个具体的测试来说明效果。构造两张各有 1 万行记录的表,分别写入序列数据,然后执行一条带有标量子查询的聚合查询:
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));
SELECT (SELECT sum(id) FROM t2 WHERE t1.id = t2.id) FROM t1;
在子查询消除功能关闭的情况下,优化器对 t1 的每一条记录都要对 t2 进行一次全表扫描,共扫描 t2 表 1 万次,总执行时间约 32 秒。
在子查询消除功能开启后,优化器将子查询改写为 Hash Left Join,对 t2 只执行一次扫描,总执行时间约 24 毫秒。
性能提升超过 1300 倍,数量级差距一目了然。从执行计划来看,改写前的计划树中存在 SubPlan 节点,loops=10000 清晰地记录了 1 万次重复扫描;改写后的计划树变为 Hash Left Join,底层扫描的 loops=1,印证了一次扫描完成全部计算的效果。
五、从优化器能力到原生 HTAP
这项优化的意义不止于一条 SQL 的加速。在 2026 年的技术背景下,HTAP 架构已经成为大型业务系统的主流选择,同一份数据需要同时支撑高频写入的事务处理和复杂的分析查询。标量子查询带来的重复扫描问题,正是 HTAP 场景下分析侧对事务侧产生干扰的典型根因之一。
一个能够在优化器层面自动识别、安全消除并合并标量子查询的数据库,本质上是将原本需要业务层或 DBA 手工改写的 SQL 调优工作内化到了引擎内部,让 OLAP 风格的查询写法也能在混合负载环境下以接近原生效率运行。这正是"原生 HTAP 能力"的一个具体体现------不是靠硬件资源堆量,而是靠优化器的智能决策,让相同的 SQL 在不同的负载压力下都能稳定输出。
六、总结
标量子查询消除的核心逻辑并不复杂:将一个会被重复执行 N 次的子查询,改写为只执行一次的连接操作,并将结构相似的多个子查询合并,减少底层表的访问次数。难的是如何在保证语义等价的前提下安全地完成这一改写,这对优化器的等价性判定能力提出了很高的要求。
从 32 秒到 24 毫秒,这不是参数调优,也不是硬件升级,而是优化器在查询计划层面做出的一次正确决策。对于大量依赖报表 SQL 的业务系统而言,这种优化所释放的性能空间,往往比任何基础设施投入都更直接、更持久。