【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.pswp.cn/pingmian/91255.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/91255.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/91255.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

行業要聞|正式落地!新思科技宣布完成對Ansys的收購

2025年7月17日——新思科技&#xff08;Synopsys, Inc.&#xff0c;納斯達克股票代碼&#xff1a;SNPS&#xff09;宣布完成對Ansys的收購。該交易旨在整合芯片設計、IP核以及仿真與分析領域的領先企業&#xff0c;助力開發者快速創新AI驅動的產品。在擴大至310億美元的總潛在市…

Elasticsearch 基礎速成 5 步跑通索引、文檔、映射與查詢

1 準備工作運行環境 curl -fsSL https://elastic.co/start-local | sh # 一條命令拉起本地單節點集群 # 瀏覽器打開 http://localhost:5601 進入 Kibana → DevTools → Console已有云端或 Serverless 集群可以直接跳到第 2 步。操作界面 以下所有請求均可在 Kibana → DevT…

語音表示學習論文總結

語音表示學習&#xff08;Speech Representation Learning&#xff09;是語音信號處理與機器學習交叉領域的核心技術&#xff0c;其目標是通過數據驅動的方式&#xff0c;從原始語音信號中自動提取具有判別性、魯棒性和泛化能力的特征表示&#xff0c;以替代傳統手工設計的聲學…

國產芯+單北斗防爆終端:W5-D防爆智能手機,助力工業安全通信升級

在石油石化、煤礦開采、電力檢修等高危行業&#xff0c;防爆設備的定位精度、通信可靠性及供應鏈安全性直接決定作業安全與生產效率。傳統防爆手機依賴GPS定位與進口芯片&#xff0c;存在信號盲區、數據泄露風險及斷供隱患。針對此&#xff0c;我們推出W5-D防爆智能終端&#x…

Kafka簡述及學習課程

Kafka是由Apache軟件基金會開發的一個開源流處理平臺&#xff0c;由Scala和Java編寫。Kafka是一種高吞吐量的分布式發布訂閱消息系統&#xff0c;它可以處理消費者在網站中的所有動作流數據。 這種動作&#xff08;網頁瀏覽&#xff0c;搜索和其他用戶的行動&#xff09;是在現…

BLE PHY 幀結構

BLE&#xff08;低功耗藍牙&#xff09;的 PHY&#xff08;物理層&#xff09;幀結構根據傳輸模式&#xff08;廣播、數據&#xff09;和 PHY 類型&#xff08;1M、2M、Coded PHY&#xff09;有所差異&#xff0c;但基本框架一致。以下是 BLE PHY 幀的通用結構及各部分含義&…

海外貨運 app 系統架構分析

一、引言海外貨運業務涉及眾多復雜環節&#xff0c;從貨物攬收、倉儲管理、運輸調度到最后交付&#xff0c;需要一個高效、穩定且功能全面的 APP 系統來協調各方資源&#xff0c;提升物流效率&#xff0c;保障貨物安全準確送達。本文將對海外貨運 APP 系統架構進行詳細剖析&…

【硬件-筆試面試題】硬件/電子工程師,筆試面試題-52,(知識點:簡單一階低通濾波器的設計,RC濾波電路,截止頻率)

目錄 1、題目 2、解答 3、相關知識點 一、一階低通濾波器的核心原理 1. 電路結構 2. 關鍵特性參數 二、一階低通濾波器的設計步驟&#xff08;以 RC 電路為例&#xff09; 1. 確定截止頻率\(f_c\) 2. 選擇電阻 R 的阻值 3. 計算電容 C 的容值 4. 驗證與調整 三、典…

防火墻安全實驗

