SQL (Structured Query Language) — язык запросов к реляционным базам данных: им создают таблицы, кладут и достают строки, а нескольких таблиц связывают между собой. В этой статье я разберу два блока основ: работу с одной таблицей (CREATE TABLE, INSERT, SELECT, WHERE) и соединение таблиц — виды JOIN (INNER, LEFT, RIGHT, FULL, CROSS, SELF) с примерами и результатом каждого запроса.
Содержание
- Создаем таблицу: CREATE TABLE
- Наполняем данными: INSERT
- Выборка: SELECT и WHERE
- Зачем нужны соединения
- INNER JOIN — только совпадения
- LEFT JOIN — все строки левой таблицы
- RIGHT JOIN — все строки правой таблицы
- FULL OUTER JOIN — объединение обеих сторон
- CROSS JOIN — декартово произведение
- SELF JOIN — таблица сама с собой
- Порядок выполнения и производительность
- Поддержка видов JOIN по СУБД
- Выводы
- Где применяется / связь с практикой
- FAQ
Реляционная база — это набор таблиц, где строка описывает объект, а колонка — его свойство. Отдельная таблица редко хранит все сразу: клиенты в одной, их заказы в другой. JOIN нужен, чтобы собрать из них единый результат по общему полю (ключу). Примеры я привожу на стандартном SQL; где диалекты расходятся, помечаю это явно.
Создаем таблицу: CREATE TABLE
Таблицу описывают командой CREATE TABLE: имя таблицы, список колонок и тип каждой колонки. Заведу две таблицы — клиентов и их заказы. Они пройдут через все примеры ниже.
CREATE TABLE customers (
customer_id INTEGER PRIMARY KEY,
name VARCHAR(50) NOT NULL
);
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
customer_id INTEGER,
amount INTEGER,
FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);
Здесь PRIMARY KEY — первичный ключ, уникальный идентификатор строки. FOREIGN KEY (внешний ключ) в orders указывает, что customer_id ссылается на клиента. Колонку orders.customer_id я оставил без NOT NULL намеренно: это позволит завести заказ без клиента (гостевой), и на нем будет видно поведение внешних соединений.
Замечание про индексы по диалектам: под PRIMARY KEY СУБД сама создает уникальный индекс. А вот под FOREIGN KEY индекс автоматически создается не везде — например, в PostgreSQL под внешний ключ индекс не создается, его добавляют вручную, а в MySQL/InnoDB создается. Это стоит держать в голове при оптимизации соединений.
Наполняем данными: INSERT
Строки добавляют командой INSERT INTO. Заполню обе таблицы так, чтобы разные виды JOIN дали разный результат.
INSERT INTO customers (customer_id, name) VALUES
(1, 'Anna'),
(2, 'Boris'),
(3, 'Vera'),
(4, 'Gleb');
INSERT INTO orders (order_id, customer_id, amount) VALUES
(101, 1, 500),
(102, 1, 300),
(103, 2, 700),
(104, NULL, 200);
Что здесь спрятано специально:
- у клиента Anna (id 1) два заказа, у Boris (id 2) один;
- у Vera (id 3) и Gleb (id 4) заказов нет вовсе;
- заказ 104 гостевой —
customer_idв немNULL, ему не соответствует ни один клиент.
Именно эти три случая (клиент без заказов, заказ без клиента) и разводят виды соединений.
Выборка: SELECT и WHERE
SELECT достает данные, WHERE фильтрует строки по условию. Простейшая выборка всех клиентов:
SELECT customer_id, name FROM customers;
customer_id | name
------------+-------
1 | Anna
2 | Boris
3 | Vera
4 | Gleb
Добавлю фильтр — только заказы дороже 250:
SELECT order_id, amount FROM orders WHERE amount > 250;
order_id | amount
---------+-------
101 | 500
102 | 300
103 | 700
Заказ 104 с суммой 200 в результат не попал — условие amount > 250 для него ложно. Важная деталь про NULL: сравнение вида column = NULL не работает, NULL проверяют через IS NULL / IS NOT NULL. Это пригодится ниже.
Зачем нужны соединения
Данные разложены по двум таблицам, а вопрос общий: «кто из клиентов что заказал». Ответ требует связать строки customers и orders по общему полю customer_id. Это и делает JOIN — соединяет строки двух таблиц по условию (обычно равенство ключей) в одну результирующую строку.
Различать стоит две вещи. Условие соединения (ON ...) — по какому признаку строки считаются парой. Вид соединения (INNER/LEFT/RIGHT/FULL) — что делать со строками без пары. Дальше я иду по видам на одних и тех же данных.
INNER JOIN — только совпадения
INNER JOIN возвращает лишь те пары строк, где условие соединения выполнено в обеих таблицах. Строки без пары отбрасываются. Слово INNER можно опустить: просто JOIN означает INNER JOIN.
SELECT c.name, o.order_id, o.amount
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
ORDER BY o.order_id;
name | order_id | amount
------+----------+-------
Anna | 101 | 500
Anna | 102 | 300
Boris | 103 | 700
Три строки. Vera и Gleb выпали — у них нет заказов. Гостевой заказ 104 тоже выпал: его customer_id равен NULL, а NULL не равен ни одному значению, условие не выполняется. Псевдонимы c и o (алиасы после имени таблицы) сокращают запись, дальше пользуюсь ими.
LEFT JOIN — все строки левой таблицы
LEFT JOIN (полное имя LEFT OUTER JOIN, слово OUTER необязательно) возвращает все строки левой таблицы, а из правой подставляет совпадения. Если пары в правой нет — ее колонки заполняются NULL.
SELECT c.name, o.order_id, o.amount
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
ORDER BY c.customer_id, o.order_id;
name | order_id | amount
------+----------+-------
Anna | 101 | 500
Anna | 102 | 300
Boris | 103 | 700
Vera | NULL | NULL
Gleb | NULL | NULL
Теперь строк пять: Vera и Gleb остались, потому что они в левой таблице, но их заказы — NULL. Это самый частый вид соединения на практике: «покажи всех клиентов и их заказы, включая тех, кто еще ничего не купил».
Из LEFT JOIN получается и типовой прием «анти-соединение» — найти строки левой таблицы без пары справа. Условие ставят на колонку правой таблицы через IS NULL:
SELECT c.name
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL;
name
------
Vera
Gleb
Так находят клиентов без заказов, товары без продаж и подобные «пробелы».
RIGHT JOIN — все строки правой таблицы
RIGHT JOIN (RIGHT OUTER JOIN) — зеркало LEFT: возвращает все строки правой таблицы, из левой подставляет совпадения, а где пары нет — NULL.
SELECT c.name, o.order_id, o.amount
FROM customers c
RIGHT JOIN orders o ON c.customer_id = o.customer_id
ORDER BY o.order_id;
name | order_id | amount
------+----------+-------
Anna | 101 | 500
Anna | 102 | 300
Boris | 103 | 700
NULL | 104 | 200
Четыре строки — по числу заказов. Гостевой заказ 104 попал в результат, но имя клиента у него NULL: подходящего клиента нет. Vera и Gleb, наоборот, исчезли — в правой таблице (orders) их нет.
На практике RIGHT JOIN встречается реже: тот же результат обычно пишут через LEFT JOIN, поменяв таблицы местами. Диалектная деталь: RIGHT JOIN и FULL JOIN поддерживает не всякая СУБД (таблица в конце статьи).
FULL OUTER JOIN — объединение обеих сторон
FULL OUTER JOIN возвращает все строки обеих таблиц: где пара есть — соединяет, где нет — подставляет NULL с недостающей стороны. По сути это объединение результатов LEFT и RIGHT.
SELECT c.name, o.order_id, o.amount
FROM customers c
FULL OUTER JOIN orders o ON c.customer_id = o.customer_id
ORDER BY c.customer_id, o.order_id;
name | order_id | amount
------+----------+-------
Anna | 101 | 500
Anna | 102 | 300
Boris | 103 | 700
Vera | NULL | NULL
Gleb | NULL | NULL
NULL | 104 | 200
Шесть строк: три пары с совпадением, два клиента без заказов (Vera, Gleb) и один заказ без клиента (104). FULL OUTER JOIN удобен, когда важны обе несостыковки сразу — и «клиенты без заказов», и «заказы без клиента».
CROSS JOIN — декартово произведение
CROSS JOIN соединяет каждую строку левой таблицы с каждой строкой правой. Условия ON у него нет. Если в левой a строк, а в правой b, результат — a x b строк.
SELECT c.name, o.order_id
FROM customers c
CROSS JOIN orders o;
У меня 4 клиента и 4 заказа, значит результат — 16 строк (первые три показаны для иллюстрации):
name | order_id
-----+---------
Anna | 101
Anna | 102
Anna | 103
... | ... (всего 16 строк)
Это и есть декартово произведение. Применяют его редко и осознанно — например, чтобы сгенерировать все сочетания (размеры x цвета). Случайный CROSS JOIN на больших таблицах дает взрывной рост строк, поэтому за пропущенным условием соединения следят особо.
SELF JOIN — таблица сама с собой
SELF JOIN — не отдельный вид, а прием: таблицу соединяют с ней же, дав ей два разных псевдонима. Классический случай — иерархия «сотрудник — руководитель» в одной таблице.
CREATE TABLE employees (
emp_id INTEGER PRIMARY KEY,
name VARCHAR(50),
manager_id INTEGER
);
INSERT INTO employees (emp_id, name, manager_id) VALUES
(1, 'Ivan', NULL),
(2, 'Petr', 1),
(3, 'Olga', 1);
Чтобы к каждому сотруднику подставить имя его руководителя, соединю employees с employees. Беру LEFT JOIN, чтобы не потерять сотрудника без руководителя:
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.emp_id;
employee | manager
---------+--------
Ivan | NULL
Petr | Ivan
Olga | Ivan
У Ivan руководителя нет (manager_id = NULL), поэтому в колонке manager стоит NULL. У Petr и Olga руководитель — Ivan. Псевдонимы e (сотрудник) и m (руководитель) здесь обязательны: без них СУБД не отличит одну «копию» таблицы от другой.
Порядок выполнения и производительность
Читается запрос сверху вниз, а логически выполняется иначе: сначала FROM и JOIN (формируют набор строк), затем WHERE (фильтрует), и только потом SELECT. Отсюда следствие: WHERE o.order_id IS NULL работает уже после соединения — на результате JOIN, а не на исходной таблице.
Для скорости соединений полезны индексы по колонкам из ON. Индекс под первичный ключ создается автоматически, под остальные колонки (в том числе внешние ключи в ряде СУБД) — вручную. План запроса стоит смотреть через EXPLAIN целевой СУБД: он покажет, использован ли индекс.
Поддержка видов JOIN по СУБД
Синтаксис соединений в основном общий, но крайние виды поддержаны не везде. Ориентиры на сентябрь 2026:
| Вид JOIN | PostgreSQL 18 | MySQL 8 | SQLite |
|---|---|---|---|
| INNER JOIN | да | да | да |
| LEFT JOIN | да | да | да |
| RIGHT JOIN | да | да | с 3.39 (2022) |
| FULL OUTER JOIN | да | нет (эмулируют через UNION) | с 3.39 (2022) |
| CROSS JOIN | да | да | да |
В MySQL FULL OUTER JOIN собирают вручную — объединением LEFT JOIN и RIGHT JOIN через UNION. Старые сборки SQLite (до 3.39) не знают RIGHT/FULL JOIN, поэтому эти виды доступны не в любой сборке.
Выводы
- Работа с одной таблицей строится на четырех командах:
CREATE TABLE(структура),INSERT(данные),SELECT(выборка),WHERE(фильтр). - JOIN связывает таблицы по общему полю; условие соединения (
ON) и вид соединения — разные вещи: первое задает пару, второе решает судьбу строк без пары. - INNER JOIN оставляет только совпадения; LEFT/RIGHT сохраняют все строки одной стороны; FULL OUTER — обеих; несостыковки заполняются
NULL. - CROSS JOIN дает декартово произведение (a x b строк), SELF JOIN — прием соединения таблицы с собой для иерархий.
- RIGHT и FULL JOIN есть не во всех СУБД: в MySQL FULL эмулируют через UNION, в SQLite они появились с версии 3.39.
Где применяется / связь с практикой
Соединения — повседневный инструмент аналитика, бэкенд-разработчика и дата-инженера: собрать заказы с именами клиентов, найти товары без продаж, построить иерархию сотрудников, свести данные из нескольких таблиц в один отчет. Понимание, что именно возвращает каждый вид JOIN и как ведет себя NULL, отличает рабочий запрос от тихо неверного.
Освойте тему на практике
Разобрать соединения, подзапросы, агрегаты и оптимизацию на практических задачах помогает курс SQL для разработчиков и аналитиков. Посмотреть формат и уровень занятий можно на бесплатных открытых уроках Otus — там разбирают реальные запросы вживую.
FAQ
Чем JOIN отличается от INNER JOIN?
Ничем: JOIN без уточнения означает INNER JOIN. Слова INNER, а также OUTER в LEFT/RIGHT/FULL OUTER JOIN можно опускать — на результат это не влияет.
Почему строка с NULL в ключе не попадает в INNER JOIN?
Условие соединения проверяет равенство, а NULL не равен ничему, даже другому NULL (результат сравнения — «неизвестно»). Поэтому строки с NULL в колонке соединения выпадают из INNER JOIN и требуют внешнего соединения, если их нужно сохранить.
Как получить FULL OUTER JOIN в MySQL?
Прямой поддержки нет. Результат собирают объединением: LEFT JOIN двух таблиц UNION RIGHT JOIN тех же таблиц. UNION уберет продублированные совпадающие строки.



