對於任何需要維護超大表(更新舊數據、分批刪除、數據遷移)的 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 個工作進程(這實際上給了我們 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