当前位置: 首页 > news >正文

【PostgreSQL内核学习:WindowAgg 帧优化与节点去重】

PostgreSQL内核学习:WindowAgg 帧优化与节点去重

  • 背景
  • 关键词解释
  • 本优化主要修改内容描述
    • 提交信息
    • 提交描述
    • 源码解读
      • optimize_window_clauses 函数
        • 核心逻辑拆解
        • 函数时序图
        • 新增结构体类型 SupportRequestOptimizeWindowClause
    • 优化后的效果
      • 帧优化 sql 用例
      • 查询计划输出
      • 节点去重 sql 用例
      • 查询计划输出
    • 限制与约束
      • 结果不受 `frameOptions` 影响的窗口函数
      • 其他窗口函数为何不可优化?
    • 问题思考
      • 解决方案思考
        • 问题描述
        • 方案一:支持函数中清空偏移量
        • 方案二:在优化阶段集中清空偏移量
        • 方案三:修改去重条件以忽略偏移量差异
      • 优化后执行结果

声明:本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。
本文主要参考了 postgresql-18 beta2 的开源代码和《PostgresSQL数据库内核分析》一书

背景

  在 PostgreSQL 中,窗口函数(如 row_number()rank() 等)是用于在查询中对行集进行分区和排序计算的强大工具。然而,窗口函数的默认行为可能导致性能问题。具体来说,窗口函数的 WindowClause 默认使用 RANGE 模式,这要求执行器检查“同行”(peer rows,即 ORDER BY 字段值相同的行),从而增加计算开销。对于某些窗口函数(如 row_number()),RANGEROWS 模式的计算结果相同,但 ROWS 模式的执行效率更高,因为它无需检查同行
  通过对测试查询的分析,发现使用 ROWS 模式替代 RANGE 模式可以带来很大的性能提升。因此,此补丁通过允许窗口函数动态调整其 frameOptions(框架选项),优化查询执行计划,从而减少不必要的性能开销。

关键词解释

  1. 窗口函数(Window Function):一种 SQL 函数,用于在查询中对行集进行分区(PARTITION BY)和排序(ORDER BY)计算,如 row_number()rank() 等。
  2. WindowClause:定义窗口函数的分区、排序和框架范围的结构,包含 partitionClauseorderClauseframeOptions 等字段。
  3. frameOptions:指定窗口函数的框架范围(frame specification),包括模式(ROWSRANGE)和边界(如 UNBOUNDED PRECEDINGCURRENT ROW)。
  4. ROWS vs. RANGE
    • ROWS:基于行数定义框架范围,效率较高,因为只考虑物理行位置。
    • RANGE:基于值范围定义框架范围,需要检查同行(值相同的行),开销较大。
  5. SupportRequestOptimizeWindowClause:新引入的节点类型,用于支持窗口函数优化其 WindowClause 的框架选项。
  6. WindowAgg:查询计划中的节点,负责执行窗口函数计算。减少 WindowAgg 节点数量可提升性能。
  7. 去重(De-duplication):合并重复的 WindowClause,以减少执行计划中的 WindowAgg 节点。

本优化主要修改内容描述

提交信息

  下面为本次优化的提交信息,hash值为:ed1a88ddaccfe883e4cf74d30319accfeae6cfe5
在这里插入图片描述

提交描述

  此补丁为 PostgreSQL 的查询计划器(planner)增加了对窗口函数框架选项的优化能力,允许窗口函数(如 row_number()rank()dense_rank()percent_rank()cume_dist()ntile())通过支持函数(support function)调整其 WindowClauseframeOptions,以使用更高效的 ROWS 模式代替默认的 RANGE 模式。主要修改包括:

  1. 新支持函数节点类型:
    • supportnodes.h 中引入了新的节点类型 SupportRequestOptimizeWindowClause允许窗口函数的支持函数指定优化的 frameOptions
    • 该节点为计划器提供了一个机制,用于查询每个窗口函数是否可以调整其框架选项。
  2. 计划器优化逻辑:
    • planner.c 中添加了 optimize_window_clauses 函数,遍历所有 WindowClause 及其关联的 WindowFunc 节点。
    • 对于每个 WindowClause,计划器调用每个窗口函数的 prosupport 函数,检查是否可以优化 frameOptions(通常是将 RANGE 改为 ROWS)。
    • 仅当同一 WindowClause 下的所有窗口函数一致同意优化后的 frameOptions 时,才会应用更改
    • 优化后,优化器检查是否存在重复的 WindowClause,并通过合并减少 WindowAgg 节点的生成,从而进一步优化查询计划。
  3. 窗口函数支持函数:
    • windowfuncs.c 中为 row_number()rank()dense_rank()percent_rank()cume_dist()ntile() 添加了支持函数(如 window_row_number_supportwindow_rank_support 等)。
    • 这些支持函数指定可以将 frameOptions 设置为 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,因为此模式对这些函数的结果无影响,且避免了同行检查的开销。
  4. 测试与验证:
    • window.sqlwindow.out 中添加了回归测试,验证优化后的查询计划是否正确减少了 WindowAgg 节点数量,并确保结果正确。
    • 测试用例展示了当所有窗口函数支持优化时,查询计划只包含一个 WindowAgg 节点;当某些窗口函数(如 count(*))不支持优化时,其 frameOptions 保持不变。

  以下是一个使用 Mermaid 语法编写的流程图代码,展示 PostgreSQL 补丁 0001-Allow-window-functions-to-adjust-their-frameOptions.patch 优化前后查询计划器处理窗口函数的流程对比。流程图通过一个图中的两个分支(优化前和优化后)来清晰展示差异,重点突出 frameOptions 优化和 WindowClause 去重的过程。
  优化前,计划器直接使用默认的 RANGE 模式生成 WindowAgg 节点,导致执行器需检查同行(peer rows),增加性能开销。优化后,计划器通过 optimize_window_clauses 调用窗口函数的支持函数,检查是否可将 frameOptions 优化为更高效的 ROWS 模式,并在必要时合并重复的 WindowClause,减少 WindowAgg 节点,从而避免不必要的同行检查,提升查询性能。

