Зачем нужны связи и внешние ключи
Зачем нужны связи и внешние ключи
До этого момента все запросы работали с одной таблицей. Но в реальных базах данных информация распределена по множеству таблиц, и это не случайность — это фундаментальный принцип проектирования. Прежде чем разбирать синтаксис JOIN, стоит понять, почему данные вообще нужно разделять на таблицы и как таблицы связываются друг с другом.
Проблема дублирования данных
Представьте таблицу заказов, в которой для каждого заказа записан полный адрес клиента:
id | customer_name | customer_email | customer_city | product | amount
---+---------------+--------------------+---------------+------------+-------
1 | Анна Иванова | anna@example.com | Москва | Ноутбук | 89990
2 | Борис Петров | boris@example.com | СПб | Мышь | 1200
3 | Анна Иванова | anna@example.com | Москва | Наушники | 7800
4 | Анна Иванова | anna@example.com | Москва | Клавиатура | 3500
Сразу видно три проблемы:
Дублирование. Анна Иванова сделала три заказа — и три раза продублированы её имя, email и город. Если она переедет в другой город, придётся обновлять все три строки. Забыли одну — данные противоречивы.
Аномалия обновления. При изменении email клиента нужно найти и обновить все его строки. Одна непоследовательно обновлённая строка — и данные неверны.
Аномалия удаления. Если Борис сделал только один заказ и его заказ удалить — исчезнет вся информация о Борисе, хотя мы, возможно, хотели сохранить клиента.
Решение: нормализация и внешние ключи
Решение — разделить данные на несколько таблиц. Каждая таблица хранит сущности одного типа:
-- Таблица customers
id | name | email | city
---+--------------+--------------------+-------
1 | Анна Иванова | anna@example.com | Москва
2 | Борис Петров | boris@example.com | СПб
-- Таблица orders
id | customer_id | product | amount
---+-------------+------------+-------
1 | 1 | Ноутбук | 89990
2 | 2 | Мышь | 1200
3 | 1 | Наушники | 7800
4 | 1 | Клавиатура | 3500
Теперь данные Анны хранятся один раз. В orders вместо полной информации о клиенте — только customer_id, ссылка на строку в customers. Это и есть связь между таблицами.
Первичный ключ (PRIMARY KEY)
Каждая таблица имеет колонку, которая однозначно идентифицирует каждую строку. Это первичный ключ (primary key).
В примерах выше колонка id в обеих таблицах — первичный ключ. Требования к первичному ключу: уникальность (не может быть двух строк с одинаковым id) и непустота (не может быть NULL).
Типичный выбор первичного ключа — целое число, которое автоматически увеличивается при каждой вставке. В PostgreSQL для этого используют тип SERIAL или BIGSERIAL. Подробнее о создании таблиц — в модуле 9, но для понимания JOIN достаточно знать: id — уникальный идентификатор строки.
Внешний ключ (FOREIGN KEY)
customer_id в таблице orders — это внешний ключ (foreign key). Он содержит значение первичного ключа из другой таблицы.
Внешний ключ создаёт ограничение: СУБД не позволит сохранить заказ с customer_id = 99, если в customers нет строки с id = 99. Это защита целостности данных — нельзя сослаться на несуществующего клиента.
Внешний ключ не обязательно называется _id — это соглашение об именовании. Главное — он содержит значение, которое есть в первичном ключе другой таблицы.
Виды связей между таблицами
Один к одному (1:1). Каждая строка в таблице A соответствует ровно одной строке в таблице B. Редко встречается. Пример: таблица users и таблица user_profiles — у каждого пользователя ровно один профиль.
Один ко многим (1:N). Один клиент может сделать много заказов, но каждый заказ принадлежит только одному клиенту. Это самый распространённый тип связи. Внешний ключ находится на стороне «многие» — в таблице orders.
Многие ко многим (M:N). Один студент может посещать много курсов, и один курс посещают много студентов. Для реализации нужна промежуточная таблица — «таблица соединений» (junction table):
students: id, name
courses: id, title
student_courses: student_id, course_id ← промежуточная
student_courses содержит пары (student_id, course_id) и создаёт связь M:N через две связи 1:N.
Как JOIN восстанавливает данные
Разделение данных по таблицам решает проблему дублирования, но создаёт новую задачу: как получить данные сразу из нескольких таблиц в одном запросе?
Для этого и нужен JOIN — оператор соединения таблиц. JOIN временно объединяет строки из разных таблиц в одну «большую» строку по условию совпадения значений:
SELECT o.id, c.name, o.product, o.amount
FROM orders o
JOIN customers c ON o.customer_id = c.id;
Результат:
id | name | product | amount
---+--------------+------------+-------
1 | Анна Иванова | Ноутбук | 89990
2 | Борис Петров | Мышь | 1200
3 | Анна Иванова | Наушники | 7800
4 | Анна Иванова | Клавиатура | 3500
JOIN «склеил» строки из двух таблиц там, где o.customer_id = c.id. В результате — знакомая удобная таблица, как если бы данные хранились вместе. При этом в самой БД они по-прежнему хранятся раздельно.
Проверь себя: если у клиента 5 заказов, сколько строк появится в результате JOIN для этого клиента?
Условие соединения ON
ON — это условие, по которому JOIN «склеивает» строки. Обычно это равенство первичного ключа одной таблицы и внешнего ключа другой:
JOIN customers ON orders.customer_id = customers.id
Синтаксически ON может содержать любое условие — как WHERE. Но семантически его назначение: описать, как строки двух таблиц соответствуют друг другу.
Когда имена соединяемых колонок в обеих таблицах совпадают, вместо ON col1 = col2 можно написать USING (col_name). Это сокращение, которое иногда встречается в коде — достаточно знать, что оно существует.
Алиасы таблиц
При соединении нескольких таблиц имена колонок легко запутываются. Алиасы (псевдонимы) таблиц делают запросы короче и читаемее:
SELECT o.id, c.name, o.product, o.amount
FROM orders o
JOIN customers c ON o.customer_id = c.id;
o — псевдоним для orders, c — для customers. Дальше в запросе можно использовать эти короткие обозначения вместо полных имён таблиц.
Зачем это важно понимать до изучения JOIN
Понимание первичных и внешних ключей — это фундамент. Когда вы пишете ON o.customer_id = c.id, вы говорите СУБД: «найди все пары строк, где внешний ключ в заказах совпадает с первичным ключом в клиентах». Без этого понимания JOIN превращается в магию с непредсказуемым результатом.
Второй важный момент: в реальных базах данных почти всегда есть несколько таблиц со связями. Запрос без JOIN — это обычно анализ одной сущности. Запрос с JOIN — это уже бизнес-аналитика: «покажи заказы вместе с именами клиентов», «покажи товары вместе с их категориями».
Типичная ошибка: декартово произведение
Если написать FROM table1, table2 без условия соединения — получите все возможные комбинации строк (декартово произведение):
-- ОПАСНО: декартово произведение
SELECT * FROM orders, customers;
-- 4 заказа × 2 клиента = 8 строк (вместо 4 нужных)
При 1000 строк в каждой таблице — миллион строк в результате. Всегда указывайте условие ON явно. Синтаксис JOIN ... ON делает намерение явным и защищает от этой ошибки.
Краткий итог
- Данные разделяются по таблицам, чтобы избежать дублирования и аномалий обновления/удаления
- Первичный ключ (PRIMARY KEY) — уникальный идентификатор строки в таблице
- Внешний ключ (FOREIGN KEY) — ссылка на первичный ключ другой таблицы
- Три типа связей: 1:1, 1:N (самый частый), M:N (через промежуточную таблицу)
JOIN ... ONсоединяет строки из разных таблиц по условию совпадения ключей- Без условия
ON— декартово произведение, обычно ошибка
Что дальше
Вы знаете, зачем нужны связи и как устроены ключи. Следующий урок — INNER JOIN: самый распространённый вид соединения. Он возвращает только те строки, для которых в обеих таблицах есть совпадение.