【译】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阅读 215,634评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,951评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,427评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,770评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,835评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,799评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,768评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,544评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,979评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,271评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,427评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,121评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,756评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,375评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,579评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,410评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,315评论 2 352

推荐阅读更多精彩内容