SQLLab
Теория· 12 мин· 20 XP

Типы связей: один к одному, один ко многим, многие ко многим

Реляционная БД строится из таблиц, связанных между собой. Существует три фундаментальных типа связей — и то, какой тип вы выберете, определяет, где именно окажется внешний ключ и понадобится ли отдельная таблица.


1:1 — Один к одному

Каждой записи таблицы A соответствует ровно одна запись таблицы B и наоборот.

Пример: покупатель и его паспорт, пользователь и его профиль настроек.

код
customers (id) ──── customer_passports (customer_id)

Используется редко — обычно можно объединить в одну таблицу. Применяют для:

  • разделения редко используемых данных (оптимизация — не тащить редко нужные колонки в каждый SELECT по основной таблице)
  • разграничения прав доступа (например, зарплатные данные сотрудника — в отдельной таблице с более узким доступом)

Как это живёт в SQL: внешний ключ в дочерней таблице получает ограничение UNIQUE — без него связь превратится в обычную 1:N (один покупатель — много паспортов, что не имеет смысла).

sql
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:1N:M

Пример: «Сколько заказов у одного покупателя?» → много. «Сколько покупателей у одного заказа?» → один. Итог: 1:N.


В нашем датасете

СвязьТип
customers → orders1:N
stores → orders1:N
orders → order_items1:N
products → order_items1:N
orders ↔ products (через order_items)N:M
categories → products1: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 — в одной корзине обычно несколько товаров, но связь идёт от корзины к позициям, а не наоборот.

Прочитайте урок до конца — прогресс засчитается автоматически