27.1 Четыре способа добраться до строк
| Метод | Идея | Когда выгоден |
|---|---|---|
| Seq Scan | прочитать все страницы подряд | нужна большая доля строк |
| Index Scan | спуститься по индексу, сходить за каждой строкой | нужно мало строк |
| Bitmap Heap Scan | собрать карту совпадений, прочитать страницы пачкой | средняя доля строк |
| Index-Only Scan | взять данные прямо из индекса, не трогая таблицу | все нужные колонки в индексе |
Все четыре дают один результат - разница только в стоимости при разной селективности. Seq Scan платит за чтение всех страниц, но читает их последовательно (дёшево за страницу). Index Scan читает мало, но платит за случайные обращения к таблице. Между ними - Bitmap, а Index-Only обходит таблицу вовсе.
27.2 Seq Scan и Index Scan: два полюса
Seq Scan читает таблицу страница за страницей и применяет фильтр к каждой строке. Его стоимость не зависит от селективности условия - он всё равно прочитает всё. Поэтому он выгоден, когда нужна большая доля строк: незачем прыгать по индексу, чтобы в итоге прочитать почти всю таблицу.
Index Scan идёт по индексу к совпадающим записям и за каждой
строкой отдельно ходит в таблицу (heap). Каждое такое обращение -
случайное чтение страницы (random_page_cost = 4). Если строк
мало - выгодно: прочитали пару записей индекса, сходили за двумя
строками. Если много - разорительно: тысячи случайных обращений к
таблице обойдутся дороже, чем прочитать её целиком последовательно.
EXPLAIN SELECT * FROM big WHERE id = 42; -- Index Scan: одна строка
EXPLAIN SELECT * FROM big WHERE id > 0; -- Seq Scan: почти всё
27.3 Bitmap: компромисс через карту страниц
Когда строк не «мало» и не «почти всё», а посередине, оба полюса плохи: Index Scan утонет в случайных обращениях, Seq Scan прочитает лишнее. Решение - Bitmap Scan в два узла.
Сначала Bitmap Index Scan проходит индекс и строит битовую карту:
какие страницы таблицы содержат совпадения. Потом Bitmap Heap Scan читает эти страницы - но не в порядке индекса, а в физическом
порядке таблицы, по одной странице за раз. Так случайные обращения
превращаются в почти последовательные, и каждая нужная страница
читается ровно один раз, даже если на ней несколько совпадений.
Bitmap Heap Scan on tickets (cost=... rows=...)
Recheck Cond: (flight_id = ANY ('{1,2,3}'))-> Bitmap Index Scan on tickets_flight_idx
Index Cond: (flight_id = ANY ('{1,2,3}'))Сверх того: две битовые карты можно пересечь (BitmapAnd) или объединить
(BitmapOr) - так PostgreSQL использует сразу два индекса для
условия a = 1 AND b = 2.
27.4 Index-Only Scan: не трогать таблицу
Index Scan ходит в таблицу за строкой, потому что в индексе лежат только проиндексированные колонки плюс ссылка (ctid). Но если запросу нужны ровно те колонки, что есть в индексе, в таблицу можно не ходить - всё уже в индексе. Это Index-Only Scan, самый дешёвый доступ.
-- индекс по (flight_id), запрос просит только flight_id
EXPLAIN SELECT flight_id FROM tickets WHERE flight_id = 7;
-- Index Only Scan using tickets_flight_idx
Чтобы покрыть запрос, которому нужны и другие колонки, их добавляют
в индекс через INCLUDE - такой индекс называют покрывающим:
CREATE INDEX ON tickets (flight_id) INCLUDE (passenger);
Теперь SELECT flight_id, passenger WHERE flight_id = 7 берёт обе
колонки из индекса. Но есть условие, без которого Index-Only Scan
не сработает, - и это главный подводный камень.
27.4.1 Подводный камень: Index-Only Scan и Heap Fetches
Индекс не хранит информацию о видимости версий строк - её знает только сам кортеж в таблице. Поэтому, чтобы вернуть строку из индекса, не сходив в таблицу, PostgreSQL должен быть уверен, что она видна всем. Эту уверенность даёт visibility map: бит «вся страница видна всем» (см. visibility-map).
Если страница в visibility map помечена как all-visible, индекс
отдаёт строку сам. Если нет - приходится всё-таки сходить в таблицу
проверить видимость. В EXPLAIN ANALYZE это видно как Heap Fetches:
Index Only Scan using ... (actual ...)
Heap Fetches: 4310 ← пришлось сходить в таблицу 4310 раз
Большой Heap Fetches означает, что visibility map не выставлена -
обычно после массовых изменений без VACUUM. Лекарство: VACUUM
выставляет биты, и при следующем запуске Heap Fetches: 0 -
Index-Only Scan работает в полную силу.
27.5 Точка перелома
Почему планировщик переключается между методами? Из-за стоимости, которая по-разному растёт с селективностью.
стоимость
│ Seq Scan (горизонталь: не зависит от доли строк)
│ ───────────────────────────
│ ╱ Index Scan (круто растёт: random I/O на строку)
│ ╱
│ ╱
└──┴───────────────────── доля строк
^ точка перелома (~2-5%)
Index Scan дёшев при малой доле и дорожает быстро (каждая строка -
случайное обращение). Seq Scan стоит одинаково при любой доле.
Линии пересекаются где-то на единицах процентов: левее выгоден
индекс, правее - последовательное чтение, в зоне перехода - Bitmap.
Точное место перелома зависит от random_page_cost: снизишь его -
индекс станет выгоден на большей доле. Разбор - в selectivity-crossover.
27.6 Параллельные сканы
На большой таблице один процесс читает её долго. PostgreSQL умеет
распараллелить: ведущий процесс запускает рабочие (workers), и они
делят страницы между собой. В плане это видно как Parallel Seq Scan под узлом Gather.
Делёж устроен так, что воркеры не топчутся: каждый берёт следующий
блок страниц из общего «прилавка», обрабатывает и возвращается за
новым. Параллельный план включается, когда таблица крупная и есть
свободные слоты воркеров (max_parallel_workers_per_gather). Это не
новый метод доступа, а параллельная форма уже знакомых - Seq Scan и
Index Scan тоже бывают параллельными.
Уроки в sandbox
lab-27.1. Провести запрос от Seq Scan до Index-Only Scan
Создадим таблицу побольше, навесим индекс и, меняя только границу в WHERE, проведём план через все методы доступа. Перед каждым EXPLAIN предскажи тип узла.
Создай таблицу:
CREATE TABLE big AS SELECT g AS id, (g % 1000) AS k FROM generate_series(1, 200000) g;затемCREATE INDEX ON big(k); ANALYZE big;.Узкое условие:
EXPLAIN SELECT * FROM big WHERE k = 7;- предскажи и проверь (ожидается Index/Bitmap Scan, мало строк).Широкое условие:
EXPLAIN SELECT * FROM big WHERE k < 900;- предскажи (ожидается Seq Scan, почти вся таблица).Среднее условие подбери так, чтобы получить
Bitmap Heap Scan(напримерk < 50). Найди узлы Bitmap Index Scan + Bitmap Heap Scan.Index-Only:
EXPLAIN SELECT k FROM big WHERE k = 7;- ожидается Index Only Scan. СделайEXPLAIN ANALYZEи посмотри Heap Fetches.Выполни
VACUUM big;и повтори EXPLAIN ANALYZE того же Index-Only запроса - Heap Fetches должно упасть к нулю.
sandbox с автопроверкой - открыть в песочнице
Резюме
- Четыре метода доступа дают один результат, но разную стоимость: Seq Scan, Index Scan, Bitmap Heap Scan, Index-Only Scan. Выбор - по селективности.
- Seq Scan читает всё последовательно, его стоимость не зависит от условия; выгоден, когда нужна большая доля строк.
- Index Scan ходит за каждой строкой в таблицу случайным чтением - дёшев при малой доле, разорителен при большой.
- Bitmap Scan строит карту совпадений и читает страницы пачкой в физическом порядке - компромисс для средней доли; умеет пересекать индексы (BitmapAnd/Or).
- Index-Only Scan берёт данные из индекса, не трогая таблицу, если все нужные колонки в индексе (в том числе через INCLUDE) и visibility map подтверждает видимость.
- Большой Heap Fetches у Index-Only Scan значит, что visibility map не выставлена; VACUUM её выставляет, и Heap Fetches падает к нулю.
- Точка перелома seq↔index лежит на единицах процентов и зависит от random_page_cost; параллельные сканы - не новый метод, а параллельная форма Seq/Index Scan.
Контрольные вопросы
Почему на условии `id > 0` (почти вся таблица) выбирается Seq Scan, а не индекс, даже если индекс есть?
Показать ответ
Потому что Index Scan ходит за каждой подходящей строкой в таблицу отдельным случайным чтением (random_page_cost = 4 за страницу). Когда подходит почти вся таблица, это тысячи случайных обращений, что дороже, чем прочитать все страницы один раз последовательно. Seq Scan читает всё подряд по seq_page_cost = 1 и не зависит от доли отобранных строк. Индекс выгоден только при малой селективности; на широком условии Seq Scan дешевле.
Чем Bitmap Heap Scan лучше обычного Index Scan на средней селективности?
Показать ответ
Index Scan ходит в таблицу за строками в порядке индекса - получаются случайные обращения, и одну и ту же страницу могут прочитать несколько раз. Bitmap сначала собирает битовую карту всех совпадающих мест, а затем читает страницы таблицы в физическом порядке, по одной, ровно один раз каждую. Это превращает случайный доступ в почти последовательный и убирает повторные чтения страниц. На средней доле строк это заметно дешевле Index Scan.
Что нужно, чтобы запрос пошёл по Index-Only Scan?
Показать ответ
Два условия. Первое: все колонки, которые запрос читает и проверяет, должны быть в индексе (либо в ключе, либо добавлены через INCLUDE) - иначе придётся идти в таблицу за недостающими. Второе: страница должна быть помечена в visibility map как видимая всем, потому что индекс не хранит информацию о видимости версий. Если оба выполнены, строка берётся из индекса без обращения к таблице. Иначе случается heap fetch.
В `EXPLAIN ANALYZE` у Index Only Scan большое значение Heap Fetches. Что это и как убрать?
Показать ответ
Heap Fetches - сколько раз пришлось всё-таки сходить в таблицу, потому что visibility map не подтвердила видимость страницы. Так бывает после массовых вставок/обновлений, когда VACUUM ещё не выставил биты all-visible. Index-Only Scan при этом работает не в полную силу. Лекарство -
VACUUMтаблицы: он выставляет visibility map, и при следующем запуске Heap Fetches падает к нулю, а скан перестаёт трогать таблицу.От чего зависит, на какой доле строк происходит переключение seq↔index?
Показать ответ
В первую очередь от random_page_cost. Index Scan дорожает с долей строк, потому что каждая строка - случайное обращение к таблице, оценённое в random_page_cost. Seq Scan стоит одинаково при любой доле. Точка пересечения их стоимостей и есть перелом, обычно на единицах процентов. Снизишь random_page_cost (например, на SSD) - индексный доступ станет дешевле, и перелом сдвинется вправо: индекс будет выбираться на большей доле строк.