# Занятие 02: Архитектура / Многоверсионность

> Исходная страница в нативном Markdown: Архитектура / Многоверсионность

---

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

---

<!-- Generated by scripts/generate_skeleton.py. -->

> [!NOTE]
> Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета.
> **HTML source:** [dev2_02_arch_mvcc.html](https://edu.postgrespro.ru/16/dev2-16/dev2_02_arch_mvcc.html)
> **Archive:** [DEV2-handouts-16.zip](https://edu.postgrespro.ru/16/DEV2-handouts-16.zip)
> **Страниц PDF:** 30

## Page 1: Архитектура / Многоверсионность {#source-page-001}

### Авторские права

© Postgres Professional, 2017–2024 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)
### Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.
### Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru
### Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

## Page 2: Темы {#source-page-002}

- Страницы
- Версии строк
- Снимки данных

## Page 3: Страницы {#source-page-003}

**Структура страниц**
- Формат данных

## Page 4: Страницы {#source-page-004}

![Source slide page 4: Страницы](images/page-004.png){#source-figure-004 caption="Source slide page 4: Страницы"}


### Комментарии преподавателя

Размер страницы составляет 8 Кбайт. Это значение можно увеличить (вплоть до 32 Кбайт), но только при сборке. И таблицы, и индексы, и большинство других объектов, которые в PostgreSQL обозначаются термином relation, используют одинаковую структуру страниц, чтобы пользоваться общим буферным кешем. В начале страницы идет заголовок (24 байта), содержащий общие сведения о странице и размер ее областей: указателей, свободного пространства, версий строк и специальной области.

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

Указатели имеют фиксированный размер (4 байта) и составляют массив, позиция в котором определяет идентификатор строки (tuple id, tid). Указатели ссылаются на версии строк (tuple), расположенные в конце блока. Такая косвенная адресация удобна тем, что во-первых, позволяет найти нужную строку, не перебирая все содержимое блока (строки имеют разную длину), а во-вторых, позволяет перемещать строку внутри блока, не ломая ссылки из индексов.

Между указателями и версиями строк находится свободное место.

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

<https://postgrespro.ru/docs/postgresql/16/storage-page-layout>

## Page 5: Формат данных {#source-page-005}

![Source slide page 5: Формат данных](images/page-005.png){#source-figure-005 caption="Source slide page 5: Формат данных"}

**Страницы читаются в оперативную память «как есть»**
  - данные не переносимы между разными платформами
  - между полями данных возможны пропуски из-за выравнивания

### Комментарии преподавателя

Формат данных на диске полностью совпадает с представлением данных в оперативной памяти. Страница читается в буферный кеш «как есть», без преобразований.

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

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

Из-за этого размер табличной строки зависит от порядка расположения полей. Обычно этот эффект не сильно заметен, но в некоторых случаях он может привести к существенному увеличению размера. Например, если располагать поля типов char(1) и integer вперемешку, между ними, как правило, будет пропадать 3 байта.

<https://pgconf.ru/rmedia/2016/05/13/tuple-internals-ru.pdf>

## Page 6: Подготовка {#source-page-006}

```sql
=> CREATE DATABASE arch_mvcc;
```
```sql
CREATE DATABASE
```
```sql
=> \c arch_mvcc
```
```text
You are now connected to database "arch_mvcc" as user "student".
```
Создадим таблицу и индекс по одному из полей.
```sql
=> CREATE TABLE t(
  id integer,
  s text
);
```
```sql
CREATE TABLE
```
```sql
=> CREATE INDEX t_s on t(s);
```
```sql
CREATE INDEX
```
```sql
=> INSERT INTO t(id, s) VALUES (42, 'FOO');
```
```sql
INSERT 0 1
```
### Структура страницы
Чтобы изучить структуру страницы и версий строк, воспользуемся расширением pageinspect.
```sql
=> CREATE EXTENSION pageinspect;
```
```sql
CREATE EXTENSION
```
Посмотрим на поля заголовка страницы, определяющие границы ее областей.
```sql
=> SELECT lower, upper, special, pagesize
FROM page_header(get_raw_page('t',0));
```
```text
 lower | upper | special | pagesize
-------+-------+---------+----------
    28 |  8160 |    8192 |     8192
(1 row)
```
Области занимают следующие диапазоны адресов:
0 — начало заголовка страницы и указатели на версии строк, lower — начало свободного места, upper — начало данных (версий строк), special — начало спец. данных (только для индексов), pagesize — конец страницы.

## Page 7: Версии строк {#source-page-007}

**Структура версий строк**
- Как работают операции над данными
- HOT-обновления

## Page 8: Вставка {#source-page-008}

![Source slide page 8: Вставка](images/page-008.png){#source-figure-008 caption="Source slide page 8: Вставка"}


### Комментарии преподавателя

Рассмотрим, как устроены версии строк (tuples) и как выполняются операции со строками на низком уровне. Начнем со вставки.

В нашем примере — таблица с двумя столбцами (число и текст). При вставке строки в табличной странице (heap page) появится указатель с номером 1, ссылающийся на первую и единственную версию строки. Каждый такой указатель помимо ссылки содержит длину версии строки и несколько бит, определяющих ее статус.

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

● xmin и xmax — поля, определяющие видимость данной версии строки в терминах начального и конечного номеров транзакций;

● карту неопределенных значений (в ней отмечены поля со значением NULL);

● ряд признаков, показывающих, например, статус транзакций xmin и xmax, если он уже известен (зафиксированы или оборваны).

В нашей версии строки поле xmin заполнено номером текущей транзакции (100). Поскольку изменения еще не фиксировались и транзакция активна, то в журнале статуса транзакций (CLOG) соответствующая запись заполнена нулями. CLOG можно представить себе как массив, в котором для каждой транзакции (начиная с некоторой) отводится ровно два бита.

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

## Page 9: Вставка {#source-page-009}

![Source slide page 9: Вставка](images/page-009.png){#source-figure-009 caption="Source slide page 9: Вставка"}

индекс не содержит информации о версионности

### Комментарии преподавателя

Пусть также имеется индекс B-дерево, созданный по текстовому полю.

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

Тем не менее, обычно в странице имеется массив указателей и индексные записи (аналогичные версиям строк в табличной странице).

Индексные записи тоже могут иметь очень разную структуру в зависимости от типа индекса. Например, для B-дерева записи в листовых страницах содержат значение ключа индексирования и ссылку (ctid) на соответствующую строку таблицы (структура B-дерева разбирается в теме «Классы операторов» и в учебном курсе QPT «Оптимизация запросов»).

В общем случае индекс может быть устроен совсем другим образом, но как правило он все равно будет содержать ссылки на версии строк.

В нашем примере в индексной странице также создается указатель с номером 1, который ссылается на индексную запись, которая, в свою очередь, ссылается на первую строку в табличной странице. Чтобы не загромождать рисунок, указатель и строка таблицы объединены.

Важный момент состоит в том, что никакой индекс не содержит информацию о версионности (нет полей xmin и xmax). Прочитав индексную запись, невозможно определить ее видимость, не заглянув в табличную страницу (для оптимизации служит карта видимости).

## Page 10: Фиксация изменений {#source-page-010}

![Source slide page 10: Фиксация изменений](images/page-010.png){#source-figure-010 caption="Source slide page 10: Фиксация изменений"}


### Комментарии преподавателя

Когда транзакция фиксируется, в CLOG для этой транзакции выставляется признак committed. Это, по сути, единственная операция (не считая журнала предзаписи), которая необходима.

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

1) завершилась ли транзакция 100 (надо проверить список активных процессов и их транзакций; такая структура в общей памяти имеет название ProcArray),

2) а если завершилась, то фиксацией или отменой (свериться с CLOG).

Поскольку выполнять проверку по CLOG каждый раз накладно, выясненный однажды статус транзакции записывается в информаци-онные биты-подсказки заголовка строки. Если один из этих битов установлен, то состояние транзакции xmin считается известным и следующей транзакции уже не придется обращаться к CLOG.

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

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

## Page 11: Удаление {#source-page-011}

![Source slide page 11: Удаление](images/page-011.png){#source-figure-011 caption="Source slide page 11: Удаление"}


### Комментарии преподавателя

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

Заметим, что установленное значение xmax, соответствующее активной транзакции (что определяется другими транзакциями по ProcArray), выступает в качестве блокировки. Если другая транзакция намерена обновить или удалить эту строку, она будет вынуждена дождаться завершения транзакции xmax.

Подробнее блокировки рассматриваются в теме «Блокировки» этого модуля. Пока отметим только, что число блокировок строк ничем не ограничено. Они не занимают место в оперативной памяти, производительность системы не страдает от их количества (разумеется, за исключением того, что первый процесс, обратившийся к версии строки, должен будет проставить биты-подсказки).

## Page 12: Отмена изменений {#source-page-012}

![Source slide page 12: Отмена изменений](images/page-012.png){#source-figure-012 caption="Source slide page 12: Отмена изменений"}


### Комментарии преподавателя

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

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

## Page 13: Обновление {#source-page-013}

![Source slide page 13: Обновление](images/page-013.png){#source-figure-013 caption="Source slide page 13: Обновление"}

при любом обновлении строки надо изменять все индексы на таблице в индексе накапливаются ссылки на неактуальные версии

### Комментарии преподавателя

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

Старая версия помечается номером текущей транзакции в поле xmax. Обратите внимание, что новое значение 102 записалось поверх старого 101, так как транзакция 101 была отменена. Кроме того, биты-подсказки сброшены, так как статус текущей транзакции еще не известен.

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

Так же, как и при удалении, значение xmax в первой версии строки служит признаком того, что строка заблокирована.

Чем плохо такое обновление?

Во-первых, при любом изменении строки приходится обновлять все индексы, созданные для таблицы (даже если измененные поля не входят в индекс). Очевидно, это снижает производительность.

Во-вторых, в индексах накапливаются ссылки на исторические версии строки, которые потом приходится очищать.

Более того, есть особенность реализации B-дерева в PostgreSQL. Если на индексной странице недостаточно места для вставки новой записи, страница делится на две и все данные перераспределяются между ними. Однако при удалении (или очистке) записей две индексные страницы не «склеиваются» в одну. Из-за этого размер индекса может не уменьшиться даже при удалении существенной части данных.

## Page 14: Версии строк {#source-page-014}

Начнем с чистого листа.
```text
=> TRUNCATE TABLE t;
```
```text
TRUNCATE TABLE
```
Вставим одну строку, предварительно начав транзакцию.
```text
=> BEGIN;
```
```text
BEGIN
```
```sql
=> INSERT INTO t(id, s) VALUES (42, 'FOO');
```
```sql
INSERT 0 1
```
Номер нашей текущей транзакции:
```sql
=> SELECT pg_current_xact_id();
```
```text
 pg_current_xact_id
--------------------
                821
(1 row)
```
Посмотрим на таблицу, добавив системные столбцы:
```sql
=> SELECT ctid, xmin, xmax, * FROM t;
```
```text
 ctid  | xmin | xmax | id |  s
-------+------+------+----+-----
 (0,1) |  821 |    0 | 42 | FOO
(1 row)
```
Xmin совпадает с номером нашей транзакции, а xmax равен фиктивному значению 0.
Чтобы заглянуть в индекс, воспользуемся расширением pageinspect. Нулевая страница индекса содержит метаинформацию, поэтому смотрим в первую.
```sql
=> SELECT itemoffset, ctid, data FROM bt_page_items('t_s',1);
```
```text
 itemoffset | ctid  |          data
------------+-------+-------------------------
          1 | (0,1) | 09 46 4f 4f 00 00 00 00
(1 row)
```
Видим один указатель на единственную строку таблицы. (В поле data можно угадать ASCII-коды букв FOO.)
Pageinspect позволяет увидеть и полную информацию о том, что находится в табличной странице, но в этой теме мы не будем углубляться в эти детали.
Зафиксируем изменение.
```text
=> COMMIT;
```
```text
COMMIT
```
Теперь проверим, как работает обновление.
Начнем второй сеанс.
```sh
student$ psql arch_mvcc
```
В нем прочитаем таблицу на уровне Repeatable Read:
```text
=> BEGIN ISOLATION LEVEL REPEATABLE READ;
```
```text
BEGIN
```
```sql
=> SELECT ctid, xmin, xmax, * FROM t;
```
```text
 ctid  | xmin | xmax | id |  s
-------+------+------+----+-----
 (0,1) |  821 |    0 | 42 | FOO
(1 row)
```

## Page 15: Page 15 {#source-page-015}

Теперь обновляем строку.
```text
=> BEGIN;
```
```text
BEGIN
```
```sql
=> SELECT pg_current_xact_id();
```
```text
 pg_current_xact_id
--------------------
                822
(1 row)
```
```sql
=> UPDATE t SET s = 'BAR';
```
```sql
UPDATE 1
```
Запрос в первом сеансе выдает одну строку (новую версию):
```sql
=> SELECT ctid, xmin, xmax, * FROM t;
```
```text
 ctid  | xmin | xmax | id |  s
-------+------+------+----+-----
 (0,2) |  822 |    0 | 42 | BAR
(1 row)
```
Но в странице присутствуют обе версии. Предыдущую продолжает видеть второй сеанс:
```sql
=> SELECT ctid, xmin, xmax, * FROM t;
```
```text
 ctid  | xmin | xmax | id |  s
-------+------+------+----+-----
 (0,1) |  821 |  822 | 42 | FOO
(1 row)
```
Номер транзакции, удалившей первую версию строки, записался в xmax.
При этом в индексной странице обнаруживаем указатели на обе версии:
```sql
=> SELECT itemoffset, ctid, data FROM bt_page_items('t_s',1);
```
```text
 itemoffset | ctid  |          data
------------+-------+-------------------------
          1 | (0,2) | 09 42 41 52 00 00 00 00
          2 | (0,1) | 09 46 4f 4f 00 00 00 00
(2 rows)
```
Индексные записи внутри страницы упорядочены. Поэтому первой идет запись с ключом BAR (ссылается на версию (0,2)), а второй — запись с ключом FOO (ссылается на версию (0,1)).
Завершим транзакции.
```text
=> COMMIT;
```
```text
COMMIT
```
```text
=> COMMIT;
```
```text
COMMIT
```

## Page 16: HOT-обновление {#source-page-016}

![Source slide page 16: HOT-обновление](images/page-016.png){#source-figure-016 caption="Source slide page 16: HOT-обновление"}

обновляемые столбцы не должны входить ни в один индекс цепочка обновлений — только в пределах одной страницы

### Комментарии преподавателя

Однако если индекс создан по полю, значение которого не изменилось в результате обновления строки, то нет смысла создавать дополнительную запись в B-дереве, содержащую то же самое значение ключа. Именно так работает оптимизация, называемая HOT-обновлением — Heap-Only Tuple Update.

При таком обновлении в индексной странице находится лишь одна запись, ссылающаяся на первую версию строки табличной страницы. А внутри табличной страницы организуется цепочка из версий, на которые гарантированно нет внешних ссылок (heap-only tuples).

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

Подчеркнем, что HOT-обновления работают в случае, если обновляемые поля не входят ни в один индекс — иначе в каком-либо индексе оказалась бы ссылка непосредственно на новую версию строки, что противоречит идее этой оптимизации. В частности, HOT-обновление используется, если у таблицы вообще нет индексов.

Оптимизация действует только в пределах одной страницы, поэтому дополнительный обход цепочки не требует обращения к другим страницам и не ухудшает производительность. Однако если на странице не хватит свободного места, чтобы разместить новую версию строки, цепочка прервется. На версию строки, размещенную на другой странице, будет сделана новая ссылка из индекса.

## Page 17: HOT-обновление {#source-page-017}

Снова опустошим таблицу.
```text
=> TRUNCATE TABLE t;
```
```text
TRUNCATE TABLE
```
Но пусть теперь индекс будет построен по столбцу id:
```sql
=> DROP INDEX t_s;
```
```sql
DROP INDEX
```
```sql
=> CREATE INDEX t_id on t(id);
```
```sql
CREATE INDEX
```
```sql
=> INSERT INTO t(id, s) VALUES (42, 'FOO');
```
```sql
INSERT 0 1
```
Начнем транзакцию с уровнем изоляции Repeatable Read.
```text
=> BEGIN ISOLATION LEVEL REPEATABLE READ;
```
```text
BEGIN
```
```sql
=> SELECT ctid, xmin, xmax, * FROM t;
```
```text
 ctid  | xmin | xmax | id |  s
-------+------+------+----+-----
 (0,1) |  826 |    0 | 42 | FOO
(1 row)
```
До этого мы обновляли проиндексированный столбец, а теперь столбец s не входит ни в один индекс.
```text
=> BEGIN;
```
```text
BEGIN
```
```sql
=> UPDATE t SET s = 'BAR';
```
```sql
UPDATE 1
```
Появляется вторая версия строки.
```sql
=> SELECT ctid, xmin, xmax, * FROM t;
```
```text
 ctid  | xmin | xmax | id |  s
-------+------+------+----+-----
 (0,2) |  827 |    0 | 42 | BAR
(1 row)
```
Но другой сеанс продолжает видеть самую первую версию:
```sql
=> SELECT ctid, xmin, xmax, * FROM t;
```
```text
 ctid  | xmin | xmax | id |  s
-------+------+------+----+-----
 (0,1) |  826 |  827 | 42 | FOO
(1 row)
```
Что же обнаружится в индексе?
```sql
=> SELECT itemoffset, ctid, data FROM bt_page_items('t_id',1);
```
```text
 itemoffset | ctid  |          data
------------+-------+-------------------------
          1 | (0,1) | 2a 00 00 00 00 00 00 00
(1 row)
```
Мы видим, что в индексе не добавилась новая запись. Индекс продолжает ссылаться на первую версию, а уже внутри табличной страницы первая версия связана со второй в цепочку.
Еще один способ убедиться в том, что выполнилось HOT-обновление — заглянуть в статистику текущей транзакции:

## Page 18: Page 18 {#source-page-018}

```sql
=> SELECT n_tup_upd, n_tup_hot_upd
```
```sql
FROM pg_stat_xact_all_tables
WHERE relid = 't'::regclass;
```
```text
 n_tup_upd | n_tup_hot_upd
-----------+---------------
         1 |             1
(1 row)
```
Здесь мы видим, что всего транзакция обновила одну строку (n\_tup\_upd), причем с помощью HOT (n\_tup\_hot\_upd).
```text
=> ROLLBACK;
```
```text
ROLLBACK
```
```text
=> ROLLBACK;
```
```text
ROLLBACK
```

## Page 19: Снимки данных {#source-page-019}

**Снимок данных**
- Видимость версий строк
- Горизонт снимка
- Горизонт транзакции
- Горизонт базы данных

## Page 20: Снимок данных {#source-page-020}

**Дает согласованную картину данных на момент времени**
  - видны только зафиксированные на этот момент данные
- Создается:
### снимок
  - Read Committed —
  - в начале каждого оператора
  - Repeatable Read, Serializable —
  - в начале первого оператора транзакции
  - xid

### Комментарии преподавателя

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

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

Согласованность здесь понимается в обычном ACID-смысле: данные должны быть корректны, что, по сути, означает, что видеть нужно только те данные, которые были зафиксированы на момент создания снимка. Это полностью исключает аномалию грязного чтения на любом уровне изоляции.

На уровне изоляции Read Committed снимок создается в начале каждого оператора транзакции. Снимок активен, пока выполняется оператор. Таким образом каждый оператор видит согласованные данные, но если между двумя операторами другие транзакции зафиксируют изменения, возможны неповторяемое и фантомное чтения.

На уровне Repeatable Read (и Serializable) снимок создается один раз в начале первого оператора транзакции. Такой снимок остается активным до самого конца транзакции. В этом случае неповторяемое и фантомное чтения исключаются.

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

## Page 21: Видимость версий строк {#source-page-021}

**Видимость версии строки ограничена xmin и xmax**
- Версия попадает в снимок, когда
- изменения транзакции xmin видны для снимка, изменения транзакции xmax не видны для снимка Изменения транзакции видны, когда
  - либо это та же самая транзакция, что создала снимок,
  - либо она завершилась фиксацией до момента создания снимка
- Отдельные правила для видимости собственных изменений
  - учитывается порядковый номер операции в транзакции (cmin/cmax)

### Комментарии преподавателя

Будет или нет данная версия строки видна в снимке, определяется двумя полями ее заголовка — xmin и xmax, — то есть номерами создавшей и удалившей транзакций. Такие интервалы не пересекаются, поэтому одна строка представлена в любом снимке максимум одной своей версией.

Точные правила видимости довольно сложны и учитывают различные «крайние» случаи. Чуть упрощая, можно сказать, что версия строки видна, когда в снимке видны изменения, сделанные транзакцией xmin, и не видны изменения, сделанные транзакцией xmax. Иными словами, создание версии строки должно быть видно, а удаление (если оно было) — нет.

В свою очередь, изменения транзакции видны в снимке, если либо это та же самая транзакция, что создала снимок (транзакция видит свои же изменения), либо транзакция была зафиксирована до создания снимка.

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

## Page 22: Снимок данных {#source-page-022}

![Source slide page 22: Снимок данных](images/page-022.png){#source-figure-022 caption="Source slide page 22: Снимок данных"}

- xmin — номер самой ранней активной транзакции xmax — номер, следующий за последней зафиксированной транзакцией xip\_list — список активных транзакций

### Комментарии преподавателя

В момент создания снимка данных запоминаются несколько значений, которые и определяют снимок:

● xmin — нижняя граница снимка, в качестве которой выступает номер самой ранней активной транзакции.

Все транзакции с меньшими номерами либо зафиксированы, и тогда их изменения безусловно видны в снимке, либо отменены, и тогда изменения игнорируются.

● xmax — верхняя граница снимка, в качестве которой берется номер, следующий за номером последней зафиксированной транзакции. Верхняя граница определяет момент, в который был сделан снимок. Обратите внимание, что момент задается не временем (как было условно показано на предыдущих слайдах), а увеличивающимися номерами транзакций.

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

● xip\_list (xid-in-progress list) — список активных транзакций.

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

Информацию о снимке можно получить с помощью функции pg\_current\_snapshot() и нескольких других: <https://postgrespro.ru/docs/postgresql/16/functions-info>

## Page 23: Горизонт снимка {#source-page-023}

![Source slide page 23: Горизонт снимка](images/page-023.png){#source-figure-023 caption="Source slide page 23: Горизонт снимка"}


### Комментарии преподавателя

Номер самой ранней из активных транзакций (xmin снимка) имеет важный смысл — он определяет горизонт снимка.

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

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

## Page 24: Горизонт транзакции {#source-page-024}

![Source slide page 24: Горизонт транзакции](images/page-024.png){#source-figure-024 caption="Source slide page 24: Горизонт транзакции"}


### Комментарии преподавателя

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

Если транзакция будет долго находиться в таком состоянии ожидания (оно называется idle in transaction), не выполняя никаких команд, горизонт будет оставаться на месте.

## Page 25: Горизонт базы данных {#source-page-025}

![Source slide page 25: Горизонт базы данных](images/page-025.png){#source-figure-025 caption="Source slide page 25: Горизонт базы данных"}


### Комментарии преподавателя

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

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

На слайде показан пример трех транзакций с уровнем изоляции Read Committed. Для оператора UPDATE транзакции T1 был построен снимок. Когда оператор отрабатывает, снимок удаляется и перестает удерживать горизонт. Однако транзакция T1 по-прежнему активна, и поэтому горизонты снимков транзакций T2 и T3 не сдвинутся правее номера транзакции T1. Тем самым активная транзакция удерживает горизонт самим фактом своего существования.

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

Это одна из причин, по которым транзакции не следует делать длиннее, чем абсолютно необходимо.

## Page 26: Снимки данных {#source-page-026}

И еще раз начнем транзакцию в отдельном сеансе.
```text
=> BEGIN ISOLATION LEVEL REPEATABLE READ;
```
```text
BEGIN
```
```sql
=> SELECT ctid, xmin, xmax, * FROM t;
```
```text
 ctid  | xmin | xmax | id |  s
-------+------+------+----+-----
 (0,1) |  826 |  827 | 42 | FOO
(1 row)
```
Одновременно с этим выполним несколько других транзакций.
```sql
=> UPDATE t SET s = 'BAR';
```
```sql
UPDATE 1
```
```sql
=> INSERT INTO t(id, s)  VALUES (43, 'BAZ');
```
```sql
INSERT 0 1
```
Горизонт транзакций можно увидеть следующим образом:
```sql
=> SELECT query,backend_xmin
FROM pg_stat_activity
WHERE backend_xmin IS NOT NULL
AND datname='arch_mvcc';
```
```sql
               query                | backend_xmin
------------------------------------+--------------
 SELECT ctid, xmin, xmax, * FROM t; |          828
 SELECT query,backend_xmin         +|          830
 FROM pg_stat_activity             +|
 WHERE backend_xmin IS NOT NULL    +|
 AND datname='arch_mvcc';           |
(2 rows)
```
Видно, что открытая транзакция удерживает горизонт, поскольку ей нужны старые версии строк.
Завершим ее.
```text
=> COMMIT;
```
```text
COMMIT
```
После этого горизонт базы данных продвигается вперед, позволяя очищать неактуальные версии:
```sql
=> SELECT query,backend_xmin
FROM pg_stat_activity
WHERE backend_xmin IS NOT NULL
AND datname='arch_mvcc';
```
```sql
             query              | backend_xmin
--------------------------------+--------------
 SELECT query,backend_xmin     +|          830
 FROM pg_stat_activity         +|
 WHERE backend_xmin IS NOT NULL+|
 AND datname='arch_mvcc';       |
(1 row)
```

## Page 27: Итоги {#source-page-027}

**PostgreSQL использует многоверсионность и изоляцию на основе снимков данных**
- Индексы не содержат информации о версионности
  - чтобы вернуть данные, нужно проверить видимость
  - (по таблице или карте видимости)
- Лишние индексы не только влекут накладные расходы, но и препятствуют HOT-обновлениям
- Транзакции должны быть настолько длинными, насколько необходимо, но не более

## Page 28: Практика {#source-page-028}

1. Функционал точек сохранения должен позволять откатывать часть транзакции. Чтобы разобраться, как это работает, выполните операции внутри одной транзакции:
  - вставьте строку в таблицу;
  - установите точку сохранения (SAVEPOINT);
  - вставьте в таблицу еще одну строку;
  - откатите изменения до точки сохранения;
  - вставьте еще одну строку;
  - зафиксируйте изменения.
  - При этом после каждой операции выводите номер текущей
  - транзакции и проверяйте значения столбцов xmin и xmax.

## Page 29: 1. Точки сохранения {#source-page-029}

```sql
=> CREATE DATABASE arch_mvcc;
```
```sql
CREATE DATABASE
```
```sql
=> \c arch_mvcc
```
```text
You are now connected to database "arch_mvcc" as user "student".
```
Создадим таблицу:
```sql
=> CREATE TABLE t(s text);
```
```sql
CREATE TABLE
```
Начинаем транзакцию.
```text
=> BEGIN;
```
```text
BEGIN
```
Вставляем первую строку:
```sql
=> INSERT INTO t VALUES ('first');
```
```sql
INSERT 0 1
```
Вот номер нашей транзакции и состояние таблицы:
```sql
=> SELECT pg_current_xact_id();
```
```text
 pg_current_xact_id
--------------------
                817
(1 row)
```
```sql
=> SELECT *, xmin, xmax FROM t;
```
```text
   s   | xmin | xmax
-------+------+------
 first |  817 |    0
(1 row)
```
Пока все как обычно.
Устанавливаем точку сохранения и вставляем вторую строку:
```text
=> SAVEPOINT sp;
```
```text
SAVEPOINT
```
```sql
=> INSERT INTO t VALUES ('second');
```
```sql
INSERT 0 1
```
```sql
=> SELECT pg_current_xact_id();
```
```text
 pg_current_xact_id
--------------------
                817
(1 row)
```
Номер транзакции прежний.
```sql
=> SELECT *, xmin, xmax FROM t;
```
```text
   s    | xmin | xmax
--------+------+------
 first  |  817 |    0
 second |  818 |    0
(2 rows)
```
А вот в xmin второй строки указан другой номер. Это вложенная транзакция, она начинается сразу после установки точки сохранения.
Откатим изменения до точки сохранения и вставим третью строку:

## Page 30: Page 30 {#source-page-030}

```text
=> ROLLBACK TO sp;
```
```text
ROLLBACK
```
```sql
=> INSERT INTO t VALUES ('third');
```
```sql
INSERT 0 1
```
```sql
=> SELECT pg_current_xact_id();
```
```text
 pg_current_xact_id
--------------------
                817
(1 row)
```
```sql
=> SELECT *, xmin, xmax FROM t;
```
```text
   s   | xmin | xmax
-------+------+------
 first |  817 |    0
 third |  819 |    0
(2 rows)
```
У третьей строки опять другой номер. Это еще одна вложенная транзакция, которая началась сразу после отката к точке сохранения.
Зафиксируем изменения:
```text
=> COMMIT;
```
```text
COMMIT
```
Каждая вложенная транзакция имеет собственный статус и может быть оборвана независимо от статуса основной транзакции. Откат части транзакции — это фактически откат одной или нескольких вложенных транзакций.
Но при завершении основной транзакции завершаются и все еще активные вложенные транзакции: при фиксации — фиксируются, при откате — откатываются.
