Соединения таблиц (JOIN)

Рейтинг: 70.2% · 15 голосов
Подробный курс по PostgreSQL: SQL по демобазе, типы данных и JSONB, индексы, транзакции и MVCC, VACUUM, EXPLAIN и планировщик, роли и привилегии, репликация, бэкап, настройка под нагрузку и под 1С. Актуально на 2026.
Ответить
Аватара пользователя
Egor_DBA
Сообщения: 47
Зарегистрирован: 11 май 2026, 05:31

Соединения таблиц (JOIN)

Сообщение Egor_DBA »

Оглавление курса (47)
  1. Что такое PostgreSQL: история, философия, где применяется
  2. Урок 1. Что нового в PostgreSQL 15 и далее (15 -> 16 -> 17)
  3. Установка и первый запуск: Linux, Windows, Docker
  4. psql детально: метакоманды и работа в консоли
  5. Демобаза Авиаперевозки Postgres Pro: структура и развёртывание
  6. SELECT: проекция, фильтрация, сортировка
  7. Транзакции и ACID
  8. Уровни изоляции транзакций: Read Committed, Repeatable Read, Serializable
  9. Соединения таблиц (JOIN) (вы здесь)
  10. Агрегация: GROUP BY, HAVING, GROUPING SETS
  11. Подзапросы и CTE (WITH), рекурсия
  12. Оконные функции
  13. Изменение данных: INSERT/UPDATE/DELETE, UPSERT, MERGE
  14. Числовые и булевы типы, NULL и трёхзначная логика
  15. Строки, текст, дата и время, таймзоны
  16. Массивы, диапазоны, enum, составные типы
  17. JSON и JSONB: операторы и индексация
  18. DDL: CREATE TABLE, схемы, ALTER
  19. Ограничения целостности
  20. Представления и материализованные представления
  21. Последовательности, IDENTITY, генерируемые столбцы
  22. Индексы: B-tree и когда индекс не используется
  23. Типы индексов: Hash, GiST, SP-GiST, GIN, BRIN
  24. Продвинутые индексы: частичные, по выражению, покрывающие
  25. Обслуживание индексов и таблиц: bloat, REINDEX, CONCURRENTLY
  26. Полнотекстовый поиск
  27. Роли и пользователи
  28. Привилегии: GRANT/REVOKE
  29. Row-Level Security и безопасность данных
  30. Подключение приложения: pg_hba.conf, SSL, пулы соединений
  31. Серверное программирование: функции и процедуры
  32. Триггеры и события
  33. Архитектура PostgreSQL: процессы и память
  34. MVCC: версии строк и видимость
  35. VACUUM, autovacuum, freeze и wraparound
  36. WAL, контрольные точки и долговечность
  37. Блокировки и взаимоблокировки
  38. Планировщик запросов
  39. EXPLAIN и EXPLAIN ANALYZE: чтение планов
  40. Конфигурация сервера: ключевые параметры
  41. Настройка под нагрузку и под 1С
  42. Параллельные запросы и партиционирование
  43. Резервное копирование и восстановление
  44. Репликация: потоковая и hot standby
  45. Логическая репликация и кластерные решения
  46. Внешние данные, расширения и сертификация
  47. Разбор планов запросов на explain.tensor.ru: глубокое чтение EXPLAIN ANALYZE
Урок 8. Соединения таблиц (JOIN)

Данные в нормализованной базе разложены по отдельным таблицам, и почти любой осмысленный отчёт требует собрать их обратно. Билет лежит в одной таблице, перелёт - в другой, рейс - в третьей, аэропорт - в четвёртой. Соединение (JOIN) - это операция, которая по условию связывает строки из нескольких таблиц в одну широкую строку результата. В этом уроке разберём все виды JOIN в postgresql, чем ON отличается от USING, как соединить цепочку из четырёх таблиц демобазы и почему именно outer join чаще всего ломает отчёты из-за NULL.

Изображение

Как это работает

Соединение всегда работает попарно: берётся строка из левой таблицы и проверяется условие соединения с каждой строкой правой. Если условие истинно - строки склеиваются в одну. Так формально работает любой JOIN, а уж как именно планировщик это исполнит (вложенным циклом, хешем или слиянием) - его дело, мы лишь описываем что хотим получить.