一、實驗拓補圖二、實驗需求1、VLAN 2屬于辦公區;VLAN 3屬于生產區2、辦公區PC在工作日時間(周一至周五&#xff0c;早8到晚6)可以正常訪OA Server&#xff0c;其他時間不允許3、辦公區PC可以在任意時刻訪問Web server4、生產區PC可以在任意時刻訪問OA Server&#xff0c;但是不…

TOC-Transformer-LSTM-ABKDE,計算機一區算法龍卷風優化算法應用到概率區間預測!Matlab實現

TOC算法概述 文獻《Tornado optimizer with Coriolis force: a novel bio-inspired meta-heuristic algorithm》核心解讀&#xff1a;科里奧利力的龍卷風優化算法&#xff08;Tornado optimizer with Coriolis force&#xff0c;TOC&#xff09;對龍卷風循環過程的觀察以及雷暴…

Adobe Illustrator安裝下載教程(附安裝包)Illustrator2025

文章目錄一、Illustrator2025 下載鏈接二、Illustrator2025 安裝步驟三、Illustrator 2025 軟件介紹一、Illustrator2025 下載鏈接 夸克下載鏈接&#xff1a;https://pan.quark.cn/s/b990bac7107c 二、Illustrator2025 安裝步驟 1.將安裝包下載并解壓&#xff0c;雙擊打開&am…

matlab - 算4個數的加減法

文章目錄matlab - 算4個數的加減法概述筆記ENDmatlab - 算4個數的加減法 概述 有個類似于下面的4個數的加減法&#xff0c;給出任意一組解就行。 反正都是遍歷, c可以&#xff0c;matlab也可以。 筆記 % file test.m % brief 用matlab來算"4個數的加減法" %a b…

C++ 1.面向對象編程(OOP)框架

目錄 面向對象編程(OOP)框架 問題背景 OOP框架開發的關鍵問題解析 步驟1&#xff1a;抽象設計階段 步驟2&#xff1a;繼承層次設計 步驟3&#xff1a;多態機制應用 步驟4&#xff1a;對象關系管理 這個案例展現的核心OOP價值 封裝的價值 繼承的價值 多態的價值 實際…

mac操作筆記

mac的操作筆記opt文件夾是干什么的&#xff1f;如何在某個訪達的文件夾里快速打開終端opt文件夾是干什么的&#xff1f; 在 macOS 中&#xff0c;/opt 目錄是一個可選&#xff08;optional&#xff09;軟件安裝目錄&#xff0c;主要用于存放第三方或非系統原生的應用程序。 /…

紅黑樹×協程×內存序:2025 C++后端核心三體問題攻防手冊

以下是2025年C后端開發全新高頻壓軸面試題&#xff0c;結合騰訊、字節、阿里等大廠最新技術棧&#xff0c;聚焦紅黑樹工程實踐、C20協程底層、Linux內核同步、分布式鎖實現及內存序重排五大核心領域&#xff0c;附工業級解決方案和手撕代碼示例&#xff1a; &#x1f333; 一、…

《人工智能導論》(python版)第2章 python基礎2.2編程基礎

書寫這篇博客的目的在于實踐并記錄《人工智能導論》&#xff08;Pyhton版&#xff09;微課視頻版這本書的內容&#xff0c;便于對人工智能有更深層次的理解。 參考文獻&#xff1a;姜春茂.人工智能導論&#xff08;Python版&#xff09;微課視頻版[M]. 北京:清華大學出版社,20…

高可用部署

一.keeplivaer nginx 高可用部署 下面為你詳細介紹基于 Keepalived 和 Nginx 在兩臺機器&#xff08;192.168.137.132 和 192.168.137.61&#xff09;上實現高可用部署的完整步驟&#xff1a; 一、環境準備&#xff08;兩臺服務器均執行&#xff09;環境準備 &#xff08;1&…

java面向對象高級02——單例類(設計模式)

1.什么是設計模式&#xff1f;一個問題可以有多種解法&#xff0c;在眾多解法的最優解法、方案就是設計模式。我們關注的點&#xff1a;某一種設計模式解決的是啥問題&#xff1f;這一設計模式怎么寫&#xff1f;2.單例設計模式a.作用單例設計模式的核心作用是確保一個類只有一…

0730 數據結構重點整理

Part 1.梳理數據結構重點一.宏1.簡單宏a. #define 宏名 宏體b. #if 宏(#ifndef)c.#endif2.多語句宏a. define 宏函數名(參數1&#xff0c;參數2......)({C語句1&#xff0c;C語句2......})b. define 宏函數名(參數1&#xff0c;參數2......)do(C語句1&#xff0c;C語句2......)…

免費版酒店押金原路退回系統之【房費押金計算器】實踐——仙盟創夢IDE

代碼<!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>未來之窗——費用計算器</title><s…