注:代码使用 Mermaidgraph TD 语法,适合在支持 Mermaid 的环境中渲染。

请添加图片描述
  下面给出对应的作图代码,感兴趣的小伙伴可以自取:

graph TDA[开始: 查询计划器处理窗口函数] --> B[识别 WindowClause 和 WindowFunc]B --> C{是否应用优化补丁?}%% 优化前分支C -->|优化前| D[默认 frameOptions 为 RANGE]D --> E[检查 WindowClause 是否重复]E --> F[生成 WindowAgg 节点<br>使用 RANGE 模式]F --> G[执行器处理<br>检查同行(Peer Rows)]G --> H[返回查询结果]%% 优化后分支C -->|优化后| I[调用 optimize_window_clauses]I --> J[遍历 WindowClause]J --> K{每个 WindowFunc<br>有 prosupport 函数?}K -->|| L[调用 prosupport 函数<br>获取优化 frameOptions]K -->|| M[保留原有 frameOptions]L --> N{所有 WindowFunc<br>同意优化 frameOptions?}N -->|| O[更新 frameOptions<br>(如 ROWS 模式)]N -->|| MO --> P[检查 WindowClause 是否重复]M --> PP -->|存在重复| Q[合并 WindowClause<br>更新 WindowFunc 的 winref]P -->|无重复| R[生成 WindowAgg 节点<br>使用优化后的 frameOptions]Q --> RR --> S[执行器处理<br>避免不必要的同行检查]S --> H[返回查询结果]

源码解读

optimize_window_clauses 函数

  optimize_window_clauses 是本次补丁的核心函数,位于 src/backend/optimizer/plan/planner.c,负责优化 WindowClauseframeOptions 并处理去重逻辑。以下是对其源码的详细解读,结合补丁的上下文说明其实现。

