【譯】PostgreSQL 14 B-Tree Index:通過自下而上刪除減少膨脹

前言

對 PostgreSQL 中數據的并發訪問由多版本并發控制 (MVCC) 模型管理。 為每個 SQL 語句維護數據快照,以便它們始終獲得一致的數據,即使其他事務正在同時對其進行修改。 當行已被一個或多個事務修改時,這將導致管理同一行的多個版本。 從用戶的角度來看,可能只有一行數據,但 PostgreSQL 內部可能維護該行的一個或多個版本。

行版本是否對事務可見是通過堆中的行數據來維護的。 為了優化可見性信息的獲取,PostgreSQL 還維護了一個“_vm”關系分支,它跟蹤那些只包含對所有活躍事務可見的元組的頁面。

對任何事務不再可見的死版本將由 vacuum 進程清除。 在此之前,索引和堆頁面可能包含大量死元組(這實際上取決于您的工作負載的性質)。 對于更新密集型工作負載,這可能是一個巨大的數字!

乍一看似乎無害,但這種死索引元組的累積會產生級聯效應,從而導致性能顯著下降。 在 PostgreSQL 13 中完成重復數據刪除工作后,下一個合乎邏輯的步驟是通過減少頁面拆分來防止 btree 索引膨脹。

物理數據存儲

PostgreSQL 將數據保存在被稱為頁面的固定大小存儲單元中。 頁面的大小是在 PostgreSQL 服務器編譯過程中定義的。 默認頁面大小為 8k,但可以將其更改為更高的值。 雖然更改頁面大小會使事情變得復雜,因為其他工具可能也需要重新編譯或重新配置。

每個表和索引都存儲在頁數組中。 將數據插入表中時,會將數據寫入具有足夠可用空間的頁面。 否則,將創建一個新頁面。

然而,索引有點不同。 索引中的第一頁是一個元頁面,其中包含有關索引的控制信息。 也有一些特殊的頁面來維護索引相關的信息。 對于 btree 索引,數據必須根據索引列和堆元組 ID(元組在表中的物理位置)進行排序。 因此,插入和更新必須在正確的頁面上進行,以保持排序順序。 如果頁面沒有足夠的空間容納傳入的元組,則必須創建新頁面,并將溢出頁面中的一些項目移動到新頁面。 如果需要,這些葉子頁面的父頁面會被遞歸拆分。

避免頁面分裂

當新的元組或新的非 HOT 元組版本要添加到索引中時,會發生 B-Tree 索引頁拆分。 HOT 是“heap only tuple”的縮寫。 基本而言,它是一種刪除給定頁面上的死行(碎片整理)并因此為新行騰出空間的方法。 通過避免或延遲頁面拆分,我們可以避免或減慢索引擴展,從而減少膨脹。 現在這很令人興奮!

雖然對新元組沒有太多可做的事情,但可以管理更新,以便可以增量刪除邏輯上未更改的索引元組(即未更改的索引列)的過時版本,以保持新版本的可用空間。 這個過程得到了規劃器的幫助,規劃器向索引方法提供了一個提示,“索引未更改”。 如果沒有任何索引列由于此更新而更改,則為 true。

自下而上的刪除是在索引操作期間完成的,當預期“版本攪動頁面拆分”時(“索引未更改”提示為真)。 邏輯上未更改的索引元組的過時版本將被刪除,從而在頁面上為較新版本騰出空間。 這種方法潛在地避免了頁面拆分。

自下而上的刪除操作

為了看到這種自下而上的刪除方法的實際好處,讓我們更深入地研究一下 B-Tree 索引。 我們將比較 PostgreSQL 版本 13 和 14 之間的 btree 索引大小。為了更詳細地檢查索引數據,我將使用 contrib 模塊中提供的“pageinspect”擴展。 “pageinspect”擴展允許我們查看索引或表的底層頁面內容。

讓我們從創建 pageinspect 擴展開始。您可能需要安裝 contrib 模塊,或者如果您是從源代碼構建,請安裝它然后繼續。

CREATE EXTENSION IF NOT EXISTS pageinspect;

現在讓我們創建一個包含兩列的表“foo”,創建兩個包含一個覆蓋索引的索引,并分析該表。

DROP TABLE IF EXISTS foo;
CREATE TABLE foo WITH (autovacuum_enabled = false) AS (SELECT GENERATE_SERIES(1, 1000) AS col1, SUBSTR(MD5(RANDOM()::TEXT), 0, 25) AS value);
CREATE INDEX ON foo(col1);
CREATE INDEX ON foo(col1) INCLUDE(value);

是時候檢查“foo”表的頁面、元組和關系大小了。

SELECT  relname
        , relkind
        , relpages
        , reltuples
        , PG_SIZE_PRETTY(PG_RELATION_SIZE(oid))
FROM    pg_class
WHERE   relname LIKE '%foo%'
ORDER
BY      relkind DESC;

      relname       | relkind | relpages | reltuples | pg_size_pretty 
--------------------+---------+----------+-----------+----------------
 foo                | r       |        8 |      1000 | 64 kB
 foo_col1_idx       | i       |        5 |      1000 | 40 kB
 foo_col1_value_idx | i       |        9 |      1000 | 72 kB
(3 rows)

14.1 和 13.5 都為上述查詢提供完全相同的輸出。

禁用順序掃描和位圖掃描以強制進行索引掃描。這將強制此示例中的查詢使用索引掃描

SET enable_seqscan = false;
SET enable_bitmapscan = false;

