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скоро
  • Уроки
  • База знаний
  • Собеседование
Часть VII — Индексы

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

Проектирование индекса под нагрузку

Знать устройство каждого AM - половина дела. Вторая половина - спроектировать индекс под конкретные запросы и не навредить записи. Здесь сходится вся часть VII: какой тип взять, в каком порядке колонки, что положить в покрытие, когда сделать индекс частичным, и почему лишний индекс - это не «бесплатная страховка», а постоянный налог на каждый INSERT и UPDATE.

Главная ментальная модель этой главы простая: индекс - это отсортированный список. Из неё выводятся все правила: почему (x, y, z) работает для поиска по x, но не по y, почему ORDER BY иногда бесплатен, и почему частичный индекс бывает в разы меньше и быстрее.

37.1 Индекс - это отсортированный список

Держи в голове одну картинку: B-tree-индекс - это данные, отсортированные по ключу, плюс ссылки на строки. Из этой картинки следует всё остальное.

Отсортированный список даёт три вещи бесплатно: быстрый поиск по значению (бинарный), быстрый поиск по диапазону (соседние записи рядом) и готовый порядок для ORDER BY (данные уже упорядочены). Чего он не даёт: искать по тому, что не в начале ключа. Если список отсортирован по фамилии, найти человека по имени он не помогает - имена разбросаны по всему списку.

Эта модель объясняет и составные индексы, и ORDER BY-оптимизацию, и sargability. Дальше просто применяем её к случаям.

37.2 Составной индекс и правило префикса

Индекс (a, b, c) сортирует записи сначала по a, при равенстве - по b, при равенстве - по c. Как телефонный справочник: сначала фамилия, потом имя, потом отчество.

Отсюда правило префикса: индекс помогает запросам, которые фиксируют ключ слева направо без пропусков:

  • WHERE a = 1 - да (префикс длины 1);
  • WHERE a = 1 AND b = 2 - да (префикс длины 2);
  • WHERE a = 1 AND b = 2 AND c = 3 - да (полный ключ);
  • WHERE b = 2 - нет: b не в начале, записи с b = 2 разбросаны по всему индексу;
  • WHERE a = 1 AND c = 3 - частично: поможет по a, c отфильтрует уже прочитанное.

Поэтому порядок колонок - главное решение в составном индексе. Колонки с равенством ставят раньше, колонку с диапазоном или сортировкой - в конец. Подробнее - в index-design.

37.3 ORDER BY без сортировки

Раз индекс отсортирован, он может отдать строки сразу в нужном порядке - без отдельного шага Sort. Это особенно ценно с LIMIT:

sql
-- частый паттерн: последние записи в ветке
SELECT * FROM repos
WHERE forked_from_id = 42
ORDER BY updated_at DESC
LIMIT 10;

Индекс (forked_from_id, updated_at DESC) обслуживает это идеально: по forked_from_id = 42 он находит нужный участок, а внутри него записи уже отсортированы по updated_at DESC - бери первые десять и стоп. Без такого индекса план - Seq Scan плюс Sort всей выборки, и его стоимость на порядки выше. Это типичный кейс «было 2.8M cost, стало 80»: добавили правильный составной индекс - и сортировка исчезла.

37.4 Покрывающие и частичные индексы

Два приёма, сокращающих работу ещё сильнее.

Покрывающий индекс кладёт в лист недостающие колонки через INCLUDE, чтобы запрос обслуживался Index-Only Scan без обращения к таблице (см. главу 32):

sql
CREATE INDEX ON repos (forked_from_id, updated_at DESC) INCLUDE (name);

Частичный индекс индексирует не всю таблицу, а только строки, проходящие условие:

sql
CREATE INDEX ON tasks (created_at) WHERE status = 'active';

Если 99% задач завершены, а запросы всегда про активные, частичный индекс в разы меньше полного и быстрее - в нём только нужные строки. Он же снижает цену записи: строки вне условия индекс не трогают.

37.5 Индексы по выражению

Когда запрос фильтрует по результату функции, обычный индекс по колонке не подходит (условие не sargable). Решение - индекс по тому же выражению:

