Модуль 03 — Объединение таблиц: JOIN

INNER, LEFT, RIGHT, FULL, CROSS, NATURAL JOIN — как собирать данные из разных таблиц

Прогресс курса Модуль 4 из 9

Что вы освоите в этом модуле

01

Что такое JOIN и зачем он нужен

Данные в реляционных базах хранятся в разных таблицах. JOIN позволяет «склеить» их по общему ключу. По сути, JOIN — это операция над двумя таблицами, которая находит подходящие пары строк.

Псевдокод JOIN-операции без индексов:
FOR r IN R LOOP → FOR s IN S LOOP → IF r.id = s.r_id THEN добавить (r, s)
Понимание этой логики помогает предсказывать результаты сложных запросов.
02

INNER JOIN — только совпавшие строки

Возвращает только те строки, для которых условие соединения выполняется в обеих таблицах.

-- Дата лайка и имя пользователя
SELECT l.liked_at,
       u.name AS user_name,
       u.age
FROM   likes l
JOIN   users u ON u.id = l.user_id
ORDER BY l.liked_at, u.name;
liked_atuser_nameage
2026-01-01Andrey21
2026-01-02Andrey21
-- JOIN трёх таблиц: имя пользователя + заголовок поста + сообщество
SELECT u.name        AS user_name,
       p.title        AS post_title,
       c.name         AS community_name
FROM   likes l
JOIN   users       u ON u.id  = l.user_id
JOIN   posts       p ON p.id  = l.post_id
JOIN   communities c ON c.id  = p.community_id
ORDER BY user_name, post_title, community_name;
03

LEFT JOIN и RIGHT JOIN — все из одной таблицы

LEFT JOIN возвращает ВСЕ строки из левой таблицы и только совпавшие из правой. Несовпавшие строки правой таблицы заполняются NULL.

-- Все сообщества, включая те, которые никто не просматривал
SELECT c.name, c.rating
FROM   communities c
LEFT JOIN views vw ON vw.community_id = c.id
WHERE  vw.id IS NULL        -- только непросмотренные
ORDER BY c.name;
Паттерн «Anti-JOIN»: LEFT JOIN + WHERE правая.id IS NULL — находит строки из левой таблицы, для которых нет пары в правой. Аналог NOT IN / NOT EXISTS, но часто быстрее.
-- Пропущенные дни в январе (1–10) для пользователей 1 и 2
SELECT gs.viewed_at::date AS missing_date
FROM   generate_series('2026-01-01'::date,
                         '2026-01-10'::date,
                         '1 day'::interval) AS gs(viewed_at)
LEFT JOIN views vw
       ON vw.viewed_at = gs.viewed_at::date
      AND vw.user_id IN (1, 2)
WHERE  vw.id IS NULL
ORDER BY missing_date;
generate_series(start, stop, step) — функция PostgreSQL, генерирующая последовательность значений. Незаменима для работы с временными рядами.
04

FULL OUTER JOIN — все из обеих таблиц

FULL OUTER JOIN возвращает все строки из обеих таблиц. Несовпавшие строки с обеих сторон заполняются NULL.

-- Полный список: пользователи И сообщества за 1–3 января 2026
-- (включая тех, кто не заходил, и сообщества без просмотров)
SELECT
    COALESCE(u.name,  '-') AS user_name,
    vw.viewed_at,
    COALESCE(c.name, '-') AS community_name
FROM   users u
FULL JOIN views vw
       ON vw.user_id = u.id
      AND vw.viewed_at BETWEEN '2026-01-01' AND '2026-01-03'
FULL JOIN communities c ON c.id = vw.community_id
ORDER BY user_name, viewed_at, community_name;
05

NATURAL JOIN и Self-JOIN

-- USING — короткая форма JOIN ... ON a.col = b.col
-- когда столбцы-ключи в обеих таблицах названы одинаково

-- ВНИМАНИЕ: в нашей схеме likes.user_id ≠ users.id по имени,
-- поэтому USING(user_id) не сработает.
-- USING применимо, если бы столбец назывался одинаково в обеих таблицах.
-- Примеры NATURAL JOIN / USING показаны ниже на условных данных:

-- Правильный способ для нашей схемы — всегда явный ON:
SELECT l.liked_at, u.name, u.age
FROM   likes l
JOIN   users u ON u.id = l.user_id
ORDER BY l.liked_at, u.name;

-- USING работает, когда имена совпадают. Если бы FK назывался 'id':
-- SELECT liked_at, name FROM likes JOIN users USING(id);
-- На практике — используйте явный ON: надёжнее при изменениях схемы.
-- Self-JOIN: пользователи из одного города
SELECT
    u1.name AS user_name1,
    u2.name AS user_name2,
    u1.city AS common_city
FROM   users u1
JOIN   users u2
       ON  u1.city = u2.city
      AND u1.id < u2.id     -- избежать дублей (A-B и B-A)
ORDER BY user_name1, user_name2, common_city;
NATURAL JOIN опасен: при изменении схемы (добавлении столбца с тем же именем) запрос начнёт давать неверные результаты. В продакшне предпочитайте явный JOIN … ON.
06

IN vs EXISTS — подзапросы для проверки существования

-- Сообщества, которые никто не просматривал — вариант с IN
SELECT name
FROM   communities
WHERE  id NOT IN (
    SELECT DISTINCT community_id
    FROM   views
);

-- Тот же результат через EXISTS
SELECT name
FROM   communities c
WHERE  NOT EXISTS (
    SELECT 1
    FROM   views vw
    WHERE  vw.community_id = c.id
);

IN vs EXISTS — когда что использовать

  • IN — проще читается, хорошо работает при небольшом подсписке. Опасен если список может содержать NULL.
  • EXISTS — останавливается при первом совпадении, эффективнее при большом подзапросе. Безопаснее с NULL.
  • NOT IN с NULL — ловушка! Если подзапрос возвращает NULL, NOT IN вернёт 0 строк. Используйте NOT EXISTS или LEFT JOIN anti-join.

Ключевые выводы модуля

Запомните главное

  • INNER JOIN возвращает только строки, у которых есть пара в обеих таблицах
  • LEFT JOIN + WHERE right.id IS NULL = Anti-JOIN: строки без пары в правой таблице
  • NOT IN с NULL в подзапросе вернёт 0 строк — используйте NOT EXISTS или Anti-JOIN
  • NATURAL JOIN опасен при изменениях схемы — предпочитайте явный JOIN ... ON
  • generate_series() незаменима для работы с временными рядами и поиска пробелов
Практические задания — на учебном портале. Задания, критерии оценивания, сдача работ и оценки преподавателя — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Модуль 02: Агрегация Все модули Модуль 04: DML