Почему index-only scan не включался
Был отчётный запрос, который считает количество событий по дню. Индекс покрывающий, колонок из таблицы не нужно ни одной, а план всё равно упорно показывал Index Scan с походом в heap.
EXPLAIN (ANALYZE, BUFFERS)
SELECT day, count(*) FROM events
WHERE day >= '2026-08-01' GROUP BY day;
В плане было вот это:
Index Only Scan using events_day_idx on events
Heap Fetches: 4118233
Buffers: shared hit=21044 read=98311
Heap Fetches — вот и ответ
Index-only scan на самом деле включался. Просто он почти всегда ходил в heap: четыре миллиона обращений вместо нуля. Причина — visibility map. Планировщик может пропустить чтение строки только если страница отмечена как «полностью видимая», а отмечает их VACUUM.
Таблица активно писалась, а autovacuum до неё доходил редко: порог по умолчанию — двадцать процентов от размера таблицы, а таблица большая. Проверить легко:
SELECT relname, last_autovacuum, n_dead_tup, n_live_tup
FROM pg_stat_user_tables WHERE relname = 'events';
last_autovacuum был двухнедельной давности.
Что сделал
Сначала руками:
VACUUM (VERBOSE, ANALYZE) events;
После этого Heap Fetches упал до нескольких тысяч, а запрос с 8.4 с ушёл на 0.2 с. Потом поправил пороги именно для этой таблицы, чтобы не возвращаться к вопросу:
ALTER TABLE events SET (
autovacuum_vacuum_scale_factor = 0.02,
autovacuum_analyze_scale_factor = 0.01,
autovacuum_vacuum_cost_delay = 2ms
);
Что стоит запомнить
Index Only Scanв плане ещё ничего не гарантирует — смотреть надо наHeap Fetches.- Ненулевые heap fetches почти всегда означают отставший vacuum, а не плохой индекс.
- Долгие открытые транзакции держат горизонт видимости и не дают vacuum пометить страницы. Если после ручного vacuum ничего не изменилось — искать их в
pg_stat_activityпоxact_start. - Глобально крутить
autovacuum_vacuum_scale_factorне надо, достаточно поштучно для больших таблиц.