Как оптимизировать запрос к проектам, задачам и истории статусов с LEFT JOIN?

Разбираем фильтр по истории статусов: почему внешние соединения фактически становятся внутренними, какие индексы рассмотреть и почему ускорение нужно измерять.

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

Что ответить на собеседовании

Условие WHERE h.code IN (1, 4, 6) исключает строки без подходящей истории, поэтому оба LEFT JOIN в данном запросе можно заменить на INNER JOIN без изменения результата. Но это не гарантирует ускорение: оптимизатор мог уже выполнить преобразование. Проверяйте план, объемы данных и подходящие индексы; не подменяйте выбор всех записей истории выбором последней.

Подробный разбор

Ответ с пояснениями

Исходный запрос

SELECT
    p.id AS project_id, p.name AS project_name,
    i.id AS issue_id, i.subject AS issue_subject,
    i.status_name AS last_status,
    h.status AS current_status, h.date_update
FROM project AS p
LEFT JOIN issue AS i ON p.id = i.project_id
LEFT JOIN issue_history AS h ON h.id_issue = i.id
WHERE h.code IN (1, 4, 6);

Сначала сохранить смысл

Если подходящей строки истории нет, поля h равны NULL и условие WHERE не истинно. Такая строка исключается. Без строки i также невозможно получить совпавшую историю по h.id_issue = i.id. Поэтому в этом конкретном запросе оба внешних соединения эквивалентны внутренним:

SELECT
    p.id AS project_id, p.name AS project_name,
    i.id AS issue_id, i.subject AS issue_subject,
    i.status_name AS last_status,
    h.status AS current_status, h.date_update
FROM project AS p
JOIN issue AS i ON p.id = i.project_id
JOIN issue_history AS h ON h.id_issue = i.id
WHERE h.code IN (1, 4, 6);

Перенос фильтра в ON с сохранением LEFT JOIN — не равнозначное исправление: тогда появятся проекты или задачи без подходящей истории. Аналогично нельзя добавить DISTINCT только ради уменьшения результата: несколько записей истории могут быть нужны.

Имена last_status и current_status — лишь псевдонимы. Запрос не выбирает последнюю строку по date_update. Добавление оконной функции или LIMIT потребовало бы отдельного требования и изменило бы исходную задачу.

Затем измерить

Сравните планы исходного и переписанного запросов на репрезентативных данных. Оптимизатор может самостоятельно устранить внешние соединения, и тогда явная замена улучшит читаемость, но не время выполнения. Проверьте оценки числа строк, порядок соединений, способ доступа к истории, сортировки и актуальность статистики.

Для истории кандидатами могут быть индексы (code, id_issue) или (id_issue, code): выбор зависит от селективности фильтра и того, от какой таблицы идет соединение. Если коды 1, 4 и 6 покрывают почти всю историю, индекс по коду может оказаться невыгодным. Проверьте существующие индексы по ключам таблиц и не создавайте дубли. Любой новый индекс увеличивает стоимость записи и занимает место.

Например, в PostgreSQL EXPLAIN (ANALYZE, BUFFERS) показывает фактическое выполнение, но действительно запускает запрос. Используйте его в безопасной среде или с согласованными ограничениями нагрузки. Без плана и распределения данных нельзя честно обещать конкретное ускорение. См. документацию PostgreSQL по EXPLAIN.

Практика в реальном времени

Подготовьтесь к следующему собеседованию

Interview Boost учитывает вакансию, резюме и технологии и помогает сформулировать ответ прямо во время интервью.

Начать подготовку