創建四個新版本的元組

UPDATE foo SET value = value || 'x';
UPDATE foo SET value = value || 'x';
UPDATE foo SET value = value || 'x';
UPDATE foo SET value = value || 'x';

上述語句每次更新 1000 行。 ANALYZE 表以確保我們的統計數據準確無誤。還讓我們回顧一下“foo”表的頁數、元組和關系大小。

ANALYZE foo;

SELECT  relname
        , relkind
        , relpages
        , reltuples
        , PG_SIZE_PRETTY(PG_RELATION_SIZE(oid))
FROM    pg_class
WHERE   relname LIKE '%foo%'
ORDER
BY      relkind DESC;

--PostgreSQL 14.1
      relname       | relkind | relpages | reltuples | pg_size_pretty 
--------------------+---------+----------+-----------+----------------
 foo                | r       |        8 |      1000 | 288 kB
 foo_col1_idx       | i       |        5 |      1000 | 88 kB
 foo_col1_value_idx | i       |        9 |      1000 | 216 kB
(3 rows)


--PostgreSQL 13.5
--------------------+---------+----------+-----------+----------------
 foo                | r       |        8 |      1000 | 288 kB
 foo_col1_idx       | i       |        5 |      1000 | 104 kB
 foo_col1_value_idx | i       |        9 |      1000 | 360 kB
(3 rows)

兩個版本的表大小都增加了相同的數量,但是 14.1 中的索引與 13.5 相比明顯更小。 很好,讓我們檢查頁面內容以了解幕后發生的事情。

查看第一個索引頁面(不是元頁面)的內容清楚地顯示了自下而上的刪除如何使索引大小保持較小。

SELECT  itemoffset
        , ctid
        , itemlen
        , nulls
        , vars
        , dead
        , htid
FROM    bt_page_items('foo_col1_value_idx', 1)
LIMIT   15;

PostgreSQL 14.1
 itemoffset |  ctid   | itemlen | nulls | vars | dead |  htid   
------------+---------+---------+-------+------+------+---------
          1 | (7,1)   |      16 | f     | f    |      | 
          2 | (7,181) |      40 | f     | t    | f    | (7,181)
          3 | (7,225) |      48 | f     | t    | f    | (7,225)
          4 | (7,182) |      40 | f     | t    | f    | (7,182)
          5 | (7,226) |      48 | f     | t    | f    | (7,226)
          6 | (7,183) |      40 | f     | t    | f    | (7,183)
          7 | (7,227) |      48 | f     | t    | f    | (7,227)
          8 | (7,184) |      40 | f     | t    | f    | (7,184)
          9 | (7,228) |      48 | f     | t    | f    | (7,228)
         10 | (7,185) |      40 | f     | t    | f    | (7,185)
         11 | (7,229) |      48 | f     | t    | f    | (7,229)
         12 | (7,186) |      40 | f     | t    | f    | (7,186)
         13 | (7,230) |      48 | f     | t    | f    | (7,230)
         14 | (7,187) |      40 | f     | t    | f    | (7,187)
         15 | (7,231) |      48 | f     | t    | f    | (7,231)
(15 rows)


PostgreSQL 13.5
 itemoffset |  ctid   | itemlen | nulls | vars | dead |  htid   
------------+---------+---------+-------+------+------+---------
          1 | (0,1)   |      16 | f     | f    |      | 
          2 | (0,1)   |      40 | f     | t    | f    | (0,1)
          3 | (7,49)  |      40 | f     | t    | f    | (7,49)
          4 | (7,137) |      40 | f     | t    | f    | (7,137)
          5 | (7,181) |      40 | f     | t    | f    | (7,181)
          6 | (7,225) |      48 | f     | t    | f    | (7,225)
          7 | (0,2)   |      40 | f     | t    | f    | (0,2)
          8 | (7,50)  |      40 | f     | t    | f    | (7,50)
          9 | (7,138) |      40 | f     | t    | f    | (7,138)
         10 | (7,182) |      40 | f     | t    | f    | (7,182)
         11 | (7,226) |      48 | f     | t    | f    | (7,226)
         12 | (0,3)   |      40 | f     | t    | f    | (0,3)
         13 | (7,51)  |      40 | f     | t    | f    | (7,51)
         14 | (7,139) |      40 | f     | t    | f    | (7,139)
         15 | (7,183) |      40 | f     | t    | f    | (7,183)
(15 rows)

查看 14.1 的 2 到 3 和 13.5 的 2 到 6 的“itemoffset”可以告訴我們整個故事。 13.5 攜帶了整套元組版本,而 14.1 清理了死元組以騰出空間。 版本越少,頁面就越少,從而減少膨脹,并為我們提供更小的索引大小。

結論

在 PostgreSQL 14 版本中,由于底部刪除而減少索引大小是一個巨大的優勢。Btree 索引具有一種機制,其中普通索引掃描設置 LP_DEAD 標志。 這不是為位圖索引掃描設置的。 一旦設置好,就可以在不需要真空的情況下回收空間。 然而,這是完全不同的一類死元組。 從長遠來看,這種自下而上的刪除策略有助于顯著減少特定類別的重復項。 它不僅減少了vacuum 的負載,還有助于保持索引更健康,從而提高訪問速度。 因此,如果您的更新工作量很大,那么在提供更好性能的同時,肯定會節省資源利用率和成本。

原文地址

PostgreSQL 14 B-Tree Index: Reduced Bloat with Bottom-Up Deletion

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容