# 清理与冻结

LLMS 索引： [llms.txt](/zh18/llms.txt)

---

<span id="internal-ru-chapter-6" class="pdf-anchor"></span><span id="internal-ru-page-126" class="pdf-anchor"></span>

<span id="internal-ru-dummy-48" class="pdf-anchor"></span>

## 6.1 清理 {#internal-ru-section-6-1}

页内剪枝执行得很快，但只能释放一部分空间。它只在一个表页面内工作，不会触碰索引；反过来，索引的页内剪枝也只在一个索引页面内工作，不会触碰表。

主要的*常规清理*[^internal-ru-126-1]由 `VACUUM` 命令执行[^internal-ru-126-2]。它会完整处理表，不仅清除无用的行版本，还会从所有索引中清除指向这些行版本的引用。

清理与系统中的其他活动并行进行。执行清理时，表和索引仍可正常读写（但不能同时执行 `CREATE INDEX`、`ALTER TABLE` 等命令）。（*第 [261](/zh18/12-relation-level-locks/#internal-ru-page-262) 页*）

为了避免扫描多余的页面，清理会使用可见性映射。映射中已标记的页面会被跳过，因为它们保证只包含有效行版本；其他页面则可能包含可以清除的版本。如果处理后页面中只剩下位于数据库视界之外的行版本，清理就会更新可见性映射。（*第 [32](/zh18/01-introduction/#internal-ru-page-33) 页*）

清理还会更新空闲空间映射，反映页面中新增的空闲空间。

<span id="internal-ru-page-127" class="pdf-anchor"></span><span id="internal-ru-dummy-49" class="pdf-anchor"></span>

创建一个表和一个索引：

```text {label="psql"}
=> CREATE TABLE vac(
  id integer,
  s char(100)
) WITH (autovacuum_enabled = off);
=> CREATE INDEX vac_s ON vac(s);
```

这里通过存储参数 *autovacuum\_enabled* 禁用自动清理，以便只为实验目的精确控制清理时机。

插入一行，并执行两次更新：

```text {label="psql"}
=> INSERT INTO vac(id,s) VALUES (1,'A');
=> UPDATE vac SET s = 'B';
=> UPDATE vac SET s = 'C';
```

此时表中有三个行版本：

```text {label="psql"}
=> SELECT * FROM heap_page('vac',0);
 ctid  | state  | xmin  | xmax  | hhu | hot | t_ctid
-------+--------+-------+-------+-----+-----+--------
 (0,1) | normal | 845 c | 846 c |     |     | (0,2)
 (0,2) | normal | 846 c | 847   |     |     | (0,3)
 (0,3) | normal | 847   | 0 a   |     |     | (0,3)
(3 rows)
```

索引中有指向每个行版本的引用：

```text {label="psql"}
=> SELECT * FROM index_page('vac_s',1);
 itemoffset | htid  | dead
------------+-------+------
          1 | (0,1) | f
          2 | (0,2) | f
          3 | (0,3) | f
(3 rows)
```

清理后，死行版本消失，只剩下一个有效版本：

<span id="internal-ru-page-128" class="pdf-anchor"></span>

```text {label="psql"}
=> VACUUM vac;
=> SELECT * FROM heap_page('vac',0);
 ctid  | state  | xmin  | xmax | hhu | hot | t_ctid
-------+--------+-------+------+-----+-----+--------
 (0,1) | unused |       |      |     |     |
 (0,2) | unused |       |      |     |     |
 (0,3) | normal | 847 c | 0 a  |     |     | (0,3)
(3 rows)
```

此时，指向被清除版本的前两个项指针状态变为 unused，而不是页内剪枝时的 dead，因为索引中已经没有指向它们的条目：

```text {label="psql"}
=> SELECT * FROM index_page('vac_s',1);
 itemoffset | htid  | dead
------------+-------+------
          1 | (0,3) | f
(1 row)
```

状态为 unused 的项指针被视为空闲，可以用于新的行版本。

现在，该页面已在可见性映射中标记出来。我们可以通过 `pg_visibility` 扩展查看该映射：

```text {label="psql"}
=> CREATE EXTENSION pg_visibility;
=> SELECT all_visible
FROM pg_visibility_map('vac',0);
 all_visible
-------------
 t
(1 row)
```

<span id="internal-ru-dummy-50" class="pdf-anchor"></span>

页面上所有行版本在所有数据快照中都可见，这一点也记录在表页面的页头中：

```text {label="psql"}
=> SELECT flags & 4 > 0 AS all_visible
FROM page_header(get_raw_page('vac',0));
 all_visible
-------------
 t
(1 row)
```

<span id="internal-ru-page-129" class="pdf-anchor"></span>

## 6.2 再谈数据库视界 {#internal-ru-section-6-2}

清理根据数据库视界判断哪些行版本应视为死版本。这个概念非常重要，值得再回顾一次。

从头重复前面的实验：

```text {label="psql"}
=> TRUNCATE vac;
=> INSERT INTO vac(id,s) VALUES (1,'A');
=> UPDATE vac SET s = 'B';
```

<span id="internal-ru-dummy-51" class="pdf-anchor"></span>

不过，在再次更新这一行之前，我们先启动第二个事务，让它持有数据库视界（几乎任何事务都可以，只有以 Read Committed 隔离级别执行的虚拟事务例外）。例如，让这个事务修改*另一个*表中的某些内容。（*第 [106](/zh18/04-snapshots/#internal-ru-page-107) 页*）

```text {label="psql"}
=> BEGIN;
=> INSERT INTO accounts(id, client, amount)
  VALUES (1, 'alice', 1000.00);
```

```text {label="psql"}
=> UPDATE vac SET s = 'C';
```

现在表中有三个行版本，索引中有三个引用。清理后会发生什么？

```text {label="psql"}
=> VACUUM vac;
=> SELECT * FROM heap_page('vac',0);
 ctid  | state  | xmin  | xmax  | hhu | hot | t_ctid
-------+--------+-------+-------+-----+-----+--------
 (0,1) | unused |       |       |     |     |
 (0,2) | normal | 851 c | 853 c |     |     | (0,3)
 (0,3) | normal | 853 c | 0 a   |     |     | (0,3)
(3 rows)
=> SELECT * FROM index_page('vac_s',1);
 itemoffset | htid  | dead
------------+-------+------
          1 | (0,2) | f
          2 | (0,3) | f
(2 rows)
```

<span id="internal-ru-page-130" class="pdf-anchor"></span>

上一次清理后表中只剩一个行版本，而这一次却剩下两个：清理判断版本 (0,2) 还不能删除。原因当然是数据库视界；在这个例子中，视界由尚未结束的事务决定：

```text {label="psql"}
=> SELECT backend_xmin FROM pg_stat_activity
WHERE pid = pg_backend_pid();
 backend_xmin
--------------
          852
(1 row)
```

可以指定 `VERBOSE` 子句，让清理报告发生了什么：

```text {label="psql" hl_lines="6 7"}
=> VACUUM VERBOSE vac;
INFO:  vacuuming "internals.public.vac"
INFO:  finished vacuuming "internals.public.vac": index scans: 0
pages: 0 removed, 1 remain, 1 scanned (100.00% of total), 0 eagerly
scanned
tuples: 0 removed, 2 remain, 1 are dead but not yet removable
removable cutoff: 852, which was 2 XIDs old when operation ended
frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
visibility map: 0 pages set all-visible, 0 pages set all-frozen (0 were
all-visible)
index scan not needed: 0 pages from table (0.00% of total) had 0 dead
item identifiers removed
avg read rate: 0.000 MB/s, avg write rate: 0.000 MB/s
buffer usage: 14 hits, 0 reads, 0 dirtied
WAL usage: 0 records, 0 full page images, 0 bytes, 0 buffers full
system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
VACUUM
```

这里可以看到：

- 表中没有发现可以删除的行版本（`tuples: 0 removed`）；

- 找到两个不能删除的行版本（`2 remain`）；

- 两个不能删除的版本中有一个是死版本（`1 are dead but not yet removable`），另一个是有效版本；

- 当前清理视界（`removable cutoff`）与打开事务的视界相同。

<span id="internal-ru-page-131" class="pdf-anchor"></span>

结束打开的事务后，数据库视界向前推进，清理也就可以继续：

```text {label="psql"}
=> COMMIT;
```

```text {label="psql" hl_lines="6 7 10"}
=> VACUUM VERBOSE vac;
INFO:  vacuuming "internals.public.vac"
INFO:  finished vacuuming "internals.public.vac": index scans: 1
pages: 0 removed, 1 remain, 1 scanned (100.00% of total), 0 eagerly
scanned
tuples: 1 removed, 1 remain, 0 are dead but not yet removable
removable cutoff: 854, which was 0 XIDs old when operation ended
new relfrozenxid: 853, which is 2 XIDs ahead of previous value
frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
visibility map: 1 pages set all-visible, 0 pages set all-frozen (0 were
all-visible)
index scan needed: 1 pages from table (100.00% of total) had 1 dead
item identifiers removed
index "vac_s": pages: 2 in total, 0 newly deleted, 0 currently deleted,
0 reusable
avg read rate: 0.000 MB/s, avg write rate: 95.274 MB/s
buffer usage: 27 hits, 0 reads, 1 dirtied
WAL usage: 6 records, 1 full page images, 8981 bytes, 0 buffers full
system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
VACUUM
```

清理发现一个死行版本已经越过更新后的视界，于是将其删除；页面中只剩下最后一个有效版本：

```text {label="psql"}
=> SELECT * FROM heap_page('vac',0);
 ctid  | state  | xmin  | xmax | hhu | hot | t_ctid
-------+--------+-------+------+-----+-----+--------
 (0,1) | unused |       |      |     |     |
 (0,2) | unused |       |      |     |     |
 (0,3) | normal | 853 c | 0 a  |     |     | (0,3)
(3 rows)
```

索引中也只剩一个条目：

```text {label="psql"}
=> SELECT * FROM index_page('vac_s',1);
 itemoffset | htid  | dead
------------+-------+------
          1 | (0,3) | f
(1 row)
```

<span id="internal-ru-page-132" class="pdf-anchor"></span>

## 6.3 清理的执行阶段 {#internal-ru-section-6-3}

<span id="internal-ru-dummy-52" class="pdf-anchor"></span>

清理的工作看起来并不复杂，但这种简单只是表象。表和索引必须同时处理，同时还不能阻塞其他进程。为此，每个表的清理都分为多个阶段[^internal-ru-132-1]。

首先扫描表以寻找死行版本；找到的版本先从索引中清除，再从表中清除。如果死版本太多，这个过程可能重复进行。最后，表还会在可能的情况下被截断。

下面详细介绍这些阶段。

<span id="internal-ru-section-star-93" class="pdf-anchor"></span>

### 表扫描 {#internal-ru-subsection-star-94}

<span id="internal-ru-dummy-53" class="pdf-anchor"></span>

一切从*表扫描*开始[^internal-ru-132-2]，扫描时会考虑可见性映射：映射中标记的页面会跳过，因为它们保证只包含有效行版本。对于从已读取页面中发现的、已经越过视界且不再需要的行版本，会把它们的标识符（`tid`）写入内存中的专用存储。不能立即删除这些行版本，因为索引可能仍然引用它们。（v. 17）

这个存储采用前缀树[^internal-ru-132-3]。页面编号是树的键，节点中存放行版本的位图。结构大小受 *maintenance\_work\_mem* 参数限制，内存按需分配。（*第 [605](/zh18/27-sp-gist/#internal-ru-page-606) 页*）（*第 [427](/zh18/20-index-scans/#internal-ru-page-428) 页*）（*默认值：* `64MB`）

> [!NOTE]
> 与 PostgreSQL 17 之前使用的普通数组相比，前缀树存储标识符时节省了一个数量级的空间，而且其总大小可以超过 1 GB——一次分配的内存片段的最大大小。

<span id="internal-ru-page-133" class="pdf-anchor"></span><span id="internal-ru-section-star-95" class="pdf-anchor"></span>

### 索引清理 {#internal-ru-subsection-star-96}

接下来有两种可能：要么扫描到达表的末尾，要么用于保存标识符的内存耗尽。无论哪种情况，都会开始*索引清理阶段*[^internal-ru-133-1]。为了从索引页面中清除指向已记录行版本的条目，表上创建的*每一个*索引都必须被*完整*扫描。

> [!NOTE]
> 如果知道索引键，索引可以快速找到行版本；但没有办法根据表行版本的标识符快速找到索引条目。针对 B-树的这项工作目前还没有成功[^internal-ru-133-2]。

<span id="internal-ru-dummy-54" class="pdf-anchor"></span>

对于大小超过 *min\_parallel\_index\_scan\_size* 的每个索引，都可能启动一个独立的工作进程；这样，多个索引就可以并行清理。可以在 `VACUUM (parallel N)` 命令中指定所需的清理进程数。实际启动的进程数不会超过空闲后台进程数[^internal-ru-133-3]和符合条件的索引数（因为一个索引不能由多个进程同时处理）。（v. 13）（*默认值：* `512kB`）

索引清理阶段还会维护空闲空间映射，并计算清理统计信息。但如果表中只有新增行（既没有删除也没有更新），就会跳过这一阶段，因为表中不会有死行版本。此时，索引清理会在最后强制执行一次，这称为*索引规整*[^internal-ru-133-4]。

索引清理阶段结束后，索引中已经没有指向无用行版本的引用，但这些版本仍然存在于表中。这是正常情况：索引访问已经无法到达它们，而顺序扫描表时，可见性检查会将死版本排除。

<span id="internal-ru-page-134" class="pdf-anchor"></span><span id="internal-ru-section-star-97" class="pdf-anchor"></span>

### 表清理 {#internal-ru-subsection-star-98}

随后开始*表清理阶段*[^internal-ru-134-1]。表会再次扫描，以便从页面中清除先前记录的行版本并释放项指针。现在索引已经不再引用这些版本，因此可以安全地这样做。

<span id="internal-ru-dummy-55" class="pdf-anchor"></span>

清理释放的空间会记录到空闲空间映射中；只剩下在所有快照中都可见的有效版本的页面，则会在可见性映射中标记出来。

如果表扫描阶段没有读完整张表，就会清空 `tid` 存储，并从上次扫描停止的位置继续重复整个过程。

<span id="internal-ru-section-star-99" class="pdf-anchor"></span>

### 表截断 {#internal-ru-subsection-star-100}

<span id="internal-ru-dummy-56" class="pdf-anchor"></span>

清理后，表页面中会出现空闲空间；有时还可能恰好释放出完整页面。如果文件末尾形成了一些空页面，清理就可以在最后的*表截断阶段*[^internal-ru-134-2]“切掉”文件尾部，并把空间归还给操作系统。

截断需要短暂地获取表的排他锁。获取锁的尝试不会持续超过五秒，以免长时间停止其他进程。（*第 [261](/zh18/12-relation-level-locks/#internal-ru-page-262) 页*）

由于需要锁住表，只有在空页面足够多时才会执行截断：至少占文件的 \(\frac{1}{16}\)，或者对于大表至少 1000 个页面（这些阈值不能配置）。

如果锁仍然造成问题，可以通过几种方式彻底禁用截断：

- 执行 `VACUUM` 命令，并指定 `truncate off`；（v. 12）

- <span id="internal-ru-page-135" class="pdf-anchor"></span>关闭存储参数 *vacuum\_truncate* 和 *toast.vacuum\_truncate*；（v. 12）

- 关闭配置参数 *vacuum\_truncate*。（v. 18）

## 6.4 冻结 {#internal-ru-section-6-4}

<span id="internal-ru-dummy-57" class="pdf-anchor"></span><span id="internal-ru-section-star-101" class="pdf-anchor"></span>

### 事务计数器回卷 {#internal-ru-subsection-star-102}

<span id="internal-ru-dummy-58" class="pdf-anchor"></span>

PostgreSQL 为事务编号分配了 32 位。40 亿看起来是一个很大的数字，但在高负载下可能很快耗尽。例如，在每秒 1000 个事务（不包括虚拟事务）的负载下，连续运行一个半月就会耗尽。

位数耗尽后，计数器必须归零并重新开始下一轮计数；英语称之为 wraparound。但只有在编号持续递增时，才能认为编号较小的事务早于编号较大的事务。因此，不能简单地把计数器归零并重新开始编号。

为什么不为事务编号分配 64 位，这样不就能彻底排除这个问题了吗？原因在于，每个行版本的头部都保存两个事务编号：`xmin` 和 `xmax`。头部已经相当大（考虑对齐后至少 24 字节），增加位数还会额外占用 8 字节。（*第 [75](/zh18/03-pages-and-tuples/#internal-ru-page-76) 页*）

> [!NOTE]
> 64 位事务编号也会使用[^internal-ru-135-1]，它们在普通 32 位编号的基础上增加一个 32 位“纪元”，但只用于内部目的，从不会出现在数据页面中。

要正确处理计数器回卷，不能比较事务编号，而要比较事务年龄（即从该事务出现以来启动的事务数量）。因此，不再使用“小于”和“大于”，而使用“先于”（“更老”）和“后于”（“更年轻”）。

<span id="internal-ru-page-136" class="pdf-anchor"></span>

在代码中，这自然通过 32 位算术实现：求两个 32 位事务编号的差，再将结果与零比较[^internal-ru-136-1]。

可以把事务编号想象成一个环。对于任意事务，逆时针方向的一半编号更老（在过去），顺时针方向的一半编号更年轻（在未来）。

![事务计数器回卷：T1、T2、T3；过去与未来](/images/internal/ru/p136-01.svg)
{#internal-ru-fig-6-1 num="6.1" caption="事务计数器回卷" .book-original-figure width=722 height=248}

但这种环形方案会产生一个令人不快的情况。对于新的事务，旧事务 T1 位于很久以前；可是过一段时间后，对某个新事务来说，T1 会落入属于未来的那一半圆。如果真的发生这种情况，后果将是灾难性的：很久以前由 T1 做出的更改会对新事务不可见。

<span id="internal-ru-section-star-103" class="pdf-anchor"></span>

### 行版本冻结与可见性规则 {#internal-ru-subsection-star-104}

为了避免这种“时间旅行”，清理进程除了释放页面空间外，还要执行另一项任务[^internal-ru-136-2]：寻找位于数据库视界之外的行版本（因此它们在所有快照中都可见），并以特殊方式标记它们，即*冻结*这些行版本。（*第 [274](/zh18/13-row-level-locks/#internal-ru-page-275) 页*）

冻结的行版本不考虑事务编号 `xmin`，在所有数据快照中都可见，因此这个编号可以安全地重新使用。

<span id="internal-ru-page-137" class="pdf-anchor"></span>

可以认为，冻结行版本中的 `xmin` 被替换成一个约定的“负无穷”（图中用雪花表示），这表明创建该版本的事务早已结束，具体编号已经不重要。实际上，`xmin` 并不会改变；系统会同时设置提交提示位和中止提示位，作为冻结标志。

![行版本冻结与可见性规则：T1、T2、T3、T4 及冻结标记](/images/internal/ru/p137-02.svg)
{#internal-ru-fig-6-2 num="6.2" caption="行版本冻结与可见性规则" .book-original-figure width=722 height=244}

> [!NOTE]
> 许多资料（包括文档）都会提到特殊的事务编号 FrozenTransactionId = 2。这就是 9.4 版本以前用来替换 `xmin` 的那个“负无穷”。现在使用的提示位可以在行版本中保留原始事务编号，便于维护和调试。不过，在升级到最新版本的旧系统数据页面中，仍然可能遇到事务编号 2。

事务编号 `xmax` 完全不参与冻结。它只出现在无效行版本中；当这些版本不再对任何快照可见（也就是编号越过数据库视界）时，就会由清理删除。

<span id="internal-ru-dummy-59" class="pdf-anchor"></span>

手动冻结通过 `VACUUM FREEZE` 命令执行。在这个例子中，它冻结唯一的有效行版本：

```text {label="psql"}
=> VACUUM FREEZE vac;
=> SELECT * FROM heap_page('vac',0);
 ctid  | state  | xmin  | xmax | hhu | hot | t_ctid
-------+--------+-------+------+-----+-----+--------
 (0,1) | unused |       |      |     |     |
 (0,2) | unused |       |      |     |     |
 (0,3) | normal | 853 f | 0 a  |     |     | (0,3)
(3 rows)
```

<span id="internal-ru-page-138" class="pdf-anchor"></span>

现在，这个页面不仅在可见性映射中被标记，也在冻结映射中被标记：

```text {label="psql"}
=> SELECT all_visible, all_frozen
FROM pg_visibility_map('vac',0);
 all_visible | all_frozen
-------------+------------
 t           | t
(1 row)
```

## 6.5 分析 {#internal-ru-section-6-5}

<span id="internal-ru-dummy-60" class="pdf-anchor"></span>

除了清理之外，还有一项与清理没有正式关系的任务：*分析*[^internal-ru-138-1]，也就是收集统计信息。大部分统计信息供查询规划器使用，包括关系中的行数（`pg_class.reltuples`）和页面数（`pg_class.relpages`）、表列中的数据分布以及其他信息。可见性映射（`pg_class.relallvisible`）和冻结映射（`pg_class.relallfrozen`）中标记的页面数不会被规划器使用，但清理和分析本身需要用它们来管理工作。（*第 [346](/zh18/17-statistics/#internal-ru-page-347) 页*）

可以单独执行 `ANALYZE` 命令来手动分析[^internal-ru-138-2]，也可以把它和清理合并为 `VACUUM ANALYZE`。不过，这时清理和分析仍然依次执行，并不会节省时间。

> [!NOTE]
> 从历史上看，`VACUUM ANALYZE` 这一形式最早出现于 6.1 版，而独立的 `ANALYZE` 命令直到 7.2 版才出现。更早的版本通过 TCL 脚本更新统计信息。

[^internal-ru-126-1]: [postgrespro.ru/docs/postgresql/18/routine-vacuuming](<https://postgrespro.ru/docs/postgresql/18/routine-vacuuming>).
[^internal-ru-126-2]: [postgrespro.ru/docs/postgresql/18/sql-vacuum](<https://postgrespro.ru/docs/postgresql/18/sql-vacuum>); [backend/commands/vacuum.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/commands/vacuum.c;hb=REL_18_STABLE>).
[^internal-ru-132-1]: [backend/access/heap/vacuumlazy.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/heap/vacuumlazy.c;hb=REL_18_STABLE>), 函数 heap\_vacuum\_rel.
[^internal-ru-132-2]: [backend/access/heap/vacuumlazy.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/heap/vacuumlazy.c;hb=REL_18_STABLE>), 函数 lazy\_scan\_heap.
[^internal-ru-132-3]: [backend/access/common/tidstore.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/common/tidstore.c;hb=REL_18_STABLE>).
[^internal-ru-133-1]: [backend/access/heap/vacuumlazy.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/heap/vacuumlazy.c;hb=REL_18_STABLE>), 函数 lazy\_vacuum\_all\_indexes.
[^internal-ru-133-2]: [commitfest.postgresql.org/21/1802](<https://commitfest.postgresql.org/21/1802>).
[^internal-ru-133-3]: [postgrespro.ru/docs/postgresql/18/bgworker](<https://postgrespro.ru/docs/postgresql/18/bgworker>).
[^internal-ru-133-4]: [backend/access/heap/vacuumlazy.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/heap/vacuumlazy.c;hb=REL_18_STABLE>), 函数 lazy\_cleanup\_all\_indexes; [backend/access/nbtree/nbtree.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/nbtree/nbtree.c;hb=REL_18_STABLE>), 函数 btvacuumcleanup.
[^internal-ru-134-1]: [backend/access/heap/vacuumlazy.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/heap/vacuumlazy.c;hb=REL_18_STABLE>), 函数 lazy\_vacuum\_heap.
[^internal-ru-134-2]: [backend/access/heap/vacuumlazy.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/heap/vacuumlazy.c;hb=REL_18_STABLE>), 函数 lazy\_truncate\_heap.
[^internal-ru-135-1]: [include/access/transam.h](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/include/access/transam.h;hb=REL_18_STABLE>), 类型 FullTransactionId.
[^internal-ru-136-1]: [backend/access/transam/transam.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/transam/transam.c;hb=REL_18_STABLE>), 函数 TransactionIdPrecedes.
[^internal-ru-136-2]: [postgrespro.ru/docs/postgresql/18/routine-vacuuming#VACUUM-FOR-WRAPAROUND](<https://postgrespro.ru/docs/postgresql/18/routine-vacuuming#VACUUM-FOR-WRAPAROUND>).
[^internal-ru-138-1]: [postgrespro.ru/docs/postgresql/18/routine-vacuuming#VACUUM-FOR-STATISTICS](<https://postgrespro.ru/docs/postgresql/18/routine-vacuuming#VACUUM-FOR-STATISTICS>).
[^internal-ru-138-2]: [backend/commands/analyze.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/commands/analyze.c;hb=REL_18_STABLE>).
