awesome-everything EN
↑ Обратно к восхождению

Базы данных

Index-only scan, Visibility Map и INCLUDE

Суть Index-only scan возвращает данные прямо из индекса без обращения к heap — но только когда Visibility Map подтверждает видимость строки. Урок охватывает VM, VACUUM, настройку autovacuum и INCLUDE-clause для covering-индексов.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 16 min

EXPLAIN ANALYZE показывает “Index Only Scan” на одном запросе и “Index Scan” на другом — одинаковый индекс, разные таблицы. Разница не в индексе. Она в Visibility Map. Одна таблица была VACUUMed вчера; другая имеет 12% мёртвых tuple. Понимание причины — это разница между 1 мс и 10 мс запросом на тех же данных.

Что стоит index scan vs index-only scan

Обычный index scan для запроса SELECT id, status FROM orders WHERE user_id = 42:

  1. Проход B-tree до matching leaf-записей — O(log n), несколько page reads.
  2. Для каждого matching TID — fetch heap-строки для получения id и status — один heap page read на строку, если строки разбросаны.

Шаг 2 — узкое место при масштабе. Для 10 000 matching строк на 10 000 разбросанных heap-страницах это 10 000 случайных I/O операций.

Index-only scan полностью устраняет шаг 2. Если индекс содержит все нужные запросу столбцы, Postgres читает только leaf-страницы индекса. Для плотных диапазонов (1000 строк на 10 leaf-страницах) I/O падает с тысяч до десятков.

Тип scanI/O паттернКогда используется
Index scanIndex walk + heap fetch на строкуНе все нужные столбцы в индексе
Bitmap heap scanIndex walk + sorted heap read (batched)Много строк; снижает random I/O
Index only scanТолько index leaves — нет heapВсе нужные столбцы в индексе И VM-биты установлены
Sequential scanПолное чтение heapНет подходящего индекса или низкая селективность

Visibility Map: почему index-only scan требует её

Postgres использует MVCC — каждая строка может иметь несколько версий (старые и новые tuple от параллельных обновлений). Чтобы определить видимость строки текущей транзакции, Postgres обычно читает системные столбцы строки (xmin, xmax, биты статуса транзакции). Эти столбцы находятся в heap, не в индексе.

Index-only scan не читает heap. Как тогда определить видимость?

Visibility Map (VM) — per-table bitmap с одним битом на heap-страницу. Бит устанавливается в 1 («all visible»), когда каждый tuple на странице виден каждой текущей и будущей транзакции — то есть нет pending параллельных обновлений или удалений. VACUUM устанавливает эти биты после очистки мёртвых tuple. Любая запись, затрагивающая страницу, сбрасывает бит.

При index-only scan для matching TID:

  • Если VM-бит для этой страницы установлен: строка гарантированно видима без чтения heap. Нет heap fetch.
  • Если VM-бит сброшен: Postgres должен получить heap-страницу для проверки видимости. Это heap fetch, считаемый в EXPLAIN ANALYZE как «Heap Fetches: N».

Высокий счётчик «Heap Fetches» в index-only scan означает, что VM не актуальна — «index-only» scan фактически делает heap fetch для большинства строк.

VACUUM поддерживает VM актуальной

VACUUM освобождает мёртвые tuple и устанавливает VM-биты. Autovacuum запускает его автоматически (когда мёртвые tuple превышают autovacuum_vacuum_scale_factor × размер таблицы, дефолт 20%). Для таблиц, зависящих от index-only scan:

  • 20% мёртвых tuple — слишком мягкий порог. Установи autovacuum_vacuum_scale_factor = 0.05 и autovacuum_analyze_scale_factor = 0.02.
  • Long-running транзакции блокируют VACUUM от освобождения tuple — одна транзакция, открытая часами, может блокировать VM-обновления на это время.
  • Мониторь pg_stat_user_tables.n_dead_tup как leading indicator свежести VM.
-- Настройка autovacuum per-table на горячих OLTP-таблицах
ALTER TABLE orders SET (
  autovacuum_vacuum_scale_factor = 0.05,
  autovacuum_analyze_scale_factor = 0.02
);

INCLUDE-clause: covering-индексы

Postgres 11+ поддерживает CREATE INDEX ... INCLUDE (cols) — INCLUDE-столбцы хранятся в leaf-страницах индекса, но не являются частью sort key. Результат: индекс становится «covering» для запросов, которым нужны эти дополнительные столбцы в SELECT-projection.

-- Ключевые столбцы: workspace_id, created_at DESC (sort key)
-- INCLUDE: id, total_cents (payload projection — не в sort key)
CREATE INDEX CONCURRENTLY idx_orders_ws_recent_covering
  ON orders (workspace_id, created_at DESC)
  INCLUDE (id, total_cents);

