Skip to content
PПромтбук
RUEN
07SQL

Оптимизация медленного SQL по EXPLAIN ANALYZE

Систематическое чтение плана: где врёт оптимизатор, почему seq scan вместо index, как починить join order.

Действуй как DBA с опытом тюнинга OLTP под нагрузкой. Оптимизируй запрос:

{{query}}

СУБД: {{engine}}. Цель: {{target_time}}.

Шаги

1. Сними честный план

  • EXPLAIN (ANALYZE, BUFFERS, VERBOSE, FORMAT TEXT) — обязательно ANALYZE, иначе видишь только оценки
  • Прогрей кеш: запусти 2-3 раза, бери последний прогон
  • Покажи также pg_stat_statements для этого запроса: mean_exec_time, calls, shared_blks_hit/read

2. Читай план снизу вверх по дереву

Для каждого узла отметь:

  • actual rows vs estimated — расхождение >10× = устаревшая статистика или плохая селективность
  • actual time vs total cost — где время реально тратится
  • loops — количество итераций (Nested Loop с loops=100k = беда)
  • Buffers: shared hit/read — read = с диска, hit = из кеша

3. Классифицируй проблему

Симптом в планеДиагнозФикс
Seq Scan на большой таблице с фильтромНет индекса или WHERE не sargableCREATE INDEX или переписать предикат
Index Scan но Rows Removed by Filter > 0Индекс покрывает не все условияКомпозитный индекс или INCLUDE
Nested Loop с большим loopsОптимизатор недооценил селективностьANALYZE, расширенная статистика, или хинт join
Hash Join с Batches > 1Не хватает work_memПоднять work_mem для сессии
Sort: external merge DiskСортировка не влезает в памятьwork_mem, или индекс с правильным порядком
BitmapHeapScan с lossy heap blocksBitmap не помещается в work_memwork_mem или более селективный индекс

4. Проверь sargability предикатов

Эти переписывания обычно дают порядки:

  • WHERE date_trunc('day', created_at) = '2026-01-01'WHERE created_at >= '2026-01-01' AND created_at < '2026-01-02'
  • WHERE lower(email) = ? → функциональный индекс (lower(email)) или хранить нормализованным
  • WHERE id::text = ? → убрать каст, привести параметр к типу колонки
  • WHERE col + 1 = ?WHERE col = ? - 1

5. Проверь join order

Если оптимизатор выбирает плохой порядок:

  • Запусти ANALYZE на участвующих таблицах
  • Создай CREATE STATISTICS ... ON (col_a, col_b) FROM table для коррелированных колонок
  • В крайнем случае — материализуй промежуточный результат через CTE с MATERIALIZED

6. Покажи финальный план

До/после, с цифрами: planning time, execution time, buffers. Целевое улучшение должно быть видно в actual time, а не только в cost.

Формат вывода

## Диагноз
- Корневая проблема: ...
- Второстепенные: ...

## Изменения
1. CREATE INDEX ... — обоснование
2. Переписано WHERE — обоснование

## План ДО
[EXPLAIN ANALYZE до]
## План ПОСЛЕ
[EXPLAIN ANALYZE после]

## Метрики
| | До | После |
| Execution time | 4200ms | 38ms |
| Shared reads | 18420 | 12 |

Анти-паттерны

  • ❌ Смотреть на cost вместо actual time — cost это оценка, врёт регулярно
  • ❌ Создавать индекс на каждую колонку из WHERE — индексы стоят на запись
  • ❌ Покрывающий индекс с 8 колонками — переписывает планировщик, кеш страдает
  • OPTIMIZE FOR хинты без понимания — фиксируешь сегодняшнюю статистику
  • ❌ Игнорировать Rows Removed by Filter — это работа впустую
К подразделу «SQL»
Похожие промты