linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Главы
  • How it worksскоро
  • Уроки
  • База знаний
  • Собеседование
Часть VI — Планировщик и выполнение

$ глава 27 · 55 минут

Табличные и индексные методы доступа

Получить строки по условию можно несколькими способами, и планировщик выбирает между ними по селективности. Прочитать всю таблицу? Пройти по индексу и сходить за каждой строкой? Собрать битовую карту и прочитать страницы пачкой? Или вообще не трогать таблицу, если всё нужное есть в индексе? Это четыре метода доступа, и понимать их переключение - половина искусства чтения планов.

В этой главе мы заставим один и тот же запрос, меняя только границу в WHERE, пройти весь путь: Seq Scan на широком условии, Bitmap на среднем, Index Scan на узком, и Index-Only Scan, когда таблицу можно не трогать совсем. И поймём, откуда берётся «точка перелома».

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). Если строк мало - выгодно: прочитали пару записей индекса, сходили за двумя строками. Если много - разорительно: тысячи случайных обращений к таблице обойдутся дороже, чем прочитать её целиком последовательно.

sql
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, самый дешёвый доступ.

sql
-- индекс по (flight_id), запрос просит только flight_id
EXPLAIN SELECT flight_id FROM tickets WHERE flight_id = 7;
-- Index Only Scan using tickets_flight_idx

Чтобы покрыть запрос, которому нужны и другие колонки, их добавляют в индекс через INCLUDE - такой индекс называют покрывающим:

sql
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 предскажи тип узла.

  1. Создай таблицу: 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;.

  2. Узкое условие: EXPLAIN SELECT * FROM big WHERE k = 7; - предскажи и проверь (ожидается Index/Bitmap Scan, мало строк).

  3. Широкое условие: EXPLAIN SELECT * FROM big WHERE k < 900; - предскажи (ожидается Seq Scan, почти вся таблица).

  4. Среднее условие подбери так, чтобы получить Bitmap Heap Scan (например k < 50). Найди узлы Bitmap Index Scan + Bitmap Heap Scan.

  5. Index-Only: EXPLAIN SELECT k FROM big WHERE k = 7; - ожидается Index Only Scan. Сделай EXPLAIN ANALYZE и посмотри Heap Fetches.

  6. Выполни 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.

Контрольные вопросы

  1. Почему на условии `id > 0` (почти вся таблица) выбирается Seq Scan, а не индекс, даже если индекс есть?

    Показать ответ

    Потому что Index Scan ходит за каждой подходящей строкой в таблицу отдельным случайным чтением (random_page_cost = 4 за страницу). Когда подходит почти вся таблица, это тысячи случайных обращений, что дороже, чем прочитать все страницы один раз последовательно. Seq Scan читает всё подряд по seq_page_cost = 1 и не зависит от доли отобранных строк. Индекс выгоден только при малой селективности; на широком условии Seq Scan дешевле.

  2. Чем Bitmap Heap Scan лучше обычного Index Scan на средней селективности?

    Показать ответ

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

  3. Что нужно, чтобы запрос пошёл по Index-Only Scan?

    Показать ответ

    Два условия. Первое: все колонки, которые запрос читает и проверяет, должны быть в индексе (либо в ключе, либо добавлены через INCLUDE) - иначе придётся идти в таблицу за недостающими. Второе: страница должна быть помечена в visibility map как видимая всем, потому что индекс не хранит информацию о видимости версий. Если оба выполнены, строка берётся из индекса без обращения к таблице. Иначе случается heap fetch.

  4. В `EXPLAIN ANALYZE` у Index Only Scan большое значение Heap Fetches. Что это и как убрать?

    Показать ответ

    Heap Fetches - сколько раз пришлось всё-таки сходить в таблицу, потому что visibility map не подтвердила видимость страницы. Так бывает после массовых вставок/обновлений, когда VACUUM ещё не выставил биты all-visible. Index-Only Scan при этом работает не в полную силу. Лекарство - VACUUM таблицы: он выставляет visibility map, и при следующем запуске Heap Fetches падает к нулю, а скан перестаёт трогать таблицу.

  5. От чего зависит, на какой доле строк происходит переключение seq↔index?

    Показать ответ

    В первую очередь от random_page_cost. Index Scan дорожает с долей строк, потому что каждая строка - случайное обращение к таблице, оценённое в random_page_cost. Seq Scan стоит одинаково при любой доле. Точка пересечения их стоимостей и есть перелом, обычно на единицах процентов. Снизишь random_page_cost (например, на SSD) - индексный доступ станет дешевле, и перелом сдвинется вправо: индекс будет выбираться на большей доле строк.

← Предыдущая26-planner-statisticsСледующая →28-joins
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки