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

前言

對 PostgreSQL 中數(shù)據(jù)的并發(fā)訪問由多版本并發(fā)控制 (MVCC) 模型管理癣丧。 為每個 SQL 語句維護(hù)數(shù)據(jù)快照,以便它們始終獲得一致的數(shù)據(jù)栈妆,即使其他事務(wù)正在同時對其進(jìn)行修改坎缭。 當(dāng)行已被一個或多個事務(wù)修改時,這將導(dǎo)致管理同一行的多個版本签钩。 從用戶的角度來看掏呼,可能只有一行數(shù)據(jù),但 PostgreSQL 內(nèi)部可能維護(hù)該行的一個或多個版本铅檩。

行版本是否對事務(wù)可見是通過堆中的行數(shù)據(jù)來維護(hù)的憎夷。 為了優(yōu)化可見性信息的獲取,PostgreSQL 還維護(hù)了一個“_vm”關(guān)系分支昧旨,它跟蹤那些只包含對所有活躍事務(wù)可見的元組的頁面拾给。

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

乍一看似乎無害额衙,但這種死索引元組的累積會產(chǎn)生級聯(lián)效應(yīng),從而導(dǎo)致性能顯著下降。 在 PostgreSQL 13 中完成重復(fù)數(shù)據(jù)刪除工作后窍侧,下一個合乎邏輯的步驟是通過減少頁面拆分來防止 btree 索引膨脹县踢。

物理數(shù)據(jù)存儲

PostgreSQL 將數(shù)據(jù)保存在被稱為頁面的固定大小存儲單元中。 頁面的大小是在 PostgreSQL 服務(wù)器編譯過程中定義的伟件。 默認(rèn)頁面大小為 8k硼啤,但可以將其更改為更高的值。 雖然更改頁面大小會使事情變得復(fù)雜斧账,因為其他工具可能也需要重新編譯或重新配置谴返。

每個表和索引都存儲在頁數(shù)組中。 將數(shù)據(jù)插入表中時咧织,會將數(shù)據(jù)寫入具有足夠可用空間的頁面亏镰。 否則,將創(chuàng)建一個新頁面拯爽。

然而,索引有點不同钧忽。 索引中的第一頁是一個元頁面毯炮,其中包含有關(guān)索引的控制信息。 也有一些特殊的頁面來維護(hù)索引相關(guān)的信息耸黑。 對于 btree 索引桃煎,數(shù)據(jù)必須根據(jù)索引列和堆元組 ID(元組在表中的物理位置)進(jìn)行排序。 因此大刊,插入和更新必須在正確的頁面上進(jìn)行为迈,以保持排序順序。 如果頁面沒有足夠的空間容納傳入的元組缺菌,則必須創(chuàng)建新頁面葫辐,并將溢出頁面中的一些項目移動到新頁面。 如果需要伴郁,這些葉子頁面的父頁面會被遞歸拆分耿战。

避免頁面分裂

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

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

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

自下而上的刪除操作

為了看到這種自下而上的刪除方法的實際好處程腹,讓我們更深入地研究一下 B-Tree 索引匣吊。 我們將比較 PostgreSQL 版本 13 和 14 之間的 btree 索引大小。為了更詳細(xì)地檢查索引數(shù)據(jù)寸潦,我將使用 contrib 模塊中提供的“pageinspect”擴(kuò)展色鸳。 “pageinspect”擴(kuò)展允許我們查看索引或表的底層頁面內(nèi)容。

讓我們從創(chuàng)建 pageinspect 擴(kuò)展開始见转。您可能需要安裝 contrib 模塊命雀,或者如果您是從源代碼構(gòu)建,請安裝它然后繼續(xù)斩箫。

CREATE EXTENSION IF NOT EXISTS pageinspect;

現(xiàn)在讓我們創(chuàng)建一個包含兩列的表“foo”吏砂,創(chuàng)建兩個包含一個覆蓋索引的索引,并分析該表乘客。

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”表的頁面狐血、元組和關(guān)系大小了。

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 都為上述查詢提供完全相同的輸出易核。

禁用順序掃描和位圖掃描以強(qiáng)制進(jìn)行索引掃描氛雪。這將強(qiáng)制此示例中的查詢使用索引掃描

SET enable_seqscan = false;
SET enable_bitmapscan = false;

創(chuàng)建四個新版本的元組

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 表以確保我們的統(tǒng)計數(shù)據(jù)準(zhǔn)確無誤耸成。還讓我們回顧一下“foo”表的頁數(shù)报亩、元組和關(guān)系大小。

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)

兩個版本的表大小都增加了相同的數(shù)量井氢,但是 14.1 中的索引與 13.5 相比明顯更小弦追。 很好,讓我們檢查頁面內(nèi)容以了解幕后發(fā)生的事情花竞。

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

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 清理了死元組以騰出空間零远。 版本越少苗分,頁面就越少,從而減少膨脹牵辣,并為我們提供更小的索引大小摔癣。

結(jié)論

在 PostgreSQL 14 版本中,由于底部刪除而減少索引大小是一個巨大的優(yōu)勢纬向。Btree 索引具有一種機(jī)制择浊,其中普通索引掃描設(shè)置 LP_DEAD 標(biāo)志。 這不是為位圖索引掃描設(shè)置的逾条。 一旦設(shè)置好琢岩,就可以在不需要真空的情況下回收空間。 然而师脂,這是完全不同的一類死元組担孔。 從長遠(yuǎn)來看,這種自下而上的刪除策略有助于顯著減少特定類別的重復(fù)項吃警。 它不僅減少了vacuum 的負(fù)載糕篇,還有助于保持索引更健康,從而提高訪問速度汤徽。 因此,如果您的更新工作量很大灸撰,那么在提供更好性能的同時谒府,肯定會節(jié)省資源利用率和成本。

原文地址

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末浮毯,一起剝皮案震驚了整個濱河市完疫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌债蓝,老刑警劉巖壳鹤,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異饰迹,居然都是意外死亡芳誓,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進(jìn)店門啊鸭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來锹淌,“玉大人,你說我怎么就攤上這事赠制÷赴冢” “怎么了?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長烟号。 經(jīng)常有香客問我绊谭,道長,這世上最難降的妖魔是什么汪拥? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任达传,我火速辦了婚禮,結(jié)果婚禮上喷楣,老公的妹妹穿的比我還像新娘趟大。我一直安慰自己,他們只是感情好铣焊,可當(dāng)我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布逊朽。 她就那樣靜靜地躺著,像睡著了一般曲伊。 火紅的嫁衣襯著肌膚如雪叽讳。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天坟募,我揣著相機(jī)與錄音岛蚤,去河邊找鬼。 笑死懈糯,一個胖子當(dāng)著我的面吹牛涤妒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播赚哗,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼她紫,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了屿储?” 一聲冷哼從身側(cè)響起贿讹,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎够掠,沒想到半個月后民褂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡疯潭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年赊堪,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片竖哩。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡雹食,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出期丰,到底是詐尸還是另有隱情群叶,我是刑警寧澤吃挑,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站街立,受9級特大地震影響舶衬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜赎离,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一逛犹、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧梁剔,春花似錦虽画、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至个盆,卻和暖如春脖岛,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背颊亮。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工柴梆, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人终惑。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓绍在,卻偏偏與公主長得像,于是被迫代替她去往敵國和親雹有。 傳聞我的和親對象是個殘疾皇子偿渡,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,871評論 2 354

推薦閱讀更多精彩內(nèi)容