Перейти к содержанию
Educora
Продвинутый20 мин17 / 22

Операции над множествами и сложные соединения

Объединяй и сравнивай результаты запросов с помощью UNION, UNION ALL, INTERSECT и EXCEPT; соединяй таблицу саму с собой, строй все комбинации через CROSS JOIN и находи отсутствующее с помощью NOT EXISTS.

Проверь себя
В этом уроке ты узнаешь
  • Объяснять разницу между UNION, UNION ALL, INTERSECT и EXCEPT и соблюдать их правила
  • Строить пары и комбинации с помощью самосоединения (self join) и CROSS JOIN
  • Писать антисоединение с NOT EXISTS и избегать ловушки NOT IN с NULL

Школа хочет разослать одну и ту же рассылку и покупателям, и ученикам — нужен единый список адресов из двух таблиц. Директор спрашивает: «В каких городах у нас есть и ученики, и покупатели?», а отдел продаж ищет «покупателей, которые ни разу не брали электронику». На такие вопросы проще всего отвечать на языке множеств из математики: объединение, пересечение, разность. Для них в SQL есть специальные операторы, а несколько «необычных» видов соединений дополняют картину.

UNION и UNION ALL

UNION записывает результаты двух запросов друг под другом и убирает повторы, а UNION ALL их сохраняет. Правила: в обоих запросах должно быть одинаковое число столбцов совместимых типов; имена столбцов берутся из первого запроса; ORDER BY пишется один раз, в самом конце, и относится ко всему результату.

SQL
SELECT
  (SELECT COUNT(*) FROM (SELECT city FROM students
                         UNION
                         SELECT city FROM customers)) AS with_union,
  (SELECT COUNT(*) FROM (SELECT city FROM students
                         UNION ALL
                         SELECT city FROM customers)) AS with_union_all;
▸ Ожидаемый результат
with_union | with_union_all
11 | 19
12 учеников и 7 покупателей дают 19 записей о городах, но разных городов всего 11.
SQL
SELECT 'student' AS type, first_name AS name, city
FROM students
WHERE city = 'Bakı'
UNION ALL
SELECT 'customer', name, city
FROM customers
WHERE city = 'Bakı'
ORDER BY type, name;
▸ Ожидаемый результат
type | name | city
customer | Anar Mustafayev | Bakı
customer | Zəhra Hüseynli | Bakı
student | Aysel | Bakı
student | Kamran | Bakı
student | Leyla | Bakı
student | Rəşad | Bakı
Столбец с постоянным текстом (type) показывает, откуда взялась каждая строка.

INTERSECT и EXCEPT

INTERSECT возвращает строки, которые есть в обоих результатах (пересечение), а EXCEPT — строки, которые есть в первом результате, но отсутствуют во втором (разность). Оба убирают повторы. Ниже мы находим города, где есть и ученики, и покупатели, а затем города, где живут только ученики.

SQL
SELECT city FROM students
INTERSECT
SELECT city FROM customers
ORDER BY city;
▸ Ожидаемый результат
city
Bakı
Gəncə
SQL
SELECT city FROM students
EXCEPT
SELECT city FROM customers
ORDER BY city;
▸ Ожидаемый результат
city
Lənkəran
Naxçıvan
Quba
Sumqayıt
Şəki
ОператорВ математикеПоддержка
UNION / UNION ALLA ∪ Bвезде
INTERSECTA ∩ BSQLite, PostgreSQL, SQL Server, Oracle; MySQL с 8.0.31
EXCEPTA \ BSQLite, PostgreSQL, SQL Server; MySQL с 8.0.31; в Oracle — MINUS

Самосоединение и CROSS JOIN

При самосоединении одна и та же таблица используется дважды под двумя разными псевдонимами — как будто у неё есть две копии. Например, мы хотим познакомить учеников из одного города попарно. Условие a.city = b.city сопоставляет города, а a.id < b.id оставляет каждую пару только один раз.

SQL
SELECT a.first_name AS student_1,
       b.first_name AS student_2,
       a.city
