Карта видимости - это достаточно простой механизм в СУБД PostgreSQL, но даже он имеет множество интересных тайн, если погрузиться в детали реализации.
В этой статье мы выясним:
Какие особенности есть у механизма сбрасывания и установки бита полной видимости.
Как Index only scan использует бит полной видимости.
Зачем записывать информацию об изменении карты видимости в WAL.
Каким образом карта видимости участвует в оптимизации предвыборки Bitmap scan.
Зачем механизму оценки селективности нужна карта видимости.
Все тесты, представленные в данной статье, были выполнены в PostgreSQL REL_17_STABLE.
Общая информация
Карта видимости состоит из битовой карты пар информационных битов на каждую страницу отношения. Первый – бит полной видимости – определяет, являются ли все версии строк видимыми для всех активных транзакций. Второй – бит полной заморозки – определяет, заморожены ли все версии строк на странице.
Информационные биты полной видимости устанавливаются исключительно во время работы вакуума, а сбрасываются при любом изменении содержания тела страницы.
Если в карте видимости установлен бит полной видимости, значит все активные транзакции обязаны видеть полное содержимое соответствующей страницы, а если бит сброшен, то postgres будет считать, что не все версии строк видны всем транзакциям, даже если это не так на самом деле.
Какие особенности есть у механизма сбрасывания и установки бита полной видимости
Со сбрасыванием и установкой бита полной видимости все немного сложнее, нежели может показаться. Если бы каждый раз при изменении страницы приходилось бы получать значение бита полной видимости из страницы карты видимости, чтобы его сбросить (даже если он и так уже сброшен), то данное мероприятие мало того, что нагрузит систему I/O, так еще и затратит место в буферном кэше, хоть и карта видимости намного меньше main и FSM слоев.
С целью оптимизации в заголовок основной страницы был добавлен информационный бит PD_ALL_VISIBLE, имеющий то же значение, что и соответствующий бит полной видимости в карте видимости. Данный бит позволит лишний раз не читать страницы карты видимости.
Стоит отметить, что у страниц всех слоев одинаковые заголовки, но используется PD_ALL_VISIBLE для страниц основного слоя.
Для примера рассмотрим, как функция heap_insert() (src/backend/access/heap/heapam.c) чистит биты видимости при вставке новой версии строки:
START_CRIT_SECTION();
...
if (PageIsAllVisible(BufferGetPage(buffer)))
{
all_visible_cleared = true;
PageClearAllVisible(BufferGetPage(buffer));
visibilitymap_clear(relation,
ItemPointerGetBlockNumber(&(heaptup->t_self)),
vmbuffer, VISIBILITYMAP_VALID_BITS);
}
...
if (all_visible_cleared)
xlrec.flags |= XLH_INSERT_ALL_VISIBLE_CLEARED;
...
END_CRIT_SECTION();
Алгоритм:
Начинается критическая секция, ибо, возможно, придется изменить разделяемый ресурс в виде бита PD_ALL_VISIBLE. В этой же критической секции происходит вставка версии строки.
PageIsAllVisible(page). Если бит PD_ALL_VISIBLE равен единице, выполнятся следующие три пункта:
all_visible_cleared = true. Данная булева переменная определяет, будет ли записана в журнал предзаписи информация о том, что бит полной видимости был очищен.
PageClearAllVisible() очищает бит PD_ALL_VISIBLE.
visibilitymap_clear() очищает бит полной видимости в карте видимости.
Далее, в журнал предзаписи фиксируется операция очистки бита полной видимости и бита PD_ALL_VISIBLE.
Критическая секция заканчивается.
Бит PD_ALL_VISIBLE может использоваться не только при очистке бита полной видимости в карте видимости, но еще и при установке. Рассмотрим функцию lazy_scan_new_or_empty() (src/backend/access/heap/vacuumlazy.c), которая обновляет карту видимости и карту свободного пространства для пустых/новых страниц, а именно рассмотрим фрагмент, в котором устанавливаются биты полной видимости и заморозки для пустых страниц. Причем данные биты необходимо установить только в том случае, когда PD_ALL_VISIBLE равен нулю. Это опять же сделано для того, чтобы лишний раз не читать страницы карты видимости.
if (PageIsEmpty(page))
{
...
if (!PageIsAllVisible(page))
{
START_CRIT_SECTION();
MarkBufferDirty(buf);
...
PageSetAllVisible(page);
visibilitymap_set(vacrel->rel, blkno, buf, InvalidXLogRecPtr,
vmbuffer, InvalidTransactionId,
VISIBILITYMAP_ALL_VISIBLE | VISIBILITYMAP_ALL_FROZEN);
END_CRIT_SECTION();
}
...
}
Алгоритм:
Весь рассматриваемый блок кода выполнится для какой-либо страницы только в том случае, если данная страница пустая, а ее бит видимости PD_ALL_VISIBLE равен нулю.
Начинается критическая секция.
MarkBufferDirty() делает буфер со страницей грязным.
PageSetAllVisible() устанавливает бит PD_ALL_VISIBLE.
visibilitymap_set() устанавливает биты полной видимости и заморозки в карте видимости.
Критическая секция заканчивается.
Как Index only scan использует бит полной видимости
Карта видимости зачастую используется индексным сканированием Index only scan. Суть данного индексного сканирования заключается в том, что нам не нужно обращаться непосредственно к странице основного слоя для получения какой-то выборки, когда всю необходимую информацию можно получить непосредственно из индекса. В индексах не хранится информация о видимости версий строк страниц основного слоя, а если мы будем специально считывать страницу из основного слоя для определения видимости при использовании Index only scan, вся эффективность данного сканирования сойдет на нет. Поэтому Index only scan использует бит полной видимости из карты видимости. Если бит установлен, то можно использовать информацию о версии строки прямиком из индекса, а если бит очищен - придется прочитать страницу основного слоя для определения видимости. Данный механизм реализован в функции IndexOnlyNext() (src/backend/executor/nodeIndexonlyscan.c).
while ((tid = index_getnext_tid(scandesc, direction)) != NULL)
{
bool tuple_from_heap = false;
...
if (!VM_ALL_VISIBLE(scandesc->heapRelation,
ItemPointerGetBlockNumber(tid),
&node->ioss_VMBuffer))
{
...
if (!index_fetch_heap(scandesc, node->ioss_TableSlot))
continue; /* no visible tuple, try next index entry */
...
tuple_from_heap = true;
}
...
}
Алгоритм:
Начинается цикл, работающий до тех пор, пока не закончатся версии строк либо пока не найдется видимая для текущей транзакции версия строки.
С помощью макроса VM_ALL_VISIBLE определяется значение бита полной видимости. Если бит установлен, то необходимая версия строки может быть взята прямиком из индекса. Иначе, если бит очищен, выполнится следующий пункт:
Необходимо прочитать страницу, чтобы определить видимость версии строки. Если строка не видна текущей транзакцией - начнется новая итерация цикла.
Зачем записывать информацию об изменении карты видимости в WAL
Казалось бы, карта видимости является чисто подсказкой для оптимизации, поэтому изменение карты видимости не нужно фиксировать в журнале предзаписи, но это не совсем так.
Рассмотрим ситуацию, представленную на рисунке 1.
У нас есть две страницы:
Страница основного слоя (m).
Страница карты видимости (vm), содержащая информационные биты нашей страницы основного слоя.
Бит полной видимости равен нулю, PD_ALL_VISIBLE тоже равен нулю. Бит полной заморозки для дальнейших рассуждений нам не нужен. На текущий момент данные страницы имеют одинаковые биты видимости что на диске, что в буферном кэше (1). Далее, выполняем VACUUM – бит полной видимости и PD_ALL_VISIBLE теперь равны единице, но только в буферном кэше (2). Теперь вытесним наши страницы на диск, причем страница карты видимости успеет записаться на диск, а страница основного слоя – нет, из-за сбоя в системе (3-5). После восстановления сервера бит полной видимости в карте видимости и бит PD_ALL_VISIBLE уже не согласованы (6).
Мы уже разобрали функцию heap_insert(), в которой бит полной видимости может очиститься только в том случае, если PD_ALL_VISIBLE = 1, но в нашем случае PD_ALL_VISIBLE = 0, поэтому никакая вставка строки на страницу не сможет очистить соответствующий бит полной видимости в карте видимости. Вследствие этого все вставленные на эту страницу строки будут считаться видимыми для всех активных транзакций из-за испорченных данных в карте видимости. Это может привести как минимум к двум последствиям:
Первый вакуум после сбоя не очистит страницы с испорченными битами видимости, даже если они будут содержать мертвые – ненужные ни одному снимку данных – версии строк.
Index only scan может увидеть версии строк, которые не должен видеть, вследствие чего выборка строк может привести к неверным результатам.
Таким образом, если на карте видимости установлен ложный бит полной видимости, это может привести к фантомному чтению.
Во избежание данных последствий postgres фиксирует значение битов видимости в журнале предзаписи (Рисунок 2), поэтому данную информацию при восстановлении после сбоя можно будет выудить из журнала предзаписи, если страница основного слоя с измененным PD_ALL_VISIBLE не успела попасть на диск.
Теперь проверим на практике, что произойдет, если в WAL не будет записываться информация об очистке бита видимости.
Чтобы воссоздать данную ситуацию, закомментируем немного исходного кода и перекомпилируем postgres:
В функции heap_insert() (src/backend/access/heap/heapam.c) закомментируем:
if (all_visible_cleared)
xlrec.flags |= XLH_INSERT_ALL_VISIBLE_CLEARED;
В функции visibilitymap_set() (src/backend/access/heap/visibilitymap.c) закомментируем все, что связано с WAL:
if (RelationNeedsWAL(rel))
{
if (XLogRecPtrIsInvalid(recptr))
{
Assert(!InRecovery);
recptr = log_heap_visible(rel, heapBuf, vmBuf, cutoff_xid, flags);
/*
* If data checksums are enabled (or wal_log_hints=on), we
* need to protect the heap page from being torn.
*
* If not, then we must *not* update the heap page's LSN. In
* this case, the FPI for the heap page was omitted from the
* WAL record inserted above, so it would be incorrect to
* update the heap page's LSN.
*/
if (XLogHintBitIsNeeded())
{
Page heapPage = BufferGetPage(heapBuf);
PageSetLSN(heapPage, recptr);
}
}
PageSetLSN(page, recptr);
}
Теперь подключим все необходимые расширения:
pg_visibility - Стандартное расширение, позволяющее заглянуть внутрь карты видимости.
buffercache_tools - мое расширение, которое позволит нам манипулировать буферным кэшем. https://github.com/04ina/buffercache_tools
Затем выключим автоматическое выполнение контрольной точки. Для этого можно в postgresql.conf поставить задержку между автоматическими контрольными точками в один день.
checkpoint_timeout = 1d
Теперь создадим новую тестовую таблицу и индекс для первого столбца.
DROP TABLE IF EXISTS test_table;
CREATE TABLE test_table(a integer, b integer, c integer);
CREATE INDEX on test_table(a);
Вставим в таблицу миллион строк и сделаем VACUUM, чтобы все вставленные строки были помечены как полностью видимые для всех транзакций.
INSERT INTO test_table VALUES (generate_series(1,1000000), 1,1);
VACUUM ANALYZE test_table;
Далее, узнаем количество страниц в отношении, чтобы выяснить номер последней страницы.
SELECT relpages FROM pg_class WHERE relname = 'test_table';
relpages
----------
5406
(1 row)
Работать мы будем именно с последней страницей, имеющей номер 5405 (нумерация страниц начинается с нуля), так как именно в нее будут вставляться все новые версии строк.
SELECT all_visible, pd_all_visible FROM pg_visibility('test_table', 5405);
all_visible | pd_all_visible
-------------+----------------
t | t
(1 row)
На данный момент все буферы, в которых находятся страницы отношения, грязные, а информация на них видима для всех версий строк.
Далее, вставим еще одну строку в последнюю страницу.
INSERT INTO test_table values (1, 1, 1);
Таким образом, только последняя страница будет не полностью видимой.
SELECT all_visible, pd_all_visible FROM pg_visibility('test_table', 5405);
all_visible | pd_all_visible
-------------+----------------
f | f
(1 row)
Теперь запишем все страницы из буферного кэша на диск. Следовательно, на диске и в буферном кэше будут лежать одинаковые страницы.
CHECKPOINT;
Таким образом, на диск запишется и последняя – единственная видимая не для всех транзакций – страница.
Далее, после CHECKPOINT, опять сделаем VACUUM, чтобы обновить информацию о видимости последней страницы.
VACUUM test_table;
SELECT all_visible, pd_all_visible FROM pg_visibility('test_table', 5405);
all_visible | pd_all_visible
-------------+----------------
t | t
(1 row)
На данный момент обновленная информация о видимости данной страницы, находящаяся на себе же (PD_ALL_VISIBLE) и на странице карты видимости, находится в буферном кэше.
Сейчас нам необходимо сымитировать ситуацию, когда страница карты видимости записалась на диск, а страница основного слоя не успела записаться из-за сбоя в системе. Для этого воспользуемся функцией pg_flush_relation_fork_buffers() из расширения buffercache_tools, чтобы записать на диск только страницы карты видимости.
SELECT pg_change_relation_fork_buffers('flush', 'test_table', 'vm');
Затем, чтобы быть на сто процентов уверенными, что наша страница из карты видимости не затерялась в кэше операционной системы, очистим этот кэш. Для этого воспользуемся drop_caches (https://www.kernel.org/doc/Documentation/sysctl/vm.txt) с аргументом “1”, так как нам нужно очистить лишь сами данные на страницах (pagecache), метаданные страниц (dentrie, inode) можно не трогать.
sudo sh -c "echo 1 > /proc/sys/vm/drop_caches"
Далее, сымитируем сбой в системе.
pg_ctl -m immediate stop
В конечном итоге бит полной видимости будет равен единице, а бит PD_ALL_VISIBLE будет равен нулю.
pg_ctl start
psql
SELECT all_visible, pd_all_visible FROM pg_visibility('test_table', 5405);
all_visible | pd_all_visible
-------------+----------------
t | f
(1 row)
Теперь, из-за специфики функции heap_insert(), никакая вставка версий строк в нашу последнюю страницу не изменит бит полной видимости, указанный в карте видимости, что может привести к чтению строк, которые должны быть невидимы для некоторых транзакций. Рассмотрим пример с уже разобранной функцией IndexOnlyNext(), которая всецело доверяет установленному биту видимости из карты видимости. Откроем две сессии в разных терминалах.
В первом терминале начнем транзакцию с уровнем изоляции repeatable read.
-- терминал 1
BEGIN ISOLATION LEVEL REPEATABLE READ;
Как мы помним, снимок данных для данного уровня изоляции делается всего один раз, поэтому эта транзакция не должна видеть новые версии строк, которые были вставлены после начала repeatable read транзакции другими транзакциями, у которых снимок данных будет сделан позже.
На данный момент в таблице test_table есть всего одна строка со значением 500 в первом столбце. Убедимся, что для выборки используется именно Index only scan.
-- терминал 1
EXPLAIN SELECT count(*) FROM test_table WHERE a = 500;
QUERY PLAN
------------------------------------------------------------
Aggregate
-> Index Only Scan using test_table_a_idx on test_table
Index Cond: (a = 500)
(3 rows)
SELECT count(*) FROM test_table WHERE a = 500;
count
-------
1
(1 row)
Теперь попробуем вставить еще одну строку, удовлетворяющую вышестоящему запросу, но в другой сессии.
-- терминал 2
INSERT INTO test_table values (500, 1, 1);
INSERT 0 1
По идее, наша транзакция не должна видеть только что вставленную строку, но у карты видимости и IndexOnlyNext() другие планы.
-- терминал 1
SELECT count(*) FROM test_table WHERE a = 500;
count
-------
2
(1 row)
Таким образом, наша транзакция увидела строку, которую не должна видеть, и основной причиной данной проблемы является отсутствие информации о битах видимости в WAL.
Каким образом карта видимости участвует в оптимизации предвыборки Bitmap scan
У индексного сканирования Bitmap scan имеется собственная предвыборка, учитывающая специфику bitmap scan, что способствует уменьшению временных затрат на операции I/O. Карта видимости участвует в механизме оптимизации, позволяющим пропускать предвыборку для некоторых страниц, когда вышестоящим узлам не нужны непосредственно данные на ней. (src/backend/executor/nodeBitmapHeapscan.c BitmapHeapNext())
Перед тем, как рассмотреть данную оптимизацию на примере, вспомним узлы, из которых состоит bitmap scan. Если вы с ними вообще не знакомы, советую обратиться к статье Егора Рогова “Запросы в PostgreSQL: 4. Индексное сканирование” подтема “Сканирование по битовой карте” https://habr.com/ru/companies/postgrespro/articles/578196/
Для тестов создадим таблицу с тремя столбцами, на два столбца повесим индексы. Далее, заполним таблицу и сделаем вакуум.
CREATE TABLE test_table(a integer, b integer, c integer);
CREATE INDEX on test_table(a);
CREATE INDEX on test_table(b);
INSERT INTO test_table VALUES (generate_series(1,100), generate_series(101,200), generate_series(201,300));
VACUUM test_table;
Упраздним основные виды сканирования, кроме bitmap scan.
SET enable_seqscan = false;
SET enable_indexscan = false;
SET enable_indexonlyscan = false;
Рассмотрим план данного запроса
EXPLAIN (costs off) SELECT 1 FROM (VALUES (1))
WHERE EXISTS (SELECT * FROM test_table WHERE a = 4 OR b = 104);
QUERY PLAN
-----------------------------------------------------------
Result
One-Time Filter: (InitPlan 1).col1
InitPlan 1
-> Bitmap Heap Scan on test_table
Recheck Cond: ((a = 4) OR (b = 104))
-> BitmapOr
-> Bitmap Index Scan on test_table_a_idx
Index Cond: (a = 4)
-> Bitmap Index Scan on test_table_b_idx
Index Cond: (b = 104)
Два узла Bitmap Index Scan, используя индексы столбцов “a” и “b”, создают битовые карты страниц отношения по условиям Index Cond: (a = 4) и Index Cond: (b = 104) соответственно.
Планировщик может решить использовать узлы BitmapAnd или BitmapOr между узлом Bitmap Heap Scan и узлами Bitmap Index Scan в контексте структуры дерева плана, если условие наложено на несколько индексируемых столбцов и выгоднее будет соединить несколько битовых карт (которые являются результатами работы узлов Bitmap Index Scan) с помощью логического и/или, нежели для одного условия сделать битовую карту, а другое обработать с помощью Filter на уровне узла Bitmap Heap Scan.
Узел Bitmap Heap Scan, используя сгенерированную битовую карту, читает необходимые страницы, выуживает нужные версии строк и, если битовая карта неточная, перепроверяет условие выборки – Recheck Cond: ((a = 4) OR (b = 104)).
Перепроверка условия Recheck Cond: ((a = 4) OR (b = 104)) на уровне Bitmap Heap Scan нужна лишь в том случае, когда битовая карта была построена с грубыми фрагментами из-за нехватки work_mem для создания полной битовой карты. При чтении табличной страницы, соответствующей грубому фрагменту битовой карты, необходимо перепроверять условия выборки для каждой версии строки на странице. Подробнее про Recheck Cond можно почитать в упомянутой выше статье.
Теперь, освежив память, рассмотрим данную оптимизацию на примере. Для этого проанализируем запрос:
SELECT 1 FROM (VALUES (1))
WHERE EXISTS (SELECT * FROM test_table WHERE a = 4);
По сути, внешнему запросу безразлично, какие именно версии строк вернет внутренний запрос. Все, что важно внешнему запросу, это наличие хотя бы одной версии строки в конечной выборке внутреннего запроса. Благодаря планировщику внутренний запрос закончит обрабатываться, как только обнаружит хотя бы одну версию строки, но на этом оптимизация не заканчивается. Так как внешний запрос требует сам факт наличия строки, мы можем и не читать никакие страницы и версии строк на них, чтобы лишний раз не нагружать систему I/O. Данное свойство сканирования bitmap scan напоминает принцип работы index only scan, когда мы делаем выборку, опираясь на индексы, но в случае bitmap scan мы опираемся на битовую карту, сгенерированную с помощью индекса. Данная оптимизация вступит в силу, если сойдутся все четыре звезды:
-
Условий выборки на уровне узла Bitmap Heap Scan нет.
Для оптимизации необходимо, чтобы условия выборки Filter на уровне узла Bitmap Heap Scan отсутствовали. Если мы добавим дополнительное условие выборки во внутренний запрос по неиндексируемому столбцу, на уровне узла Bitmap Heap Scan добавится Filter, который будет требовать информацию непосредственно из страниц, вследствие чего рассматриваемая оптимизация не будет применена.
EXPLAIN (costs off) SELECT 1 FROM (VALUES (1))
WHERE EXISTS (SELECT * FROM test_table WHERE a = 4 AND c = 3);
QUERY PLAN
-----------------------------------------------------
Result
One-Time Filter: $0
InitPlan 1 (returns $0)
-> Bitmap Heap Scan on test_table
Recheck Cond: (a = 4)
Filter: (c = 3)
-> Bitmap Index Scan on test_table_a_idx
Index Cond: (a = 4)
Также стоит отметить, что планировщик может решить не использовать узлы BitmapAnd или BitmapOr при условии выборки, наложенном на индексируемые столбцы, что приведет к появлению Filter на уровне узла Bitmap Heap Scan.
EXPLAIN (costs off) SELECT 1 FROM (VALUES (1))
WHERE EXISTS (SELECT * FROM test_table WHERE a = 4 AND b > 3);
QUERY PLAN
-----------------------------------------------------
Result
One-Time Filter: $0
InitPlan 1 (returns $0)
-> Bitmap Heap Scan on test_table
Recheck Cond: (a = 4)
Filter: (b > 3)
-> Bitmap Index Scan on test_table_a_idx
Index Cond: (a = 4)
-
Список отображаемых столбцов на уровне узла Bitmap Heap Scan пуст.
Если родительскому узлу, стоящему выше узла Bitmap Heap Scan, не нужна непосредственно информация из версий строк, значит список результирующих столбцов (targetlist) на уровне узла Bitmap Heap Scan должен быть пуст. Это как раз наш случай, ибо внутренний запрос связан с внешним запросом предложением EXISTS.
-
Перепроверка условий выборки не нужна.
Хоть в плане на уровне узла Bitmap Heap Scan всегда находится перепроверка условия, на деле она нужна для работы с битовыми картами, имеющими грубые фрагменты, которые генерируются из-за нехватки work_mem. Таким образом, рассматриваемая оптимизация применима только к страницам, точная информация о которых есть в битовой карте.
-
Страница со строкой, которую можно не читать, должна иметь бит полной видимости.
Чтобы не проводить предвыборку, нам необходимо убедиться, что все версии строк конкретной страницы актуальны для данной транзакции, причем даже не читая саму страницу, иначе в данной оптимизации не будет смысла. Для этого необходимо лишь проверить бит полной видимости в карте видимости.
Таким образом, карта видимости участвует в оптимизации, позволяющей не читать ненужные страницы во время предвыборки bitmap сканирования.
Зачем механизму оценки селективности нужна карта видимости
Карта видимости также используется для повышения точности при подсчете селективности выборки запроса, имеющего условие вида: VAR < (или <=, или >, или >=) CONST, где у переменной (столбца) VAR имеется btree индекс.
Для подсчета селективности данной выборки используется статистика из столбца stavaluesN таблицы системного каталога pg_statistic. В stavaluesN хранится 101 значение из индексированного столбца, являющихся границами сегментов, образованных при делении значений столбца определенной таблицы на 100 равных – насколько это возможно – сегментов. Например, в столбце с 404 значениями в stavaluesN попадет каждое четвертое значение. Таким образом, имея значение CONST, один оператор сравнения из четырех представленных выше и статистику stavaluesN, можно без проблем выяснить, сколько именно сегментов из ста нужно будет прочитать при выполнении данной выборки. Например, у нас имеется оператор сравнения “>”, CONST = 500, 41-ое значение stavaluesN, равное 495, и 42-ое значение stavaluesN, равное 505. Таким образом, CONST принадлежит 41-ому сегменту, а выше этого сегмента находится еще 59 сегментов. Следовательно, селективность данной выборки будет равна примерно 0.59.
На деле данный механизм сложнее, но в данной статье подробности мы затрагивать не будем.
Однако данный алгоритм оценки селективности при некоторых обстоятельствах может сильно ошибиться, например, если после обновления статистики в таблицу было вставлено приличное количество записей, у которых значение рассматриваемого столбца меньше или больше минимального или максимального значения в stavaluesN соответственно. Чтобы исправить данную ситуацию, можно прочитать крайнее – соответственно наименьшее или наибольшее на момент подсчета селективности – значение из столбца рассматриваемой таблицы, а затем, имея крайнее значение из stavaluesN и актуальное крайнее значение, с помощью линейной интерполяции попытаться предсказать селективность.
Чтобы найти точное крайнее значение на момент подсчета селективности, можно воспользоваться индексом, ибо рассматриваемый столбец имеет btree индекс. По сути, для получения крайнего значения используется практически такой же механизм чтения, как и у index only scan, но происходит это вне данного механизма сканирования в отдельной функции get_actual_variable_endpoint() (src/backend/utils/adt/selfuncs.c). В ней так же, как и в уже рассмотренной нами функции IndexOnlyNext(), проверяется, полностью ли видима страница с необходимой версией строки. Если страница имеет бит полной видимости, можно получить нужное значение прямиком из индекса, в противном случае придется читать страницу из основного слоя и проверять видимость.
Таким образом, модуль подсчета селективности selfuncs.c может использовать карту видимости для своих целей.
Я планирую написать статью про данный механизм оценки селективности. В ней мы рассмотрим внутреннее устройство и узнаем, при каких именно обстоятельствах используется функция get_actual_variable_endpoint(). Подпишитесь, чтобы не пропустить!
Вывод
В данной статье мы познакомились с достаточно необычными случаями использования карты видимости, а также с особенностями ее устройства. Еще мы в очередной раз убедились, что карта видимости играет важную роль в оптимизации postgres-а.