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