/* * optimize_window_clauses*      调用每个 WindowFunc 的 prosupport 函数,检查是否可以调整 WindowClause 的设置,*      以便执行器以更优化的方式执行窗口函数。** 目前仅允许调整 WindowClause 的 frameOptions。未来可能支持更多优化。*/
static void
optimize_window_clauses(PlannerInfo *root, WindowFuncLists *wflists)
{List       *windowClause = root->parse->windowClause; // 获取查询解析树中的 WindowClause 列表ListCell   *lc; // 声明用于遍历 WindowClause 列表的单元指针foreach(lc, windowClause) // 遍历 WindowClause 列表{WindowClause *wc = lfirst_node(WindowClause, lc); // 获取当前 WindowClause 节点ListCell   *lc2; // 声明用于遍历 WindowFunc 列表的单元指针int        optimizedFrameOptions = 0; // 初始化优化后的 frameOptions,默认为 0Assert(wc->winref <= wflists->maxWinRef); // 断言:确保 WindowClause 的 winref 不超过最大值/* 跳过没有任何 WindowFunc 的 WindowClause */if (wflists->windowFuncs[wc->winref] == NIL)continue; // 如果当前 WindowClause 无关联 WindowFunc,跳到下一个 WindowClauseforeach(lc2, wflists->windowFuncs[wc->winref]) // 遍历当前 WindowClause 的 WindowFunc 列表{SupportRequestOptimizeWindowClause req; // 声明支持优化请求的结构体SupportRequestOptimizeWindowClause *res; // 声明指向优化结果的指针WindowFunc *wfunc = lfirst_node(WindowFunc, lc2); // 获取当前 WindowFunc 节点Oid        prosupport; // 声明支持函数的 OIDprosupport = get_func_support(wfunc->winfnoid); // 获取当前 WindowFunc 的支持函数 OID/* 检查当前 WindowFunc 是否有支持函数 */if (!OidIsValid(prosupport))break; // 如果没有支持函数,退出 WindowFunc 循环,无法优化此 WindowClausereq.type = T_SupportRequestOptimizeWindowClause; // 设置请求类型为优化 WindowClausereq.window_clause = wc; // 设置请求的 WindowClause 为当前 WindowClausereq.window_func = wfunc; // 设置请求的 WindowFunc 为当前 WindowFuncreq.frameOptions = wc->frameOptions; // 设置请求的 frameOptions 为当前 WindowClause 的 frameOptions/* 调用支持函数 */res = (SupportRequestOptimizeWindowClause *)DatumGetPointer(OidFunctionCall1(prosupport,PointerGetDatum(&req))); // 调用支持函数,获取优化结果/** 如果支持函数不支持此请求类型,跳到下一个 WindowClause*/if (res == NULL)break; // 如果支持函数返回 NULL,退出 WindowFunc 循环,跳到下一个 WindowClause/** 对于当前 WindowClause 的第一个 WindowFunc,保存其优化后的 frameOptions*/if (foreach_current_index(lc2) == 0)optimizedFrameOptions = res->frameOptions; // 保存第一个 WindowFunc 的优化 frameOptions/** 对于后续 WindowFunc,如果 frameOptions 与第一个不同,* 则无法优化此 WindowClause 的 frameOptions*/else if (optimizedFrameOptions != res->frameOptions)break; // 如果 frameOptions 不一致,退出循环,跳到下一个 WindowClause}/* 如果所有 WindowFunc 一致同意优化 frameOptions,则调整 frameOptions */if (lc2 == NULL && wc->frameOptions != optimizedFrameOptions){ListCell   *lc3; // 声明用于遍历 WindowClause 进行去重的单元指针/* 应用新的 frameOptions */wc->frameOptions = optimizedFrameOptions; // 更新当前 WindowClause 的 frameOptions/** 检查更改 frameOptions 后是否导致此 WindowClause 与其他 WindowClause 重复。* 如果只有 1 个 WindowClause,无需检查去重。*/if (list_length(windowClause) == 1)continue; // 如果只有 1 个 WindowClause,跳过去重检查/** 执行去重检查,如果找到重复的 WindowClause,则重用已有 WindowClause*/foreach(lc3, windowClause) // 遍历所有 WindowClause 进行去重检查{WindowClause *existing_wc = lfirst_node(WindowClause, lc3); // 获取已有 WindowClause/* 跳过当前正在编辑的 WindowClause */if (existing_wc == wc)continue; // 如果是同一个 WindowClause,跳过比较/** 执行与 transformWindowFuncCall 中相同的去重检查*/if (equal(wc->partitionClause, existing_wc->partitionClause) &&equal(wc->orderClause, existing_wc->orderClause) &&wc->frameOptions == existing_wc->frameOptions &&equal(wc->startOffset, existing_wc->startOffset) &&equal(wc->endOffset, existing_wc->endOffset)) // 检查 partitionClause、orderClause 等是否相同{ListCell   *lc4; // 声明用于遍历 WindowFunc 的单元指针/** 将 wc 的所有 WindowFunc 移动到 existing_wc。* 需要调整每个 WindowFunc 的 winref,并将 wc 的 WindowFunc 列表* 移动到 existing_wc 的 WindowFunc 列表。*/foreach(lc4, wflists->windowFuncs[wc->winref]) // 遍历当前 WindowClause 的 WindowFunc{WindowFunc *wfunc = lfirst_node(WindowFunc, lc4); // 获取当前 WindowFuncwfunc->winref = existing_wc->winref; // 更新 WindowFunc 的 winref 为 existing_wc 的 winref}/* 移动 WindowFunc 列表 */wflists->windowFuncs[existing_wc->winref] = list_concat(wflists->windowFuncs[existing_wc->winref],wflists->windowFuncs[wc->winref]); // 合并 WindowFunc 列表wflists->windowFuncs[wc->winref] = NIL; // 清空当前 WindowClause 的 WindowFunc 列表/** transformWindowFuncCall 应已确保没有其他重复,* 因此无需继续检查其他 WindowClause*/break; // 找到重复后,退出去重循环}}}}
}

参数

  • root:查询计划器的上下文,包含解析后的查询树(如 windowClause)。
  • wflistsWindowFuncLists 结构,存储每个 WindowClause 关联的 WindowFunc 列表,wflists->windowFuncs[winref]winref 索引窗口函数。

作用:遍历所有 WindowClause,为每个 WindowClause 检查是否可优化 frameOptions,并在优化后处理重复的 WindowClause

核心逻辑拆解

  函数通过以下步骤实现优化:

  1. 遍历 WindowClause
