腾讯云数据库 PostgreSQL 让相关子查询跑上多核

一条带相关子查询的 SQL,在原生 PostgreSQL 里只能串行执行;腾讯云数据库 PostgreSQL 通过改造执行流程,支持了 SubPlan的并行执行,同样的这条 SQL 获得了 4 倍以上的加速。本文从子查询的基本概念讲起,带你看看 SubPlan 并行执行背后的内核原理。

在业务系统里,类似下面这样的 SQL 非常常见------"找出库存量高于同类平均值的商品"、"找出成绩高于本班平均分的学生":

在原生 PostgreSQL 中,这类查询有一个让人头疼的特性:只要查询里出现了相关子查询,整个查询就无法并行执行,哪怕外表有几百万行、哪怕机器有几十个核,也只能单进程从头扫到尾。

我们在腾讯云数据库 PostgreSQL 内核中支持了 SubPlan 并行执行(开启tencentdb_enable_subplan_parallel 参数),让这类查询也能享受多核加速。

1 什么是子查询?

子查询(subquery)就是嵌套在另一条 SQL 语句内部的 SELECT 语句。按照出现的位置,常见的有两种:

1.出现在 FROM 子句中,也叫派生表(derived table):

这种子查询在优化器里有被"上拉"(pull up)到外层的机会,合并成一条查询统一规划,通常不是性能瓶颈。

2.出现在 WHERE / SELECT 列表等表达式位置,PostgreSQL 在解析阶段被表示为 SubLink 节点:

第二种又按是否引用了外层查询的列分为两类:

  • **不相关子查询:**子查询完全自包含,不依赖外层的任何变量。

    WHERE b > (SELECT avg(b) FROM t2) ------ 不管外层当前是哪一行,子查询的结果都一样。

  • 相关子查询(correlated subquery) :子查询里引用了外层的列,比如上面例子里的 t2.a = t1.a。外层每换一行,子查询的语义就跟着变,理论上必须逐行重新计算

相关子查询是业务 SQL 的常客,只能靠优化器阶段做子查询的拉升(转成JOIN),而对于那些不能转 JOIN 的相关子查询,由于 PostgreSQL 长期不支持带有相关子查询的 SQL 并行,经常导致比较严重的性能劣化。

2 SubPlan 与 InitPlan:一对容易混淆的兄弟

优化器在规划子查询时,会把表达式位置的 SubLink 转换成计划树里的一个 SubPlan 执行器节点。但打开 EXPLAIN,你会看到两种不同的显示:SubPlan N 和 InitPlan N。它们虽然都由 SubPlan 节点承载,执行模型却完全不同。

InitPlan:先算一次,结果存进参数

如果子查询是不相关的,规划器会把它做成 InitPlan (初始计划):它在外层计划开始执行前只执行一次,结果被写入一个内部参数槽位(PARAM_EXEC 类型的 Param),外层表达式直接引用这个参数。

注意两个细节:InitPlan 的结果在外层 Filter 里被引用为 (InitPlan 1).col1;而且这条查询整体是并行的 ------InitPlan 自己内部可以并行,外层扫描也可以并行。也就是说,原生 PostgreSQL 对 InitPlan 与并行的兼容是没问题的

SubPlan:逐行触发,随时重算

如果子查询是相关的 ,它就成了真正的 SubPlan :它作为外层表达式的一部分,在扫描外表时每行都要重新执行(或者每组新的相关参数到达时重算一次)。(注:PostgreSQL 还支持一种 hash 变体(hashed SubPlan):对不相关的 IN/ANY 子查询先建一张哈希表,之后逐行探测,避免反复扫描)。

这就是原生 PostgreSQL的行为:整条查询退化为纯串行------外表是普通的 Seq Scan,连 Gather 节点的影子都没有。

InitPlan 和 SubPlan 的区别如下表:

3 我们是怎么支持 SubPlan 并行的

原生 PG 拒绝并行的三步链条

先看内核里发生了什么。规划器判断一个表达式能否放进并行计划,靠的是 clauses.c 中的 max_parallel_hazard_walker。对相关子查询,存在一条环环相扣的"连坐"链条:

**1.相关参数被判定为并行受限。**规划子查询时,外层的 t1.a 会被换算成一个内部参数(PARAM_EXEC)传进子查询。这个参数出现在子查询的过滤条件里(t2.a = $1),而并行检查针对 Param 节点的规则是:凡是不能由 leader 算好再广播给 worker 的 PARAM_EXEC 参数,一律视为 parallel-restricted。

**2.子计划内部无法生成并行路径。**带着这个受限参数,子查询自己的扫描路径全部不满足 parallel_safe,这个 parallel_safe=false 的情况也顺延传递给了 SubPlan 节点。

**3.外层表达式被"连坐"。**外层 Filter 是 b > (SubPlan 1),并行检查遇到 SubPlan 节点时检查它的 parallel_safe 标志是 false,整个 Filter 被判为受限,于是外表的并行扫描路径全部作废,整条查询串行。

注意这里的关键:这条链条其实过度保守了。

