按页面级别进行清理
在 PostgreSQL 的热更新 (HOT) 中,我们覆盖了页面修剪及其清理 HOT 链的过程,这是 PostgreSQL 在普通读取期间回收死元组空间的一种优雅捷径。所有这些都不需要等待任何后台进程。但修剪正是这种捷径:它仅在单个页面内工作,并仅适用于经过 HOT 更新的元组。对于其他所有情况(触及索引列的冷更新、普通 DELETE、索引条目清理、空闲空间映射注册、可见性映射维护),我们需要执行 VACUUM。本文不会重复 VACUUM 的操作功能。删除操作的复杂性使得本文涵盖自动真空调优、工作分配和死元组清理的操作侧面。在这里,我们将逐个字节观察 VACUUM 的工作。我们将在每个阶段之前和之后对页面进行快照,准确跟踪页面头部、行指针、元组头、空闲空间映射和可见性映射的变化。使用与往常相同的工具:pageinspect、pg_visibility 和 pg_freespacemap。设置 我们需要一个足够行数的表,以使得前后比较具有意义,并且有索引来演示完整的 VACUUM 周期。创建扩展,如果它不存在的话:CREATE EXTENSION IF NOT EXISTS pageinspect; CREATE EXTENSION IF NOT EXISTS pg_visibility; CREATE EXTENSION IF NOT EXISTS pg_freespacemap; 创建表 vacuum_demo (id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY, category text NOT NULL, payload text); 插入示例数据:INSERT INTO vacuum_demo (category, payload) SELECT 'cat_' || (i % 5), repeat('x', 100) FROM generate_series(1, 50) AS i; 每行包含 100 字节的有效负载,共有五十行。主键会给我们一个索引,这一点很重要:当索引参与时,VACUUM 的行为会发生变化。预先运行一次 VACUUM,以便我们以干净的基线开始:VACUUM vacuum_demo; 在任何删除之前快照 记录页面 0 的基线状态。首先是页面头部:SELECT lower, upper, special, pagesize FROM page_header(get_raw_page('vacuum_demo', 0)); lower | upper | special | pagesize -------+-------+---------+---------- 224 | 1392 | 8192 | 8192 (1 row) pd_lower 是 224:这是 24 字节的页面头部加上 50 个 4 字节的行指针 (24 + 200 = 224)。pd_upper 是 1392,因此我们的元组占用字节 1392 到 8191。空闲空间为 1392 - 224 = 1168 字节。剩余空间不多;那些 100 字节的有效负载加起来很快就填满了。接下来查看行指针和元组头:SELECT lp, lp_flags, lp_off, lp_len, t_xmin, t_xmax, t_ctid FROM heap_page_items(get_raw_page('vacuum_demo', 0)) LIMIT 10; lp | lp_flags | lp_off | lp_len | t_xmin | t_xmax | t_ctid ----+----------+--------+--------+--------+--------+-------- 1 | 1 | 8056 | 135 | 746 | 0 | (0,1) 2 | 1 | 7920 | 135 | 746 | 0 | (0,2) 3 | 1 | 7784 | 135 | 746 | 0 | (0,3) 4 | 1 | 7648 | 135 | 746 | 0 | (0,4) 5 | 1 | 7512 | 135 | 746 | 0 | (0,5) 6 | 1 | 7376 | 135 | 746 | 0 | (0,6) 7 | 1 | 7240 | 135 | 746 | 0 | (0,7) 8 | 1 | 7104 | 135 | 746 | 0 | (0,8) 9 | 1 | 6968 | 135 | 746 | 0 | (0,9) 10 | 1 | 6832 | 135 | 746 | 0 | (0,10) (10 rows) lp_len 为 135,这是元组的实际字节长度,但每个元组在页面上占用了一个经过 MAXALIGN 对齐的 136 字节的槽;注意 lp_off 值每次减少 136。对齐的步幅就是下面空闲空间计算所依据的。每个行指针都是 LP_NORMAL (lp_flags = 1)。每个元组的 t_xmax = 0:自从插入后,没人触碰过这些行。每个 t_ctid 指向自身。这是一个完全干净的页面。创建死元组 现在使一些行变得无效:DELETE FROM vacuum_demo WHERE id % 3 = 0; DELETE 16 这大约删除了每第三行:IDs 3、6、9、12,等等。现在有十六行是死的。在 VACUUM 运行前查看页面:SELECT lp, lp_flags, lp_off, lp_len, t_xmin, t_xmax, t_ctid FROM heap_page_items(get_raw_page('vacuum_demo', 0)) LIMIT 10; lp | lp_flags | lp_off | lp_len | t_xmin | t_xmax | t_ctid ----+----------+--------+--------+--------+--------+-------- 1 | 1 | 8056 | 135 | 746 | 0 | (0,1) 2 | 1 | 7920 | 135 | 746 | 0 | (0,2) 3 | 1 | 7784 | 135 | 746 | 747 | (0,3) 4 | 1 | 7648 | 135 | 746 | 0 | (0,4) 5 | 1 | 7512 | 135 | 746 | 0 | (0,5) 6 | 1 | 7376 | 135 | 746 | 747 | (0,6) 7 | 1 | 7240 | 135 | 746 | 0 | (0,7) 8 | 1 | 7104 | 135 | 746 | 0 | (0,8) 9 | 1 | 6968 | 135 | 746 | 747 | (0,9) 10 | 1 | 6832 | 135 | 746 | 0 | (0,10) (10 rows) 查看第 3、6 和 9 行。它们的 t_xmax 现在是 747,即 DELETE 语句的事务 ID。但其他一切保持不变。lp_flags 仍然是 1 (LP_NORMAL)。lp_off 和 lp_len 也没有变化。元组仍然物理上保留在页面上,占用空间。页面头部也没有变化:SELECT lower, upper, special, pagesize FROM page_header(get_raw_page('vacuum_demo', 0)); lower | upper | special | pagesize -------+-------+---------+---------- 224 | 1392 | 8192 | 8192 (1 row) pd_lower 和 pd_upper 与 DELETE 之前相同。PostgreSQL 将行标记为无效(通过标记 t_xmax),但并没有回收出一个字节。死元组造成了空间浪费,它们会一直保持这样的状态,直到 VACUUM 进行处理。VACUUM 如何处理一个表 在我们运行 VACUUM 之前……
本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。
☕请我喝杯咖啡