> Что можно увидеть в explain-плане запросов в Postgres (Python)

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

Компании: Сбер

Стек: Python

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

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

Explain-план в Postgres показывает, как оптимизатор собирается выполнить запрос: порядок соединений таблиц, методы доступа (Seq Scan, Index Scan, Bitmap Heap Scan), условия фильтрации, сортировки, агрегации и оценку стоимости каждого узла. Для senior-позиции важно не просто перечислить узлы, а уметь читать план как дерево, понимать реальные строки vs оценку, и находить узкие места - например, неожиданный Seq Scan на большой таблице или неправильную оценку кардинальности.

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

Explain-план - это дерево узлов, где каждый узел описывает операцию. Ключевые элементы:

  • Узлы доступа к данным: Seq Scan (полное сканирование), Index Scan (по индексу), Index Only Scan (только из индекса), Bitmap Heap Scan + Bitmap Index Scan (для больших выборок).
  • Узлы соединений: Nested Loop, Hash Join, Merge Join. Выбор зависит от размеров таблиц и наличия индексов.
  • Узлы обработки: Sort, GroupAggregate, HashAggregate, Limit, Filter.
  • Стоимость (cost): три числа - startup cost (до первой строки), total cost (до конца), rows (оценка строк), width (средняя ширина строки в байтах).
  • actual time / rows / loops: появляются при EXPLAIN ANALYZE, показывают реальные значения. Расхождение с оценкой - сигнал для анализа статистики.
  • Filter: условие, применяемое после чтения строки.
  • Index Cond: условие, переданное в индекс.
  • Planning time / Execution time: общее время планирования и выполнения.

Важные нюансы:

  • Оценка кардинальности - главный источник ошибок. Если статистика устарела (ANALYZE не запускался) или распределение данных неравномерное, rows будут сильно отличаться от actual.
  • Параметры планировщика: random_page_cost, seq_page_cost, work_mem влияют на выбор плана. Например, низкий random_page_cost заставляет оптимизатор предпочитать индексы.
  • Parallel plan: узлы с Parallel Seq Scan, Gather - показывают, что запрос использует параллельные воркеры.
  • CTE и подзапросы: в Postgres 12+ CTE могут быть инлайнированы, но если встречается Materialize - это отдельная материализация результата.
  • EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) - расширенный формат для глубокого анализа: показывает чтения/записи в буферный кеш.

На практике

Для senior-роли важно уметь:

  1. Читать дерево снизу вверх - сначала выполняются листовые узлы, затем родительские.
  2. Сравнивать оценку и факт - если rows сильно отличается от actual rows, проблема в статистике или в сложных предикатах (например, функции в WHERE).
  3. Искать узкие места:
    • Seq Scan на таблице > 10k строк при наличии фильтра - вероятно, нет подходящего индекса.
    • Nested Loop с большим количеством итераций - может быть лучше Hash Join.
    • Sort на большом объёме - возможно, нужен индекс с сортировкой.
  4. Использовать EXPLAIN (ANALYZE, BUFFERS) для оценки кеширования - если Buffers: shared hit высокий, данные в кеше.
  5. Проверять loops - если узел выполняется много раз (например, Nested Loop с loops=100k), это указывает на проблему с корреляцией.

Типичный сценарий на собеседовании: дают план с Seq Scan на таблице с миллионом строк и спрашивают, что не так. Правильный ответ - проверить наличие индекса, статистику, и возможно, изменить запрос (например, добавить условие на селективный столбец).

Пример кода

SQL
-- Базовый план
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
-- С фактическим временем и буферами
EXPLAIN (ANALYZE, BUFFERS)
SELECT u.name, o.total
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.created_at > '2024-01-01';
-- JSON формат для программной обработки
EXPLAIN (FORMAT JSON)
SELECT * FROM orders WHERE status = 'pending';

Пример вывода с пояснением:

Hash Join  (cost=12.34..56.78 rows=100 width=32)
  Hash Cond: (o.user_id = u.id)
  -> Seq Scan on orders o  (cost=0.00..34.50 rows=2450 width=16)
       Filter: (status = 'pending'::text)
  -> Hash  (cost=10.20..10.20 rows=500 width=16)
       -> Seq Scan on users u  (cost=0.00..10.20 rows=500 width=16)

Здесь видно: сначала сканируются orders с фильтром, затем users, строится hash-таблица, и выполняется соединение. Если бы users был большим, оптимизатор мог бы выбрать Nested Loop с индексом.

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

Начните с определения: explain-план - это дерево операций с оценкой стоимости. Затем перечислите основные узлы и их назначение. Обязательно упомяните разницу между EXPLAIN и EXPLAIN ANALYZE - первое показывает оценку, второе - факт. Покажите, что понимаете, как оптимизатор принимает решения: на основе статистики, стоимости операций, параметров конфигурации.

Приведите пример из практики: как вы находили проблему через план (например, неожиданный Seq Scan из-за функции в WHERE, которая блокирует использование индекса). Подчеркните, что для senior важно не просто прочитать план, а предложить исправление: добавить индекс, переписать запрос, обновить статистику.

Если спросят про EXPLAIN ANALYZE - скажите, что он реально выполняет запрос, поэтому для INSERT/UPDATE/DELETE нужно использовать EXPLAIN (ANALYZE, ...) с осторожностью или оборачивать в транзакцию с откатом.

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

Интервьюер оценивает:

  • Глубину понимания: знаете ли вы не только названия узлов, но и как оптимизатор выбирает между ними.
  • Практический опыт: можете ли вы по плану найти проблему и предложить решение.
  • Умение работать со статистикой: понимаете ли вы, что rows - это оценка, и как её проверить.
  • Внимание к деталям: замечаете ли вы loops, Buffers, Filter vs Index Cond.
  • Способность объяснять: можете ли вы простыми словами описать, почему план такой, а не иной.

Для senior-уровня ожидается, что вы упомянете про work_mem (когда Hash Join переполняется и уходит на диск), про random_page_cost (влияние на выбор индекса), и про параллельное выполнение.

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

  • Путать EXPLAIN и EXPLAIN ANALYZE - первое не выполняет запрос, второе выполняет.
  • Игнорировать actual rows - если смотреть только на cost, можно пропустить серьёзную ошибку оценки.
  • Считать, что Seq Scan - всегда плохо - на маленьких таблицах или при выборке > 10% строк он эффективнее индекса.
  • Не учитывать loops - узел с loops=100000 может быть хуже, чем узел с большим cost, но одним выполнением.
  • Забывать про Buffers - без них не видно, читает ли запрос с диска или из кеша.
  • Предлагать индекс без проверки селективности - индекс на столбце с 50% уникальных значений не поможет.
  • Не упоминать про ANALYZE - если статистика устарела, любой план будет неточным.
  • Путать Filter и Index Cond - Filter применяется после чтения строки, Index Cond - внутри индекса.

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

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