INNER JOIN оставляет только пары, где условие совпало. Если для строки слева пары справа нет - она просто выпадает из результата. Это поведение по умолчанию: слово INNER можно опускать, просто JOIN.

Внешние соединения (OUTER) сохраняют строки даже без пары. LEFT JOIN гарантирует, что каждая строка левой таблицы попадёт в результат хотя бы раз; если пары справа не нашлось, столбцы правой таблицы заполняются значением NULL. RIGHT JOIN - то же самое, но сохраняется правая таблица. FULL OUTER JOIN сохраняет несовпавшие строки с обеих сторон. На практике RIGHT почти не пишут: его всегда можно превратить в LEFT, поменяв таблицы местами, а читать слева направо привычнее.

CROSS JOIN - это декартово произведение: каждая строка слева соединяется с каждой строкой справа без всякого условия. Если в таблицах 100 и 50 строк, получится 5000. Чаще всего огромный CROSS JOIN возникает случайно - когда забыли условие соединения.

Self-join - это соединение таблицы самой с собой. Чтобы различать два экземпляра одной таблицы, им дают разные псевдонимы (алиасы). Типичные задачи: сравнить строки одной таблицы между собой или пройти по иерархии типа сотрудник-руководитель.

Условие соединения задают двумя способами. ON принимает любое логическое выражение и подходит всегда. USING - сокращение для частого случая, когда столбцы в обеих таблицах называются одинаково: USING (flight_id) эквивалентно ON a.flight_id = b.flight_id, но при этом склеивает два одноимённых столбца в один общий в выводе. Это важная деталь: после USING нельзя писать table.flight_id - столбец становится общим и обращаться к нему надо просто по имени.

SQL и примеры

Соберём по билету его перелёты - связь tickets и ticket_flights по ticket_no. Это INNER JOIN, в результат попадут только билеты, у которых есть перелёты:

Код: Выделить всё

SELECT t.ticket_no, t.passenger_name, tf.flight_id, tf.amount
FROM tickets t
JOIN ticket_flights tf ON tf.ticket_no = t.ticket_no
LIMIT 10;
Теперь цепочка из четырёх таблиц: билеты -> перелёты -> рейсы -> аэропорт вылета. Каждый следующий JOIN добавляет к строке новые столбцы по своему условию. Показываем пассажира, номер рейса и город вылета:

Код: Выделить всё

SELECT t.passenger_name,
       f.flight_no,
       dep.city AS departure_city
FROM tickets t
JOIN ticket_flights tf ON tf.ticket_no = t.ticket_no
JOIN flights f ON f.flight_id = tf.flight_id
JOIN airports dep ON dep.airport_code = f.departure_airport
LIMIT 10;
Аэропорт участвует в рейсе дважды - как вылет и как прилёт. Соединяем таблицу airports два раза под разными алиасами (dep и arr) - это и есть применение нескольких соединений к одной таблице:

Код: Выделить всё

SELECT f.flight_no,
       dep.city AS from_city,
       arr.city AS to_city
FROM flights f
JOIN airports dep ON dep.airport_code = f.departure_airport
JOIN airports arr ON arr.airport_code = f.arrival_airport
LIMIT 10;
Покажем смысл LEFT JOIN. Найдём билеты, для которых ещё не оформлен посадочный талон. boarding_passes есть не для каждого перелёта, поэтому сохраняем левую таблицу и ищем, где правая дала NULL:

Код: Выделить всё

SELECT tf.ticket_no, tf.flight_id
FROM ticket_flights tf
LEFT JOIN boarding_passes bp
       ON bp.ticket_no = tf.ticket_no
      AND bp.flight_id = tf.flight_id
WHERE bp.boarding_no IS NULL;
Условие соединения здесь составное - по двум столбцам сразу, потому что перелёт идентифицируется парой (ticket_no, flight_id). Проверка bp.boarding_no IS NULL отбирает именно непришедшие пары - классический приём "антисоединения".

То же соединение через USING выглядит короче, так как имена столбцов совпадают в обеих таблицах:

Код: Выделить всё

