PostgreSQL内核学习:修复 WindowAgg Run Condition 判断逻辑错误的优化
- 一、补丁基础信息
- 二、Patch背景与问题描述
-
- [1. WindowAgg Run Condition 核心思想](#1. WindowAgg Run Condition 核心思想)
- [2. 原逻辑存在的核心问题](#2. 原逻辑存在的核心问题)
- 三、Patch整体优化目标
-
- [1. 修复`WindowFunc`包含`subplan`时的误判问题](#1. 修复
WindowFunc包含subplan时的误判问题) - [2. 增强`qual pushdown`安全检查机制](#2. 增强
qual pushdown安全检查机制) - [3. 重构`pushdown`判断逻辑,支持三态决策](#3. 重构
pushdown判断逻辑,支持三态决策)
- [1. 修复`WindowFunc`包含`subplan`时的误判问题](#1. 修复
- 四、核心代码结构变化
-
- [check_output_expressions 函数](#check_output_expressions 函数)
- [五、`Pushdown` 安全标志位设计](#五、
Pushdown安全标志位设计) - [六、pushdown_safety_info 结构重构](#六、pushdown_safety_info 结构重构)
-
- [1. 旧结构(存在缺陷)](#1. 旧结构(存在缺陷))
- [2. 新结构(优化后)](#2. 新结构(优化后))
- [3. 设计意义](#3. 设计意义)
- [七、qual_is_pushdown_safe() 逻辑重构](#七、qual_is_pushdown_safe() 逻辑重构)
-
- [1. 函数返回值变更](#1. 函数返回值变更)
- [2. 枚举类型定义](#2. 枚举类型定义)
- [3. 核心逻辑重构(关键代码解析)](#3. 核心逻辑重构(关键代码解析))
- [4. 重构意义](#4. 重构意义)
- 八、关键安全检查逻辑
-
- [1. 检查1:条件(qual)中是否包含subplan](#1. 检查1:条件(qual)中是否包含subplan)
- [2. 检查2:条件/窗口函数中是否包含volatile函数](#2. 检查2:条件/窗口函数中是否包含volatile函数)
- [3. 检查3:subquery输出列的安全性(结合unsafeFlags)](#3. 检查3:subquery输出列的安全性(结合unsafeFlags))
- [九、WindowFunc Subplan 检查](#九、WindowFunc Subplan 检查)
- 九、Planner流程变化
-
- [1. 旧流程(PostgreSQL 15之前,未修复时)](#1. 旧流程(PostgreSQL 15之前,未修复时))
- [2. 新流程(Patch修复后)](#2. 新流程(Patch修复后))
- [3. 流程变化的核心价值](#3. 流程变化的核心价值)
- 十、执行计划变化示例
-
- [示例1:正常场景(可作为`Run Condition`,修复前后无变化)](#示例1:正常场景(可作为
Run Condition,修复前后无变化)) - [示例2:`volatile`函数场景(修复后不再生成`Run Condition`)](#示例2:
volatile函数场景(修复后不再生成Run Condition)) - [示例3:`subplan`场景(修复后不再生成`Run Condition`)](#示例3:
subplan场景(修复后不再生成Run Condition))
- [示例1:正常场景(可作为`Run Condition`,修复前后无变化)](#示例1:正常场景(可作为
- 十一、总结
声明 :本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。
本文主要参考了 postgresql-18 beta2 的开源代码和《PostgresSQL数据库内核分析》一书
一、补丁基础信息
- 补丁作者 :David Rowley drowley@postgresql.org
- 提交日期:2023年3月17日(时区+1300)
- 补丁用途 :修复判断
WindowAgg运行条件(run condition)安全性的逻辑错误,主要解决未正确检查条件中包含子查询的问题,同时修正其他不允许将条件转换为WindowClause运行条件的场景 - 关联Bug :
#17826 - 讨论链接 :https://postgr.es/m/17826-7d8750952f19a5f5@postgresql.org
- 回溯版本 :回溯至
15版本(9d9c02ccd提交引入了有问题的逻辑)
二、Patch背景与问题描述
在 PostgreSQL 查询优化器中,存在一种重要的执行优化机制------WindowAgg Run Condition Optimization,其核心目标是通过提前终止窗口函数计算,降低执行开销,提升查询性能。
1. WindowAgg Run Condition 核心思想
当SQL语句存在"子查询包含窗口函数 + 外层过滤窗口函数结果 "的结构时,优化器可将外层过滤条件转换为WindowAgg节点的Run Condition,从而在WindowAgg执行阶段提前停止窗口计算,避免对整个窗口的所有行进行无效计算。
典型场景示例(优化前SQL结构):
sql
SELECT *
FROM (
SELECT empno,
salary,
dense_rank() OVER (ORDER BY salary DESC) dr -- 窗口函数:按薪资倒序排名
FROM empsalary
) t
WHERE dr = 1; -- 过滤排名为1的记录
优化器可将外层 WHERE dr = 1 转换为 WindowAgg 的 Run Condition,执行计划优化后如下:
sql
WindowAgg
Run Condition: (dense_rank() <= 1) -- 提前终止条件:排名超过1则停止计算
-> Sort
Sort Key: empsalary.salary DESC
-> Seq Scan on empsalary
优化价值 :无需计算所有行的排名,当排名超过1时直接终止窗口计算,大幅减少计算量,尤其适用于大表窗口查询场景。
2. 原逻辑存在的核心问题
在 commit 9d9c02ccd 中,PostgreSQL 引入了上述Run Condition优化逻辑,但该逻辑存在严重疏漏------没有正确检测过滤条件(qual)及窗口函数(WindowFunc)中是否存在子查询(subquery)或子计划(subplan)。
问题场景示例1 :WindowFunc 包含子计划
sql
SELECT *
FROM (
SELECT empno,
salary,
count((SELECT 1)) OVER (ORDER BY empno) c -- 窗口函数内包含子查询(子计划)
FROM empsalary
) t
WHERE c = 1;
问题分析 :窗口函数 count((SELECT 1)) OVER (...) 内部包含子查询,会生成子计划(InitPlan),但原逻辑未检测该场景,仍可能将 WHERE c = 1 转换为Run Condition,导致执行语义错误------子计划的执行逻辑与WindowAgg提前终止机制冲突,可能返回错误结果。
问题场景示例2:过滤条件包含子查询
sql
SELECT *
FROM (
SELECT empno,
salary,
dense_rank() OVER (ORDER BY salary DESC) dr
FROM empsalary
) t
WHERE dr = (SELECT 1 FROM dual); -- 过滤条件包含子查询
问题分析 :原逻辑未检查过滤条件中的子查询,误将该条件转换为Run Condition,而子查询的执行依赖于外层上下文,提前终止WindowAgg会导致子查询执行异常,破坏SQL语义。
核心风险 :此类误判会导致WindowAgg执行结果错误,违背"优化不能改变SQL语义"的核心设计原则,因此必须通过补丁修复。
三、Patch整体优化目标
该Patch的核心目标是 更严格地判断 WindowAgg Run Condition 的安全性 ,在不影响原有优化效果的前提下,堵住语义错误漏洞,同时增强条件下推(qual pushdown)的安全检查机制,使优化逻辑更严谨、更灵活。具体包含三大核心目标:
1. 修复WindowFunc包含subplan时的误判问题
新增对WindowFunc的子计划检查,确保当窗口函数内部包含subplan时,不会将外层过滤条件转换为Run Condition,避免语义错误。
2. 增强qual pushdown安全检查机制
完善过滤条件(qual)的安全检查,补充"子查询/子计划检查""易失性函数检查"等边界场景,确保只有安全的条件才能被下推到子查询。
3. 重构pushdown判断逻辑,支持三态决策
将原有的"布尔型(是/否)"pushdown判断,升级为"三态决策",区分三种不同的安全状态,使优化器能根据具体场景做更细粒度的优化决策,具体分类如下:
| 类型 | 含义 | 优化器处理方式 |
|---|---|---|
PUSHDOWN_SAFE |
条件可安全下推到子查询 | 将条件下推至子查询,减少上层计算压力 |
PUSHDOWN_UNSAFE |
条件完全不安全,既不能下推,也不能作为Run Condition |
将条件保留在上级查询,作为上层过滤条件 |
PUSHDOWN_WINDOWCLAUSE_RUNCOND |
条件不能下推,但可作为WindowAgg的Run Condition |
不进行条件下推,将条件转换为Run Condition,优化WindowAgg执行 |
四、核心代码结构变化
Patch主要修改文件为 src/backend/optimizer/path/allpaths.c(PostgreSQL查询优化器路径生成的核心文件),同时修改测试文件以验证修复效果,具体文件修改如下:
src/backend/optimizer/path/allpaths.c:核心逻辑修改,包括数据结构重构、函数逻辑优化、安全检查新增,是本次Patch的核心文件。src/test/regress/expected/window.out:同步更新测试用例的执行计划输出,确保修复后的逻辑符合预期。src/test/regress/sql/window.sql:新增2个测试用例,分别验证"WindowFunc包含易失性函数""WindowFunc包含subplan"场景,防止回归。
核心代码结构变化 :新增2个关键数据结构 (枚举+位掩码宏),重构3个核心函数 ,新增2处安全检查逻辑,具体如下:
- 新增枚举
pushdown_safe_type:定义三态pushdown决策类型。 - 新增位掩码宏
UNSAFE_*:记录子查询输出列不安全的具体原因。 - 重构结构体
pushdown_safety_info:新增unsafeFlags,替代原有的unsafeColumns。 - 重构函数
qual_is_pushdown_safe:返回值从bool改为pushdown_safe_type,新增子查询检查逻辑。 - 修改函数
check_output_expressions:适配unsafeFlags,精细化标记不安全原因。 - 修改函数
find_window_run_conditions:新增WindowFunc子计划检查。
check_output_expressions 函数
check_output_expressions 函数的核心作用是检查子查询输出列的安全性,为谓词下推提供判断依据。
修改前,函数仅能简单标记输出列"是否不安全 ",无具体标志位区分不安全原因,无法适配三态pushdown决策,导致优化器无法区分"不能下推但可作为Run Condition"等场景,存在优化浪费和语义误判风险。
是
否
是
否
是
否
否
是
否
是
继续遍历下一列
当前列是resjunk列?
检查是否含返回集合函数(SRF)
标记不安全(无具体标志位,仅记录列不安全)
检查是否含volatile函数
检查是否在DISTINCT ON子句中
检查是否在所有窗口PARTITION BY中
注:仅标记列是否不安全,无法区分具体不安全原因;无精细化标志位,不支持三态pushdown决策
修改后,函数新增位掩码标志位(UNSAFE_*系列),针对返回集合函数、volatile函数、非DISTINCT ON列、非窗口PARTITION BY列四种场景,分别标记具体不安全原因,既实现了不安全原因的精细化记录,又为后续qual_is_pushdown_safe函数的三态判断提供支撑,确保优化器能在保证语义安全的前提下,做出更合理的优化决策,同时堵住了原有逻辑的漏洞。
是
否
是
否
是
否
否
是
否
是
继续遍历下一列
当前列是resjunk列?
检查是否含返回集合函数(SRF)
标记UNSAFE_HAS_SET_FUNC标志位
检查是否含volatile函数
标记UNSAFE_HAS_VOLATILE_FUNC标志位
检查是否在DISTINCT ON子句中
标记UNSAFE_NOTIN_DISTINCTON_CLAUSE标志位
检查是否在所有窗口PARTITION BY中
标记UNSAFE_NOTIN_PARTITIONBY_CLAUSE标志位
注:通过位掩码标志位精细化记录不安全原因,支持后续三态pushdown决策,区分致命/非致命不安全场景
函数源码如下所示:
c
/*
* 检查子查询的输出表达式(targetList),判断哪些输出列对于谓词下推(predicate pushdown)是不安全的。
* 如果某个输出列被判定为不安全,则在 safetyInfo->unsafeFlags 对应位置打上标记。
*/
static void
check_output_expressions(Query *subquery, pushdown_safety_info *safetyInfo)
{
ListCell *lc; // 用于遍历 targetList 的列表单元指针
// 遍历子查询的 targetList(投影列表)中的每一项
foreach(lc, subquery->targetList)
{
// 获取当前 TargetEntry(目标列定义)
TargetEntry *tle = (TargetEntry *) lfirst(lc);
// 如果是 resjunk 列(内部使用的临时列,不会出现在最终输出),直接跳过,不检查
if (tle->resjunk)
continue; /* 忽略 resjunk 列,不参与安全检查 */
/* ------------------ 检查点 1:是否存在返回集合的函数(Set Returning Functions) ------------------ */
// 只有当子查询明确包含目标级别的 SRF 且该列尚未被标记为含有SRF时才检查
if (subquery->hasTargetSRFs &&
(safetyInfo->unsafeFlags[tle->resno] & UNSAFE_HAS_SET_FUNC) == 0 &&
// 判断该表达式是否会返回多行(典型如 unnest、generate_series 等)
expression_returns_set((Node *) tle->expr))
{
// 标记该输出列含有 set-returning 函数,此类列通常不能安全下推过滤条件
safetyInfo->unsafeFlags[tle->resno] |= UNSAFE_HAS_SET_FUNC;
continue; // 已确定不安全,跳过后续检查
}
/* ------------------ 检查点 2:是否存在 volatile 函数 ------------------ */
// 如果该列尚未被标记为含有 volatile 函数,则检查表达式中是否包含
if ((safetyInfo->unsafeFlags[tle->resno] & UNSAFE_HAS_VOLATILE_FUNC) == 0 &&
// volatile 函数指每次执行结果可能不同的函数(如 random(), clock_timestamp(), nextval() 等)
contain_volatile_functions((Node *) tle->expr))
{
// 标记该列含有 volatile 函数,volatile 函数不允许安全下推(会改变语义)
safetyInfo->unsafeFlags[tle->resno] |= UNSAFE_HAS_VOLATILE_FUNC;
continue; // 已不安全,跳过后续检查
}
/* ------------------ 检查点 3:子查询使用了 DISTINCT ON,但该列不在 DISTINCT ON 依据中 ------------------ */
if (subquery->hasDistinctOn &&
// 仅当尚未标记为 DISTINCT ON 不安全时才检查
(safetyInfo->unsafeFlags[tle->resno] & UNSAFE_NOTIN_DISTINCTON_CLAUSE) == 0 &&
// 判断当前 TargetEntry 是否出现在 DISTINCT ON 所依赖的排序子句(distinctClause)中
!targetIsInSortList(tle, InvalidOid, subquery->distinctClause))
{
// 如果不在 DISTINCT ON 的排序列表中,则认为是非 DISTINCT 列,下推过滤条件可能导致错误结果
safetyInfo->unsafeFlags[tle->resno] |= UNSAFE_NOTIN_DISTINCTON_CLAUSE;
continue; // 已判定不安全,跳过后续
}
/* ------------------ 检查点 4:子查询使用了窗口函数,但该列未出现在所有窗口的 PARTITION BY 中 ------------------ */
if (subquery->hasWindowFuncs &&
// 注意:这里复用了前一个标志位,确保 DISTINCT ON 不安全时不再检查窗口(优先级处理)
(safetyInfo->unsafeFlags[tle->resno] & UNSAFE_NOTIN_DISTINCTON_CLAUSE) == 0 &&
// 检查该列是否出现在**每一个**窗口函数的 PARTITION BY 子句中
!targetIsInAllPartitionLists(tle, subquery))
{
// 如果至少有一个窗口函数的 PARTITION BY 不包含该列,则下推过滤条件可能改变窗口计算结果
safetyInfo->unsafeFlags[tle->resno] |= UNSAFE_NOTIN_PARTITIONBY_CLAUSE;
continue; // 已不安全,结束对该列的检查
}
}
// 函数结束,所有需要标记的列都已处理
}
五、Pushdown 安全标志位设计
为了精细化记录子查询输出列"为什么不安全",Patch新增了bitmask(位掩码)标志,通过不同的位来标记不同的不安全原因,既节省内存,又能支持多原因同时标记,设计简洁高效。
核心源码定义:
c
/* Bitmask flags for pushdown_safety_info.unsafeFlags */
#define UNSAFE_HAS_VOLATILE_FUNC (1 << 0) // 第0位:包含易失性函数
#define UNSAFE_HAS_SET_FUNC (1 << 1) // 第1位:包含返回集合的函数
#define UNSAFE_NOTIN_DISTINCTON_CLAUSE (1 << 2) // 第2位:不在DISTINCT ON子句中
#define UNSAFE_NOTIN_PARTITIONBY_CLAUSE (1 << 3) // 第3位:不在所有窗口函数PARTITION BY子句中
#define UNSAFE_TYPE_MISMATCH (1 << 4) // 第4位:集合操作中列类型不匹配
各标志位含义及设计原因详解:
| 标志位 | 含义 | 设计原因(为什么标记为不安全) |
|---|---|---|
UNSAFE_HAS_VOLATILE_FUNC |
子查询输出列包含易失性函数(如random()、now()) |
易失性函数每次执行结果不同,下推条件会导致子查询执行结果不一致,破坏语义;作为Run Condition会导致WindowAgg提前终止逻辑异常。 |
UNSAFE_HAS_SET_FUNC |
子查询输出列包含返回集合的函数(如unnest()) |
集合函数会返回多行结果,下推条件会引入集合运算,破坏子查询的输出结构,导致上层查询语义错误。 |
UNSAFE_NOTIN_DISTINCTON_CLAUSE |
子查询输出列不在DISTINCT ON子句中 |
DISTINCT ON的语义是"按指定列去重",非DISTINCT ON列的取值不唯一,下推条件会破坏去重语义,导致结果错误。 |
UNSAFE_NOTIN_PARTITIONBY_CLAUSE |
子查询输出列不在所有窗口函数的PARTITION BY子句中 |
窗口函数的计算依赖PARTITION BY分区,非分区列的取值在分区内不固定,下推条件会破坏窗口计算语义;但作为Run Condition仅提前终止分区计算,不影响分区内语义,因此单独标记。 |
UNSAFE_TYPE_MISMATCH |
集合操作(如UNION、INTERSECT)中,子查询输出列与顶层集合操作的列类型不匹配 |
集合操作要求所有子查询的输出列类型一致,类型不匹配时,下推条件会导致类型转换错误,破坏执行逻辑。 |
设计亮点:位掩码的使用的优势在于"高效存储+多状态组合",例如一个列既包含易失性函数,又不在DISTINCT ON子句中,可通过 UNSAFE_HAS_VOLATILE_FUNC |
`UNSAFE_NOTIN_DISTINCTON_CLAUSE 一次性标记,无需额外变量,节省内存且查询高效。 |
六、pushdown_safety_info 结构重构
原有的 pushdown_safety_info 结构体仅能标记"某列是否不安全",但无法记录"为什么不安全",导致优化器无法做细粒度决策(比如无法区分"不能下推但可作为Run Condition"的场景)。因此Patch对该结构体进行了重构,核心是新增 unsafeFlags 字段。
1. 旧结构(存在缺陷)
c
typedef struct pushdown_safety_info
{
bool *unsafeColumns; // 仅标记"某列是否不安全",无具体原因
bool unsafeVolatile; // 不允许下推易失性条件
bool unsafeLeaky; // 不允许下推泄漏性条件
} pushdown_safety_info;
缺陷分析:unsafeColumns 是一个布尔数组,仅能表示"是/否",无法区分不安全的具体原因。例如,一个列因"包含易失性函数"不安全,与因"不在PARTITION BY子句"不安全,在旧结构中无法区分,导致优化器只能统一处理(均视为"不能下推 "),无法利用"可作为Run Condition"的场景做优化。
2. 新结构(优化后)
c
typedef struct pushdown_safety_info
{
unsigned char *unsafeFlags; // 新增:位掩码数组,记录每列不安全的具体原因
bool unsafeVolatile; // 保留:不允许下推易失性条件
bool unsafeLeaky; // 保留:不允许下推泄漏性条件
} pushdown_safety_info;
优化点详解:
- 删除
bool *unsafeColumns:用unsafeFlags替代,避免冗余。 - 新增
unsigned char *unsafeFlags:数组长度与子查询输出列数量一致,每一个元素是一个位掩码,存储该列的所有不安全原因(通过第四节的UNSAFE_*标志位组合)。 - 保留原有字段:
unsafeVolatile和unsafeLeaky用于全局控制易失性、泄漏性条件的下推,与unsafeFlags形成"全局+局部"的双层安全检查。
3. 设计意义
重构后的结构体,核心价值是实现了"不安全原因的精细化记录",使优化器能够根据具体的不安全原因,做出不同的优化决策------例如:
- 若某列标记为
UNSAFE_NOTIN_PARTITIONBY_CLAUSE(仅不在PARTITION BY子句):不能下推,但可作为Run Condition。 - 若某列标记为
UNSAFE_HAS_VOLATILE_FUNC(包含易失性函数):既不能下推,也不能作为Run Condition。
这种设计让优化逻辑更灵活,既保证了语义安全,又不浪费优化机会,体现了PostgreSQL优化器"精细优化、安全优先"的设计理念。
七、qual_is_pushdown_safe() 逻辑重构
qual_is_pushdown_safe() 是查询优化器中判断"过滤条件(qual)能否下推到子查询"的核心函数,Patch对其进行了彻底重构,核心变化是"返回值从bool改为三态枚举",同时新增了关键的安全检查逻辑。
1. 函数返回值变更
-
旧版本返回值:
booltrue:条件可下推false:条件不可下推(无进一步区分)
-
新版本返回值:
pushdown_safe_type(枚举类型)PUSHDOWN_SAFE:条件可安全下推PUSHDOWN_UNSAFE:条件完全不安全PUSHDOWN_WINDOWCLAUSE_RUNCOND:不能下推,但可作为Run Condition
2. 枚举类型定义
c
/* Return type for qual_is_pushdown_safe */
typedef enum pushdown_safe_type
{
PUSHDOWN_UNSAFE, // 不安全,不能下推,也不能作为Run Condition
PUSHDOWN_SAFE, // 安全,可下推
PUSHDOWN_WINDOWCLAUSE_RUNCOND // 不能下推,但可作为WindowClause的run condition
} pushdown_safe_type;
3. 核心逻辑重构(关键代码解析)
重构后的函数逻辑,新增了5个核心检查步骤,同时适配了 unsafeFlags 的精细化判断,核心代码片段如下(关键部分加注释):
c
static pushdown_safe_type
qual_is_pushdown_safe(Query *subquery, Index rti, RestrictInfo *rinfo,
pushdown_safety_info *safetyInfo)
{
pushdown_safe_type safe = PUSHDOWN_SAFE; // 默认安全,逐步检查
Node *qual = (Node *) rinfo->clause;
List *vars;
ListCell *vl;
/* 检查1:条件中不能包含子计划(核心修复点) */
if (contain_subplans(qual))
return PUSHDOWN_UNSAFE;
/* 检查2:易失性条件不能下推(全局控制) */
if (safetyInfo->unsafeVolatile &&
contain_volatile_functions((Node *) rinfo))
return PUSHDOWN_UNSAFE;
/* 检查3:泄漏性条件不能下推(全局控制) */
if (safetyInfo->unsafeLeaky &&
contain_leaked_vars(qual))
return PUSHDOWN_UNSAFE;
/* 检查4:提取条件中的变量,验证是否为当前子查询的变量 */
vars = pull_var_clause(qual, PVC_RECURSE_AGGREGATES | PVC_RECURSE_WINDOWFUNCS);
foreach(vl, vars)
{
Node *node = (Node *) lfirst(vl);
// 变量必须是Var类型(不能是常量、函数等)
if (!IsA(var, Var))
{
safe = PUSHDOWN_UNSAFE;
break;
}
// 变量必须属于当前子查询(varno == 子查询的rti)
if (var->varno != rti)
{
safe = PUSHDOWN_UNSAFE;
break;
}
// 变量不能是系统列(varattno == 0 为系统列)
if (var->varattno == 0)
{
safe = PUSHDOWN_UNSAFE;
break;
}
/* 检查5:根据unsafeFlags判断变量对应的列是否安全 */
if (safetyInfo->unsafeFlags[var->varattno] != 0)
{
// 若包含"致命不安全"标志,直接标记为UNSAFE
if (safetyInfo->unsafeFlags[var->varattno] &
(UNSAFE_HAS_VOLATILE_FUNC | UNSAFE_HAS_SET_FUNC |
UNSAFE_NOTIN_DISTINCTON_CLAUSE | UNSAFE_TYPE_MISMATCH))
{
safe = PUSHDOWN_UNSAFE;
break;
}
else
{
// 仅包含UNSAFE_NOTIN_PARTITIONBY_CLAUSE,标记为RUNCOND
safe = PUSHDOWN_WINDOWCLAUSE_RUNCOND;
// 不break,继续检查其他变量(可能有其他致命不安全变量)
}
}
}
return safe;
}
4. 重构意义
该函数的重构是本次Patch的核心,其价值在于:
- 修复了核心
bug:新增contain_subplans(qual)检查,堵住了"条件包含子查询时误判为安全"的漏洞。 - 实现了三态决策:通过返回不同的枚举值,让优化器能根据具体场景做差异化处理,提升优化灵活性。
- 精细化安全检查 :结合
unsafeFlags,区分"致命不安全 "和"非致命不安全 ",既保证语义安全,又不浪费Run Condition优化机会。
八、关键安全检查逻辑
Patch新增/强化了3处关键安全检查逻辑,分别对应"子计划检查""易失性函数检查""子查询输出列检查",这三处检查是保证WindowAgg Run Condition安全的核心,也是PostgreSQL优化器"安全优先"设计理念的体现。
1. 检查1:条件(qual)中是否包含subplan
核心代码:
c
if (contain_subplans(qual))
return PUSHDOWN_UNSAFE;
设计原因 :子查询(subquery)会生成子计划(subplan),子计划的执行依赖于外层上下文,若将包含子计划的条件下推或作为Run Condition,会导致子计划执行时机错误,破坏SQL语义。例如,子计划中引用外层变量时,提前终止WindowAgg会导致变量取值异常,返回错误结果。
作用范围 :所有过滤条件(qual),无论是否用于Run Condition,只要包含subplan,均视为完全不安全。
2. 检查2:条件/窗口函数中是否包含volatile函数
核心代码(条件检查):
c
if (safetyInfo->unsafeVolatile &&
contain_volatile_functions((Node *) rinfo))
return PUSHDOWN_UNSAFE;
核心代码 (WindowFunc检查):
c
// 在check_output_expressions函数中
if ((safetyInfo->unsafeFlags[tle->resno] & UNSAFE_HAS_VOLATILE_FUNC) == 0 &&
contain_volatile_functions((Node *) tle->expr))
{
safetyInfo->unsafeFlags[tle->resno] |= UNSAFE_HAS_VOLATILE_FUNC;
continue;
}
设计原因 :易失性函数(如random()、now())的特点是"每次执行结果不同",若将包含易失性函数的条件作为Run Condition,会导致WindowAgg每次执行的提前终止时机不同,返回结果不一致;若将其下推,会导致子查询执行结果不稳定,破坏语义。
示例场景 :count(random()) OVER (ORDER BY empno DESC),由于random()每次取值不同,窗口函数的结果也会不同,若将 WHERE c = 1 作为Run Condition,每次执行可能会提前终止不同的行数,导致结果错误。
3. 检查3:subquery输出列的安全性(结合unsafeFlags)
核心逻辑 :当过滤条件引用了子查询的输出列时,需要检查该列的 unsafeFlags,根据具体的不安全原因,判断条件的安全状态(如前所述)。
关键设计 :UNSAFE_NOTIN_PARTITIONBY_CLAUSE 与其他标志位的区别
- 其他标志位(如
UNSAFE_HAS_VOLATILE_FUNC):属于"致命不安全",既不能下推,也不能作为Run Condition。 UNSAFE_NOTIN_PARTITIONBY_CLAUSE:属于"非致命不安全",不能下推,但可作为Run Condition。
设计原因 :Run Condition的作用是"提前终止WindowAgg的分区计算",而分区计算的核心依赖是PARTITION BY子句,非PARTITION BY列的取值不影响分区内的计算语义------即使提前终止,分区内的窗口函数结果仍然正确;但条件下推会改变子查询的执行逻辑,破坏窗口分区的语义,因此禁止下推,但允许作为Run Condition。
九、WindowFunc Subplan 检查
除了检查过滤条件(qual)中的subplan,Patch还新增了对WindowFunc本身的subplan检查,确保当窗口函数内部包含subplan时,不会将外层条件转换为Run Condition。
核心代码(find_window_run_conditions函数)
c
static bool
find_window_run_conditions(Query *subquery, RangeTblEntry *rte, Index rti,
Node *qual, List **run_cond_attrs)
{
// ... 原有逻辑 ...
if (!IsA(wfunc, WindowFunc))
return false;
/* 新增:检查WindowFunc中是否包含subplan,若包含则不能作为Run Condition */
if (contain_subplans((Node *) wfunc))
return false;
// ... 后续逻辑 ...
}
设计原因
WindowFunc内部的subplan(如 count((SELECT 1)) OVER (...)),其执行逻辑与WindowAgg的提前终止机制冲突:subplan需要在窗口计算过程中多次执行,而Run Condition的提前终止会导致subplan执行不完整,进而导致窗口函数结果错误。
示例场景 :count((SELECT 1)) OVER (ORDER BY empno),子查询 (SELECT 1) 会生成InitPlan,每次窗口计算行时都会执行该子查询;若设置Run Condition c = 1,提前终止计算,会导致部分行的子查询未执行,count结果统计错误。
作用效果
新增该检查后,当WindowFunc包含subplan时,find_window_run_conditions 会返回false,优化器不会将外层条件转换为Run Condition,而是将条件保留在上级查询,作为过滤条件执行,确保语义正确。
九、Planner流程变化
Patch重构了查询优化器(Planner)中"过滤条件处理"的流程,核心变化是将原有的"布尔型判断"升级为"三态决策流程",使条件处理更精细、更灵活。
1. 旧流程(PostgreSQL 15之前,未修复时)
true
false
外层WHERE clause(过滤条件)
调用 qual_is_pushdown_safe()
// 返回 bool 类型
返回结果?
执行条件下推
subquery_push_qual
将条件保留在上级查询
upper filter
缺陷 :无法区分"不能下推但可作为Run Condition"的场景,浪费优化机会;同时未检查subplan,存在语义错误风险。
2. 新流程(Patch修复后)
PUSHDOWN_SAFE
PUSHDOWN_WINDOWCLAUSE_RUNCOND
PUSHDOWN_UNSAFE
是
否
外层WHERE clause(过滤条件)
调用 qual_is_pushdown_safe() 进行安全检查
安全状态判断
执行条件下推(subquery_push_qual)
调用find_window_run_conditions
保留在上级查询(upper filter)
可作为RunCond?
设置WindowAgg RunCondition
3. 流程变化的核心价值
-
更精细的条件处理 :区分三种安全状态,既保证语义安全,又不浪费
Run Condition优化机会。 -
堵住语义漏洞 :新增
subplan检查,避免错误优化导致的结果异常。 -
提升扩展性:三态流程为未来优化预留了空间(如后续可新增更多安全状态)。
十、执行计划变化示例
通过三个典型场景,对比Patch修复前后的执行计划变化,直观理解Patch的作用效果。
示例1:正常场景(可作为Run Condition,修复前后无变化)
SQL语句:
sql
explain analyze
SELECT *
FROM (
SELECT empno,
salary,
dense_rank() OVER (ORDER BY salary DESC) dr
FROM empsalary
) t
WHERE dr = 1;
执行计划(修复后,与修复前一致):
sql
postgres=# explain analyze
postgres-# SELECT *
postgres-# FROM (
postgres(# SELECT empno,
postgres(# salary,
postgres(# dense_rank() OVER (ORDER BY salary DESC) dr
postgres(# FROM empsalary
postgres(# ) t
postgres-# WHERE dr = 1;
QUERY PLAN
------------------------------------------------------------------------------------------------------------
---------------
Subquery Scan on t (cost=74.54..106.64 rows=5 width=20) (actual time=0.042..0.049 rows=1 loops=1)
Filter: (t.dr = 1)
-> WindowAgg (cost=74.54..93.26 rows=1070 width=20) (actual time=0.041..0.046 rows=1 loops=1)
Run Condition: (dense_rank() OVER (?) <= 1) -- 正常生成Run Condition
-> Sort (cost=74.54..77.21 rows=1070 width=12) (actual time=0.034..0.034 rows=2 loops=1)
Sort Key: empsalary.salary DESC
Sort Method: quicksort Memory: 25kB
-> Seq Scan on empsalary (cost=0.00..20.70 rows=1070 width=12) (actual time=0.016..0.020 ro
ws=10 loops=1)
Planning Time: 0.118 ms
Execution Time: 0.099 ms
(10 rows)
说明 :该场景无subplan、无易失性函数,符合Run Condition安全要求,因此修复前后执行计划一致,优化效果保留。
示例2:volatile函数场景(修复后不再生成Run Condition)
SQL语句:
sql
explain analyze
SELECT *
FROM (
SELECT empno,
count(random()) OVER (ORDER BY empno DESC) c -- 包含volatile函数random()
FROM empsalary
) t
WHERE c = 1;
执行计划(修复前,存在错误):
sql
postgres=# explain analyze
postgres-# SELECT *
postgres-# FROM (
postgres(# SELECT empno,
postgres(# count(random()) OVER (ORDER BY empno DESC) c -- 包含volatile函数random()
postgres(# FROM empsalary
postgres(# ) t
postgres-# WHERE c = 1;
QUERY PLAN
------------------------------------------------------------------------------------------------------------
--------------
Subquery Scan on t (cost=88.34..123.12 rows=5 width=16) (actual time=10.874..10.880 rows=1 loops=1)
Filter: (t.c = 1)
-> WindowAgg (cost=88.34..109.74 rows=1070 width=16) (actual time=10.871..10.876 rows=1 loops=1)
Run Condition: (count(random()) OVER (?) <= 1) -- 错误生成Run Condition
-> Sort (cost=88.34..91.02 rows=1070 width=8) (actual time=0.067..0.070 rows=3 loops=1)
Sort Key: empsalary.empno DESC
Sort Method: quicksort Memory: 25kB
-> Seq Scan on empsalary (cost=0.00..20.70 rows=1070 width=8) (actual time=0.008..0.011 row
s=10 loops=1)
Planning Time: 0.168 ms
Execution Time: 10.987 ms
(10 rows)
执行计划(修复后,正确处理):
sql
postgres=# explain analyze
postgres-# SELECT *
postgres-# FROM (
postgres(# SELECT empno,
postgres(# count(random()) OVER (ORDER BY empno DESC) c -- 包含volatile函数random()
postgres(# FROM empsalary
postgres(# ) t
postgres-# WHERE c = 1;
QUERY PLAN
------------------------------------------------------------------------------------------------------------
--------------
Subquery Scan on t (cost=74.54..109.31 rows=5 width=16) (actual time=0.087..0.108 rows=1 loops=1)
Filter: (t.c = 1) -- 条件保留在上级查询
Rows Removed by Filter: 9
-> WindowAgg (cost=74.54..95.94 rows=1070 width=16) (actual time=0.085..0.105 rows=10 loops=1)
-> Sort (cost=74.54..77.21 rows=1070 width=8) (actual time=0.041..0.043 rows=10 loops=1)
Sort Key: empsalary.empno DESC
Sort Method: quicksort Memory: 25kB
-> Seq Scan on empsalary (cost=0.00..20.70 rows=1070 width=8) (actual time=0.007..0.032 row
s=10 loops=1)
Planning Time: 0.129 ms
Execution Time: 0.159 ms
(10 rows)
Subquery Scan on t
Filter: (t.c = 1)
-> WindowAgg
-> Sort
Sort Key: empsalary.empno DESC
-> Seq Scan on empsalary
说明 :修复后,优化器检测到窗口函数包含volatile函数,不再生成Run Condition,避免了结果错误。
示例3:subplan场景(修复后不再生成Run Condition)
SQL语句:
sql
explain analyze
SELECT *
FROM (
SELECT empno,
count((SELECT 1)) OVER (ORDER BY empno DESC) c -- WindowFunc包含subplan
FROM empsalary
) t
WHERE c = 1;
执行计划(修复前,存在错误):
sql
postgres=# explain analyze
postgres-# SELECT *
postgres-# FROM (
postgres(# SELECT empno,
postgres(# count((SELECT 1)) OVER (ORDER BY empno DESC) c -- WindowFunc包含subplan
postgres(# FROM empsalary
postgres(# ) t
postgres-# WHERE c = 1;
2026-03-11 15:51:50.879 CST [17763] ERROR: WindowFunc not found in subplan target lists
2026-03-11 15:51:50.879 CST [17763] STATEMENT: explain analyze
SELECT *
FROM (
SELECT empno,
count((SELECT 1)) OVER (ORDER BY empno DESC) c -- WindowFunc包含subplan
FROM empsalary
) t
WHERE c = 1;
ERROR: WindowFunc not found in subplan target lists
执行计划(修复后,正确处理):
sql
postgres=# explain analyze
postgres-# SELECT *
postgres-# FROM (
postgres(# SELECT empno,
postgres(# count((SELECT 1)) OVER (ORDER BY empno DESC) c -- WindowFunc包含subplan
postgres(# FROM empsalary
postgres(# ) t
postgres-# WHERE c = 1;
QUERY PLAN
------------------------------------------------------------------------------------------------------------
--------------
Subquery Scan on t (cost=74.55..106.65 rows=5 width=16) (actual time=0.045..0.066 rows=1 loops=1)
Filter: (t.c = 1)
Rows Removed by Filter: 9
-> WindowAgg (cost=74.55..93.27 rows=1070 width=16) (actual time=0.044..0.063 rows=10 loops=1)
InitPlan 1 (returns $0)
-> Result (cost=0.00..0.01 rows=1 width=4) (actual time=0.001..0.001 rows=1 loops=1)
-> Sort (cost=74.54..77.21 rows=1070 width=8) (actual time=0.029..0.031 rows=10 loops=1)
Sort Key: empsalary.empno DESC
Sort Method: quicksort Memory: 25kB
-> Seq Scan on empsalary (cost=0.00..20.70 rows=1070 width=8) (actual time=0.015..0.019 row
s=10 loops=1)
Planning Time: 0.128 ms
Execution Time: 0.122 ms
(12 rows)
说明 :修复后,优化器检测到WindowFunc包含subplan,不再生成Run Condition,同时生成InitPlan处理subplan,确保语义正确。
十一、总结
本次Patch主要针对WindowAgg Run Condition优化存在的语义误判问题进行修复。核心问题为原逻辑未检测过滤条件及窗口函数中的子计划、易失性函数等场景,可能导致执行结果错误。
Patch核心优化包括:重构pushdown_safety_info结构体,新增位掩码标志精细化记录不安全原因;将qual_is_pushdown_safe函数返回值改为三态枚举,区分可下推、完全不安全、不能下推但可作为Run Condition三种场景;新增子计划、易失性函数等安全检查,避免错误优化。
优化后,查询优化器可在保证语义安全的前提下,合理利用Run Condition优化提升性能,同时堵住原有逻辑漏洞,使WindowAgg执行更严谨、高效。