Уровень 3.0
Анализ HOT-обновлений и внутристраничной очистки
Предусловия:
- Изучена лекция «Внутристраничная очистка и оптимизация HOT»
Подготовка
-
Откройте терминал и подключитесь в psql к базе данных
postgresc рольюpostgres:[student@pkles-gt0040523 ~]$ sudo -iu postgres-bash-4.4$ psqlpsql (15.5)Введите "help", чтобы получить справку. -
Создайте базу данных и подключитесь к ней с ролью
postgres:postgres=# CREATE DATABASE mvcc_db;CREATE DATABASEpostgres=# \c mvcc_dbВы подключены к базе данных "mvcc_db" как пользователь "postgres". -
Создайте расширение
pageinspect:mvcc_db=# CREATE EXTENSION pageinspect;CREATE EXTENSIONВспомним, что расширение
pageinspectпозволяет исследовать страницы данных на низком уровне. -
Создайте простую таблицу и индекс типа B-дерево по одному столбцу:
mvcc_db=# CREATE TABLE some_table(id integer, value char(1000)) WITH (autovacuum_enabled=false, fillfactor=50);CREATE TABLEmvcc_db=# CREATE INDEX id_idx ON some_table(id);CREATE INDEXТаблица создана с параметром хранения
autovacuum_enabled = false, чтобы автоочистка не мешала проведению эксперимента.Также задан параметр хранения
fillfactor = 50для того, чтобы после заполнения табличной страницы на 50 % срабатывала внутристраничная очистка.Табличная версия строки без заголовка будет занимать 1004 байта (
integer + char(1000)) + 24 байта заголовка.Итого 1028 байт на табличную версию строки.
Страница имеет размер 8192 байт, из них заголовок страницы и специальное пространство — по 24 байта каждый, а также по 4 байта на указатели версий строк. Значение
fillfactor, равное 50 %, резервирует не более 4072 байт.Таким образом, без превышения значения
fillfactorв странице может храниться не более трех версий строк объемом 1028 байт. -
Создайте функцию, которая будет выводить информацию о версиях строк в заданной табличной странице:
mvcc_db=# CREATE OR REPLACE FUNCTION get_tuples_info(relname text, pagenum bigint)RETURNS TABLE(ctid text, status text, xmin xid, xmax xid, next_ctid text,heap_hot_updated boolean, heap_only_tuple boolean)AS $$SELECT '(' || $2 || ', ' || lp || ')',CASE lp_flagsWHEN 0 THEN 'unused'WHEN 1 THEN 'normal'WHEN 2 THEN 'redirect to '||lp_offWHEN 3 THEN 'dead'END,t_xmin, t_xmax, t_ctid,'HEAP_HOT_UPDATED' = ANY(raw_flags),'HEAP_ONLY_TUPLE' = ANY(raw_flags)FROM heap_page_items(get_raw_page($1, 'main', $2)),LATERAL heap_tuple_infomask_flags(t_infomask, t_infomask2)ORDER BY lp;$$ LANGUAGE SQL;CREATE FUNCTIONПохожая функция уже использовалась в предыдущих лабораторных работах.
В данной реализации добавлены возвращаемые параметры, необходимые для отслеживания HOT-обновлений:
status— статус указателя на версию строкиheap_hot_updated— информационный бит, указывающий, что версия строки имеет ссылку на следующую версию строки в HOT-цепочкеheap_only_tuple— информационный бит, указывающий, что на версию строки нет ссылок из индексов
Также из возвращаемых параметров исключены информационные биты со статусами транзакций
xminиxmax.На вход функции передаются имя отношения (
relname) и номер страницы (pagenum).
HOT-обновления
-
Вставьте одну строку в таблицу
some_tableи посмотрите содержимое табличной и индексной страниц:mvcc_db=# INSERT INTO some_table VALUES (1, '100');INSERT 0 1mvcc_db=# SELECT * FROM get_tuples_info('some_table', 0);ctid | status | xmin | xmax | next_ctid | heap_hot_updated | heap_only_tuple--------+--------+---------+------+-----------+------------------+-----------------(0, 1) | normal | 2901148 | 0 | (0,1) | f | f(1 строка)mvcc_db=# SELECT itemoffset, ctid, dead FROM bt_page_items('id_idx', 1);itemoffset | ctid | dead------------+-------+------1 | (0,1) | f(1 строка)На данный момент в табличной странице одна «живая» версия строки (0, 1) со статусом
normal.Информационные биты
heap_hot_updatedиheap_only_tupleне выставлены, так как строка еще не обновлялась и на нее имеется ссылка из индекса.В индексной странице находится соответствующая индексная строка, ссылающаяся на версию строки в табличной странице.
При этом, как и ожидалось, индексная строка не помечена флагом
dead. -
Обновите значение столбца
valueвставленной строки и посмотрите содержимое табличной и индексной страниц:mvcc_db=# UPDATE some_table SET value = '200' WHERE id = 1;UPDATEmvcc_db=# SELECT * FROM get_tuples_info('some_table', 0);ctid | status | xmin | xmax | next_ctid | heap_hot_updated | heap_only_tuple--------+--------+---------+---------+-----------+------------------+-----------------(0, 1) | normal | 2901148 | 2901149 | (0,2) | t | f(0, 2) | normal | 2901149 | 0 | (0,2) | f | t(2 строки)mvcc_db=# SELECT itemoffset, ctid, dead FROM bt_page_items('id_idx', 1);itemoffset | ctid | dead------------+-------+------1 | (0,1) | f(1 строка)В табличной странице создана вторая версия строки (0, 2).
В первой версии строки (0, 1) проставлен информационный бит
heap_hot_updated, свидетельствующий, что произошло HOT-обновление.Также в первой версии строки в поле
next_ctidуказана ссылка на следующую версию строки (0, 2) в HOT-цепочке.Во второй версии строки (0, 2) проставлен информационный бит
heap_only_tuple, свидетельствующий, что ссылок из индексов на данную версию строки нет.В индексной странице ничего не поменялось. Осталась только одна индексная строка, ссылающаяся на первую версию строки в цепочке.
Это произошло, потому что обновление не затронуло столбец
id, по которому построен индекс, и соответственно сработала оптимизация HOT. -
Еще дважды обновите значение столбца
valueи посмотрите содержимое табличной и индексной страниц:mvcc_db=# UPDATE some_table SET value = '300' WHERE id = 1;UPDATE 1mvcc_db=# UPDATE some_table SET value = '400' WHERE id = 1;UPDATE 1mvcc_db=# SELECT * FROM get_tuples_info('some_table', 0);ctid | status | xmin | xmax | next_ctid | heap_hot_updated | heap_only_tuple--------+--------+---------+---------+-----------+------------------+-----------------(0, 1) | normal | 2901148 | 2901149 | (0,2) | t | f(0, 2) | normal | 2901149 | 2901150 | (0,3) | t | t(0, 3) | normal | 2901150 | 2901151 | (0,4) | t | t(0, 4) | normal | 2901151 | 0 | (0,4) | f | t(4 строки)mvcc_db=# SELECT itemoffset, ctid, dead FROM bt_page_items('id_idx', 1);itemoffset | ctid | dead------------+-------+------1 | (0,1) | f(1 строка)Теперь в цепочке 4 версии строки. В индексе по-прежнему одна строка.
Значение
fillfactorдолжно быть уже превышено. Проверить это можно, обратившись к данной странице и выполнив любой запрос, который должен вызвать внутристраничную очистку в случае превышенияfillfactor.
Внутристраничная очистка
-
Выполните запрос к таблице
some_tableи посмотрите содержимое табличной и индексной страниц:mvcc_db=# SELECT id FROM some_table;id----1(1 строка)mvcc_db=# SELECT * FROM get_tuples_info('some_table', 0);ctid | status | xmin | xmax | next_ctid | heap_hot_updated | heap_only_tuple--------+---------------+---------+------+-----------+------------------+-----------------(0, 1) | redirect to 4 | | | | |(0, 2) | unused | | | | |(0, 3) | unused | | | | |(0, 4) | normal | 2901151 | 0 | (0,4) | f | t(4 строки)mvcc_db=# SELECT itemoffset, ctid, dead FROM bt_page_items('id_idx', 1);itemoffset | ctid | dead------------+-------+------1 | (0,1) | f(1 строка)Как и предполагалось, после превышения значения
fillfactorв табличной странице сработала внутристраничная очистка.Очищены версии строк (0, 1), (0, 2) и (0, 3).
Однако указатель на версию строки (0, 1) не освобожден, так как на него по-прежнему имеется ссылка в индексе.
При этом статус указателя изменен на
redirect to 4для сохранения возможности перехода на версию строки (0, 4) при индексном доступе.В индексной странице по-прежнему без изменений.
Указатели версий строк (0, 2) и (0, 3) освобождены и могут быть переиспользованы, о чем говорит статус
unused. -
Обновите столбец
idтой же строки и посмотрите содержимое табличной и индексной страниц:mvcc_db=# UPDATE some_table SET id = 2 WHERE id = 1;UPDATE 1mvcc_db=# SELECT * FROM get_tuples_info('some_table', 0);ctid | status | xmin | xmax | next_ctid | heap_hot_updated | heap_only_tuple--------+---------------+---------+---------+-----------+------------------+-----------------(0, 1) | redirect to 4 | | | | |(0, 2) | normal | 2901153 | 0 | (0,2) | f | f(0, 3) | unused | | | | |(0, 4) | normal | 2901151 | 2901153 | (0,2) | f | t(4 строки)mvcc_db=# SELECT itemoffset, ctid, dead FROM bt_page_items('id_idx', 1);itemoffset | ctid | dead------------+-------+------1 | (0,1) | f2 | (0,2) | f(2 строки)На этот раз обновление затронуло проиндексированный столбец, поэтому HOT-обновление не сработало и была создана новая индексная строка.
При этом в табличной странице для новой версии строки был задействован недавно освобожденный указатель (0, 2).
-
Еще дважды обновите значение столбца
idи посмотрите содержимое табличной и индексной страниц:mvcc_db=# UPDATE some_table SET id = 3 WHERE id = 2;UPDATE 1mvcc_db=# UPDATE some_table SET id = 4 WHERE id = 3;UPDATE 1mvcc_db=# SELECT * FROM get_tuples_info('some_table', 0);ctid | status | xmin | xmax | next_ctid | heap_hot_updated | heap_only_tuple--------+---------------+---------+---------+-----------+------------------+-----------------(0, 1) | redirect to 4 | | | | |(0, 2) | normal | 2901153 | 2901154 | (0,3) | f | f(0, 3) | normal | 2901154 | 2901155 | (0,5) | f | f(0, 4) | normal | 2901151 | 2901153 | (0,2) | f | t(0, 5) | normal | 2901155 | 0 | (0,5) | f | f(5 строк)mvcc_db=# SELECT itemoffset, ctid, dead FROM bt_page_items('id_idx', 1);itemoffset | ctid | dead------------+-------+------1 | (0,1) | f2 | (0,2) | f3 | (0,3) | f4 | (0,5) | f(4 строки)Теперь в табличной странице 4 версии строки. Для каждой из них в индексе имеется своя строка, поскольку оптимизация HOT не срабатывает.
Значение параметра
fillfactorдолжно быть превышено. -
Выполните запрос к таблице
some_tableи посмотрите содержимое табличной и индексной страниц:mvcc_db=# SELECT id FROM some_table;id----4(1 строка)mvcc_db=# SELECT * FROM get_tuples_info('some_table', 0);ctid | status | xmin | xmax | next_ctid | heap_hot_updated | heap_only_tuple--------+--------+---------+------+-----------+------------------+-----------------(0, 1) | dead | | | | |(0, 2) | dead | | | | |(0, 3) | dead | | | | |(0, 4) | unused | | | | |(0, 5) | normal | 2901155 | 0 | (0,5) | f | f(5 строк)mvcc_db=# SELECT itemoffset, ctid, dead FROM bt_page_items('id_idx', 1);itemoffset | ctid | dead------------+-------+------1 | (0,1) | f2 | (0,2) | f3 | (0,3) | f4 | (0,5) | f(4 строки)В результате очищены три версии строки, указатели на которые не освобождены, поскольку на них имеются ссылки из индексов.
Однако их статус помечен как
dead.При следующем обращении к ним через индекс в самом индексе соответствующие строки также будут помечены статусом
dead.Обратите внимание, что статус указателя на версию строки (0, 1) изменился с
redirect to 4наdead.Это произошло в связи с тем, что версия строки (0, 4) была вычищена, но при этом в индексе по-прежнему имеется ссылка на версию строки (0, 1).
-
Отключите возможность использования путей доступа «последовательное сканирование» и «сканирование по битовой карте»:
mvcc_db=# SET enable_seqscan = off;SETmvcc_db=# SET enable_bitmapscan = off;SETМетоды «последовательное сканирование» и «сканирование по битовой карте» отключены, чтобы любое обращение к таблице выполнялось через обращение к индексной странице.
Для таблиц малого объема планировщик выбирает метод «последовательное сканирование» независимо от селективности условия поиска.
-
Выполните запрос к таблице
some_tableи посмотрите содержимое индексной страницы:mvcc_db=# SELECT id FROM some_table;id----4(1 строка)mvcc_db=# SELECT itemoffset, ctid, dead FROM bt_page_items('id_idx', 1);itemoffset | ctid | dead------------+-------+------1 | (0,1) | t2 | (0,2) | t3 | (0,3) | t4 | (0,5) | f(4 строки)Обращение к таблице выполнено посредством индекса.
При обращении было определено, что три версии строки имеют статус
deadи такие же статусы проставлены в соответствующих индексных строках.Далее, если при очередной вставке новой индексной строки в странице не окажется места для нее, будет вызвана внутристраничная очистка индексной страницы и строки со статусом
deadбудут очищены.Вспомним, что внутристраничная очистка индексных страниц предотвращает их расщепление.
-
Посмотрите статистику обновлений таблицы
some_tableс помощью представленияpg_stat_all_tables:mvcc_db=# SELECT relname, n_tup_upd, n_tup_hot_upd FROM pg_stat_all_tables WHERE relname = 'some_table';relname | n_tup_upd | n_tup_hot_upd------------+-----------+---------------some_table | 6 | 3(1 строка)Всего было выполнено 6 обновлений, из которых 3 с использованием оптимизации HOT.
Разрыв HOT-цепочки
-
Очистите таблицу
some_tableи установите параметрfillfactorв 100 %:mvcc_db=# TRUNCATE some_table;TRUNCATE TABLEmvcc_db=# ALTER TABLE some_table SET (fillfactor=100);ALTER TABLE -
Вставьте одну строку, 5 раз ее обновите и вставьте еще одну строку:
mvcc_db=# INSERT INTO some_table VALUES(1, '100');INSERT 0 1mvcc_db=# UPDATE some_table SET value = '200';UPDATE 1mvcc_db=# UPDATE some_table SET value = '300';UPDATE 1mvcc_db=# UPDATE some_table SET value = '400';UPDATE 1mvcc_db=# UPDATE some_table SET value = '500';UPDATE 1mvcc_db=# UPDATE some_table SET value = '600';UPDATE 1mvcc_db=# INSERT INTO some_table VALUES(2, '200');INSERT 0 1 -
Посмотрите содержимое табличной и индексной страниц:
mvcc_db=# SELECT * FROM get_tuples_info('some_table', 0);ctid | status | xmin | xmax | next_ctid | heap_hot_updated | heap_only_tuple--------+--------+--------+--------+-----------+------------------+-----------------(0, 1) | normal | 139407 | 139408 | (0,2) | t | f(0, 2) | normal | 139408 | 139409 | (0,3) | t | t(0, 3) | normal | 139409 | 139410 | (0,4) | t | t(0, 4) | normal | 139410 | 139411 | (0,5) | t | t(0, 5) | normal | 139411 | 139412 | (0,6) | t | t(0, 6) | normal | 139412 | 0 | (0,6) | f | t(0, 7) | normal | 139413 | 0 | (0,7) | f | f(7 строк)mvcc_db=# SELECT itemoffset, ctid, dead FROM bt_page_items('id_idx', 1);itemoffset | ctid | dead------------+-------+------1 | (0,1) | f2 | (0,7) | f(2 строки)Как и ожидалось, в индексе имеется всего две записи — по одной для каждой вставленной.
При этом для первой вставленной строки в табличной странице организована HOT-цепочка.
-
Еще выполните обновление первой вставленной строки и посмотрите содержимое табличной и индексной страниц:
mvcc_db=# UPDATE some_table SET value = 700 WHERE id = 1;UPDATE 1mvcc_db=# SELECT * FROM get_tuples_info('some_table', 0);ctid | status | xmin | xmax | next_ctid | heap_hot_updated | heap_only_tuple--------+--------+--------+--------+-----------+------------------+-----------------(0, 1) | normal | 139407 | 139408 | (0,2) | t | f(0, 2) | normal | 139408 | 139409 | (0,3) | t | t(0, 3) | normal | 139409 | 139410 | (0,4) | t | t(0, 4) | normal | 139410 | 139411 | (0,5) | t | t(0, 5) | normal | 139411 | 139412 | (0,6) | t | t(0, 6) | normal | 139412 | 139414 | (1,1) | f | t(0, 7) | normal | 139413 | 0 | (0,7) | f | f(7 строк)mvcc_db=# SELECT itemoffset, ctid, dead FROM bt_page_items('id_idx', 1);itemoffset | ctid | dead------------+-------+------1 | (0,1) | f2 | (1,1) | f3 | (0,7) | f(3 строки)Количество версий строк в табличной странице не изменилось, поскольку она полностью заполнена.
Новая версия строки была размещена на новой странице, о чем свидетельствует значение
next_ctid = (1, 1)в заголовке версии строки (0, 6).При этом в индексной странице появилась новая строка, ссылающаяся на версию строки (1, 1).
Таким образом, произошел разрыв цепочки.
Проверим, что новая версия строки действительно попала на новую страницу.
-
Посмотрите содержимое первой табличной страницы:
mvcc_db=# SELECT * FROM get_tuples_info('some_table', 1);ctid | status | xmin | xmax | next_ctid | heap_hot_updated | heap_only_tuple--------+--------+--------+--------+-----------+------------------+-----------------(1, 1) | normal | 139414 | 0 | (1,1) | f | f(1 строка)Разрыва HOT-цепочки можно было бы избежать, если бы значение
fillfactorбыло бы меньше 100 %.В этом случае последняя вставленная строка командой
INSERTбыла бы помещена на новую страницу, а версия строки, полученная в результате последнего обновления, была бы размещена на первой странице без разрыва цепочки.Соответственно, в индексной странице не пришлось бы создавать лишней индексной строки.
Завершение
-
Подключитесь к базе данных
postgres:mvcc_db=# \c postgresВы подключены к базе данных "postgres" как пользователь "postgres". -
Удалите базу данных
mvcc_dbи завершите сеанс psql:postgres=# DROP DATABASE mvcc_db;DROP DATABASEpostgres=# \q
Самопроверка
Вопрос 1
Состояние заголовков версий строк табличной страницы с соответствующими идентификаторами ctid представлено ниже:
| ctid | next ctid | heap hot updated | heap only tuple | | --- | --- | --- | --- | | (0, 1) | (0, 2) | 1 | 0 | | (0, 2) | (0, 4) | 1 | 1 | | (0, 3) | (0, 3) | 0 | 0 | | (0, 4) | (1, 1) | 0 | 1 |
Для таблицы создан индекс типа B-дерево, покрывающий все множество строк.
На какие версии строк имеются ссылки из индекса? Выберите все верные варианты ответа
Вопрос 2
Состояние указателей на версии строк в табличной странице представлено ниже:
| ctid | status | | --- | --- | | (0, 1) | dead | | (0, 2) | normal | | (0, 3) | unused | | (0, 4) | redirect to 2|
Сколько версий строк имеется в табличной странице?
Вопрос 3
В сеансе psql последовательно выполнены следующие команды:
mvcc_db=# \d+ some_table
Таблица "public.some_table"
Столбец | Тип | Правило сортировки | Допустимость NULL | По умолчанию | Хранилище | Сжатие | Цель для статистики | Описание
---------+---------+--------------------+-------------------+--------------+-----------+--------+---------------------+----------
id | integer | | | | plain | | |
value | integer | | | | plain | | |
Индексы:
"id_idx" btree (id)
Метод доступа: heap
Параметры: autovacuum_enabled=off
mvcc_db=# SELECT itemoffset, ctid, dead FROM bt_page_items('id_idx', 1);
itemoffset | ctid | dead
------------+-------+-----
1 | (0,1) | t
2 | (0,2) | t
3 | (0,3) | f
4 | (0,4) | f
(1 строка)
mvcc_db=# INSERT INTO some_table VALUES (100, 1000);
Индекс "id_idx" содержит только одну листовую страницу.
При выполнении команд параллельных транзакций не выполнялось.
Сколько строк будет очищено из листовой страницы индекса при выполнении последней команды?
Вопрос 4
Состояние указателей на версии строк в табличной странице представлено ниже:
| ctid | status | | --- | --- | | (0, 1) | dead | | (0, 2) | normal | | (0, 3) | unused | | (0, 4) | redirect to 2|
Сколько версий строк имеется в табличной странице?
Вопрос 5
В сеансе psql последовательно выполнены следующие команды:
mvcc_db=# CREATE TABLE some_table(id integer, value char(3000)) WITH (fillfactor = 50, autovacuum_enabled = off);
CREATE TABLE
mvcc_db=# CREATE INDEX id_idx ON some_table(id);
CREATE INDEX
mvcc_db=# INSERT INTO some_table VALUES(1, '100');
INSERT 0 1
mvcc_db=# INSERT INTO some_table VALUES(2, '200');
INSERT 0 1
mvcc_db=# UPDATE some_table SET value = '150' WHERE id = 1;
UPDATE 1
mvcc_db=# SELECT count(*) FROM bt_page_items('id_idx', 1);
При выполнении команд параллельных транзакций не выполнялось.
Какое число будет получено в результате выполнения последнего запроса?