为什么翻转 parallel_safe 是安全的?

parallel-restricted 的本意是"这个计算不能交给并行 worker"。对于参数,担心的是"参数由一个进程写入、另一个进程读取"------跨进程确实做不到。

但相关 SubPlan 的执行模型是:子计划整体嵌在外层表达式里,参数的写入(外层当前行的值)和读取(子查询里的引用)发生在同一个进程的同一个执行器状态(EState)中。并行查询中,每个 worker 会收到 Gather 之下完整计划树的一份副本,包括 SubPlan 及其参数槽位。worker 处理自己分到的外表数据块时,用自己的行值设置参数、再自己执行子计划------全程单进程内闭环,根本不存在跨进程共享。

换句话说:相关 SubPlan 天然是"进程内自治"的,把它标成 parallel_safe 在语义上是成立的,原生 PostgreSQL 只是用了一刀切的参数规则。

因此,如果我们能够把 SubPlan 的粒度划分的细一些,就会发现,有些 SubPlan 支持并行执行是完全没有问题的,PostgreSQL 现在一刀切的政策是一种懒政。

腾讯云数据库 PostreSQL 在支持 SubPlan 并行的时候,也严谨的指定了以下边界:

  • **有 CTE 就放弃:**任一上层查询带 CTE 时 hook 直接退出,保持原生行为。CTE 子计划在原生 PostgreSQL 中永远是 parallel_safe = false(CteScan 之间通过共享参数槽位协调),我们不去动它。

  • **只对真相关 SubPlan 生效:**不相关子查询原生已能与并行共存,无需干预。

  • **非 SELECT 语句不支持:**原生 PostgreSQL的顶层规划器对非 CMD_SELECT 语句直接不考虑并行路径,UPDATE ... SET x = (SELECT ...) 之类不在本特性范围内。

4 SubPlan 并行的性能优势

测试环境

  • 32 核测试机,基于 PostgreSQL 18 的内核。

  • 外表 bench_t1:50 万行;内表 bench_t2:100 万行,t2(a) 上有索引。

  • 查询即本文开头的那条相关子查询,数据全部预热到 shared buffer。

  • max_parallel_workers_per_gather = 4,bench_t1 强制 4 个并行 worker。

执行计划长这样

GUC 打开后,外表变成并行扫描,逐行触发的 SubPlan 保持原样、跟随计划副本进入每个 worker:

实测数据

每组跑 3 次取典型值,两份计划的结果完全一致 ,但并行 SubPlan 性能提升4.2x

EXPLAIN ANALYZE 里的一个细节很能说明问题:

SubPlan 1 的 loops=500000------子查询的总执行次数一点没少,50 万行还是 50 万次。区别在于:串行时这 50 万次子查询全部压在一个进程上;并行后,leader 和 4 个 worker 各自认领 10 万行,各自执行自己那 10 万次子查询。总工作量不变,墙钟时间被 5 个进程摊薄了,这是接近线性加速的本质原因。

什么时候收益最大

  • **外表行数多:**子查询是逐行触发的,外表行数就是子查询执行次数,行数越多,被摊薄的工作量越大。

  • **子查询单次执行不便宜:**哪怕每次只是一次索引扫描 + 聚合,乘上百万级的执行次数也是大头。

  • 反之,外表很小(比如几千行)时,worker 启动和 Gather 的固定开销可能吃掉收益------不过这无需担心,代价模型会替用户把关,不值得并行时它不会选并行计划。

5 结语

相关子查询是业务 SQL 中最自然的写法之一,"因为写了子查询所以无法并行"对用户来说始终是一种遗憾。SubPlan 并行的核心洞察其实很朴素:相关子查询的参数读写天然封闭在单个进程的执行器状态里,把它放行给并行 worker 是语义安全的,原生 PostgreSQL 的参数规则只是过于一刀切。

如果你也有关联子查询跑不快的困扰,不妨在自己的工作负载上试试tencentdb_enable_subplan_parallel。

相关推荐
陈皮波比茶1 小时前
计算机二级MySQL笔记
java·数据库
zzzll11111 小时前
LangChain 1.3 新特性详解与实战指南
java·数据库·langchain
石小千1 小时前
MySQL 8.4设置密码策略
数据库·mysql
神明不懂浪漫1 小时前
【第四章】索引——B+树、回表,加快数据库的查找能力的利器
开发语言·数据结构·数据库·经验分享·笔记·b树
zcmodeltech2 小时前
智慧矿山沙盘模型多场景控制系统设计——基于STM32与Modbus RTU的智能开采、无人矿卡、数字孪生全场景联动方案
数据库·stm32·单片机·嵌入式硬件·物联网
小码过河.2 小时前
ocean自增主键模式说明
数据库·oracle
leoZ2312 小时前
2026-09-08-mysql-57-init-walkthrough
前端·javascript·数据库·vue.js·opencv·mysql·adb
云飞云共享云桌面2 小时前
钣金制造研发优化:不采购传统工作站,实现 SolidWorks 团队共用服务器开展三维建模
运维·服务器·网络·数据库·3d·制造