FROM students AS a
JOIN students AS b ON b.city = a.city AND a.id < b.id
ORDER BY a.city, a.id, b.id;
▸ Ожидаемый результат
student_1 | student_2 | city
Aysel | Leyla | Bakı
Aysel | Rəşad | Bakı
Aysel | Kamran | Bakı
Leyla | Rəşad | Bakı
Leyla | Kamran | Bakı
Rəşad | Kamran | Bakı
Murad | Tural | Gəncə
Elvin | Orxan | Sumqayıt

CROSS JOIN намеренно строит все комбинации. Обычно это ошибка, но иногда именно то, что нужно: построив сетку «каждый ученик × каждый курс» и присоединив к ней через LEFT JOIN реальные зачисления, мы видим, кто куда не записан. Ниже такая матрица для восьмиклассников и курсов математики: NULL — зачисления нет.

SQL
SELECT s.first_name, c.title, e.score
FROM students AS s
CROSS JOIN courses AS c
LEFT JOIN enrollments AS e
       ON e.student_id = s.id AND e.course_id = c.id
WHERE s.grade = 8 AND c.subject = 'Math'
ORDER BY s.id, c.id;
▸ Ожидаемый результат
first_name | title | score
Leyla | Algebra | NULL
Leyla | Geometry | 95
Günay | Algebra | NULL
Günay | Geometry | NULL
Orxan | Algebra | NULL
Orxan | Geometry | 70

Антисоединение: NOT EXISTS

«Покупатели, которые ни разу не брали электронику» — это вопрос для антисоединения: оставляем строки левой таблицы, у которых нет пары в правой. Надёжнее всего записать его через NOT EXISTS: подзапрос для каждого покупателя спрашивает «есть ли заказ электроники?», и если ответ «нет», покупатель попадает в результат.

SQL
SELECT c.name, c.country
FROM customers AS c
WHERE NOT EXISTS (
  SELECT 1
  FROM orders AS o
  JOIN products AS p ON p.id = o.product_id
  WHERE o.customer_id = c.id
    AND p.category = 'Electronics'
)
ORDER BY c.name;
▸ Ожидаемый результат
name | country
John Carter | United Kingdom
Mehmet Kaya | Türkiye
Olga Ivanova | Russia

Обратный вопрос — «кто хотя бы раз купил электронику» — записывают через EXISTS (полусоединение). Если написать его обычным JOIN, Анар, у которого два заказа электроники, появится дважды; EXISTS возвращает каждого покупателя один раз, потому что прекращает поиск, как только нашёл пару.

Задание

Найди названия курсов, на которые записан хотя бы один ученик из Баку (Bakı) и хотя бы один из Сумгайыта (Sumqayıt). Пересеки результаты двух запросов с помощью INTERSECT и отсортируй по названию.

Задание · SQL
SELECT c.title
FROM courses AS c
JOIN enrollments AS e ON e.course_id = c.id
JOIN students AS s ON s.id = e.student_id
WHERE s.city = 'Bakı'
-- INTERSECT the same query for 'Sumqayıt'
ORDER BY title;
▸ Ожидаемый результат
title
Geometry
Mechanics
Задание

С помощью NOT EXISTS найди имена учеников, которые не записаны ни на один курс математики (subject = 'Math'), и отсортируй по имени.

Задание · SQL
SELECT s.first_name
FROM students AS s
WHERE NOT EXISTS (
  -- an enrollment of this student in a Math course
)
ORDER BY s.first_name;
▸ Ожидаемый результат
first_name
Elvin
Günay
Kamran
Rəşad
Tural

Главное

  • UNION убирает повторы, UNION ALL сохраняет их и работает быстрее; число и типы столбцов должны совпадать.
  • INTERSECT — пересечение, EXCEPT — разность; ORDER BY пишется один раз в конце всей конструкции.
  • При самосоединении таблица используется под двумя псевдонимами; a.id < b.id даёт пары без повторов.
  • CROSS JOIN + LEFT JOIN показывает все комбинации и то, какие из них пусты.
  • Для антисоединений выбирай NOT EXISTS: NOT IN ничего не возвращает, если в подзапросе есть NULL.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Чем UNION отличается от UNION ALL?