foreach(lc, windowClause)
{WindowClause *wc = lfirst_node(WindowClause, lc);ListCell   *lc2;int        optimizedFrameOptions = 0;...
}
  • 使用 foreach 宏遍历 windowClause 列表,获取每个 WindowClausewc)。
  • 定义 optimizedFrameOptions 变量,初始化为 0,用于存储支持函数建议的优化框架选项。
  1. 跳过无 WindowFuncWindowClause
Assert(wc->winref <= wflists->maxWinRef);
if (wflists->windowFuncs[wc->winref] == NIL)continue;
  • 检查 wc->winref 是否有效(不大于 wflists->maxWinRef)。
  • 如果 WindowClause 没有关联的 WindowFuncwflists->windowFuncs[wc->winref] == NIL),跳过处理。
  1. 检查每个 WindowFunc 的支持函数:
foreach(lc2, wflists->windowFuncs[wc->winref])
{SupportRequestOptimizeWindowClause req;SupportRequestOptimizeWindowClause *res;WindowFunc *wfunc = lfirst_node(WindowFunc, lc2);Oid        prosupport;prosupport = get_func_support(wfunc->winfnoid);if (!OidIsValid(prosupport))break; /* can't optimize this WindowClause */...
}
  • 遍历当前 WindowClause 的所有 WindowFunc(通过 wflists->windowFuncs[wc->winref])。
  • 获取每个 WindowFunc 的支持函数 OID(prosupport),通过 get_func_support(wfunc->winfnoid)
  • 如果 WindowFunc 没有支持函数(prosupport 无效),终止优化,跳到下一个 WindowClause
  1. 调用支持函数获取优化 frameOptions
req.type = T_SupportRequestOptimizeWindowClause;
req.window_clause = wc;
req.window_func = wfunc;
req.frameOptions = wc->frameOptions;res = (SupportRequestOptimizeWindowClause *)DatumGetPointer(OidFunctionCall1(prosupport, PointerGetDatum(&req)));
if (res == NULL)break; /* skip to next WindowClause */
  • 初始化 SupportRequestOptimizeWindowClause 结构(req),设置类型、当前 WindowClause、窗口函数和当前 frameOptions
  • 调用支持函数(OidFunctionCall1),传入 req,获取返回值 res
  • 如果支持函数返回 NULL(不支持优化),终止优化,跳到下一个 WindowClause
  1. 验证所有 WindowFunc 的一致性:
if (foreach_current_index(lc2) == 0)optimizedFrameOptions = res->frameOptions;
else if (optimizedFrameOptions != res->frameOptions)break; /* skip to next WindowClause */
  • 对于第一个 WindowFuncforeach_current_index(lc2) == 0),记录其建议的 frameOptionsoptimizedFrameOptions
  • 对于后续 WindowFunc,检查其建议的 frameOptions 是否与第一个一致。如果不一致,终止优化,跳到下一个 WindowClause
  1. 应用优化后的 frameOptions
if (lc2 == NULL && wc->frameOptions != optimizedFrameOptions)
{wc->frameOptions = optimizedFrameOptions;...
}
  • 如果遍历完成(lc2 == NULL)且 optimizedFrameOptions 与当前 frameOptions 不同,更新 wc->frameOptions 为优化值(通常为 ROWS 模式)。
  1. 检查和合并重复 WindowClause
if (list_length(windowClause) == 1)continue;foreach(lc3, windowClause)
{WindowClause *existing_wc = lfirst_node(WindowClause, lc3);if (existing_wc == wc)continue;if (equal(wc->partitionClause, existing_wc->partitionClause) &&equal(wc->orderClause, existing_wc->orderClause) &&wc->frameOptions == existing_wc->frameOptions &&equal(wc->startOffset, existing_wc->startOffset) &&equal(wc->endOffset, existing_wc->endOffset)){ListCell   *lc4;foreach(lc4, wflists->windowFuncs[wc->winref]){WindowFunc *wfunc = lfirst_node(WindowFunc, lc4);wfunc->winref = existing_wc->winref;}wflists->windowFuncs[existing_wc->winref] = list_concat(wflists->windowFuncs[existing_wc->winref],wflists->windowFuncs[wc->winref]);wflists->windowFuncs[wc->winref] = NIL;break;}
}
  • 如果 windowClause 列表长度为 1,跳过去重检查。
  • 遍历其他 WindowClauseexisting_wc),检查是否与当前 wc 重复(比较 partitionClauseorderClauseframeOptionsstartOffsetendOffset)。
  • 如果找到重复:
    • wc 的所有 WindowFuncwinref 更新为 existing_wc->winref
    • 合并 wflists->windowFuncs[wc->winref]wflists->windowFuncs[existing_wc->winref]
    • 清空 wflists->windowFuncs[wc->winref],标记为 NIL
    • 终止检查(假设 transformWindowFuncCall 已确保无其他重复)。
函数时序图

