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

Базы данных

Partial, expression и covering-индексы

Суть Partial-индексы строятся только над строками, соответствующими WHERE-условию, что значительно уменьшает их размер. Expression-индексы позволяют делать быстрыми функциональные запросы. INCLUDE создаёт covering-индексы для index-only scan.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 16 min

Таблица tasks: 100M строк, 95% из них status = ‘done’. Дашборд показывает только открытые задачи. Полный B-tree индекс на status обслуживает 100M записей, большинство из которых никогда не запрашиваются горячо. Partial-индекс WHERE status = 'open' содержит 5M записей — в 20 раз меньше, в 20 раз дешевле по write-cost, в 20 раз меньше на диске. Это один из самых недоиспользованных рычагов производительности в Postgres.

Partial-индексы

Partial-индекс — это B-tree (или другой тип), построенный только над строками, соответствующими WHERE-условию.

-- Только pending-orders — обычно малая доля таблицы
CREATE INDEX ON orders(user_id) WHERE status = 'pending';

-- Только активные пользователи (soft-delete паттерн)
CREATE INDEX ON users(email) WHERE deleted_at IS NULL;

-- Feature flag — только включённые строки
CREATE INDEX ON subscriptions(plan_id) WHERE feature_x_enabled = true;

Преимущества:

  • Меньший размер: если 5% строк соответствуют предикату, индекс занимает ~5% от полного.
  • Меньше write-overhead: индекс трогается только при изменении строк, соответствующих предикату.
  • Быстрее поиск: меньше leaf-страниц для сканирования.

Ограничение: запросы ОБЯЗАНЫ включать WHERE-условие, идентичное предикату индекса (или более строгое), чтобы планировщик использовал partial-индекс. WHERE user_id = $1 AND status = 'pending' использует индекс; WHERE user_id = $1 — нет.

Partial unique индексы — мощный паттерн:

-- Email уникален среди не-удалённых пользователей
CREATE UNIQUE INDEX ON users (email) WHERE deleted_at IS NULL;
-- Позволяет soft-delete пользователя и re-register того же email
-- Невозможно с простым UNIQUE constraint на email

Expression-индексы

Expression-индекс строится на результате функции, применённой к столбцу, а не на самом столбце.

-- Без индекса: LOWER() применяется к каждой строке, индекс не используется
SELECT * FROM users WHERE LOWER(email) = 'alice@x.com';

-- С expression-индексом: планировщик использует его
CREATE INDEX ON users (LOWER(email));

Распространённые паттерны:

  • Case-insensitive поиск: LOWER(email), UPPER(name)
  • Truncation даты: DATE_TRUNC('day', created_at) для группировки по дням
  • JSON path extraction: (payload->>'event_type') для JSONB-полей
  • Вычисляемые значения: любое выражение, совпадающее с формой запроса

Expression-индексы — способ сделать быстрыми функциональные запросы без изменения схемы.

Covering-индексы с INCLUDE

INCLUDE (Postgres 11+) добавляет столбцы в leaf-страницы индекса без включения их в ключ сортировки. Результат: индекс становится “covering” для запросов, которым нужны эти столбцы в projection, что позволяет index-only scan (без fetch из heap).

-- Ключевые столбцы: workspace_id, created_at DESC (sort key)
-- INCLUDE: id, total_cents (payload для projection — не в sort key)
CREATE INDEX CONCURRENTLY idx_orders_ws_recent
  ON orders (workspace_id, created_at DESC)
  WHERE status = 'pending'
  INCLUDE (id, total_cents);

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

МодификаторДля чегоОграничение
WHERE (partial)Уменьшить размер; снизить write-costЗапрос должен включать предикат
Выражение в ключеПоддержать функциональные запросыЗапрос должен точно совпадать с выражением
INCLUDEПокрыть projection для index-only scanТребуется актуальный Visibility Map (VACUUM)
Partial и covering-индексы: числа
Partial-индекс (5% строк соответствуют)
~5% от размера полного B-tree
Write-overhead (partial, 5% matching)
~5% от full-index write-cost
Index-only scan vs index scan
10-100x быстрее на hot reads
INCLUDE поддерживается с версии Postgres
11 (2018)
Overhead INCLUDE на leaf-страницах
размер включённых столбцов × кол-во строк
Partial unique: паттерн soft-delete
UNIQUE WHERE deleted_at IS NULL
Expression-индекс: требование совпадения
точное совпадение выражения в запросе
Partial-индекс для B2B SaaS 'open tasks'
обычно 5-20% таблицы — огромная экономия
Limit 50 на index-only scan (50M строк)
~1 мс (только 50 leaf-записей)
Тот же запрос без covering: heap fetch
178k heap fetch → секунды

Выбрать правильный индекс под hot dashboard-запрос

1/3
Викторина

Почему LOWER(email) = 'alice@x.com' не использует обычный B-tree индекс на email?

Викторина

Чем INCLUDE отличается от добавления столбцов в ключ сортировки composite-индекса?

Почему это работает

Почему partial-индексы так редко используются в продакшне, несмотря на огромную выгоду? Потому что требуют понимания распределения данных. Команды знают «добавить индекс на столбец», но не знают «добавить индекс только на горячее подмножество». Признак возможного partial-индекса: таблица с историческими данными (95% cold), но горячий путь запрашивает только свежие или определённые status. Аудит: проверь, какую долю таблицы занимает hot подмножество — если меньше 20%, partial-индекс почти всегда выгоднее полного.

Вспомните перед уходом
  1. 01
    В чём разница между partial-индексом, expression-индексом и covering-индексом (INCLUDE)? Дай практический пример каждого.
  2. 02
    Команда хочет обеспечить уникальность email среди активных пользователей, разрешив повторное использование email после soft-delete. Как это сделать?
  3. 03
    Запрос EXPLAIN ANALYZE показывает Index Only Scan с Heap Fetches: 2000 на 50 результирующих строк. Что это означает и как исправить?
Итог

Три продвинутых модификатора индексов расширяют B-tree модель. Partial-индексы (WHERE в CREATE INDEX) строятся только над строками, соответствующими предикату — если горячее подмножество 5% таблицы, индекс в 20x меньше и в 20x дешевле на запись. Expression-индексы (функция в ключе) позволяют LOWER(email), DATE_TRUNC(), JSON-path использовать индекс — без них функция на каждой строке отключает индекс. INCLUDE добавляет payload-столбцы в leaf-страницы без влияния на sort key — enabling index-only scan без heap fetch.

Все три часто комбинируются: (workspace_id, created_at DESC) WHERE status=‘pending’ INCLUDE (id, total_cents) — composite для filter/sort, partial для экономии, INCLUDE для covering. Partial unique indexes решают проблему uniqueness в soft-delete паттернах. EXPLAIN ANALYZE — единственный способ убедиться, что индекс работает как задумано.

Связанные уроки
встречается в174
Продолжить восхождение ↑Типы индексов: GIN, GiST, BRIN, Hash, Bloom и HOT-обновления
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.