> Как оптимизировать SQL запросы (Python)

Уровень: senior · Роль: backend · Язык: Python · Категория: Технические вопросы

Компании: ProFinansy, IPChain

Стек: Python

> Пример ответа

Короткий ответ

Оптимизация SQL начинается с анализа: EXPLAIN ANALYZE, поиск медленных запросов в логах, проверка индексов. Основные направления - индексация (B-tree, composite, partial, covering), переписывание запросов (избегание SELECT *, OR, функций в WHERE, неявных приведений типов), нормализация/денормализация, пагинация через keyset вместо OFFSET. В Python важно использовать ORM осознанно: избегать N+1 через selectinload/joinedload, батчить операции. Критично измерять до и после - оптимизация без метрик это гадание.

Подробное объяснение

Оптимизация SQL - это итеративный процесс, который начинается с диагностики, а не с переписывания кода. Первый шаг - найти узкие места: включить slow_query_log в PostgreSQL/MySQL, собрать статистику по времени выполнения, частоте вызовов. Затем для каждого проблемного запроса выполнить EXPLAIN (ANALYZE, BUFFERS) в PostgreSQL или EXPLAIN ANALYZE в MySQL - это покажет фактический план выполнения, стоимость операций, количество строк, использование индексов.

Основные категории оптимизации:

  1. Индексы - самый частый инструмент. Но индекс - это trade-off: ускоряет чтение, замедляет запись, занимает место. Важно понимать:

    • composite index работает для префиксных комбинаций колонок (a, b, c) - для (a), (a,b), (a,b,c), но не для (b,c) или (a,c)
    • условия LIKE '%text' и LIKE '_text' не используют B-tree индекс, только LIKE 'text%'
    • функции в WHERE (WHERE lower(email) = ...) убивают индекс - нужен expression index
    • partial index (WHERE status = 'active') для фильтрации по часто встречающемуся значению
    • covering index (INCLUDE в PostgreSQL 11+) для index-only scan
  2. Переписывание запросов:

    • SELECT * - лишние данные, особенно с JOIN и TEXT-колонками
    • OR часто превращается в seq scan - заменить на UNION ALL или IN
    • NOT IN с NULL-значениями ведёт себя неожиданно - использовать NOT EXISTS
    • неявные приведения типов (WHERE varchar_col = 123) - индекс не сработает
    • подзапросы в SELECT - часто заменить на JOIN с GROUP BY
  3. Пагинация: OFFSET сканирует и выбрасывает строки - на больших объёмах это O(N). Keyset pagination (WHERE id > last_id ORDER BY id LIMIT 20) работает за O(log N) при наличии индекса.

  4. JOIN и GROUP BY: убедиться, что колонки соединения и группировки индексированы. Для GROUP BY по нескольким колонкам - composite index. Избегать JOIN с таблицами, где нет индекса на колонке соединения.

  5. ORM-специфика (Python):

    • N+1 проблема - главный враг. В SQLAlchemy: selectinload, joinedload, subqueryload для eager loading
    • lazy loading в цикле - каждый вызов генерирует отдельный запрос
    • bulk operations: bulk_insert_mappings, bulk_update_mappings вместо циклов с session.add
    • session.query vs select() - в SQLAlchemy 2.0 рекомендуется select()
    • избегать query.all() для больших результатов - использовать yield_per или серверные курсоры
  6. Денормализация - осознанный trade-off: добавлять вычисляемые колонки (например, order_total вместо SUM по позициям), кэшировать счётчики, использовать materialized views для тяжёлых агрегатов.

На практике

Начинать с метрик: EXPLAIN (ANALYZE, BUFFERS) для конкретного запроса, pg_stat_statements для общей картины в PostgreSQL. Смотреть на:

  • Seq Scan на больших таблицах - кандидат на индекс
  • Nested Loop с большим количеством итераций - возможно, нужен Hash Join
  • Sort без ORDER BY - лишняя сортировка
  • Rows Removed by Filter - индекс не используется или селективность низкая

Правило: сначала индекс, потом переписывание, потом денормализация. Каждое изменение - замер до и после на реальных данных, а не на тестовых.

Для Python-бэкенда: профилировать не только SQL, но и количество запросов. Добавить middleware, который логирует все запросы с длительностью (например, через событие before_cursor_execute в SQLAlchemy). Это быстро покажет N+1 и повторяющиеся запросы.

Пример кода

PYTHON
# Плохо: N+1
users = session.query(User).all()
for user in users:
print(user.orders) # каждый доступ - отдельный SELECT
# Хорошо: eager load
from sqlalchemy.orm import selectinload
users = session.execute(
select(User).options(selectinload(User.orders))
).scalars().all()
# Keyset пагинация вместо OFFSET
def get_users_after(last_id: int | None, limit: int = 20):
stmt = select(User).order_by(User.id).limit(limit)
if last_id is not None:
stmt = stmt.where(User.id > last_id)
return session.execute(stmt).scalars().all()
# Bulk insert вместо цикла
from sqlalchemy.dialects.postgresql import insert
stmt = insert(User).values([
{"name": "Alice", "email": "alice@example.com"},
{"name": "Bob", "email": "bob@example.com"},
])
stmt = stmt.on_conflict_do_nothing(index_elements=["email"])
session.execute(stmt)

Как отвечать на собеседовании

Начинать с диагностики, а не с рецептов: "Сначала я бы посмотрел EXPLAIN ANALYZE и slow query log". Затем структурировать ответ по уровням: индексы → переписывание → схема данных → инфраструктура (реплики, кэш). Показать понимание trade-off: индекс ускоряет чтение, но замедляет запись; денормализация ускоряет SELECT, но усложняет консистентность. Упомянуть специфику Python/ORM - это сильный сигнал для бэкенд-роли. Привести конкретный пример из практики: "был запрос с N+1, заменил на selectinload, время упало с 2 секунд до 50 мс". Если спрашивают про конкретный запрос - попросить показать схему и EXPLAIN, а не гадать.

Что проверяет интервьюер

  • Понимание того, как работает планировщик: seq scan vs index scan, стоимость операций
  • Умение читать EXPLAIN и делать выводы
  • Знание типов индексов и их ограничений (composite, partial, expression, covering)
  • Понимание trade-off между скоростью чтения и записи
  • Практический опыт с ORM: N+1, eager loading, bulk operations
  • Системный подход: не "вот так надо", а "сначала измеряем, потом оптимизируем"
  • Умение объяснять сложное простыми словами, без воды

Типичные ошибки

  • Начинать с "добавь индекс на всё" без анализа - индексы на низкоселективных колонках бесполезны
  • Игнорировать EXPLAIN и оптимизировать вслепую
  • Забывать про влияние на запись: индекс на горячей таблице может убить производительность INSERT
  • Использовать OFFSET для пагинации на больших таблицах
  • В Python: не замечать N+1, потому что локально данные маленькие
  • Применять LIKE '%...%' на больших текстовых колонках - нужен полнотекстовый поиск или trigram index
  • Оптимизировать запрос, который выполняется раз в день, вместо горячего пути - приоритизация важна
  • Не измерять результат после изменений - "я добавил индекс, должно стать быстрее" не работает без метрик

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью