Skip to content
PПромтбук
RUEN
04База данных

Оптимизация SQL-запроса

От EXPLAIN до индексов, переписки запроса и материализованных views.

Оптимизируй запрос:

{{query}}

Подход: измерь → пойми → исправь → проверь.

1. Замерь baseline

EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
{{query}};

Запиши:

  • Total time
  • Самые дорогие операции
  • Какие строки сканируются vs нужны

2. Прочитай план

Снизу вверх. Ищи:

ЧтоЧто значитЧасто фиксится
Seq Scan на большой таблицеНет индекса или статистика устарелаСоздать индекс / ANALYZE
Nested Loop с большим outer rowsНеэффективный JOINHash Join, переписать
Index Scan + Filter много строкЧасть условия не покрывается индексомComposite index
Sort на много строкORDER BY без подходящего индексаКомпозитный индекс с DESC
Hash на много памятиНужен work_memTune work_mem
External mergeНе помещается в work_memУвеличить work_mem или переписать

3. Распространённые фиксы

A. Отсутствующий индекс

-- Запрос:
SELECT * FROM orders WHERE customer_id = 123 ORDER BY created_at DESC;

-- Индекс:
CREATE INDEX idx_orders_customer_created ON orders (customer_id, created_at DESC);

B. SELECT *

-- плохо
SELECT * FROM users WHERE id = 1;

-- хорошо — только нужные поля
SELECT id, name, email FROM users WHERE id = 1;

Особенно важно если есть TEXT/JSONB колонки которые не нужны.

C. N+1

-- плохо: цикл с подзапросом
SELECT * FROM orders;
-- для каждого ↓
SELECT * FROM users WHERE id = ?;

-- хорошо: JOIN
SELECT o.*, u.name FROM orders o JOIN users u ON u.id = o.user_id;

-- или ORM: include / preload

D. JOIN order

PostgreSQL обычно выбирает сам, но если запрос сложный — иногда нужно подсказать:

-- Маленькую таблицу левее в JOIN
FROM small_table s
JOIN huge_table h ON s.id = h.small_id

E. LIMIT без ORDER BY

-- может вернуть случайные строки
SELECT * FROM orders LIMIT 10;

-- лучше явный порядок
SELECT * FROM orders ORDER BY created_at DESC LIMIT 10;

F. WHERE на функции

-- плохо — функция блокирует индекс
WHERE LOWER(email) = 'foo@bar.com'

-- хорошо — функциональный индекс или нормализованная колонка
CREATE INDEX idx_users_email_lower ON users (LOWER(email));

G. OR через UNION

-- иногда быстрее
SELECT * FROM t WHERE a = 1
UNION ALL
SELECT * FROM t WHERE b = 2;

-- чем
SELECT * FROM t WHERE a = 1 OR b = 2;

H. EXISTS вместо JOIN для existence-check

-- быстрее
WHERE EXISTS (SELECT 1 FROM child WHERE child.parent_id = parent.id)

-- медленнее (JOIN + DISTINCT)
SELECT DISTINCT parent.* FROM parent JOIN child ON ...

4. Когда переписать

  • Если запрос постоянно медленный и индексы не помогают — ищи структурную проблему
  • Может нужна денормализация (например, счётчик)
  • Может материализованное view (для тяжёлой агрегации)
  • Может CTE для читаемости

5. Когда индексы не панацея

  • Если запрос возвращает > 10% строк таблицы — Seq Scan может быть быстрее
  • Если table очень маленькая — Seq Scan быстрее
  • Если есть много INSERT/UPDATE — индексы замедляют их

6. Финальная проверка

После изменений:

  • Повторный EXPLAIN ANALYZE
  • Сравни total time
  • Проверь на разных размерах данных (не только маленьких)
  • Убедись что план стабилен (PostgreSQL может выбрать другой план при росте данных)

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

  • ❌ "Просто добавь индекс" не глядя на план
  • ❌ Создание индекса в продакшене во время пика трафика (используй CREATE INDEX CONCURRENTLY)
  • ❌ ORDER BY RAND() — пересортирует всю таблицу
  • ❌ Сравнение времени без EXPLAIN — может быть кеш

В конце

  • Текущий план
  • Найденные проблемы
  • Предложенные изменения (с обоснованием)
  • Ожидаемый импакт
К подразделу «База данных»
Похожие промты