【PostgreSQL内核学习:修复 WindowAgg Run Condition 判断逻辑错误的优化】

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判断逻辑,支持三态决策)
  • 四、核心代码结构变化
    • [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))
  • 十一、总结

声明 :本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。

本文主要参考了 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 转换为 WindowAggRun 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)。

问题场景示例1WindowFunc 包含子计划

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 条件不能下推,但可作为WindowAggRun Condition 不进行条件下推,将条件转换为Run Condition,优化WindowAgg执行

四、核心代码结构变化

Patch主要修改文件为 src/backend/optimizer/path/allpaths.cPostgreSQL查询优化器路径生成的核心文件),同时修改测试文件以验证修复效果,具体文件修改如下:

  • 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 集合操作(如UNIONINTERSECT)中,子查询输出列与顶层集合操作的列类型不匹配 集合操作要求所有子查询的输出列类型一致,类型不匹配时,下推条件会导致类型转换错误,破坏执行逻辑。
设计亮点:位掩码的使用的优势在于"高效存储+多状态组合",例如一个列既包含易失性函数,又不在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_* 标志位组合)。
  • 保留原有字段:unsafeVolatileunsafeLeaky 用于全局控制易失性、泄漏性条件的下推,与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. 函数返回值变更

  • 旧版本返回值:bool

    • true:条件可下推
    • 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)中的subplanPatch还新增了对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执行更严谨、高效。

相关推荐
_OP_CHEN2 小时前
【MySQL数据库基础】(五)MySQL 数据类型深度解析:选对类型 = 性能拉满!
linux·开发语言·数据库·sql·mysql·数据类型·c/c++
chuxinweihui2 小时前
MySQL复合查询
数据库·mysql
被AI抢饭碗的人2 小时前
MYSQL基础(二)
数据库·mysql
优秀的老黄2 小时前
MySQL配置从库
linux·数据库·mysql
修行者Java2 小时前
(六)从“缓存混乱难管控”到“Redis高效赋能”——Redis实战进阶指南
数据库·redis·缓存
YangYang9YangYan2 小时前
2026大专电子商务专业学习数据分析的价值分析
学习·数据挖掘·数据分析
AnalogElectronic2 小时前
在后端开发学习方面,如何充分发挥i5 3470低配电脑的性能
学习
搞机械的假程序猿2 小时前
STC32G学习笔记-FreeRTOS for STC32G12K128
笔记·单片机·学习
onebyte8bits2 小时前
NestJS 系列教程(十五):缓存体系设计 —— Redis、接口缓存与缓存三大问题解决方案
数据库·redis·后端·缓存·nestjs