# Страницы и версии строк

Индекс LLMS: [llms.txt](/ru/llms.txt)

---

<span id="internal-ru-chapter-3" class="pdf-anchor"></span><span id="internal-ru-page-76" class="pdf-anchor"></span>

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

## 3.1. Структура страниц {#internal-ru-section-3-1}

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

Каждая страница имеет внутреннюю разметку и, как правило, содержит следующие разделы [^internal-ru-76-1]:

- заголовок;

- массив указателей на версии строк;

- свободное пространство;

- версии строк;

- специальную область.

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

### Заголовок страницы {#internal-ru-subsection-star-68}

*Заголовок* страницы располагается в младших адресах и имеет фиксированный размер. Он хранит различную информацию о странице, такую как контрольная сумма, а также размеры всех остальных областей. (*с.* [127](/ru/06-vacuum-and-freezing/#internal-ru-page-128))

Размеры легко получить с помощью расширения `pageinspect`[^internal-ru-76-2]. Заглянем в самую первую страницу таблицы (нумерация начинается с нуля):

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

```text {label="psql"}
=> CREATE EXTENSION pageinspect;
=> SELECT lower, upper, special, pagesize
FROM page_header(get_raw_page('accounts',0));
 lower | upper | special | pagesize
-------+-------+---------+----------
   152 |  6904 |    8192 |     8192
(1 row)
```

![Заголовок страницы: 0; заголовок; 24; массив указателей на версии строк; lower; свободное пространство; upper; версии строк; special; специальная область; pagesize](/images/internal/ru/p077-01.svg)
{#internal-ru-fig-3-1 num="3.1" caption="Заголовок страницы" .book-original-figure width=441 height=236}

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

### Специальная область {#internal-ru-subsection-star-70}

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

*Специальная область* расположена в противоположном конце страницы, в старших адресах. Она используется некоторыми типами индексов для хранения вспомогательной информации. В остальных случаях, в том числе в табличных страницах, эта область имеет нулевой размер.

В целом индексные страницы устроены более разнообразно, и их содержимое зависит от конкретного типа индекса. И даже у одного типа индекса бывают разные виды страниц: например, у B-дерева есть нулевая страница метаданных с особой структурой и «обычные» страницы, организованные как табличные. (*с.* [529](/ru/25-b-tree/#internal-ru-page-530))

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

### Версии строк {#internal-ru-subsection-star-72}

Перед специальной областью располагаются *строки* (rows) — те самые данные, которые хранятся в базе, с добавлением некоторой служебной информации.

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

В случае таблиц мы говорим не просто о строках, а о *версиях строк* (row versions, tuples), поскольку многоверсионность предполагает существование нескольких версий одной и той же строки. На индексы многоверсионность не распространяется; вместо этого индексы ссылаются на все возможные табличные версии строк, среди которых по правилам видимости выбираются подходящие.

> [!NOTE]
> Термин tuple заимствован из реляционной теории и переводится как *кортеж*. Это еще одно наследие академического прошлого PostgreSQL. Чтобы не смешивать теорию с устройством СУБД, я буду использовать перевод *версия строки* (иногда, если это не вызывает неоднозначностей, заменяя более коротким словом *строка*).

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

### Указатели на версии строк {#internal-ru-subsection-star-74}

*Массив указателей* на версии строк служит оглавлением страницы. Он располагается сразу за заголовком.

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

Индексные строки должны как-то ссылаться на версии строк в таблице. Для этого используются шестибайтные *идентификаторы версий строк* (tuple id, tid). Идентификатор состоит из номера страницы в файле основного слоя и должен еще содержать какое-то указание на версию строки в этой странице. (*с.* [29](/ru/01-introduction/#internal-ru-page-30))

В качестве такого указания можно было бы использовать смещение относительно начала страницы. Но тогда версию строки нельзя было бы перемещать внутри страницы, не сломав ссылки из индексов. А это привело бы к фрагментации места внутри страниц и другим неприятным последствиям.

Поэтому используется косвенная адресация: идентификатор версии ссылается на номер указателя, а уже указатель — на текущую позицию версии строки в странице. При перемещении версии строки ее идентификатор не меняется; достаточно изменить указатель, который находится на той же странице.

Каждый указатель занимает ровно четыре байта и содержит:

- смещение версии строки относительно начала страницы;

- длину версии строки;

- несколько битов, определяющих статус версии строки.

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

### Свободное место {#internal-ru-subsection-star-76}

Между указателями и версиями строк может оставаться *свободное место* (которое и отмечено в карте свободного пространства). Никакой фрагментации внутри страницы не бывает, все свободное место всегда представлено одним фрагментом [^internal-ru-79-1]. (*с.* [31](/ru/01-introduction/#internal-ru-page-32))

## 3.2. Структура версий строк {#internal-ru-section-3-2}

Версия строки состоит из заголовка, за которым следуют собственно данные. Заголовок версии содержит множество полей, среди которых:

**xmin, xmax** — номера транзакций, которые отличают данную версию от других версий той же строки;

**infomask** — ряд информационных битов, определяющих свойства версии;

**ctid** — ссылка на следующую, более новую версию той же строки;

**битовая карта неопределенных значений** — массив битов, отмечающих столбцы, которые допускают неопределенные значения (`NULL`).

В результате заголовок получается довольно большой — минимум 23 байта на каждую версию строки, а обычно больше из-за карты неопределенных значений и из-за обязательного выравнивания начала данных. Для «узкой» таблицы объем служебных данных вполне может превышать объем полезной информации.

Формат данных на диске полностью совпадает с представлением данных в оперативной памяти. Страница вместе с версиями строк читается в буферный кеш «как есть», без каких бы то ни было преобразований. Поэтому файлы данных с одной платформы оказываются несовместимыми с другими платформами[^internal-ru-79-2].

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

Одна из причин несовместимости — порядок следования байтов. Например, в архитектуре x86 принят порядок от младших разрядов к старшим (little-endian), z/Architecture использует обратный порядок (big-endian), а в ARM порядок переключаемый.

Несовместимость вызывается также выравниванием данных по границам машинных слов, которое требуется многим архитектурам. Например, в 32-битной системе архитектуры x86 целые числа (тип `integer`, занимает четыре байта) будут выровнены по границе четырехбайтных слов, как и числа с плавающей точкой двойной точности (тип `double precision`, восемь байт). А в 64-битной системе значения `double` будут выровнены по границе восьмибайтных слов.

Из-за выравнивания размер табличной строки зависит от порядка расположения полей. Обычно этот эффект не сильно заметен, но в некоторых случаях он может привести к существенному увеличению размера. Вот пример:

```text {label="psql"}
=> CREATE TABLE padding(
  b1 boolean,
  f1 float,
  b2 boolean,
  f2 float
);
=> INSERT INTO padding VALUES (true,1.0,false,2.0);
=> SELECT lp_len FROM heap_page_items(get_raw_page('padding', 0));
 lp_len
--------
     56
(1 row)
```

Я использовал функцию `heap_page_items` расширения `pageinspect`, которая позволяет получить информацию об указателях и версиях строк.

> [!NOTE]
> Словом heap (куча) в PostgreSQL обозначаются таблицы. Это еще одно не вполне очевидное употребление термина, намекающее на сходство механизма выделения места под версии строк с динамическим распределением оперативной памяти. Конечно, некоторую аналогию усмотреть можно, но для управления таблицей используются совсем другие алгоритмы. Можно считать, что слово употребляется в смысле «все свалено в кучу», подчеркивая отличие от упорядоченных индексов.

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

Строка занимает 56 байт. Из них 24 байта уходит на заголовок, столбцы типа `float` занимают по 8 байт, `boolean` — по 1 байту. В сумме имеем 42, а 14 байт пропадают из-за выравнивания `float` по границе восьмибайтных слов. Перестроив таблицу, можно использовать место более эффективно:

```text {label="psql"}
=> DROP TABLE padding;
=> CREATE TABLE padding(
  f1 float,
  f2 float,
  b1 boolean,
  b2 boolean
);
```

Надо, однако, учитывать, что выравниваются и версии строк внутри страницы. Как и предполагалось, строки стали занимать 42 байта, но 6 байт все-таки пропадают между строками:

```text {label="psql"}
=> INSERT INTO padding VALUES (1.0,2.0,true,false);
=> SELECT lp_len, 8192 - lp_off AS allocated, lp_off
FROM heap_page_items(get_raw_page('padding', 0));
 lp_len | allocated | lp_off
--------+-----------+--------
     42 |        48 |   8144
(1 row)
```

Еще одна возможная микрооптимизация — перенести в начало таблицы все столбцы фиксированного размера, не допускающие неопределенных значений. Доступ к таким полям будет более эффективным благодаря возможности закешировать смещение поля от начала версии строки[^internal-ru-81-1].

## 3.3. Выполнение операций над версиями строк {#internal-ru-section-3-3}

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

Чтобы разные версии одной и той же строки можно было различить, каждая из версий имеет две отметки, определяющие ее «время действия», — `xmin` и `xmax`. Но используется не время как таковое, а постоянно увеличивающийся счетчик номеров транзакций. (*с.* [134](/ru/06-vacuum-and-freezing/#internal-ru-page-135))

Когда строка создается, значение `xmin` устанавливается равным номеру транзакции, выполнившей команду `INSERT`.

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

Когда строка удаляется, значению `xmax` текущей версии присваивается номер транзакции, выполнившей команду `DELETE`.

Команду `UPDATE` можно в некотором приближении рассматривать как две операции: `DELETE` и `INSERT`. В текущей версии строки устанавливается значение `xmax`, равное номеру транзакции, выполнившей `UPDATE`. Затем создается новая версия той же строки; значение `xmin` у нее совпадает со значением `xmax` предыдущей версии. Точно так же и команда `MERGE` «распадается» на элементарные вставки и удаления. (v. 15)

Дальше я подробно покажу, как выполняются разные операции с версиями строк на низком уровне[^internal-ru-82-1].

Для экспериментов понадобится таблица с двумя столбцами и индексом по одному из них:

```text {label="psql"}
=> CREATE TABLE t(
  id integer GENERATED ALWAYS AS IDENTITY,
  s text
);
=> CREATE INDEX ON t(s);
```

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

### Вставка {#internal-ru-subsection-star-78}

Вставляем одну строку, предварительно начав транзакцию:

```text {label="psql"}
=> BEGIN;
=> INSERT INTO t(s) VALUES ('FOO');
```

Вот номер нашей текущей транзакции:

```text {label="psql"}
=> --  txid_current() до v.13
SELECT pg_current_xact_id();
 pg_current_xact_id
--------------------
                795
(1 row)
```

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

> [!NOTE]
> Для понятия транзакции в PostgreSQL часто используется сокращение xact, которое можно найти и в именах функций, и в коде. Соответственно, номер транзакции обозначается как xact id, txid или даже просто xid. Эти названия еще не раз нам встретятся.

Заглянем в содержимое страницы. Функция `heap_page_items` дает нам всю необходимую информацию, но показывает данные «как есть», в формате, сложном для восприятия:

```text {label="psql"}
=> SELECT *
FROM heap_page_items(get_raw_page('t',0))
\gx
-[ RECORD 1 ]-------------------
lp          | 1
lp_off      | 8160
lp_flags    | 1
lp_len      | 32
t_xmin      | 795
t_xmax      | 0
t_field3    | 0
t_ctid      | (0,1)
t_infomask2 | 2
t_infomask  | 2050
t_hoff      | 24
t_bits      |
t_oid       |
t_data      | \x0100000009464f4f
```

Чтобы разобраться, оставим только часть информации и расшифруем некоторые столбцы:

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

```text {label="psql"}
=> SELECT '(0,'||lp||')' AS ctid,
     CASE lp_flags
       WHEN 0 THEN 'unused'
       WHEN 1 THEN 'normal'
       WHEN 2 THEN 'redirect to '||lp_off
       WHEN 3 THEN 'dead'
     END AS state,
     t_xmin as xmin,
     t_xmax as xmax,
     (t_infomask & 256) > 0  AS xmin_committed,
     (t_infomask & 512) > 0  AS xmin_aborted,
     (t_infomask & 1024) > 0 AS xmax_committed,
     (t_infomask & 2048) > 0 AS xmax_aborted
FROM heap_page_items(get_raw_page('t',0))
\gx
-[ RECORD 1 ]--+-------
ctid           | (0,1)
state          | normal
xmin           | 795
xmax           | 0
xmin_committed | f
xmin_aborted   | f
xmax_committed | f
xmax_aborted   | t
```

Вот что здесь сделано:

- Указатель `lp` приведен к обычному виду идентификатора версии строки: (номер страницы, номер указателя).

- Расшифровано состояние указателя `lp_flags`. Здесь он имеет значение normal — это значит, что указатель действительно ссылается на версию строки.

- Из всех информационных битов пока выделены только две пары. Биты `xmin_committed` и `xmin_aborted` показывают, зафиксирована ли и отменена ли транзакция с номером `xmin`. Аналогичную информацию о транзакции `xmax` дают биты `xmax_committed` и `xmax_aborted`.

В расширении `pageinspect` есть функция `heap_tuple_infomask_flags`, расшиф- (v. 13)

> [!NOTE]
> ровывающая все информационные биты, но я буду выводить только те, что нужны в данный момент, и в более компактном виде.

Итак, при вставке строки в табличной странице появился указатель с номером 1, ссылающийся на первую и пока единственную версию строки.

Поле `xmin` в версии строки заполнено номером текущей транзакции. Транзакция еще активна, поэтому биты `xmin_committed` и `xmin_aborted` не установлены.

Поле `xmax` заполнено фиктивным номером 0, поскольку данная версия строки не удалена и является актуальной. Транзакции не будут обращать внимания на этот номер, поскольку установлен бит `xmax_aborted`.

> [!NOTE]
> Может показаться странным, что бит оборванной транзакции установлен у транзакции, которой не было. Но с точки зрения изоляции это одно и то же: отмененная транзакция не оставляет следов и, следовательно, не существовала.

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

Такой запрос нам еще не раз понадобится, поэтому я оберну его в функцию. А заодно для компактности вывода уберу столбцы информационных битов, приписав статус транзакции к ее номеру:

```text {label="psql"}
=> CREATE FUNCTION heap_page(relname text, pageno integer)
RETURNS TABLE(ctid tid, state text, xmin text, xmax text)
AS $$
SELECT (pageno,lp)::text::tid AS ctid,
     CASE lp_flags
       WHEN 0 THEN 'unused'
       WHEN 1 THEN 'normal'
       WHEN 2 THEN 'redirect to '||lp_off
       WHEN 3 THEN 'dead'
     END AS state,
     t_xmin || CASE
       WHEN (t_infomask & 256) > 0 THEN ' c'
       WHEN (t_infomask & 512) > 0 THEN ' a'
       ELSE ''
     END AS xmin,
     t_xmax || CASE
       WHEN (t_infomask & 1024) > 0 THEN ' c'
       WHEN (t_infomask & 2048) > 0 THEN ' a'
       ELSE ''
     END AS xmax
FROM heap_page_items(get_raw_page(relname,pageno))
ORDER BY lp;
$$ LANGUAGE sql;
```

В таком виде уже значительно понятнее, что творится в заголовке версии:

```text {label="psql"}
=> SELECT * FROM heap_page('t',0);
 ctid  | state  | xmin | xmax
-------+--------+------+------
 (0,1) | normal | 795  | 0 a
(1 row)
```

Похожую, но существенно менее детальную информацию можно получить и из самой таблицы, используя псевдостолбцы `xmin` и `xmax`:

```text {label="psql"}
=> SELECT xmin, xmax, * FROM t;
 xmin | xmax | id |  s
------+------+----+-----
  795 |    0 |  1 | FOO
(1 row)
```

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

### Фиксация {#internal-ru-subsection-star-80}

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

При успешном завершении транзакции нужно запомнить ее статус — отметить, что она *зафиксирована*. Для этого используется структура, называемая clog [^internal-ru-86-1] (commit log). Это не таблица системного каталога, а специальные файлы в каталоге PGDATA/pg\_xact.

На несколько файлов clog разбит исключительно для удобства. Работа с этими файлами ведется постранично, через буферы в общей памяти сервера. Размер кеша clog настраивается параметром *transaction\_buffers*, а по умолчанию составляет \(\frac{1}{512}\) размера буферного кеша. (*с.* [309](/ru/15-locks-on-memory-structures/#internal-ru-page-310)) (v. 17)

В clog, как и в заголовке версии строки, для каждой транзакции отведено два бита: `committed` и `aborted`.

Итак, при фиксации транзакции в clog выставляется бит `committed` для данной транзакции. Когда какая-либо другая транзакция обратится к нашей табличной странице, ей придется ответить на вопрос: завершилась ли транзакция с номером `xmin`?

- Если нет, то созданная версия строки не должна быть видна.

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

Чтобы проверить, выполняется ли транзакция, просматривается еще одна структура, которая располагается в общей памяти экземпляра и называется ProcArray. В ней находится список всех активных процессов, и для каждого указан номер его текущей (активной) транзакции.

- Если завершилась, то фиксацией или отменой? При отмене версия строки тоже не должна быть видна.

<span id="internal-ru-page-87" class="pdf-anchor"></span><span id="internal-ru-dummy-33" class="pdf-anchor"></span>

Для этой проверки как раз и нужна структура clog. Но хотя последние страницы clog сохраняются в буферах в оперативной памяти, все равно такую проверку накладно выполнять каждый раз. Поэтому выясненный однажды статус транзакции записывается в заголовок версии строки в информационные биты `xmin_committed` и `xmin_aborted`; их еще называют *битами-подсказками* (hint bits). Если один из этих битов установлен, то состояние транзакции `xmin` считается известным, и следующей транзакции уже не придется обращаться ни к clog, ни к ProcArray.

Почему эти биты не устанавливаются той транзакцией, которая вставляет строку? Дело в том, что в это время транзакция еще не знает, завершится ли она успешно. А в момент фиксации уже не понятно, какие именно строки в каких именно страницах были изменены. Таких страниц может оказаться много, и запоминать их невыгодно. К тому же часть страниц может быть вытеснена из буферного кеша на диск; читать их заново, чтобы изменить биты, означало бы существенно замедлить фиксацию.

Обратная сторона экономии состоит в том, что любая транзакция (даже выполняющая простое чтение — `SELECT`) может начать выставлять информационные биты в страницах данных. Конечно, страницы в буферном кеше при этом становятся грязными.

Зафиксируем наконец вставку строки, с которой мы начали транзакцию:

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

В странице ничего не изменилось (но мы знаем, что статус транзакции уже записан в clog):

```text {label="psql"}
=> SELECT * FROM heap_page('t',0);
 ctid  | state  | xmin | xmax
-------+--------+------+------
 (0,1) | normal | 795  | 0 a
(1 row)
```

Теперь транзакция, первой обратившаяся к странице («нормальным» образом, не с помощью `pageinspect`), должна будет определить статус транзакции `xmin` и записать его в информационные биты:

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

```text {label="psql" hl_lines="9"}
=> SELECT * FROM t;
 id |  s
----+-----
  1 | FOO
(1 row)
=> SELECT * FROM heap_page('t',0);
 ctid  | state  | xmin  | xmax
-------+--------+-------+------
 (0,1) | normal | 795 c | 0 a
(1 row)
```

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

### Удаление {#internal-ru-subsection-star-82}

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

При удалении строки в поле `xmax` актуальной версии записывается номер удаляющей транзакции, а бит `xmax_aborted` сбрасывается. (*с.* [268](/ru/13-row-level-locks/#internal-ru-page-269))

> [!NOTE]
> Это же значение `xmax`, соответствующее активной транзакции, выступает в качестве блокировки строки. Если другая транзакция собирается обновить или удалить эту строку, она будет вынуждена дождаться завершения транзакции `xmax`.

Удалим строку:

```text {label="psql"}
=> BEGIN;
=> DELETE FROM t;
=> SELECT pg_current_xact_id();
 pg_current_xact_id
--------------------
                796
(1 row)
```

Номер транзакции записался в поле `xmax`, но информационные биты еще не установлены:

```text {label="psql" hl_lines="4"}
=> SELECT * FROM heap_page('t',0);
 ctid  | state  | xmin  | xmax
-------+--------+-------+------
 (0,1) | normal | 795 c | 796
(1 row)
```

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

### Отмена {#internal-ru-subsection-star-84}

Отмена изменений работает аналогично фиксации и выполняется так же быстро, только в clog вместо бита committed выставляется бит aborted. Хоть команда и называется `ROLLBACK`, отката изменений не происходит: все, что транзакция успела изменить в страницах данных, остается на месте.

```text {label="psql"}
=> ROLLBACK;
=> SELECT * FROM heap_page('t',0);
 ctid  | state  | xmin  | xmax
-------+--------+-------+------
 (0,1) | normal | 795 c | 796
(1 row)
```

При обращении к странице проверяется статус, и в версии строки устанавливается бит-подсказка `xmax_aborted`. Сам номер `xmax` при этом остается в странице, но смотреть на него уже никто не будет:

```text {label="psql" hl_lines="9"}
=> SELECT * FROM t;
 id |  s
----+-----
  1 | FOO
(1 row)
=> SELECT * FROM heap_page('t',0);
 ctid  | state  | xmin  | xmax
-------+--------+-------+-------
 (0,1) | normal | 795 c | 796 a
(1 row)
```

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

### Обновление {#internal-ru-subsection-star-86}

Обновление работает так, как будто сначала выполнилось удаление текущей версии строки, а затем — вставка новой:

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

```text {label="psql"}
=> BEGIN;
=> UPDATE t SET s = 'BAR';
=> SELECT pg_current_xact_id();
 pg_current_xact_id
--------------------
                797
(1 row)
```

Запрос выдает одну строку (новую версию):

```text {label="psql"}
=> SELECT * FROM t;
 id |  s
----+-----
  1 | BAR
(1 row)
```

Но в странице хранятся обе версии:

```text {label="psql" hl_lines="4 5"}
=> SELECT * FROM heap_page('t',0);
 ctid  | state  | xmin  | xmax
-------+--------+-------+------
 (0,1) | normal | 795 c | 797
 (0,2) | normal | 797   | 0 a
(2 rows)
```

Удаленная ранее версия помечена номером текущей транзакции в поле `xmax`. Это значение записано поверх старого, поскольку предыдущая транзакция была отменена. А бит `xmax_aborted` сброшен, так как статус текущей транзакции еще неизвестен.

Ну и завершим транзакцию.

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

## 3.4. Индексы {#internal-ru-section-3-4}

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

В индексах любого типа никогда не бывает версий строк, каждая строка представлена ровно одним экземпляром. Иными словами, в заголовке индексной строки не бывает полей `xmin` и `xmax`. Ссылки из индекса ведут на все табличные версии строк. Транзакции требуется заглянуть в таблицу, чтобы разобраться, какая из версий ей видна (если только страница не отмечена в карте видимости). (*с.* [115](/ru/05-page-pruning-and-hot-updates/#internal-ru-page-116))

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

Для удобства создадим простую функцию, показывающую с помощью расширения `pageinspect` все индексные записи на странице (в пределах страницы B-дерева записи располагаются в виде плоского списка): (*с.* [523](/ru/25-b-tree/#internal-ru-page-524))

```text {label="psql"}
=> CREATE FUNCTION index_page(relname text, pageno integer)
RETURNS TABLE(itemoffset smallint, htid tid)
AS $$
SELECT itemoffset,
       htid -- ctid до v.13
FROM bt_page_items(relname,pageno);
$$ LANGUAGE sql;
```

Обнаруживаем на странице указатели на обе версии табличной строки, как на актуальную, так и на старую:

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

Поскольку BAR &lt; FOO, указатель на вторую версию строки стоит в индексе на первом месте.

## 3.5. TOAST {#internal-ru-section-3-5}

Toast-таблица является, по сути, обычной таблицей, и для нее поддерживается собственная версионность: версии «тостов» не связаны с версиями строк основной таблицы. Но внутренняя работа с toast-таблицей построена так, что строки никогда не обновляются, а только добавляются и удаляются, так что версионность в данном случае несколько вырожденная. (*с.* [33](/ru/01-introduction/#internal-ru-page-34))

Когда данные меняются, в основной таблице всегда создается новая версия строки. Но если обновление не затрагивает «длинное» значение, хранящееся в TOAST, то новая версия строки будет ссылаться на прежнее значение в toast-таблице. И только когда обновление поменяет «длинное» значение, будут созданы и новая версия строки в основной таблице, и новые «тосты».

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

## 3.6. Виртуальные транзакции {#internal-ru-section-3-6}

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

PostgreSQL использует оптимизацию, позволяющую «экономить» номера транзакций.

Если транзакция только читает данные, то она никак не влияет на видимость версий строк. Поэтому вначале обслуживающий процесс выдает транзакции *виртуальный номер* [^internal-ru-92-1] (virtual xid). Номер состоит из идентификатора обслуживающего процесса и последовательного числа. Выдача этого номера не требует синхронизации между всеми процессами и поэтому выполняется очень быстро. Настоящего номера у транзакции еще нет: (*с.* [259](/ru/12-relation-level-locks/#internal-ru-page-260))

```text {label="psql"}
=> BEGIN;
=> --  txid_current_if_assigned() до v.13
SELECT pg_current_xact_id_if_assigned();
 pg_current_xact_id_if_assigned
--------------------------------
```

```text
(1 row)
```

В разные моменты времени в системе вполне могут оказаться виртуальные транзакции с номерами, которые уже использовались. И это нормально, поскольку виртуальные номера существуют только в оперативной памяти, пока транзакция активна; они никогда не записываются в страницы данных и не попадают на диск.

Если же транзакция начинает менять данные, ей выдается настоящий, уникальный номер транзакции:

```text {label="psql"}
=> UPDATE accounts
SET amount = amount - 1.00;
=> SELECT pg_current_xact_id_if_assigned();
 pg_current_xact_id_if_assigned
--------------------------------
                            799
(1 row)
=> COMMIT;
```

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

## 3.7. Вложенные транзакции {#internal-ru-section-3-7}

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

### Точки сохранения {#internal-ru-subsection-star-88}

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

В SQL определены *точки сохранения* (savepoint), которые позволяют отменить часть операций транзакции, не прерывая ее полностью. Но это не укладывается в приведенную выше схему, поскольку статус транзакции один на все изменения, а физически никакие данные не откатываются.

Чтобы реализовать такой функционал, транзакция с точкой сохранения разбивается на несколько *вложенных транзакций* [^internal-ru-93-1] (subtransaction), статусом которых можно управлять отдельно.

Вложенные транзакции имеют свой собственный номер (больший, чем номер основной транзакции). Статус вложенных транзакций записывается в clog обычным образом, но зафиксированные вложенные транзакции одновременно отмечаются двумя битами, committed и aborted. Финальный статус зависит от статуса основной транзакции: если она отменена, то и все вложенные транзакции считаются отмененными.

Информация о вложенности транзакций хранится в файлах внутри каталога PGDATA/pg\_subtrans, а обращение к страницам этих файлов происходит через SLRU-кеш. Его размер настраивается параметром *subtransaction\_buffers*, а по умолчанию составляет \(\frac{1}{512}\) размера буферного кеша. (*с.* [309](/ru/15-locks-on-memory-structures/#internal-ru-page-310))

> [!NOTE]
> Не путайте вложенные транзакции с автономными. Автономные транзакции никак не зависят друг от друга, в отличие от вложенных. Автономных транзакций в обычном PostgreSQL нет, и, пожалуй, к лучшему: по делу они нужны очень и очень редко, а их наличие в других СУБД провоцирует злоупотребления, от которых потом все страдают.

Опустошим таблицу, начнем транзакцию и вставим строку:

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

```text {label="psql"}
=> TRUNCATE TABLE t;
=> BEGIN;
=> INSERT INTO t(s) VALUES ('FOO');
=> SELECT pg_current_xact_id();
 pg_current_xact_id
--------------------
                801
(1 row)
```

Теперь поставим точку сохранения и вставим еще одну строку:

```text {label="psql"}
=> SAVEPOINT sp;
=> INSERT INTO t(s) VALUES ('XYZ');
=> SELECT pg_current_xact_id();
 pg_current_xact_id
--------------------
                801
(1 row)
```

Заметьте, что функция `pg_current_xact_id` выдает номер основной, а не вложенной транзакции.

```text {label="psql"}
=> SELECT *
FROM heap_page('t',0) p
  LEFT JOIN t ON p.ctid = t.ctid;
 ctid  | state  | xmin | xmax | id |  s
-------+--------+------+------+----+-----
 (0,1) | normal | 801  | 0 a  |  2 | FOO
 (0,2) | normal | 802  | 0 a  |  3 | XYZ
(2 rows)
```

Откатимся к точке сохранения и вставим третью строку:

```text {label="psql"}
=> ROLLBACK TO sp;
=> INSERT INTO t(s) VALUES ('BAR');
=> SELECT *
FROM heap_page('t',0) p
  LEFT JOIN t ON p.ctid = t.ctid;
 ctid  | state  | xmin | xmax | id |  s
-------+--------+------+------+----+-----
 (0,1) | normal | 801  | 0 a  |  2 | FOO
 (0,2) | normal | 802  | 0 a  |    |
 (0,3) | normal | 803  | 0 a  |  4 | BAR
(3 rows)
```

В странице мы продолжаем видеть строку, добавленную отмененной вложенной транзакцией.

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

Фиксируем изменения:

```text {label="psql"}
=> COMMIT;
=> SELECT * FROM t;
 id |  s
----+-----
  2 | FOO
  4 | BAR
(2 rows)
=> SELECT * FROM heap_page('t',0);
 ctid  | state  | xmin  | xmax
-------+--------+-------+------
 (0,1) | normal | 801 c | 0 a
 (0,2) | normal | 802 a | 0 a
 (0,3) | normal | 803 c | 0 a
(3 rows)
```

Теперь хорошо видно, что каждая вложенная транзакция имеет собственный статус.

Вложенные транзакции нельзя использовать в SQL напрямую, то есть нельзя начать новую транзакцию, не завершив текущую:

```text {label="psql"}
=> BEGIN;
BEGIN
=> BEGIN;
WARNING:  there is already a transaction in progress
BEGIN
=> COMMIT;
COMMIT
=> COMMIT;
WARNING:  there is no transaction in progress
COMMIT
```

Этот механизм задействуется неявно: при использовании точек сохранения, при обработке исключений PL/pgSQL и в некоторых других, более экзотических случаях.

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

### Ошибки и атомарность операций {#internal-ru-subsection-star-90}

Что случится, если в процессе выполнения операции произойдет ошибка?

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

Например:

```text {label="psql"}
=> BEGIN;
=> SELECT * FROM t;
 id |  s
----+-----
  2 | FOO
  4 | BAR
(2 rows)
=> UPDATE t SET s = repeat('X', 1/(id-4));
ERROR:  division by zero
```

После сбоя транзакция считается прерванной, и больше ни одна операция в ней не допускается:

```text {label="psql"}
=> SELECT * FROM t;
ERROR:  current transaction is aborted, commands ignored until end of
transaction block
```

И даже если попытаться зафиксировать изменения, PostgreSQL сообщит об отмене:

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

Почему нельзя продолжить выполнение транзакции после сбоя? Поскольку уже выполненные действия никогда не откатываются, мы получили бы доступ к части изменений, выполненных до ошибки, — была бы нарушена атомарность даже не транзакции, а оператора.

Как в нашем примере, где оператор до ошибки успел обновить одну строку из двух:

```text {label="psql" hl_lines="4 7"}
=> SELECT * FROM heap_page('t',0);
 ctid  | state  | xmin  | xmax
-------+--------+-------+------
 (0,1) | normal | 801 c | 804
 (0,2) | normal | 802 a | 0 a
 (0,3) | normal | 803 c | 0 a
 (0,4) | normal | 804   | 0 a
(4 rows)
```

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

К слову, в psql имеется режим, который все-таки позволяет продолжать работу транзакции после сбоя так, как будто действия ошибочного оператора откатываются:

```text {label="psql"}
=> \set ON_ERROR_ROLLBACK on
=> BEGIN;
=> UPDATE t SET s = repeat('X', 1/(id-4));
ERROR:  division by zero
=> SELECT * FROM t;
 id |  s
----+-----
  2 | FOO
  4 | BAR
(2 rows)
=> COMMIT;
COMMIT
```

Нетрудно догадаться, что в таком режиме psql фактически ставит перед каждой командой неявную точку сохранения, а в случае сбоя инициирует откат к ней. Такой режим не используется по умолчанию, поскольку установка точек сохранения (даже без отката к ним) сопряжена с существенными накладными расходами.

[^internal-ru-76-1]: [postgrespro.ru/docs/postgresql/18/storage-page-layout](<https://postgrespro.ru/docs/postgresql/18/storage-page-layout>); [include/storage/bufpage.h](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/include/storage/bufpage.h;hb=REL_18_STABLE>).
[^internal-ru-76-2]: [postgrespro.ru/docs/postgresql/18/pageinspect](<https://postgrespro.ru/docs/postgresql/18/pageinspect>).
[^internal-ru-79-1]: [backend/storage/page/bufpage.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/page/bufpage.c;hb=REL_18_STABLE>), функция PageRepairFragmentation.
[^internal-ru-79-2]: [include/access/htup\_details.h](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/include/access/htup_details.h;hb=REL_18_STABLE>).
[^internal-ru-81-1]: [backend/access/common/heaptuple.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/common/heaptuple.c;hb=REL_18_STABLE>), функция heap\_deform\_tuple.
[^internal-ru-82-1]: [backend/access/transam/README](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/transam/README;hb=REL_18_STABLE>).
[^internal-ru-86-1]: [include/access/clog.h](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/include/access/clog.h;hb=REL_18_STABLE>); [backend/access/transam/clog.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/transam/clog.c;hb=REL_18_STABLE>).
[^internal-ru-92-1]: [postgrespro.ru/docs/postgresql/18/transaction-id](<https://postgrespro.ru/docs/postgresql/18/transaction-id>); [backend/access/transam/xact.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/transam/xact.c;hb=REL_18_STABLE>).
[^internal-ru-93-1]: [postgrespro.ru/docs/postgresql/18/subxacts](<https://postgrespro.ru/docs/postgresql/18/subxacts>); [backend/access/transam/subtrans.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/transam/subtrans.c;hb=REL_18_STABLE>).
