# 数据快照

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

---

<span id="internal-ru-chapter-4" class="pdf-anchor"></span><span id="internal-ru-page-98" class="pdf-anchor"></span>

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

## 4.1 什么是数据快照 {#internal-ru-section-4-1}

数据页面在物理上可能包含同一行的多个版本，但每个事务最多只能看到其中一个。事务所看到的不同数据行的所有版本合在一起，构成一个*数据快照*（snapshot）。快照提供特定时刻的一致（在 ACID 意义上）数据视图，其中只包含创建快照时已经提交的最新数据。（*第 [54](/zh18/02-isolation/#internal-ru-page-55) 页*）

为了实现隔离，每个事务都使用自己的快照。因此，不同事务看到的数据可能不同，来自不同的时间点，但仍然保持一致。

在 Read Committed 隔离级别下，快照在事务每条语句开始时创建，并在该语句执行期间保持有效。

在 Repeatable Read 和 Serializable 隔离级别下，快照在事务第一条语句开始时创建一次，并一直保持有效，直到事务结束。

![什么是数据快照：快照1、快照2、快照；操作1、操作2、操作1、操作2；xid；Read Committed；Repeatable Read、Serializable](/images/internal/ru/p098-01.svg)
{#internal-ru-fig-4-1 num="4.1" caption="什么是数据快照" .book-original-figure width=718 height=209}

图中文字（从左到右）：快照1、快照2、快照；操作1、操作2、操作1、操作2；xid；Read Committed；Repeatable Read、Serializable。

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

## 4.2 快照中的行版本可见性 {#internal-ru-section-4-2}

当然，数据快照并不是所有所需行版本的物理副本。实际上，快照由几个数字定义，而快照中的行版本可见性则由规则决定。

某个行版本是否在快照中可见，取决于其头部的 `xmin` 和 `xmax` 字段（也就是创建它和删除它的事务编号）以及对应的信息位。`xmin`–`xmax` 区间不会相交，因此在任何快照中，每行最多由自己的一个版本表示。

精确的可见性规则[^internal-ru-99-1]相当复杂，还要考虑许多不同的情况和边界条件。简单地说，当 `xmin` 事务所做的更改在快照中可见，而 `xmax` 事务所做的更改不可见时，这个行版本就是可见的（换句话说，该行版本已经出现，但还没有被删除）。

相应地，如果事务在创建快照之前已提交，那么该事务的更改在快照中可见。还有一个例外：事务在快照中可以看到自己尚未提交的更改。中止事务的更改当然不会在任何快照中可见。

下面看一个简单例子。图中用线段表示事务（从开始到提交）：

![快照中的行版本可见性：快照；xid；1、2、3](/images/internal/ru/p099-02.svg)
{#internal-ru-fig-4-2 num="4.2" caption="快照中的行版本可见性" .book-original-figure width=472 height=235}

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

这里：

- 事务 2 的更改可见，因为它在快照创建之前已结束；

- 事务 1 的更改不可见，因为创建快照时它仍处于活动状态；

- 事务 3 的更改不可见，因为它在快照创建之后才开始（无论它是否已结束都不重要）。

## 4.3 快照由什么组成 {#internal-ru-section-4-3}

遗憾的是，PostgreSQL 实际看到的情况与图中所示完全不同[^internal-ru-100-1]。原因在于系统不知道事务何时提交。它只知道事务何时开始（由事务编号确定），而事务结束这一事实不会记录在任何地方。（*默认为：* `off`）

> [!NOTE]
> 提交时间会在启用参数 *track\_commit\_timestamp* 时被跟踪[^internal-ru-100-2]，但它完全不参与可见性检查（不过可能有用，例如用于第三方复制方案）。
>
> 此外，PostgreSQL 总会在相应的日志记录中保存事务的提交时间和中止时间，但只会在恢复到目标时间点时使用这些信息。

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

只能知道事务的*当前*状态。这些信息位于服务器共享内存的 ProcArray 结构中，其中包含所有活动会话及其事务的列表。但事后无法判断某个事务在快照创建时是否处于活动状态。（*第 [211](/zh18/10-write-ahead-log/#internal-ru-page-212) 页*）

因此，创建快照时仅保存创建时刻还不够：还必须记住该时刻每个事务的状态。没有这些状态信息，之后就无法判断哪些行版本应在快照中可见，哪些不应可见。

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

请比较一下，在快照创建时和一段时间之后，系统分别能获得哪些信息（白色圆圈表示活动事务，黑色圆圈表示已结束事务）：

![快照由什么组成：xid；1、2、3；创建快照时；一段时间后](/images/internal/ru/p101-03.svg)
{#internal-ru-fig-4-3 num="4.3" caption="快照由什么组成" .book-original-figure width=638 height=184}

图中文字（从左到右）：创建快照时；一段时间后；xid，以及事务编号 1、2、3。

如果不记住创建快照时第一个事务仍在执行，而第三个事务还不存在，就无法把它们与当时已经提交的第二个事务区分开，也就无法将它们排除在外。

因此，PostgreSQL 无法创建一个显示过去某个时刻一致数据状态的快照，即使表页面中存在所有所需的行版本。因此也无法实现回溯查询（也称时态查询或 flashback 查询）。

> [!NOTE]
> 有意思的是，这项功能曾被列为 Postgres 的目标之一，并且从一开始就已实现；但项目转由社区维护后，它被从数据库系统中移除了[^internal-ru-101-1]。

所以，数据快照由创建时保存的几个值组成[^internal-ru-101-2]：

**快照的下边界** `xmin`：最老的活动事务编号。

编号小于它的所有事务要么已提交，此时它们的更改在快照中可见；要么已中止，此时这些更改会被忽略。（*第 [134](/zh18/06-vacuum-and-freezing/#internal-ru-page-135) 页*）

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

**快照的上边界** `xmax`：它取最近一次已提交事务编号加一的值。上边界定义了快照创建的时刻。

编号大于或等于 `xmax` 的所有事务要么尚未结束，要么并不存在，因此这些事务的更改绝不会可见。

**活动事务列表** `xip_list`（xid-in-progress list）包含所有活动事务的编号（小于 `xmax`），但不包括完全不影响可见性的虚拟事务。（*第 [91](/zh18/03-pages-and-tuples/#internal-ru-page-92) 页*）

快照中还会保存几个其他参数，不过现在还不重要。

从图形上看，可以把快照表示为一个覆盖 `xmin` 到 `xmax` 之间事务的矩形：

<span id="internal-ru-figure-caption-91" class="pdf-anchor"></span>

![快照由什么组成：xip\_list；xmin、xmax；xid；1、2、3](/images/internal/ru/p102-04.svg)
{#internal-ru-fig-4-4 num="4.4" caption="快照由什么组成" .book-original-figure width=373 height=206}

为了看看快照如何决定可见性，我们在 `accounts` 表上重现上面场景。

```text {label="psql"}
=> TRUNCATE TABLE accounts;
```

第一个事务向表中插入第一行，并保持活动状态：

```text {label="psql"}
=> BEGIN;
=> INSERT INTO accounts VALUES (1, 'alice', 1000.00);
=> SELECT pg_current_xact_id();
 pg_current_xact_id
--------------------
                809
(1 row)
```

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

第二个事务插入第二行并立即提交：

```text {label="psql"}
=> BEGIN;
=> INSERT INTO accounts VALUES (2, 'bob', 100.00);
=> SELECT pg_current_xact_id();
 pg_current_xact_id
--------------------
                810
(1 row)
=> COMMIT;
```

此时，在另一个会话中创建新快照。为此执行任意查询就足够了，但我们直接使用专用函数查看该快照：

```text {label="psql"}
=> BEGIN ISOLATION LEVEL REPEATABLE READ;
=> --  txid_current_snapshot() до v.13
SELECT pg_current_snapshot();
 pg_current_snapshot
---------------------
 809:811:809
(1 row)
```

该函数用冒号分隔输出快照的三个组成部分：`xmin`、`xmax` 和 `xip_list`（本例中只包含一个编号）。

快照创建后，提交第一个事务：

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

第三个事务在快照创建后开始执行，并修改第二行。于是出现了新版本：

```text {label="psql"}
=> BEGIN;
=> UPDATE accounts SET amount = amount + 100 WHERE id = 2;
=> SELECT pg_current_xact_id();
 pg_current_xact_id
--------------------
                811
(1 row)
=> COMMIT;
```

创建的快照中只有一个版本可见：

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

```text {label="psql"}
=> SELECT ctid, * FROM accounts;
 ctid  | id | client | amount
-------+----+--------+--------
 (0,2) |  2 | bob    | 100.00
(1 row)
```

但表中有三个版本：

```text {label="psql"}
=> SELECT * FROM heap_page('accounts',0);
 ctid  | state  | xmin  | xmax
-------+--------+-------+-------
 (0,1) | normal | 809 c | 0 a
 (0,2) | normal | 810 c | 811 c
 (0,3) | normal | 811 c | 0 a
(3 rows)
```

那么 PostgreSQL 如何判断要显示哪些版本？按照上面的规则，只有以下已提交事务的更改会出现在快照中：

- 编号 `xid` &lt; `xmin` 的事务：其更改无条件可见（例如创建 `accounts` 表的事务）；

- `xmin` ⩽ `xid` &lt; `xmax` 的事务：其更改可见，但位于 `xip_list` 中的除外。

第一行（0,1）不可见，因为创建它的事务编号位于活动事务列表中（虽然它也位于快照范围内）。

第二行的最后一个版本（0,3）不可见，因为创建它的事务编号超出了快照的上边界。

不过，第二行的第一个版本（0,2）可见：创建它的事务编号落在快照范围内但不在活动事务列表中（插入可见），而删除它的事务编号超出了快照的上边界（删除不可见）。

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

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

## 4.4 事务自身更改的可见性 {#internal-ru-section-4-4}

确定事务自身更改的可见性时，情况会更复杂，因为有时只应看到其中一部分。例如，在某个时刻打开的游标，无论隔离级别如何，都不能看到之后发生的更改。

为此，行版本头部包含一个特殊字段（显示为伪列 `cmin` 和 `cmax`），用于表示事务内操作的序号。`cmin` 表示插入操作的编号，`cmax` 表示删除操作的编号；不过为节省行版本头部的空间，实际上它们共用一个字段，而不是两个不同字段。一般认为，在同一个事务中插入并删除同一行的情况很少。（如果确实发生了，系统会在同一个字段中写入一个特殊的“组合编号”，后台进程会为它记录真实的 `cmin` 和 `cmax`[^internal-ru-105-1]。）

举个例子，先开启事务并向表中插入一行：

```text {label="psql"}
=> BEGIN;
=> INSERT INTO accounts VALUES (3, 'charlie', 100.00);
=> SELECT pg_current_xact_id();
 pg_current_xact_id
--------------------
                812
(1 row)
```

现在打开一个游标，用于执行返回表中行数的查询：

```text {label="psql"}
=> DECLARE c CURSOR FOR SELECT count(*) FROM accounts;
```

然后再插入一行：

```text {label="psql"}
=> INSERT INTO accounts VALUES (4, 'charlie', 200.00);
```

现在显示表的内容，并增加一列 `cmin`，只为本事务插入的行显示该值（对其他行来说，这个值没有意义）：

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

```text {label="psql"}
=> SELECT xmin, CASE WHEN xmin = 812 THEN cmin END cmin, *
FROM accounts;
 xmin | cmin | id | client  | amount
------+------+----+---------+---------
  809 |      |  1 | alice   | 1000.00
  811 |      |  2 | bob     |  200.00
  812 |    0 |  3 | charlie |  100.00
  812 |    1 |  4 | charlie |  200.00
(4 rows)
```

游标查询会得到三行而不是四行——游标打开后插入的行不会进入快照，因为快照只考虑满足 `cmin` &lt; 1 的行版本：

```text {label="psql"}
=> FETCH c;
 count
-------
     3
(1 row)
```

当然，这个 `cmin` 编号也会保存在快照中，但无法通过 SQL 显示。

## 4.5 事务视界 {#internal-ru-section-4-5}

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

快照的下边界（创建快照时处于活动状态的最老事务的 `xmin` 编号）具有重要含义——它定义了使用该快照的事务的*视界*。

没有活动快照的事务（例如，Read Committed 隔离级别下两条语句执行之间的事务）的视界由其自身编号决定，前提是它已经分配到编号。

位于视界之外的所有事务（即编号 `xid` &lt; `xmin` 的事务）都已经确定提交。这意味着，在自己的视界之外，事务总是只能看到当前的行版本。

> [!NOTE]
> 这个名称当然来自物理学中的*事件视界*概念。

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

PostgreSQL 跟踪所有进程当前的事务视界；事务可以在 `pg_stat_activity` 表中查看自己的视界：

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

虚拟事务虽然没有真正的编号，但也像普通事务一样使用快照，因此同样拥有自己的视界。例外是没有活动快照的虚拟事务：对它们而言，视界这个概念没有意义；从数据快照和可见性的角度看，它们对系统完全“透明”（即使 `pg_stat_activity.backend_xmin` 中可能还保留着旧快照的编号）。

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

类似地，可以定义*数据库视界*。为此，取在该数据库中运行的所有事务的视界，找出其中“最远”的一个，即 `xmin` 最老的那个[^internal-ru-107-1]。这就是数据库视界：在它之外，该数据库中的任何事务都不可能再看到过期的行版本。*这些行版本可以由清理安全删除*——这就是视界概念在实践中如此重要的原因。

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

由此可以得出：

- 如果处于 Repeatable Read 或 Serializable 隔离级别的事务（无论是真实事务还是虚拟事务）长时间运行，它就会保持数据库视界，从而阻碍清理；

- Read Committed 隔离级别下的真实事务同样会保持数据库视界，无论它是在执行语句，还是只是处于空闲状态（idle in transaction）；

- Read Committed 隔离级别下的虚拟事务只在执行语句期间保持视界。

<span id="internal-ru-page-108" class="pdf-anchor"></span><span id="internal-ru-figure-caption-92" class="pdf-anchor"></span>

![事务视界：数据库视界；过期行版本；可以清理；xid；1、2、3、4、5、6、7、8、9、10](/images/internal/ru/p108-05.svg)
{#internal-ru-fig-4-5 num="4.5" caption="事务视界" .book-original-figure width=692 height=397}

图中文字：数据库视界、过期行版本、可以清理、xid，以及事务编号 1 至 10。

不过，整个数据库只有一个视界；如果某个事务持有这个视界，它就不允许清理该视界以内的数据，即使事务从未访问过这些数据。

> [!NOTE]
> 对于集群范围的系统目录表，会使用一个单独的视界，该视界会考虑所有数据库中的事务。相反，对于临时表，除了当前进程中正在执行的事务外，无需考虑任何事务。

继续这个例子。第一个会话中的事务仍在执行，并继续持有视界；可以通过增加事务计数器来验证：

```text {label="psql"}
=> SELECT pg_current_xact_id();
 pg_current_xact_id
--------------------
                813
(1 row)
```

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

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

只有在事务结束后，视界才会向前推进，清理才能删除过期的行版本：

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

理想情况下，不应把产生新行版本的更新操作与长事务结合起来，因为这会导致表和索引膨胀。（*第 [178](/zh18/08-rebuilding-tables-and-indexes/#internal-ru-page-179) 页*）

## 4.6 系统目录的快照 {#internal-ru-section-4-6}

虽然系统目录也是由普通表组成的，但访问它们时不能使用事务或语句所用的同一个数据快照。快照必须足够“新”，能够包含所有最新更改，否则事务可能看到已经过时的表列定义，或错过新建的完整性约束。

下面是一个简单例子：

```text {label="psql"}
=> BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
=> SELECT 1; -- создан снимок для транзакции
```

```text {label="psql"}
=> ALTER TABLE accounts ALTER amount SET NOT NULL;
```

```text {label="psql"}
=> INSERT INTO accounts(client, amount) VALUES ('alice', NULL);
ERROR:  null value in column "amount" of relation "accounts" violates
not-null constraint
DETAIL:  Failing row contains (1, alice, null).
=> ROLLBACK;
```

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

`INSERT` 命令“看到了”在事务快照创建后才出现的完整性约束。看起来这种行为似乎破坏了隔离性，但如果事务已经访问了 `accounts` 表，`ALTER TABLE` 命令就会一直阻塞到该事务结束。（*第 [261](/zh18/12-relation-level-locks/#internal-ru-page-262) 页*）

总的来说，系统的行为就像为每次访问系统目录都创建一个新快照。不过，实现当然更复杂[^internal-ru-110-1]，因为不断创建新快照会影响性能；此外还要考虑许多系统目录对象都被缓存这一事实。

## 4.7 导出数据快照 {#internal-ru-section-4-7}

有时，多个并行事务必须确保看到完全相同的数据视图。例如，pg\_dump 工具可以以并行模式运行：所有工作进程都必须看到数据库的同一状态，才能生成一致的备份。

当然，不能只因为事务“同时”启动，就指望它们的视图相同。快照导出和导入机制可以提供这样的保证。

函数 `pg_export_snapshot` 返回一个快照 ID，可以通过数据库系统外部的手段传递给另一个事务：

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

```text {label="psql"}
=> BEGIN ISOLATION LEVEL REPEATABLE READ;
=> SELECT count(*) FROM accounts;
 count
-------
     4
(1 row)
=> SELECT pg_export_snapshot();
 pg_export_snapshot
---------------------
 00000011-00000008-1
(1 row)
```

在执行第一条语句之前，另一个事务可以通过执行 `SET TRANSACTION SNAPSHOT` 命令导入这个快照。必须先将隔离级别设置为 Repeatable Read 或 Serializable，因为在 Read Committed 级别下，每条语句都会使用自己的快照：

```text {label="psql"}
=> DELETE FROM accounts;
=> BEGIN ISOLATION LEVEL REPEATABLE READ;
=> SET TRANSACTION SNAPSHOT '00000011-00000008-1';
```

现在第二个事务会使用第一个事务的快照，因此它会看到四行（而不是零行）：

```text {label="psql"}
=> SELECT count(*) FROM accounts;
 count
-------
     4
(1 row)
```

显然，第一个事务在导出快照后所做的更改不会被第二个事务看到，反之亦然：仍然遵循通常的可见性规则。

导出快照的生命周期与导出它的事务相同。

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

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

[^internal-ru-99-1]: [backend/access/heap/heapam\_visibility.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/heap/heapam_visibility.c;hb=REL_18_STABLE>).
[^internal-ru-100-1]: [include/utils/snapshot.h](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/include/utils/snapshot.h;hb=REL_18_STABLE>). [backend/utils/time/snapmgr.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/time/snapmgr.c;hb=REL_18_STABLE>).
[^internal-ru-100-2]: [backend/access/transam/commit\_ts.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/transam/commit_ts.c;hb=REL_18_STABLE>).
[^internal-ru-101-1]: *Joseph Hellerstein*. 回顾 Postgres // [habr.com/ru/company/postgrespro/blog/](<https://habr.com/ru/company/postgrespro/blog/438890>) [438890](<https://habr.com/ru/company/postgrespro/blog/438890>).
[^internal-ru-101-2]: [backend/storage/ipc/procarray.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/ipc/procarray.c;hb=REL_18_STABLE>), 函数 GetSnapshotData.
[^internal-ru-105-1]: [backend/utils/time/combocid.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/time/combocid.c;hb=REL_18_STABLE>).
[^internal-ru-107-1]: [backend/storage/ipc/procarray.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/ipc/procarray.c;hb=REL_18_STABLE>), 函数 ComputeXidHorizons.
[^internal-ru-110-1]: [backend/utils/time/snapmgr.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/time/snapmgr.c;hb=REL_18_STABLE>), 函数 GetCatalogSnapshot.