SELECT *
FROM ticket_flights
LEFT JOIN boarding_passes USING (ticket_no, flight_id)
LIMIT 10;
Частые грабли
  • Условие на правую таблицу outer join в WHERE превращает LEFT JOIN обратно в INNER. Если написать LEFT JOIN boarding_passes bp ... WHERE bp.seat_no = '1A', то строки с NULL отфильтруются, и эффект сохранения левой таблицы пропадёт. Условие на правую таблицу пишите в ON, а не в WHERE.
  • Сравнение с NULL через знак равенства всегда даёт неизвестность, а не истину. Чтобы найти несовпавшие строки после outer join, используйте IS NULL, а не = NULL.
  • Забытое условие соединения молча даёт CROSS JOIN. Запрос отработает, но вернёт миллионы строк и подвесит сервер. Всегда проверяйте, что у каждого JOIN есть ON или USING.
  • COUNT(*) после LEFT JOIN считает и строки-пустышки с NULL. Если нужно посчитать реальные совпадения, считайте конкретный непустой столбец: COUNT(bp.boarding_no).
  • USING склеивает одноимённые столбцы в один. После USING (flight_id) обращение f.flight_id вызовет ошибку - пишите просто flight_id.
  • Дубли строк после JOIN - частый сюрприз. Если справа на одну левую строку приходится несколько пар, левая строка размножится. Это не баг соединения, а связь один-ко-многим.
Мини-лаба
  • Подключитесь к демобазе: psql demo
  • Соедините flights и aircrafts по aircraft_code (INNER JOIN) и выведите flight_no и model самолёта для 10 рейсов.
  • Перепишите это соединение через USING (aircraft_code) и убедитесь, что в выводе aircraft_code остался один столбец.
  • Сделайте LEFT JOIN flights к aircrafts и найдите рейсы, у которых aircraft_code не сопоставился с моделью (model IS NULL).
  • Постройте цепочку tickets -> ticket_flights -> flights -> airports (вылет) и выведите passenger_name и город вылета для рейсов со статусом 'Arrived'.
  • Намеренно уберите условие в одном JOIN, выполните EXPLAIN (без ANALYZE) и посмотрите на оценку числа строк - так выглядит случайный CROSS JOIN.
  • Сравните два аэропорта в одном рейсе self-join-подобным двойным соединением airports и выведите пары from_city - to_city.
Контрольные вопросы
  • Чем результат INNER JOIN отличается от LEFT JOIN, если у части левых строк нет пары справа?
  • Почему условие на правую таблицу, помещённое в WHERE, отменяет смысл LEFT JOIN, а в ON - нет?
  • В каких случаях USING удобнее ON и какое ограничение он накладывает на обращение к столбцам?
  • Как с помощью outer join и проверки на NULL найти строки, у которых нет соответствия в другой таблице?
  • Почему RIGHT JOIN на практике почти не используют и чем его заменяют?
  • Откуда после соединения берутся дубликаты строк и как это связано со связью один-ко-многим?
👍4 ❤️ 🔥1 😄 🤔
Аватара пользователя
Sgill77
Сообщения: 1
Зарегистрирован: 15 май 2026, 16:15

Re: Соединения таблиц (JOIN)

Сообщение Sgill77 »

А почему у меня COUNT(*) после LEFT JOIN больше, чем строк в исходной таблице? Думал left join не добавляет строк
👍 ❤️1 🔥 😄 🤔
Аватара пользователя
cpp_guru
Сообщения: 1
Зарегистрирован: 13 май 2026, 21:06

Re: Соединения таблиц (JOIN)

Сообщение cpp_guru »

Понял на антисоединении фишку: LEFT JOIN + WHERE правая IS NULL = найти то, чего нет в другой таблице. Раньше городил NOT IN с подзапросом, тут короче и не спотыкается о NULL
👍3 ❤️ 🔥1 😄 🤔
Ответить
← Предыдущая глава
Уровни изоляции транзакций: Read Committed, Repeatable Read, Serializable
Следующая глава →
Агрегация: GROUP BY, HAVING, GROUPING SETS

Все главы курса «PostgreSQL: от первого запроса до продакшена»

Поделиться темой: ✈ Telegram VK

Вернуться в «PostgreSQL: от первого запроса до продакшена»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 2 гостя