请添加图片描述

新增结构体类型 SupportRequestOptimizeWindowClause

  SupportRequestOptimizeWindowClause 是一个新引入的结构体,定义在 src/include/nodes/supportnodes.h 文件中,用于支持 PostgreSQL 查询计划器优化窗口函数的 frameOptions。该结构体是补丁的核心组件之一,提供了窗口函数支持函数(prosupport)与计划器之间的接口,允许动态调整 WindowClause 的框架选项(如从 RANGE 切换到更高效的 ROWS 模式)。源码定义如下:

typedef struct SupportRequestOptimizeWindowClause
{NodeTag     type; // 节点类型标识,用于 PostgreSQL 的节点系统/* 输入字段: */WindowFunc *window_func; // 指向当前窗口函数的数据结构struct WindowClause *window_clause; // 指向当前窗口子句的数据结构/* 输入/输出字段: */int         frameOptions; // 新的 frameOptions 值,若无优化则保持不变
} SupportRequestOptimizeWindowClause;

  SupportRequestOptimizeWindowClause 的主要作用是为窗口函数的优化提供一个标准化的请求-响应机制,允许窗口函数的 prosupport 函数(例如 window_row_number_support)检查并建议优化的 frameOptions,从而减少执行器的计算开销(如避免同行检查)。

优化后的效果

帧优化 sql 用例

CREATE TABLE t1 (c1 int, c2 int);
INSERT INTO t1 VALUES (1, generate_series(1, 100000));
CREATE INDEX t1_idx ON t1(c1);
EXPLAIN (COSTS OFF, ANALYZE) 
SELECT * FROM (SELECT row_number() OVER (ORDER BY c1) rn, * FROM t1
) t WHERE rn < 20;

查询计划输出

                                    QUERY PLAN
-----------------------------------------------------------------------------------WindowAgg (actual time=0.079..0.100 rows=19.00 loops=1)Window: w1 AS (ORDER BY t1.c1 ROWS UNBOUNDED PRECEDING)Run Condition: (row_number() OVER w1 < 20)Storage: Memory  Maximum Storage: 17kBBuffers: shared hit=1 read=2->  Index Scan using t1_idx on t1 (actual time=0.065..0.073 rows=20.00 loops=1)Index Searches: 1Buffers: shared hit=1 read=2Planning:Buffers: shared hit=39Planning Time: 0.361 msExecution Time: 0.149 ms
(12 rows)

在这里插入图片描述
  由执行结果可以看出,通过支持函数将 frameOptions 自动修改为 ROWS,从而无需判断是否属于同一个同行组,只需按物理行号维护帧边界即可,从而实现了提前终止扫描。底层 Index Scan 仅需读取前 20 行,极大程度的减少了执行时间和资源消耗。

节点去重 sql 用例

CREATE TEMPORARY TABLE empsalary (depname varchar,empno bigint,salary int,enroll_date date
);INSERT INTO empsalary VALUES
('develop', 10, 5200, '2007-08-01'),
('sales', 1, 5000, '2006-10-01'),
('personnel', 5, 3500, '2007-12-10'),
('sales', 4, 4800, '2007-08-08'),
('personnel', 2, 3900, '2006-12-23'),
('develop', 7, 4200, '2008-01-01'),
('develop', 9, 4500, '2008-01-01'),
('sales', 3, 4800, '2007-08-01'),
('develop', 8, 6000, '2006-10-01'),
('develop', 11, 5200, '2007-08-15');-- Ensure WindowClause frameOptions are changed so that only a single
-- WindowAgg exists in the plan.
EXPLAIN (COSTS OFF, VERBOSE)
SELECTempno,depname,row_number() OVER (PARTITION BY depname ORDER BY enroll_date) rn,rank() OVER (PARTITION BY depname ORDER BY enroll_date ROWS BETWEENUNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) rnk,dense_rank() OVER (PARTITION BY depname ORDER BY enroll_date RANGE BETWEENCURRENT ROW AND CURRENT ROW) drnk
FROM empsalary;

查询计划输出

                                                QUERY PLAN
----------------------------------------------------------------------------------------------------------WindowAggOutput: empno, depname, row_number() OVER w1, rank() OVER w1, dense_rank() OVER w1, enroll_dateWindow: w1 AS (PARTITION BY empsalary.depname ORDER BY empsalary.enroll_date ROWS UNBOUNDED PRECEDING)->  SortOutput: depname, enroll_date, empnoSort Key: empsalary.depname, empsalary.enroll_date->  Seq Scan on pg_temp.empsalaryOutput: depname, enroll_date, empno
(8 rows)

  由以上执行计划可以发现,PostgreSQL 在进行优化后,可以将多个兼容窗口的帧选项统一重写为等效的 ROWS 模式。此时所有窗口函数共享同一个 WindowClause,从而合并为一个 WindowAgg 节点。最终执行计划中仅保留一个窗口聚合节点,消除了冗余计算,显著提升了执行效率并简化执行计划结构。

