Урок курса

Соединение трёх и более таблиц

SQL с нуля: бесплатный курс с практикой

Разберём, как соединить три таблицы в SQL и проследить, откуда берётся каждая строка результата.

Синтаксис двух JOIN подряд и логика цепочки соединений

Представьте, что нужно получить даты заказов, названия купленных товаров и количество единиц. Эти данные лежат в трёх таблицах: orders, order_items и products. Прямой связи между заказом и товаром нет: её хранит промежуточная таблица order_items. Такая цепочка описывается двумя JOIN подряд:

SELECT o.order_date, p.title, oi.quantity
FROM orders AS o
JOIN order_items AS oi ON o.order_id = oi.order_id
JOIN products AS p ON oi.product_id = p.product_id;

Первый JOIN прикрепляет позиции заказа к orders по условию o.order_id = oi.order_id. На этом шаге каждая строка результата описывает одну позицию конкретного заказа. Второй JOIN добавляет сведения о товаре по условию oi.product_id = p.product_id. Он опирается на oi.product_id — столбец таблицы, которая уже находится в цепочке.

Здесь ключевое правило: каждое условие ON связывает новую таблицу с теми, что уже упомянуты выше. Написать ON p.product_id = o.order_id синтаксически допустимо, но это сопоставит разные сущности и даст случайные пары либо пустой результат.

Логически каждый JOIN добавляет к цепочке одну таблицу. Физически оптимизатор может выбрать другой порядок выполнения для скорости, но на смысл результата это не влияет: запрос описывает, какие ключи связываются.

Если к этой цепочке нужно добавить клиента, перед order_items появляется ещё один JOIN: customers → orders → order_items → products. Число JOIN само по себе не говорит о плохой схеме. Проверять нужно другое: понятна ли цепочка ключей, верна ли кратность связей, не дублируется ли логика и что показывает план выполнения.

Как меняются строки после каждого JOIN

У заказа №1 две позиции: товар №10 в количестве 2 и товар №20 в количестве 1. В справочнике товаров №10 — клавиатура, №20 — мышь.

ЭтапСтроки результата
orders1 · 2026-11-01
orders JOIN order_items1 · 2026-11-01 · 10 · 2
1 · 2026-11-01 · 20 · 1
Ещё JOIN products2026-11-01 · Клавиатура · 2
2026-11-01 · Мышь · 1

После первого соединения заказ занимает две строки, потому что в нём две позиции. Второе соединение добавляет названия товаров. Здесь ключ products.product_id уникален, поэтому строк больше не становится.

Этот пример можно запустить отдельно в SQLite — данные уже заданы через CTE:

WITH orders(order_id, order_date) AS (VALUES (1, '2026-11-01')),
order_items(order_id, product_id, quantity) AS (VALUES (1, 10, 2), (1, 20, 1)),
products(product_id, title) AS (VALUES (10, 'Клавиатура'), (20, 'Мышь'))
SELECT o.order_date, p.title, oi.quantity
FROM orders AS o
JOIN order_items AS oi ON o.order_id = oi.order_id
JOIN products AS p ON oi.product_id = p.product_id
ORDER BY p.product_id;

Результат — две строки из последней строки таблицы выше. Если справочник содержит несколько записей с одним product_id, JOIN размножит соответствующую позицию заказа; дальше в уроке разберём, как это влияет на суммы.

Псевдонимы таблиц и ошибка ambiguous column

Когда в запросе участвуют три таблицы, одно и то же имя столбца почти неизбежно встречается в нескольких из них. Классический пример — столбец id или суррогатные ключи вида customer_id, order_id, product_id: они есть и в основной таблице, и в связанной.

Если написать просто order_id без указания источника, SQLite не знает, из какой таблицы брать значение, и возвращает ошибку:

ambiguous column name: order_id

Решение — квалифицировать имя псевдонимом: o.order_id, oi.order_id. Псевдоним объявляется прямо в FROM или JOIN через ключевое слово AS:

SELECT c.name        AS клиент,
       o.order_date  AS дата,
       p.title       AS товар
FROM   customers  AS c
JOIN   orders     AS o  ON c.customer_id = o.customer_id
JOIN   order_items AS oi ON o.order_id  = oi.order_id
JOIN   products   AS p  ON oi.product_id = p.product_id
ORDER BY c.name;

Здесь к разобранной выше цепочке добавлена четвёртая таблица customers, поэтому получилось три JOIN. Псевдонимы c, o, oi, p объявляются один раз и дальше используются везде: в SELECT, в условиях ON, в WHERE и ORDER BY. Полное имя таблицы после объявления псевдонима уже нельзя использовать — только псевдоним.

