在PostgreSQL查询优化器中,WindowAgg Run Condition优化是一种重要的执行优化机制。这个机制的核心目标是通过提前终止窗口函数计算来降低执行开销,从而提升查询性能。作为一名长期从事数据库内核开发的工程师,我在实际工作中经常遇到需要优化窗口函数性能的场景。
窗口函数优化的典型应用场景是当SQL语句存在"子查询包含窗口函数+外层过滤窗口函数结果"的结构时。例如下面这个查询:
sql复制SELECT *
FROM (
SELECT empno,
salary,
dense_rank() OVER (ORDER BY salary DESC) dr
FROM empsalary
) t
WHERE dr = 1;
在这个例子中,优化器可以将外层的WHERE dr = 1条件转换为WindowAgg节点的Run Condition。这样在执行时,当排名超过1时就可以直接停止窗口计算,避免对整个窗口的所有行进行无效计算。
提示:这种优化对于大表窗口查询特别有价值,因为它可以大幅减少计算量。我在处理一个包含百万级数据的报表查询时,应用这种优化后查询时间从原来的15秒降到了不到1秒。
在PostgreSQL 15中引入的WindowAgg Run Condition优化逻辑存在一个严重缺陷——没有正确检测过滤条件(qual)及窗口函数(WindowFunc)中是否存在子查询(subquery)或子计划(subplan)。这个问题是由commit 9d9c02ccd引入的。
让我通过两个实际案例来说明这个问题:
案例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,导致执行语义错误。
案例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语义。
David Rowley提交的这个补丁主要对PostgreSQL内核做了以下几方面的优化:
补丁重构了pushdown_safety_info结构体,新增了unsafeFlags字段来替代原来的unsafeColumns。这个改变看似简单,但实际上对优化器的决策能力有重大提升。
旧结构:
c复制typedef struct pushdown_safety_info {
bool *unsafeColumns; // 仅标记"某列是否不安全"
bool unsafeVolatile;
bool unsafeLeaky;
} pushdown_safety_info;
新结构:
c复制typedef struct pushdown_safety_info {
unsigned char *unsafeFlags; // 位掩码数组,记录每列不安全的具体原因
bool unsafeVolatile;
bool unsafeLeaky;
} pushdown_safety_info;
补丁新增了多项安全检查:
补丁将原有的布尔型pushdown判断升级为三态枚举:
c复制typedef enum pushdown_safe_type {
PUSHDOWN_UNSAFE, // 不安全,不能下推,也不能作为Run Condition
PUSHDOWN_SAFE, // 安全,可下推
PUSHDOWN_WINDOWCLAUSE_RUNCOND // 不能下推,但可作为WindowClause的run condition
} pushdown_safe_type;
这种设计让优化器能够根据具体场景做出更精细的优化决策。
让我们深入分析补丁中的几个关键函数修改:
这个函数负责检查子查询输出表达式(targetList)的安全性。补丁对其进行了重要改进,新增了位掩码标志位(UNSAFE_*系列),可以区分不同的不安全原因。
函数的主要检查逻辑包括:
每个检查点都会设置相应的标志位,例如:
c复制if (contain_volatile_functions((Node *) tle->expr)) {
safetyInfo->unsafeFlags[tle->resno] |= UNSAFE_HAS_VOLATILE_FUNC;
continue;
}
这个函数是判断"过滤条件(qual)能否下推到子查询"的核心函数。补丁将其返回值从bool改为pushdown_safe_type,并新增了多项安全检查。
关键修改点:
if (contain_subplans(qual)) return PUSHDOWN_UNSAFE;这个函数新增了对WindowFunc本身的subplan检查:
c复制if (contain_subplans((Node *) wfunc))
return false;
这个检查确保当窗口函数内部包含subplan时,不会将外层条件转换为Run Condition。
通过实际例子来看看补丁带来的变化:
sql复制EXPLAIN ANALYZE
SELECT *
FROM (
SELECT empno, salary, dense_rank() OVER (ORDER BY salary DESC) dr
FROM empsalary
) t
WHERE dr = 1;
补丁前后都会生成包含Run Condition的执行计划,这是符合预期的优化。
sql复制EXPLAIN ANALYZE
SELECT *
FROM (
SELECT empno, count(random()) OVER (ORDER BY empno DESC) c
FROM empsalary
) t
WHERE c = 1;
补丁前会错误生成Run Condition,补丁后优化器检测到volatile函数,不再生成Run Condition。
sql复制EXPLAIN ANALYZE
SELECT *
FROM (
SELECT empno, count((SELECT 1)) OVER (ORDER BY empno DESC) c
FROM empsalary
) t
WHERE c = 1;
补丁前会导致错误,补丁后优化器检测到WindowFunc包含subplan,正确处理为普通过滤。
根据我的实践经验,在使用窗口函数优化时需要注意以下几点:
这个补丁虽然主要目的是修复正确性问题,但对性能也有一定影响:
我在一个实际生产环境中测试发现,对于包含子查询的复杂窗口函数,补丁后的查询时间更加稳定,不再出现偶尔变慢的情况。
David Rowley的这个补丁解决了PostgreSQL窗口函数优化中的一个重要问题。通过代码分析我们可以学到:
对于PostgreSQL使用者,我的建议是:
这个补丁体现了PostgreSQL社区对代码质量的严格要求,也展示了数据库内核开发的精妙之处。通过研究这样的补丁,我们可以更好地理解查询优化器的工作原理,写出更高效的SQL语句。