限制与约束

  补丁的优化仅适用于特定窗口函数(row_number()rank()dense_rank()percent_rank()cume_dist()ntile()),因为这些函数的计算逻辑与 frameOptionsRANGEROWS 模式无关,可以安全切换而不改变结果。以下是详细分析:

结果不受 frameOptions 影响的窗口函数

  以下六个窗口函数的语义仅依赖分区和排序顺序,与框架范围无关,因此可安全从 RANGE 优化为 ROWS 模式:

  1. row_number()
  • 逻辑:为分区中的每一行分配递增序号,仅依赖 PARTITION BYORDER BY 定义的顺序。
  • 补丁说明:在 src/backend/utils/adt/windowfuncs.cwindow_row_number_support 中,明确指出 row_number() “总是逐行递增 1”,无论 frameOptionsRANGEROWS,结果相同。
  • 原因row_number() 不考虑同行,仅按行序计数,ROWS 模式避免了 RANGE 模式检查同行的开销。
  1. rank()
  • 逻辑:为分区中具有相同排序键值的行分配相同的排名,依赖排序顺序。
  • 补丁说明:在 window_rank_support 中,rank() 等价于 (COUNT(*) OVER (... RANGE UNBOUNDED PRECEDING) - COUNT(*) OVER (... RANGE CURRENT ROW) + 1),但结果与框架范围无关,优化为 ROWS 模式不影响语义。
  • 原因:排名基于排序键的分组,ROWS 模式仍正确处理顺序。
  1. dense_rank()
  • 逻辑类似 rank(),但排名无间隙,仅依赖排序键。
  • 补丁说明window_dense_rank_support 指出 dense_rank() 不受框架选项影响,优化为 ROWS 模式与 row_number() 一致。
  • 原因:与 rank() 类似,ROWS 模式不改变排名逻辑。
  1. percent_rank()
  • 逻辑:计算 (rank - 1) / (total_rows - 1),依赖 rank() 的逻辑。
  • 补丁说明window_percent_rank_support 确认结果与框架范围无关,优化为 ROWS 模式。
  • 原因:基于 rank(),仅需排序顺序,ROWS 模式适用。
  1. cume_dist()
  • 逻辑:计算分区中排序键小于或等于当前行的行数比例(rank / total_rows)
  • 补丁说明window_cume_dist_support 指出仅依赖排序顺序,ROWS 模式不影响结果。
  • 原因:比例计算基于排序键的分组,ROWS 模式仍有效。
  1. ntile(n)
  • 逻辑将分区划分为 n 个等份,依赖行数和分区大小。
  • 补丁说明window_ntile_support 确认结果与框架范围无关,优化为 ROWS 模式。
  • 原因:分组基于行序,ROWS 模式直接适用。

其他窗口函数为何不可优化?

  其他窗口函数(如 sum()、avg()、count()、lead()、lag()、first_value()、last_value())的结果直接依赖框架范围,切换 RANGEROWS 可能改变语义:

  1. sum()、avg()、count()
  • 逻辑:这些聚合函数计算框架范围内的值(如 SUM() OVER (... RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) 计算从分区开始到当前行的总和)。
  • 问题RANGE 模式包括所有同行(排序键值相同的行),而 ROWS 模式仅包括物理行,可能遗漏同行,导致结果不同
  • 示例:若分区有 3 行排序键值相同,RANGE 模式对 sum() 包含所有 3 行,ROWS 模式可能仅包含当前行,改变总和。
  1. lead()、lag()
  • 逻辑:基于框架内的相对行位置访问前后行。
  • 问题RANGEROWS 模式定义的框架范围不同,可能导致访问不同行,改变结果。
  1. first_value()、last_value()
  • 逻辑:获取框架范围内的第一行或最后行的值
  • 问题RANGE 模式可能包括更多同行,而 ROWS 模式仅考虑物理行,改变返回值。

问题思考

  观察以下用例

CREATE TABLE sales (sale_id INTEGER,product TEXT,sale_date DATE,amount INTEGER
);INSERT INTO sales VALUES
(1, 'Laptop', '2023-01-01', 1000),
(2, 'Laptop', '2023-02-01', 1200),
(3, 'Laptop', '2023-03-01', 1100),
(4, 'Phone', '2023-01-15', 800),
(5, 'Phone', '2023-02-15', 900),
(6, 'Phone', '2023-03-15', 850);EXPLAIN (COSTS OFF, VERBOSE)
SELECTsale_id,product,sale_date,amount,row_number() OVER w1 AS row_num,rank() OVER w2 AS rank_in_window
FROM sales
WINDOWw1 AS (PARTITION BY product ORDER BY sale_dateROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW),W2 AS (PARTITION BY product ORDER BY sale_dateROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING);

  执行计划

                                              QUERY PLAN
------------------------------------------------------------------------------------------------------WindowAggOutput: sale_id, product, sale_date, amount, (row_number() OVER w1), rank() OVER w2Window: w2 AS (PARTITION BY sales.product ORDER BY sales.sale_date ROWS UNBOUNDED PRECEDING)->  WindowAggOutput: product, sale_date, sale_id, amount, row_number() OVER w1Window: w1 AS (PARTITION BY sales.product ORDER BY sales.sale_date ROWS UNBOUNDED PRECEDING)->  SortOutput: product, sale_date, sale_id, amountSort Key: sales.product, sales.sale_date->  Seq Scan on public.salesOutput: product, sale_date, sale_id, amount
(11 rows)

  从上面的执行计划可以发现,存在的问题是对于语义相同的窗口定义(如示例查询中的 w1w2,均定义为 PARTITION BY product ORDER BY sale_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW),由于 optimize_window_clauses 的去重逻辑要求 startOffsetendOffset 完全相等(equal(wc->startOffset, existing_wc->startOffset)),当解析阶段或支持函数导致偏移量不一致时(如一个为 NULL,另一个为非 NULL),本应合并为单个 WindowAgg 节点的两个 WindowClause 未能合并,导致查询计划生成了两个独立的 WindowAgg 节点,增加了执行开销。

解决方案思考