sql
-- запрос: WHERE lower(email) = 'a@b.c'
CREATE INDEX ON users (lower(email));

Теперь выражение lower(email) проиндексировано, и запрос его использует. Индекс по выражению вычисляет функцию при вставке и хранит результат - поэтому функция должна быть IMMUTABLE (давать один и тот же результат для одних входов). Тот же приём - для (created_at::date), json-извлечений, конкатенаций. Связь с sargability - в sargability.

37.6 Цена записи и неиспользуемые индексы

У индексов есть обратная сторона: каждый из них обновляется на каждый INSERT, и на UPDATE неиндексируемых колонок тоже (если это не HOT-update). Пять индексов на таблице - это пятикратная работа на запись. Индекс, который не используется ни одним запросом, - чистый убыток: занимает место, тормозит запись, ничего не ускоряет.

Найти такие легко:

sql
SELECT relname, indexrelname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;

idx_scan = 0 за разумный период работы значит «индексом ни разу не воспользовались». Такие индексы - кандидаты на удаление. То же с дублирующими: два индекса с одинаковым набором колонок в одном порядке - один лишний.

37.6.1 Подводный камень: индексов тоже бывает слишком много

Соблазн «навешать индексов на всякий случай» наказывается дважды. Первый раз - записью: больше индексов, медленнее INSERT/UPDATE. Второй раз - планированием. Планировщик рассматривает все применимые индексы, и на таблице с десятками индексов (особенно частичных) время планирования заметно растёт - он перебирает больше вариантов на каждый запрос.

Крайний случай - «тысяча частичных индексов» под каждый частный запрос: запись еле ползёт, а планирование само по себе становится узким местом. Правило простое: индекс заводят под доказанную потребность (запрос в плане берёт Seq Scan там, где должен брать индекс), а не впрок. Меньше индексов, но точных, почти всегда лучше, чем много на всякий случай.

37.7 Чеклист проектирования

Собираем часть VII в порядок решений при создании индекса.

  1. Какой оператор в условии? Это задаёт тип индекса через operator class: =/диапазон/ORDER BY - B-tree; @>/полнотекст - GIN; пересечение/гео - GiST; огромный append-only по диапазону - BRIN.
  2. Условие sargable? Колонка не должна быть под функцией; иначе индекс по выражению.
  3. Несколько колонок? Порядок: равенство раньше, диапазон/сортировка в конец; правило префикса.
  4. Можно покрыть запрос? INCLUDE под Index-Only Scan.
  5. Запрос всегда по подмножеству? Частичный индекс с WHERE.
  6. Не во вред ли записи? Проверь, что индекс реально используется (pg_stat_user_indexes), удали неиспользуемые и дубли.

Этот чеклист - прикладной итог всей части. В капстоуне он соберётся с диагностикой планов и статистики в единый алгоритм оптимизации.

Уроки в sandbox

lab-37.1. Составной индекс против Seq Scan плюс Sort

Возьмём частый паттерн «последние записи в ветке» и уберём сортировку правильным составным индексом. Перед каждым EXPLAIN предскажи, будет ли в плане узел Sort.

  1. Создай таблицу: CREATE TABLE repos AS SELECT g id, (g % 5000) AS forked_from_id, '2026-01-01'::timestamptz + (g||' min')::interval AS updated_at, 'repo'||g AS name FROM generate_series(1, 500000) g; ANALYZE repos;.

  2. Запрос без индекса: EXPLAIN SELECT * FROM repos WHERE forked_from_id = 42 ORDER BY updated_at DESC LIMIT 10; - предскажи (Seq Scan + Sort, большой cost).

  3. Создай составной индекс: CREATE INDEX ON repos (forked_from_id, updated_at DESC); ANALYZE repos;.

  4. Повтори EXPLAIN - предскажи и проверь: Index Scan, сортировка исчезла, cost упал на порядки.

  5. Проверь правило префикса: EXPLAIN SELECT * FROM repos WHERE updated_at > '2026-01-02'; - индекс по (forked_from_id, updated_at) не подходит для условия только по второй колонке (Seq Scan).

  6. Найди неиспользуемые индексы: SELECT indexrelname, idx_scan FROM pg_stat_user_indexes WHERE relname='repos'; - какие idx_scan ещё нулевые.

