В прошлом уроке каждый запрос смотрел в одну таблицу. Но в screenings у сеанса записан только film_id, число. Зритель хочет видеть в афише «Брат, Большой зал, 19:00», а не «1, 1, 19:00». Название лежит в films, имя зала в halls. Сегодня учимся собирать строку ответа из нескольких таблиц.
Склеиваем карточки по номеру
Представь две картотеки. В одной карточки сеансов, на каждой написан номер фильма. В другой карточки фильмов с номером и названием. Чтобы составить афишу, ты берёшь карточку сеанса, ищешь в соседнем ящике фильм с тем же номером и скрепляешь карточки степлером. Так проходишь по всем сеансам.
Это и есть соединение. Условие склейки пишут после слова ON: у сеанса film_id должен совпасть с id фильма. Это та самая связь по внешнему ключу из урока 10.
SELECT screenings.id, films.title, screenings.starts_at
FROM screenings
JOIN films ON films.id = screenings.film_id
ORDER BY screenings.id;
+----+-------------------------------------------+---------------------+
| id | title | starts_at |
+----+-------------------------------------------+---------------------+
| 1 | Брат | 2026-10-23 19:00:00 |
| 2 | Брат | 2026-10-24 21:30:00 |
| 3 | Москва слезам не верит | 2026-10-24 18:00:00 |
| 4 | Ирония судьбы | 2026-10-25 20:00:00 |
| 5 | Сталкер | 2026-10-25 17:00:00 |
| 6 | Ёжик в тумане | 2026-10-26 11:00:00 |
| 7 | Новые имена | 2026-10-26 22:00:00 |
+----+-------------------------------------------+---------------------+
7 rows in set (0.00 sec)
Перед именем колонки стоит имя таблицы: screenings.id, films.title. Это не украшение. Колонка id есть в обеих таблицах, и MySQL должен понять, какую ты имеешь в виду.
Ошибка 1052 и псевдонимы таблиц
Уберём имя таблицы перед id:
SELECT id, title FROM films JOIN screenings ON screenings.film_id = films.id;
ERROR 1052 (23000): Column 'id' in field list is ambiguous
Сервер честно говорит, что id двусмысленна. У title такой проблемы нет: она есть только в films, поэтому её можно писать без префикса.
Писать screenings. перед каждой колонкой утомительно. Таблице можно дать короткий псевдоним прямо во FROM, как колонке в прошлом уроке: screenings s, films f. Дальше в запросе пишешь s.id и f.title. Соединим сразу три таблицы и получим настоящую афишу:
SELECT s.id, f.title, h.name AS hall, s.starts_at
FROM screenings s
JOIN films f ON f.id = s.film_id
JOIN halls h ON h.id = s.hall_id
ORDER BY s.starts_at;
+----+-------------------------------------------+----------------+---------------------+
| id | title | hall | starts_at |
+----+-------------------------------------------+----------------+---------------------+
| 1 | Брат | Большой | 2026-10-23 19:00:00 |
| 3 | Москва слезам не верит | Большой | 2026-10-24 18:00:00 |
| 2 | Брат | Малый | 2026-10-24 21:30:00 |
| 5 | Сталкер | Малый | 2026-10-25 17:00:00 |
| 4 | Ирония судьбы | VIP | 2026-10-25 20:00:00 |
| 6 | Ёжик в тумане | Большой | 2026-10-26 11:00:00 |
| 7 | Новые имена | Малый | 2026-10-26 22:00:00 |
+----+-------------------------------------------+----------------+---------------------+
7 rows in set (0.00 sec)
Каждый следующий JOIN прицепляет ещё одну таблицу к уже собранной строке. Запросов на пять таблиц в жизни много, и все они устроены так же.
INNER JOIN теряет строки без пары
Просто JOIN в MySQL значит INNER JOIN, внутреннее соединение. Оно оставляет только те строки, для которых нашлась пара. Посмотри на первую афишу ещё раз: «Солярис» в ней нет. У него нет ни одного сеанса, скреплять его карточку не с чем, и она выпала.
С билетами то же самое. Билеты с покупателями:
SELECT t.id, t.seat, v.name
FROM tickets t
JOIN viewers v ON v.id = t.viewer_id
ORDER BY t.id;
+----+------+---------------------------+
| id | seat | name |
+----+------+---------------------------+
| 1 | A1 | Анна Ковалёва |
| 2 | A2 | Борис Орлов |
| 4 | C3 | Анна Ковалёва |
| 5 | A1 | Вера Ильина |
| 6 | A2 | Вера Ильина |
| 7 | D4 | Дарья Белова |
| 8 | V1 | Дарья Белова |
| 9 | V2 | Глеб Соколов |
| 10 | B1 | Борис Орлов |
+----+------+---------------------------+
9 rows in set (0.00 sec)
Билетов в базе одиннадцать, а в ответе девять. Пропали 3 и 11, которые продала касса: у них viewer_id равен NULL, а NULL не равен ни одному id. Если ты считаешь выручку таким запросом, ты недосчитался двух билетов и не получил ни одного предупреждения.
LEFT JOIN сохраняет всех слева
LEFT JOIN держит все строки левой таблицы, той, что стоит до слова JOIN. Если пары справа нет, на месте правых колонок будет NULL.
SELECT t.id, t.seat, v.name
FROM tickets t
LEFT JOIN viewers v ON v.id = t.viewer_id
ORDER BY t.id;
+----+------+---------------------------+
| id | seat | name |
+----+------+---------------------------+
| 1 | A1 | Анна Ковалёва |
| 2 | A2 | Борис Орлов |
| 3 | B5 | NULL |
| 4 | C3 | Анна Ковалёва |
| 5 | A1 | Вера Ильина |
| 6 | A2 | Вера Ильина |
| 7 | D4 | Дарья Белова |
| 8 | V1 | Дарья Белова |
| 9 | V2 | Глеб Соколов |
| 10 | B1 | Борис Орлов |
| 11 | E7 | NULL |
+----+------+---------------------------+
11 rows in set (0.00 sec)
Все одиннадцать билетов на месте, у кассовых вместо имени NULL. Теперь фильмы с сеансами, включая фильмы без сеансов:
SELECT f.title, s.starts_at
FROM films f
LEFT JOIN screenings s ON s.film_id = f.id
ORDER BY f.id, s.starts_at;
+-------------------------------------------+---------------------+
| title | starts_at |
+-------------------------------------------+---------------------+
| Брат | 2026-10-23 19:00:00 |
| Брат | 2026-10-24 21:30:00 |
| Москва слезам не верит | 2026-10-24 18:00:00 |
| Ирония судьбы | 2026-10-25 20:00:00 |
| Сталкер | 2026-10-25 17:00:00 |
| Ёжик в тумане | 2026-10-26 11:00:00 |
| Новые имена | 2026-10-26 22:00:00 |
| Солярис | NULL |
+-------------------------------------------+---------------------+
8 rows in set (0.00 sec)
«Солярис» вернулся с NULL вместо времени. Из этого получается частый приём: найти строки без пары. Оставляем только те, где правая сторона пустая:
SELECT f.title
FROM films f
LEFT JOIN screenings s ON s.film_id = f.id
WHERE s.id IS NULL;
+----------------+
| title |
+----------------+
| Солярис |
+----------------+
1 row in set (0.00 sec)
Проверяем s.id, первичный ключ правой таблицы. Он объявлен как NOT NULL, поэтому NULL в нём бывает только у строки без пары.
RIGHT JOIN и CROSS JOIN
RIGHT JOIN зеркален левому: сохраняет все строки правой таблицы. Зрители и их места, включая тех, кто ничего не купил:
SELECT v.name, t.seat
FROM tickets t
RIGHT JOIN viewers v ON v.id = t.viewer_id
ORDER BY v.id, t.id;
+---------------------------+------+
| name | seat |
+---------------------------+------+
| Анна Ковалёва | A1 |
| Анна Ковалёва | C3 |
| Борис Орлов | A2 |
| Борис Орлов | B1 |
| Вера Ильина | A1 |
| Вера Ильина | A2 |
| Глеб Соколов | V2 |
| Дарья Белова | D4 |
| Дарья Белова | V1 |
| Егор Ёлкин | NULL |
+---------------------------+------+
10 rows in set (0.00 sec)
Егор Ёлкин в ответе с NULL вместо места. Тот же результат даст viewers v LEFT JOIN tickets t, и так читать легче: главная таблица слева. Поэтому на практике RIGHT JOIN почти не пишут.
CROSS JOIN соединяет каждую строку с каждой, без условия. Это декартово произведение. Два фильма на три зала дают шесть пар, например чтобы составить сетку «какой фильм в каком зале можно поставить»:
SELECT f.title, h.name FROM films f CROSS JOIN halls h WHERE f.id <= 2 ORDER BY f.id, h.id;
+-------------------------------------------+----------------+
| title | name |
+-------------------------------------------+----------------+
| Брат | Большой |
| Брат | Малый |
| Брат | VIP |
| Москва слезам не верит | Большой |
| Москва слезам не верит | Малый |
| Москва слезам не верит | VIP |
+-------------------------------------------+----------------+
6 rows in set (0.00 sec)
Если в обычном JOIN забыть или перепутать условие, получишь такое же размножение строк, только случайно. Ответ, который вдруг вырос в разы, первым делом проверяй на это.
Условие в ON или в WHERE
Для INNER JOIN всё равно, где стоит условие: в ON или в WHERE. Для LEFT JOIN разница огромная. Задача: оба сеанса фильма «Брат» и оплаченные билеты на них.
Условие на статус внутри ON:
SELECT s.id, s.starts_at, t.seat
FROM screenings s
LEFT JOIN tickets t ON t.screening_id = s.id AND t.status = 'paid'
WHERE s.film_id = 1
ORDER BY s.id, t.seat;
+----+---------------------+------+
| id | starts_at | seat |
+----+---------------------+------+
| 1 | 2026-10-23 19:00:00 | NULL |
| 2 | 2026-10-24 21:30:00 | C3 |
+----+---------------------+------+
2 rows in set (0.00 sec)
Оба сеанса на месте. У первого все билеты уже used, оплаченных нет, и в колонке места NULL. То же условие в WHERE:
SELECT s.id, s.starts_at, t.seat
FROM screenings s
LEFT JOIN tickets t ON t.screening_id = s.id
WHERE s.film_id = 1 AND t.status = 'paid'
ORDER BY s.id, t.seat;
+----+---------------------+------+
| id | starts_at | seat |
+----+---------------------+------+
| 2 | 2026-10-24 21:30:00 | C3 |
+----+---------------------+------+
1 row in set (0.00 sec)
Первый сеанс исчез. WHERE работает после соединения и видит в t.status NULL у строки без пары, а NULL = 'paid' не истина. LEFT JOIN превратился в обычный JOIN. Правило: условие на правую таблицу, которое отбирает пары, пиши в ON. Подробный разбор с картинками есть в уроке для собеседования.
USING: когда колонки называются одинаково
Если связующие колонки в двух таблицах называются одинаково, вместо ON a.x = b.x можно написать USING (x). В выводе такая колонка появится один раз. В kino ключи названы по-разному (films.id и screenings.film_id), поэтому USING здесь не подходит. Вот что будет, если написать его не думая:
SELECT * FROM halls JOIN screenings USING (id) WHERE id = 1;
+----+----------------+-------+---------+---------+---------------------+--------+
| id | name | seats | film_id | hall_id | starts_at | price |
+----+----------------+-------+---------+---------+---------------------+--------+
| 1 | Большой | 120 | 1 | 1 | 2026-10-23 19:00:00 | 450.00 |
+----+----------------+-------+---------+---------+---------------------+--------+
1 row in set (0.00 sec)
Запрос сработал, но склеил зал номер 1 с сеансом номер 1, просто потому что совпали числа. Смысла в этой паре нет. USING хорош, когда колонка и правда одна и та же, например film_id в двух таблицах, которые обе ссылаются на фильм.
Что запомнить
JOIN ... ONсклеивает строки двух таблиц по условию, обычно по ключу и ссылке на него.INNER JOINвыбрасывает строки без пары,LEFT JOINоставляет всех слева и заполняет пустотуNULL.- Условие на правую таблицу в
WHEREпревращаетLEFT JOINобратно во внутреннее соединение. - Одинаковое имя колонки в двух таблицах требует псевдонима, иначе ошибка 1052.
USINGудобен, только когда колонки и правда означают одно и то же.
Прежде чем писать соединение, ответь себе на два вопроса. По какой паре колонок строки связаны? И нужны ли в ответе строки без пары? Первый ответ даёт условие ON, второй выбирает между INNER и LEFT. Порядок строк соединения без ORDER BY сервер выбирает сам, поэтому сортируй явно.
Попробуй сам
В песочнице база kino. Запрос собирает билеты с именем зрителя и названием фильма.
Замени LEFT JOIN viewers на JOIN и посчитай, сколько билетов пропало. Найди через LEFT JOIN и IS NULL зрителей без билетов. Добавь WHERE v.city = 'Москва' и подумай, куда делись кассовые билеты.
Проверь себя
Задание
Дальше
Мы научились собирать строки из нескольких таблиц. Следующий шаг: посчитать по ним итоги, сколько билетов продано на сеанс и сколько денег принёс каждый фильм. Об этом следующий урок про агрегаты и GROUP BY.
домашка