问题描述

  在 PostgreSQL 中,窗口函数(如 row_number()rank()dense_rank())的执行依赖 WindowClause 定义的帧框架(frameOptions)和偏移量(startOffsetendOffset)。对于某些窗口函数(如 row_number()),其计算结果与帧框架的偏移量值无关,只依赖分区(partitionClause)和排序(orderClause
  然而,在 optimize_window_clauses 函数中,原始去重逻辑要求 startOffsetendOffset 严格相等(equal(wc->startOffset, existing_wc->startOffset),导致语义相同的窗口定义(如示例查询中的 w1w2,均定义为 PARTITION BY product ORDER BY sale_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)因偏移量差异无法合并。
  例如,一个窗口的 startOffsetNULL,另一个为非 NULL(如 Const 节点),会生成两个 WindowAgg 节点,增加执行开销。这种问题在 prosupport 函数未统一设置偏移量或解析阶段(transformWindowFuncCall)保留非 NULL 偏移量时尤为明显。为解决此问题,我们提出了三种优化方案,逐步改进偏移量管理和去重逻辑。

方案一:支持函数中清空偏移量

方案思考
  问题的核心是窗口函数(如 row_number())的结果与偏移量无关,应统一为 NULL 以促进去重。一种直接方法是在 prosupport 函数(如 window_row_number_support)中设置 startOffsetendOffsetNULL,并返回优化的 frameOptions(如 FRAMEOPTION_ROWS | FRAMEOPTION_START_UNBOUNDED_PRECEDING | FRAMEOPTION_END_CURRENT_ROW。这可确保偏移量一致,允许合并 w1w2。修改示例如下:

Datum
window_row_number_support(PG_FUNCTION_ARGS)
{SupportRequestOptimizeWindowClause *req = (SupportRequestOptimizeWindowClause *) PG_GETARG_POINTER(0);if (req->type == T_SupportRequestOptimizeWindowClause){req->frameOptions = FRAMEOPTION_NONDEFAULT |FRAMEOPTION_ROWS |FRAMEOPTION_START_UNBOUNDED_PRECEDING |FRAMEOPTION_END_CURRENT_ROW;req->window_clause->startOffset = NULL;req->window_clause->endOffset = NULL;PG_RETURN_POINTER(req);}PG_RETURN_NULL();
}

  但该方法在多个支持函数中重复偏移量逻辑,维护性差。为此,我们探索集中管理方案。

方案二:在优化阶段集中清空偏移量

  方案一的分散逻辑降低了代码内聚性,为此,我们考虑将偏移量管理集中到 optimize_window_clauses,在一致性检查通过(lc2 == NULL)且 frameOptions 需要更新(wc->frameOptions != optimizedFrameOptions)时清空偏移量。这减少了支持函数的职责,提高维护性,同时确保偏移量与优化后的 frameOptions 一致。
  方案二修改 optimize_window_clauses,在 if (lc2 == NULL && wc->frameOptions != optimizedFrameOptions) 分支内,更新 wc->frameOptions 后,检查是否包含 FRAMEOPTION_START_OFFSETFRAMEOPTION_END_OFFSET,若不包含则清空对应偏移量。prosupport 函数仅设置 frameOptions。这集中管理偏移量,促进示例查询中 w1w2 的合并,生成单个 WindowAgg 节点。但未覆盖 wc->frameOptions == optimizedFrameOptions 的情况,需进一步优化去重逻辑。修改示例如下:

if (lc2 == NULL && wc->frameOptions != optimizedFrameOptions)
{ListCell   *lc3;wc->frameOptions = optimizedFrameOptions;if (!(wc->frameOptions & FRAMEOPTION_START_OFFSET)) {wc->startOffset = NULL;}if (!(wc->frameOptions & FRAMEOPTION_END_OFFSET)) {wc->endOffset = NULL;}/* 去重逻辑... */
}
方案三:修改去重条件以忽略偏移量差异

  方案三考虑修改 optimize_window_clauses 的去重逻辑,若 frameOptions 不含 FRAMEOPTION_START_OFFSETFRAMEOPTION_END_OFFSET,忽略对应偏移量的比较prosupport 函数仅设置 frameOptions,偏移量由解析阶段管理。这促进示例查询中 w1 和 w2 的合并,减少 WindowAgg 节点。但可能导致语义错误(如 1 PRECEDINGUNBOUNDED PRECEDING 合并),需结合方案二清空偏移量以确保一致性。修改示例如下:

foreach(lc3, root->parse->windowClause)
{WindowClause *existing_wc = (WindowClause *) lfirst(lc3);if (wc != existing_wc &&equal(wc->partitionClause, existing_wc->partitionClause) &&equal(wc->orderClause, existing_wc->orderClause) &&wc->frameOptions == existing_wc->frameOptions &&(!(wc->frameOptions & FRAMEOPTION_START_OFFSET) || equal(wc->startOffset, existing_wc->startOffset)) &&(!(wc->frameOptions & FRAMEOPTION_END_OFFSET) || equal(wc->endOffset, existing_wc->endOffset))){/* 合并逻辑 */}
}

优化后执行结果

                                           QUERY PLAN
------------------------------------------------------------------------------------------------WindowAggOutput: sale_id, product, sale_date, amount, row_number() OVER w1, rank() OVER w1Window: w1 AS (PARTITION BY sales.product ORDER BY sales.sale_date ROWS UNBOUNDED PRECEDING)->  SortOutput: product, sale_date, sale_id, amountSort Key: sales.product, sales.sale_date->  Seq Scan on public.salesOutput: product, sale_date, sale_id, amount
(8 rows)

  优化后的执行计划将示例查询中的两个 WindowAgg 节点合并为一个,窗口 w1(定义为 PARTITION BY product ORDER BY sale_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)同时处理 row_number()rank(),输出 sale_idproductsale_dateamount 以及两个窗口函数的结果。
  相比原始计划的两个 WindowAgg 节点,新计划通过优化 optimize_window_clauses 的去重逻辑,统一 frameOptions 和偏移量(startOffsetendOffset),成功合并语义相同的 WindowClause,减少了执行开销,提高了查询性能。

http://www.lryc.cn/news/605080.html

相关文章:

  • 李宏毅2025《机器学习》-第九讲:大型语言模型评测的困境与“古德哈特定律”**
  • Linux 中,命令查看系统版本和内核信息
  • LNN+XGBoost:优化多层供应链订购:缓解牛鞭效应
  • 力扣209:长度最小的子数组
  • 光谱相机自动调焦曝光控制
  • 基于Rust与HDFS、YARN、Hue、ZooKeeper、MySQL
  • Linux 系统原理深度剖析与技术实践:从内核架构到前沿应用
  • npm run dev 启动项目 报Error: listen EACCES: permission denied 0.0.0.0:80 解决方法
  • Spring boot 打包成docker image 镜像
  • vue create 项目名 和 npm init vue@latest 创建vue项目的不同
  • 3GPP TS 38.331 V18.6.0 (2025-06)中文版
  • CMS框架GetShell
  • Web3:以太坊虚拟机
  • 网络的学习 2 Socket
  • 发那科机器人P点位置号码自动变更功能为禁用状态
  • python基础:用户输入和 while 循环
  • 【机器学习】pycharm使用SSH SFTP 远程连接 ubuntu服务器 进行开发+调试+数据训练
  • IBus vs. Fcitx5:一场 Linux 输入法框架的正面交锋
  • 在 Kubernetes 上部署 Label Studio
  • Apache Kafka核心组件详解
  • 当人生低谷无人帮助时,如何独自奏响人生乐章
  • 借助 Wisdom SSH AI 助手构建 Linux 容器化开发流水线
  • 虚实共生的智能革命:元宇宙、物联网与 AI 融合生态全景图谱
  • Vue 3 入门教程 2- Vue 组件基础与模板语法
  • 推客系统开发全流程解析:从概念到落地的完整指南
  • 论文Review LSLAM BALM | 经典激光SLAM方案!港大MARS出品!RAL2021 | 激光BA优化
  • RocketMQ 核心特性解析及与 Kafka区别
  • Spring AI 海运管理应用第2部分
  • Centos 7.9安装部署cobbler-自动化部署服务器完整教程
  • 数据结构第3问:什么是线性表?