sandbox с автопроверкой - открыть в песочнице

Резюме

  • Ментальная модель: индекс - отсортированный список ключей со ссылками на строки; из неё следуют поиск, диапазоны, порядок и правило префикса.
  • Составной индекс (a,b,c) сортирует по a, затем b, затем c; помогает запросам по префиксу слева направо, но не по одной средней колонке.
  • Порядок колонок - главное решение: равенство ставят раньше, диапазон и сортировку - в конец; (forked_from_id, updated_at DESC) убирает Sort для ORDER BY+LIMIT.
  • Покрывающий индекс (INCLUDE) даёт Index-Only Scan; частичный (WHERE) индексирует только нужные строки - меньше размер, быстрее, дешевле запись.
  • Индекс по выражению нужен, когда условие по функции (lower(email)); функция должна быть IMMUTABLE.
  • Каждый индекс - налог на запись; неиспользуемые (idx_scan=0) и дублирующие индексы удаляют через pg_stat_user_indexes.
  • Слишком много индексов вредит и записи, и планированию (перебор вариантов); индекс заводят под доказанную потребность, а не впрок.

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

  1. Почему индекс `(a, b, c)` помогает запросу `WHERE a = 1`, но не `WHERE b = 2`?

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

    Потому что индекс - отсортированный список: записи упорядочены сначала по a, при равенстве по b, потом по c. Условие по a фиксирует начало ключа, и все подходящие записи лежат непрерывным участком - их легко найти. А записи с b = 2 разбросаны по всему индексу: внутри каждого значения a свой блок b, и нет одного непрерывного участка с b = 2. Найти их по индексу не проще, чем пройти всю таблицу. Это правило префикса: индекс работает для ключа слева направо без пропусков.

  2. Как составной индекс убирает шаг Sort в `ORDER BY ... LIMIT`?

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

    Индекс хранит записи уже отсортированными по ключу. Для запроса WHERE forked_from_id = 42 ORDER BY updated_at DESC LIMIT 10 индекс (forked_from_id, updated_at DESC) по равенству находит участок строк с forked_from_id = 42, а внутри него строки уже идут в порядке updated_at DESC. Планировщику достаточно взять первые десять и остановиться - отдельная сортировка не нужна. Без такого индекса пришлось бы прочитать все подходящие строки и отсортировать (Seq Scan + Sort), что на порядки дороже.

  3. Когда частичный индекс лучше полного?

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

    Когда запросы всегда обращаются к подмножеству строк, а это подмножество - малая доля таблицы. Например, при status = 'active', когда активных задач 1%, а завершённых 99%. Частичный индекс WHERE status = 'active' содержит только активные строки: он в разы меньше полного, быстрее в поиске и дешевле в обслуживании, потому что строки вне условия его не трогают при записи. Если же запросы обращаются ко всем строкам, частичный индекс не покроет часть из них и не подойдёт.

  4. Зачем нужен индекс по выражению и какое требование к функции?

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

    Он нужен, когда запрос фильтрует по результату функции, например WHERE lower(email) = 'a@b.c'. Обычный индекс по email к такому условию не подходит - оно не sargable (колонка под функцией). Индекс по выражению lower(email) хранит уже вычисленный результат, и запрос его использует. Требование: функция должна быть IMMUTABLE - давать один и тот же результат для одних и тех же входов, иначе индекс мог бы рассогласоваться с данными.

  5. Чем вредит лишний индекс, если он «просто на всякий случай»?

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

    Дважды. Во-первых, записью: каждый индекс обновляется на INSERT и на UPDATE индексируемых колонок, поэтому лишние индексы напрямую замедляют запись и занимают место. Во-вторых, планированием: планировщик рассматривает все применимые индексы, и на таблице с десятками индексов время планирования растёт. Неиспользуемый индекс (idx_scan = 0) - чистый убыток. Поэтому индексы заводят под доказанную потребность, а неиспользуемые и дублирующие находят через pg_stat_user_indexes и удаляют.

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