Когда вы оптимизируете SQL-запрос, первая интуиция: «он должен использовать индекс». Если EXPLAIN показывает Index Scan или Index Seek — все довольны.
Но реальность сложнее: index scan может быть ужасно медленным. Планировщик БД выбирает индекс, который технически подходит под запрос, и начинает сканировать. Если индекс слабо-селективный — он прокачивает миллионы строк ради тысячи результатов. С точки зрения EXPLAIN — всё хорошо. С точки зрения p99 latency — катастрофа.
Реальный кейс
Компонент Datadog обращался к Postgres-таблице со схемой:
CREATE TABLE events (
id BIGSERIAL PRIMARY KEY,
org_id BIGINT NOT NULL,
event_ts TIMESTAMPTZ NOT NULL,
kind TEXT,
payload JSONB,
-- ...
);
CREATE INDEX idx_events_org ON events (org_id);
CREATE INDEX idx_events_kind ON events (kind);
Запрос — типичная аналитика:
SELECT *
FROM events
WHERE org_id = $1
AND kind = 'login'
AND event_ts > NOW() - INTERVAL '1 hour'
ORDER BY event_ts DESC
LIMIT 100;
P99 latency — 6 секунд. P50 — 800 мс. План:
Limit (cost=0.43..2841.55 rows=100 width=...)
-> Index Scan using idx_events_kind on events
Index Cond: (kind = 'login')
Filter: (org_id = $1 AND event_ts > ...)
Формально — index scan. По факту — Postgres шёл по индексу idx_events_kind, потому что kind = 'login' показался ему селективным условием. Но login-событий было 40% от таблицы. Чтобы найти 100 строк нужного org_id за последний час, БД сканировала миллионы записей login-событий, проверяя каждую на org_id и timestamp.
Как обнаружить такие сканы
Datadog Database Monitoring (DBM) собирает:
-
Buffers read / shared blocks hit — реальное количество страниц БД, которые запрос прочёл из памяти / диска.
-
Rows examined vs rows returned — фундаментальное соотношение. Если
examined = 5_000_000иreturned = 100— это red flag, даже если план говорит «index scan». -
Wait events — что запрос ждал (CPU, IO, lock).
В DBM-консоли запросы сортируются по «efficiency score»: rows returned ÷ rows examined. Запросы с низким score попадают в топ — и это первое, на что смотрит DBA.
Что мы увидели
Для проблемного запроса в DBM показал:
-
Rows examined: ~3M в среднем за вызов.
-
Rows returned: 100.
-
Efficiency: 0.003%.
-
P99 latency: 6s.
-
Buffers read: ~25_000 страниц = 200 МБ за запрос.
Это не «индекс работает медленно». Это «индекс выбран неправильный, и сканируется кратно больше, чем нужно».
Решение
Два шага:
-
Compound index на
(org_id, event_ts DESC), потому что реальный фильтр — это «эти org внутри окна по времени»:CREATE INDEX idx_events_org_ts ON events (org_id, event_ts DESC); -
(Опционально) Partial index, чтобы дополнительно сократить размер:
CREATE INDEX idx_events_org_ts_login ON events (org_id, event_ts DESC) WHERE kind = 'login';
После миграции EXPLAIN выбирает новый составной индекс. Планировщик идёт:
-
в дерево по org_id (быстро),
-
в нужное окно по event_ts (быстро),
-
фильтрует kind = 'login' уже после.
Для нашего workload — оптимально.
Результат
-
Rows examined: ~150 (вместо 3M).
-
P99: 34 мс (вместо 6 с) — улучшение на 99.4%.
-
Buffers read: 8 страниц.
Эти цифры мы увидели в DBM-консоли в режиме «before/after» и подтвердили на production-трафике.
Уроки
«Index scan» в плане — не сигнал, что всё хорошо
План показывает какой индекс выбран. Не насколько эффективно он используется. Метрика rows examined / rows returned — лучше любого EXPLAIN-вывода.
Слабо-селективные колонки в одиночных индексах — антипаттерн
Если значение kind = 'login' встречается в 40% строк — индекс по kind бесполезен. Он будет либо полностью игнорироваться (если планировщик догадается), либо использоваться плохо. В обоих случаях — лишний размер на диске и в RAM, нагрузка на VACUUM.
Compound и partial — основные инструменты борьбы
Если у вас типичный workload «список по orgs за последний час» — compound по (org_id, event_ts) бьёт два разных индекса. Partial индексы дают ещё одно сокращение, если у вас есть «горячий» подмножество.
Observability важнее догадок
Без DBM (или эквивалентного pg_stat_statements + анализ buffers) такие баги не_заметны. EXPLAIN выглядит нормально. Запрос проходит. Только latency-распределение по реальному трафику показывает проблему. На синтетическом дев-трафике этот баг бы вообще не воспроизвёлся, потому что таблица маленькая и любой план быстрый.
Главное
«Запрос использует индекс» — это не оценка качества. Это констатация выбора планировщика. Оценка качества — это сколько работы запрос проделывает на каждую возвращённую строку. Когда это соотношение хуже 1:1000, у вас наверняка inefficient scan, который ждёт, что его найдут — обычно во время инцидента.