Запрос SELECT id, total_cents FROM orders WHERE workspace_id = $1 ORDER BY created_at DESC LIMIT 50 теперь обслуживается только из индекса. Нет heap fetch. Читается 50 leaf-записей в порядке.

Цена: INCLUDE-столбцы увеличивают размер индекса (хранятся в каждой leaf-записи). Большие INCLUDE payload могут приближать размер индекса к размеру таблицы. Включай только столбцы projection реально нужные горячему запросу.

Concurrency и timing построения индекса

CREATE INDEX CONCURRENTLY на 100M-строк таблице занимает 10-30 минут. Во время построения:

  1. Postgres сканирует таблицу дважды.
  2. Между фазами ждёт завершения всех текущих транзакций.
  3. Индекс реплицируется на standby через WAL.

Последствия:

  • Long-running аналитические запросы на той же таблице могут застопорить build часами.
  • Несколько concurrent builds на одной БД конкурируют за I/O — планируй большие builds в low-traffic окна.
  • После build проверяй pg_indexes.indisvalid — упавший build оставляет INVALID-индекс.
-- Проверка INVALID-индексов после CONCURRENTLY-build
SELECT indexrelid::regclass, indisvalid
FROM pg_index
WHERE NOT indisvalid;

Recovery из INVALID: DROP INDEX CONCURRENTLY idx_name; затем повторить.

Index-only scan и VACUUM: числа
Выигрыш index-only scan vs index scan
10-100x на hot reads
Heap Fetches: 0 в EXPLAIN ANALYZE
идеально — VM актуальна
autovacuum_vacuum_scale_factor default
20% мёртвых tuple
Рекомендуемый scale_factor для IOS-зависимых таблиц
5%
INCLUDE поддерживается с
Postgres 11 (2018)
Long-running транзакция и VM
блокирует установку VM-бит для затронутых страниц
CREATE INDEX CONCURRENTLY на 100M строк
~10-30 минут
INVALID-индекс после упавшего CONCURRENTLY
невидим планировщику; занимает место
Мониторинг: n_dead_tup в pg_stat_user_tables
leading indicator свежести VM
Проследи
1/4

Аналитический запрос на таблице orders показывает Index Only Scan с Heap Fetches: 12,000. Диагностируй и исправь.

1
Step 1 of 4
Шаг 1: что означает Heap Fetches: 12,000?
2
Locked
Шаг 2: как проверить здоровье VM?
3
Locked
Шаг 3: что исправить?
4
Locked
Шаг 4: как убедиться в долгосрочном исправлении?
Викторина

Запрос показывает 'Index Only Scan' с Heap Fetches: 8,000 в EXPLAIN ANALYZE. Вероятная причина?

Викторина

Зачем нужен INCLUDE в CREATE INDEX?

lesson.inset.warning

Метка плана «Index Only Scan» вводит в заблуждение, когда Heap Fetches высок. Название node плана отражает задуманный тип scan — реальное поведение зависит от состояния VM во время выполнения. Всегда проверяй Heap Fetches в выводе EXPLAIN ANALYZE, не только тип scan. Запрос, «отлично работающий в staging», может деградировать в продакшне при худшем расписании autovacuum.

Вспомните перед уходом
  1. 01
    Объясни, почему index-only scan требует Visibility Map и какая операционная дисциплина обеспечивает его правильную работу в продакшне.
  2. 02
    Когда использовать INCLUDE vs добавление столбцов в sort key composite-индекса?
  3. 03
    Что происходит при падении CREATE INDEX CONCURRENTLY и как восстановиться?
Итог

Index-only scan читает значения столбцов прямо из leaf-страниц индекса, полностью минуя heap — сокращая I/O в 10-100x для запросов, обращающихся только к индексированным столбцам. Visibility Map делает это возможным: per-table bitmap, где установленный бит означает, что все tuple на странице видны без heap-проверки. VACUUM устанавливает VM-биты; записи сбрасывают их. Устаревшая VM вызывает деградацию: «Index Only Scan» с высоким Heap Fetches — симптом недостаточного vacuuming.

INCLUDE-clause добавляет projection-столбцы в leaf-страницы без включения в sort key. Это создаёт «covering-индекс», отвечающий на запросы целиком из индекса в сочетании с актуальными VM-битами.

Операционные правила: autovacuum_vacuum_scale_factor = 0.05 на таблицах, зависящих от index-only scan; мониторинг n_dead_tup в pg_stat_user_tables; kill long-running транзакций, блокирующих VACUUM. После CREATE INDEX CONCURRENTLY проверяй indisvalid = true в pg_index перед завершением миграции.

Связанные уроки
встречается в258
Продолжить восхождение ↑Типичные сбои в продакшне и аудит индексов
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.