Столбцы, уникальные среди всех участвующих таблиц, технически можно писать без квалификатора. Но на практике единый стиль — всегда псевдоним.столбец — избавляет от ошибок при добавлении новой таблицы в запрос: имя, которое раньше было уникальным, может перестать им быть, и тогда SQLite завершит запрос ошибкой неоднозначного имени. Проще не делать исключений.

WHERE и ORDER BY применяются к результату всех JOIN

WHERE и ORDER BY в многотабличном запросе — не фильтр отдельной таблицы, а операция над уже собранным результатом всех соединений. Логически сначала формируется результат соединений, затем WHERE отсекает лишние строки, а ORDER BY сортирует остаток. Физический план может применить фильтр раньше или переставить JOIN ради скорости, сохраняя тот же результат.

Позиция клозов жёстко фиксирована синтаксисом: WHERE идёт после последнего JOIN, ORDER BY — в самом конце:

SELECT c.name        AS клиент,
       o.order_date  AS дата,
       p.title       AS товар
FROM   customers   AS c
JOIN   orders      AS o  ON c.customer_id = o.customer_id
JOIN   order_items AS oi ON o.order_id   = oi.order_id
JOIN   products    AS p  ON oi.product_id = p.product_id
WHERE  p.price > 10000
ORDER BY c.name;

Здесь p.price > 10000 фильтрует строки объединённого набора — те сочетания клиент–заказ–товар, где цена товара не превышает порог, просто не попадут в итог. ORDER BY c.name сортирует оставшееся по имени клиента.

В условии WHERE и в ORDER BY используются те же псевдонимы, что объявлены в FROM/JOIN. Сейчас WHERE price > 10000 тоже сработает: в показанной схеме имя price однозначно. Запись p.price предпочтительнее для ясности и защитит запрос, если позже в цепочке появится другой одноимённый столбец.

Почему сумма заказа удваивается после JOIN

Проверьте кратность до расчёта итогов. Если один заказ на 100 рублей содержит две позиции, JOIN с order_items создаст две строки: сумма заказа 100 повторится в каждой. Поэтому SUM(o.total_amount) после соединения даст 200, хотя заказ был один. Это не ошибка SQL: соединение сохраняет по строке на каждую позицию.

WITH orders(order_id, total_amount) AS (VALUES (1, 100)),
     order_items(order_id, quantity) AS (VALUES (1, 1), (1, 2))
SELECT COUNT(*) AS rows_after_join,
       SUM(o.total_amount) AS inflated_total
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.order_id;

Запрос вернёт 2 | 200. Для выручки по заказам агрегируйте таблицу orders без присоединения позиций; если нужна сумма товаров, считайте её на уровне строк order_items с соответствующей ценой. SUM(DISTINCT o.total_amount) не исправляет проблему: два разных заказа с одинаковой суммой схлопнутся. Перед добавлением третьего JOIN полезно сравнить количество строк до и после каждого соединения. Отдельная задача на условие JOIN помогает заметить случаи, когда число строк выросло из-за неверного ON.

ORDER BY может сортировать по любому столбцу из результата, в том числе по столбцу, которого нет в SELECT. Это работает, потому что сортировка применяется к строкам объединённого набора, а не только к тому, что выводится.

Задача 1

Таблицы: students (student_id PK, name), enrollments (enrollment_id PK, student_id FK, course_id FK), courses (course_id PK, title).

Какой запрос корректно возвращает имя студента и название курса, использует псевдонимы s, e, c и уточняет ими все обращения к столбцам?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 2

В запросе с тремя таблицами — orders (o), order_items (oi), products (p) — нужно оставить только строки, где quantity > 1, и отсортировать по цене товара по убыванию. Как правильно добавить эти условия?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 3

Запрос выполнится без синтаксической ошибки, если написать ON o.order_date = p.title вместо ON oi.product_id = p.product_id (оба столбца типа TEXT). Будет ли результат корректным? Ответьте одним словом: да или нет.

Без регистрации · результат не сохраняется

Задача 4

Таблица employees (employee_id PK, name, department_id FK), departments (department_id PK, dept_name, location_id FK), locations (location_id PK, city).

Сколько JOIN нужно написать, чтобы получить name сотрудника, dept_name отдела и city города в одном запросе?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 5

Строки заказов с книгами

Используйте три таблицы: orders(order_id, order_date), order_items(item_id, order_id, product_id, quantity) и products(product_id, title, category). Верните order_date, title, quantity только для строк с товарами категории Книги. Соедините таблицы по цепочке orders → order_items → products, задайте алиасы o, oi, p, а затем отсортируйте по o.order_date, o.order_id, oi.item_id по возрастанию.

РешениеSQL · SQLite

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задачи к уроку

Все задачи курса →

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку