Типы связей: один к одному, один ко многим, многие ко многим
Реляционная БД строится из таблиц, связанных между собой. Существует три фундаментальных типа связей — и то, какой тип вы выберете, определяет, где именно окажется внешний ключ и понадобится ли отдельная таблица.
1:1 — Один к одному
Каждой записи таблицы A соответствует ровно одна запись таблицы B и наоборот.
Пример: покупатель и его паспорт, пользователь и его профиль настроек.
customers (id) ──── customer_passports (customer_id)
Используется редко — обычно можно объединить в одну таблицу. Применяют для:
- разделения редко используемых данных (оптимизация — не тащить редко нужные колонки в каждый SELECT по основной таблице)
- разграничения прав доступа (например, зарплатные данные сотрудника — в отдельной таблице с более узким доступом)
Как это живёт в SQL: внешний ключ в дочерней таблице получает ограничение UNIQUE — без него связь превратится в обычную 1:N (один покупатель — много паспортов, что не имеет смысла).
CREATE TABLE customer_passports (
customer_id INT UNIQUE NOT NULL REFERENCES customers(id),
passport_number VARCHAR(20) NOT NULL
);
1:N — Один ко многим
Одна запись таблицы A связана с многими записями таблицы B.
Пример: один покупатель → много заказов, один магазин → много заказов.
customers (id) ──< orders (customer_id)
stores (id) ──< orders (store_id)
Это самый частый тип связи. Реализуется через внешний ключ (FK) в «многой» стороне — просто обычная колонка customer_id INT REFERENCES customers(id), без UNIQUE.
N:M — Многие ко многим
Запись таблицы A может быть связана со многими записями B, и наоборот.
Пример: заказ содержит много товаров, товар входит во много заказов.
orders (id) ──< order_items >── products (id)
Реализуется через промежуточную (junction) таблицу, которая хранит пары FK. У junction-таблицы обычно один из двух вариантов первичного ключа:
- составной PK прямо на паре FK:
PRIMARY KEY (order_id, product_id)— если пара уникальна по смыслу и в таблице нет собственных атрибутов, кроме этой пары; - свой SERIAL PK плюс
UNIQUE (order_id, product_id)— если у связи есть собственные данные и на неё удобно ссылаться по короткому id. Так устроенаorder_itemsв нашем датасете: у неё свойid, а не составной ключ, потому что у строки заказа есть свои атрибуты —quantity,unit_price,discount_pct.
Обязательность связи: «один» — это всегда «ровно один»?
Не всегда. У каждой связи есть ещё одно измерение — обязательна ли она:
| Обозначение | Смысл | Пример |
|---|---|---|
ровно один (1) | связь всегда есть, FK — NOT NULL | у заказа всегда есть магазин: orders.store_id NOT NULL |
ноль или один (0..1) | связь может отсутствовать, FK допускает NULL | у покупателя может не быть пригласившего: customers.referrer_id — допускает NULL |
Путать эти два случая — частая ошибка на этапе проектирования: если поставить NOT NULL там, где связь на самом деле необязательна, INSERT начнёт падать в легитимных случаях (новый покупатель без реферала).
Как определить тип связи?
Задайте два вопроса:
| Вопрос | Ответ «один» | Ответ «много» |
|---|---|---|
| Сколько B у одного A? | 1:1 или 1:N | зависит от следующего |
| Сколько A у одного B? | 1:1 или N:1 | N:M |
Пример: «Сколько заказов у одного покупателя?» → много. «Сколько покупателей у одного заказа?» → один. Итог: 1:N.
В нашем датасете
| Связь | Тип |
|---|---|
| customers → orders | 1:N |
| stores → orders | 1:N |
| orders → order_items | 1:N |
| products → order_items | 1:N |
| orders ↔ products (через order_items) | N:M |
| categories → products | 1:N |
| categories → categories (parent) | 1:N (самосвязь) |
Частые ошибки
- Делают N:M напрямую через два FK в одной строке (
orders.product_id) вместо junction-таблицы — тогда заказ физически не может содержать больше одного товара. - Забывают
UNIQUEна FK в связи 1:1 — без него ничего не мешает добавить вторую запись, и связь незаметно превращается в 1:N. - Ставят
NOT NULLна явно необязательную связь — см. раздел про обязательность выше.
Запомни
1:1 — редко, требует UNIQUE на FK. 1:N — самый частый случай, обычный FK. N:M — всегда через junction-таблицу с составным или собственным PK. Отдельно от типа связи — обязательность (NOT NULL vs допускает NULL).
Микро-практика
- У таблицы
employeesпоявляется необязательная связь «руководитель» (сотрудник может быть без начальника). Как объявить FK? →manager_id INT NULL REFERENCES employees(id)— безNOT NULL. - Один товар должен быть ровно в одной корзине активного заказа, а не в нескольких одновременно. Это 1:1 или 1:N между
cartsиcart_items? → 1:N — в одной корзине обычно несколько товаров, но связь идёт от корзины к позициям, а не наоборот.
Прочитайте урок до конца — прогресс засчитается автоматически