博客 / 詳情

返回

瀚高硬核助力 PG 社區:Postgres 19 迎來並行 TID 範圍掃描,速度提升 3 倍

對於任何需要維護超大表(更新舊數據、分批刪除、數據遷移)的 DBA 或開發者來説,使用 ctid(元組物理位置)將大表切分為多個小塊進行處理是標準操作。然而,直到現在,這種操作都有一個巨大的痛點:它嚴格依賴單進程

隨着最近的一個 Commit (0ca3b169) 合併入 PostgreSQL 19 (master 分支),TID 範圍掃描(TID Range Scans)終於支持並行了

該功能由瀚高的 Cary Huang 提出並主導開發,由微軟的 David Rowley 協助測試及審閲,並最終提交。他們密切合作以完善並行安全邏輯,確保工作進程正確處理掃描限制——最終促成了這個落地到 master 分支的健壯實現。

瀚高以“用開源鏈接世界”為使命,強調開源技術在數據庫基礎軟件領域的核心作用,致力於通過共享和合作,推動行業發展,同時鏈接和賦能全球用户。開源技術是中國軟件技術發展的必由之路,瀚高作為亞太地區 PostgreSQL 國際社區頂級貢獻者之一,長期深度參與 PostgreSQL 國際社區發展與建設。自 2025 年 7 月以來,瀚高被 PostgreSQL 社區採納的貢獻就已超過 2000 行代碼。

根據基準測試,新特性的速度提升高達 3 倍

1. 核心痛點:規劃器(Planner)的權衡

Postgres 自版本 14 起就支持了 TID Range Scans。這允許你基於物理塊號掃描表的特定切片:

SELECT * FROM my_large_table WHERE ctid >= '(0,0)' AND ctid < '(10000,0)';

這是像 AWS DMS 這樣的工具或邏輯複製初始化器拆分海量表的標準方式。問題在於,直到現在這種掃描節點嚴格來説都是單工作進程(single worker) 的。

這迫使 Postgres 查詢規劃器陷入了兩難境地。當你在大數據集上運行查詢時,規劃器必須在以下兩者之間做出選擇:

  • TID Range Scan: I/O 高效(只讀取你請求的塊),但是單工作進程。
  • Parallel Seq Scan(並行順序掃描): CPU 高效(佔用所有 CPU 內核),但 I/O 浪費(可能會為了過濾而讀取超出你範圍的塊)。

規劃器經常會錯誤地選擇並行順序掃描,CPU 收益似乎超過了 I/O 損耗帶來的負面影響。這導致數據庫為了利用可用的工作進程,讀取了比必要多得多的數據。

2. 修復方案:並行性與可變分塊

由 Cary Huang 開發並由 David Rowley 提交的代碼,引入了允許 Tid Range Scan 參與並行查詢計劃的基礎架構。該邏輯有效地將塊範圍分配給可用的並行工作進程。不再是一個進程從塊 0 掃描到 N,多個工作進程可以併發地獲取數據塊。

實現(約 500 行代碼)重用了並行順序掃描中的“塊分塊(block chunking)”邏輯。但它不僅僅是將塊範圍平均分配給工作進程,因為如果表的某個部分數據密度更高,這種簡單分配可能導致負載不均衡。

相反,它使用了衰減塊大小策略 (decaying chunk size strategy):

  • 大塊開始 (Large Start): 工作進程開始時領取大塊的塊,以最大限度地減少共享狀態上的鎖定開銷。
  • 逐漸減小 (Tapering Down): 隨着掃描的進行,分塊大小會縮小。
  • 顆粒化結束 (Granular Finish): 到掃描結束時,工作進程每次只領取 1 個塊。

這種“緩慢減少”確保了我們不會最後只剩下一個工作進程在處理一個巨大的最終塊,而其他工作進程卻閒置着。它強制所有進程大致在同一時間跨過終點線。

3. 基準測試數據

為了看到實際效果,我創建了一個包含 1000 萬行的表 bench_tid_range,並使用 ctid 範圍條件對錶的前 50% 運行了 count(*) 查詢。

測試環境:

  • 數據量:1000 萬行
  • 查詢:SELECT count(*) FROM bench_tid_range WHERE ctid >= '(0,0)' AND ctid < '(41667,0)'
環境 工作進程數 (Workers) 執行時間 (中位數) 加速比
Before (Pg 18) 0 448 ms 1.00x
After (Pg 19) 0 435 ms 1.03x
After (Pg 19) 1 238 ms 1.88x
After (Pg 19) 2 174 ms 2.58x
After (Pg 19) 3 151 ms 2.97x
After (Pg 19) 4 150 ms 2.98x
After (Pg 19) 5 147 ms 3.05x
After (Pg 19) 6 143 ms 3.14x
After (Pg 19) 7 147 ms 3.04x
After (Pg 19) 8 147 ms 3.04x

1.png

我們可以看到,僅僅啓用 1 個工作進程(這實際上給了我們 2 個掃描進程:Leader + 1 個 Worker),執行時間就大幅下降。對於這個特定的工作負載,“最佳平衡點”似乎在 2-3 個工作進程左右。

4. 為什麼不直接用“並行順序掃描”?

你可能會問:“為什麼 Postgres v18 不直接選擇並行順序掃描?用 4 個工作進程掃描整個表難道不比用 1 個進程掃描半個錶快嗎?”

我通過強制設置 enable_tidscan = off 並使用 4 個工作進程測試了這一點:

  • 執行時間: ~230 ms。
  • I/O: 訪問了所有 ~83k 個頁面。

新的並行 TID 範圍掃描(~150 ms)仍然比暴力/強制的並行順序掃描快 35%,而且它產生的 I/O 負載只有後者的一半(只訪問了 ~41k 個頁面)。這可謂兩全其美:快速的執行時間(並行)和高效的資源使用(類似索引的範圍界定)。

5. 這對工具意味着什麼

如果你維護在 Postgres 實例之間移動數據的內部腳本,你可能編寫了手動計算塊範圍並將巨大的表劃分為塊、然後生成進程來運行它們的代碼。

隨着 PostgreSQL 19 的推出,這種複雜性可能可以被刪除了。你可以發出更廣泛的 TID 範圍查詢,並相信規劃器會有效地在集羣的 I/O 和 CPU 資源之間分配工作。

6. 如何復現測試

這是設置測試表和運行基準測試的 SQL:

-- 1. 創建表
DROP TABLE IF EXISTS bench_tid_range;
CREATE TABLE bench_tid_range (id int, payload text);

-- 2. 插入 10M 行以生成 ~41k 個頁面
INSERT INTO bench_tid_range
SELECT x, 'payload_' || x
FROM generate_series(1, 10000000) x;

-- 3. Vacuum 以設置可見性映射並凍結(對於穩定的基準測試很重要)
VACUUM (ANALYZE, FREEZE) bench_tid_range;

-- 4. 為會話啓用並行
SET max_parallel_workers_per_gather = 4; -- 嘗試 2, 4, 8
SET min_parallel_table_scan_size = 0;    -- 即使對於較小的表也強制並行掃描

-- 5. 運行查詢
EXPLAIN (ANALYZE, BUFFERS)
SELECT count(*)
FROM bench_tid_range
WHERE ctid >= '(0,0)' AND ctid < '(41667,0)';

7. 結論

這是一項令人欣喜的“底層”改進。它或許不會改變您日常的臨時查詢,但對於構建自定義數據維護腳本的數據庫管理員和開發人員而言,並行執行基於 TID 的掃描功能是優化工具包中一項強大的新工具。

8. 參考

本文部分內容是來自 Grant Zhou 和 Robins Tharakan 撰寫的英文博客。

  • 提交 0ca3b169:https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=0ca3b16973a8bb1c185f56e65edcadc0d9d2c406
  • 討論貼:https://www.postgresql.org/message-id/flat/18f2c002a24.11bc2ab825151706.3749144144619388582%40highgo.ca
  • https://hornetlabs.ca/2025/12/08/speeding-up-large-table-scans-with-parallel-tid-ranges-in-postgresql-19/
  • https://www.thatguyfromdelhi.com/2025/12/3x-faster-tid-range-scans-postgres-19.html
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.