Зачем нужны связи и внешние ключи

Зачем нужны связи и внешние ключи

До этого момента все запросы работали с одной таблицей. Но в реальных базах данных информация распределена по множеству таблиц, и это не случайность — это фундаментальный принцип проектирования. Прежде чем разбирать синтаксис 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: самый распространённый вид соединения. Он возвращает только те строки, для которых в обеих таблицах есть совпадение.

Попробуйте интерактивную версию

Практические задачи, квизы и AI-наставник — бесплатный старт без карты

Перейти к практике