Связи: один-к-одному, один-ко-многим, многие-ко-многим

Связи: один-к-одному, один-ко-многим, многие-ко-многим

В модуле 6 вы использовали JOIN для соединения таблиц. За каждым JOIN стоит тип связи между сущностями. Понимание типов связей — основа проектирования реляционной схемы. Неправильный тип связи делает запросы громоздкими, а данные — избыточными или теряющими целостность.

Зачем различать типы связей

Связь описывает, сколько записей одной сущности может быть связано со сколькими записями другой. Три фундаментальных типа:

  • Один-к-одному (1:1): каждая запись A связана ровно с одной записью B, и наоборот
  • Один-ко-многим (1:N): каждая запись A связана с несколькими записями B, но каждая B — только с одной A
  • Многие-ко-многим (M:N): каждая запись A может быть связана со многими B, и каждая B — со многими A

Тип связи определяет, как расположить данные в таблицах и где ставить внешний ключ.

Один-ко-многим (1:N): самый распространённый

Клиент может сделать много заказов, но каждый заказ принадлежит одному клиенту — это 1:N.

-- Один клиент (1) → много заказов (N)
CREATE TABLE customers (
    id   SERIAL PRIMARY KEY,
    name TEXT NOT NULL
);

CREATE TABLE orders (
    id          SERIAL PRIMARY KEY,
    customer_id INTEGER NOT NULL REFERENCES customers(id),
    total       NUMERIC(10, 2)
);

Внешний ключ (customer_id) всегда находится на стороне «многие» — в orders. Каждая строка orders ссылается на одного клиента; у одного клиента может быть много заказов.

Другие примеры 1:N:

  • Пользователь → много постов
  • Категория → много товаров
  • Отдел → много сотрудников
  • Заказ → много позиций заказа

Запрос: все заказы конкретного клиента:

SELECT o.id, o.total FROM orders o
WHERE o.customer_id = 42;

Один-к-одному (1:1): расширение сущности

Каждый пользователь имеет ровно один профиль; каждый профиль принадлежит ровно одному пользователю — это 1:1.

CREATE TABLE users (
    id       SERIAL PRIMARY KEY,
    email    TEXT UNIQUE NOT NULL,
    password TEXT NOT NULL
);

CREATE TABLE user_profiles (
    user_id    INTEGER PRIMARY KEY REFERENCES users(id),
    bio        TEXT,
    avatar_url TEXT,
    website    TEXT
);

user_id — одновременно PRIMARY KEY и FOREIGN KEY. PRIMARY KEY гарантирует уникальность — у одного пользователя ровно один профиль.

Связь 1:1 применяется, когда:

  • Одна сущность используется редко (профиль читается реже, чем базовые данные)
  • Данные секретны (платёжная информация в отдельной таблице)
  • Сущность появляется позже (профиль создаётся после регистрации)

Альтернатива — все данные в одной таблице. Решение зависит от паттерна использования: если профиль читается в каждом запросе, лучше хранить вместе.

Многие-ко-многим (M:N): через промежуточную таблицу

Студент может записаться на много курсов; на один курс могут записаться много студентов — это M:N.

В реляционной модели M:N нельзя реализовать напрямую. Нужна промежуточная таблица (junction table, cross-reference table):

CREATE TABLE students (
    id   SERIAL PRIMARY KEY,
    name TEXT NOT NULL
);

CREATE TABLE courses (
    id    SERIAL PRIMARY KEY,
    title TEXT NOT NULL
);

-- Промежуточная таблица
CREATE TABLE student_courses (
    student_id  INTEGER NOT NULL REFERENCES students(id),
    course_id   INTEGER NOT NULL REFERENCES courses(id),
    enrolled_at DATE DEFAULT CURRENT_DATE,
    PRIMARY KEY (student_id, course_id)
);

student_courses хранит каждую пару «студент-курс». Составной PRIMARY KEY (student_id, course_id) гарантирует, что студент не запишется на один курс дважды. Промежуточная таблица может хранить дополнительные атрибуты — enrolled_at, оценку, статус.

Другие примеры M:N:

  • Товары ↔ Заказы (позиции заказа — промежуточная таблица)
  • Авторы ↔ Книги
  • Теги ↔ Статьи
  • Актёры ↔ Фильмы (роли — промежуточная таблица с атрибутом role_name)

Запрос: все курсы студента с именем 'Анна':

SELECT c.title, sc.enrolled_at
FROM student_courses sc
JOIN students s  ON s.id = sc.student_id
JOIN courses c   ON c.id = sc.course_id
WHERE s.name = 'Анна';

Выбор места для внешнего ключа

ТипГде FKПример
1:NВ таблице «многие»orders.customer_id → customers.id
1:1В «расширяющей» таблицеuser_profiles.user_id → users.id
M:NВ промежуточной таблицеОба FK в student_courses

Самоссылающиеся связи (рекурсивные)

Иногда сущность ссылается на себя:

-- Дерево категорий
CREATE TABLE categories (
    id        SERIAL PRIMARY KEY,
    name      TEXT NOT NULL,
    parent_id INTEGER REFERENCES categories(id)  -- NULL = корневая категория
);

-- Иерархия сотрудников
CREATE TABLE employees (
    id         SERIAL PRIMARY KEY,
    name       TEXT NOT NULL,
    manager_id INTEGER REFERENCES employees(id)  -- NULL = нет менеджера
);

parent_id IS NULL — корневой узел дерева. Для обхода деревьев произвольной глубины используются рекурсивные CTE (изучены в модуле 7) или SELF JOIN (модуль 6).

Промежуточная таблица с атрибутами

Промежуточная таблица M:N может превращаться в полноценную сущность:

-- Не просто «пользователь добавил товар в корзину»,
-- а «позиция корзины» с количеством и ценой на момент добавления
CREATE TABLE cart_items (
    id             SERIAL PRIMARY KEY,
    user_id        INTEGER NOT NULL REFERENCES users(id)    ON DELETE CASCADE,
    product_id     INTEGER NOT NULL REFERENCES products(id) ON DELETE RESTRICT,
    quantity       INTEGER NOT NULL DEFAULT 1 CHECK (quantity > 0),
    price_snapshot NUMERIC(10, 2),  -- цена зафиксирована при добавлении
    added_at       TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE (user_id, product_id)
);

Промежуточная таблица с SERIAL PRIMARY KEY и атрибутами — полноценная сущность. Это нормально и часто необходимо в реальных схемах.

Ментальная модель связей

customers ──────< orders ──────< order_items >────── products
   1 : N                1 : N        (M:N через order_items)

users ──── user_profiles
   1 : 1

employees >────── employees
    рекурсивная 1 : N (менеджер → подчинённые)

──────< читается как «один к многим». В каждом случае FK находится на стороне «<».

Полиморфные связи: антипаттерн и его решение

Иногда возникает соблазн создать «универсальный» FK, который может ссылаться на разные таблицы:

-- Антипаттерн: комментарии к разным сущностям
CREATE TABLE comments (
    id          SERIAL PRIMARY KEY,
    content     TEXT NOT NULL,
    target_type TEXT NOT NULL,    -- 'post', 'photo', 'video'
    target_id   INTEGER NOT NULL  -- ID в соответствующей таблице
    -- target_id не может быть FOREIGN KEY!
);

Это называется полиморфной ассоциацией. Проблема: target_id не может иметь FOREIGN KEY — ссылочная целостность не гарантирована. PostgreSQL не знает, в какой таблице искать.

Правильные решения:

Вариант 1: отдельная таблица для каждого типа комментариев:

CREATE TABLE post_comments    (id SERIAL PK, post_id    INTEGER REFERENCES posts(id), content TEXT);
CREATE TABLE photo_comments   (id SERIAL PK, photo_id   INTEGER REFERENCES photos(id), content TEXT);

Вариант 2: базовая таблица сущностей:

CREATE TABLE commentable_items (id SERIAL PRIMARY KEY, type TEXT NOT NULL);

CREATE TABLE posts   (id INTEGER PRIMARY KEY REFERENCES commentable_items(id), title TEXT);
CREATE TABLE photos  (id INTEGER PRIMARY KEY REFERENCES commentable_items(id), url TEXT);

CREATE TABLE comments (
    id      SERIAL PRIMARY KEY,
    item_id INTEGER NOT NULL REFERENCES commentable_items(id),
    content TEXT NOT NULL
);

Второй вариант сложнее, но обеспечивает целостность и единый интерфейс для комментариев.

Мощность (cardinality) и обязательность

Кроме типа связи (1:1, 1:N, M:N) важно указать обязательность:

  • Обязательная связь: customer_id NOT NULL — заказ обязательно должен принадлежать клиенту
  • Необязательная связь: manager_id без NOT NULL — CEO не имеет менеджера

Обязательность определяет, нужен ли NOT NULL на FK-колонке. Пропустить это — значит допустить «потерянные» строки (заказ без клиента).

Для M:N промежуточной таблицы оба FK обычно NOT NULL — запись связи без обоих участников лишена смысла.

Проверь себя

Как смоделировать связь «пользователь может лайкнуть несколько постов, пост может получить лайки от многих пользователей»? Какой тип? Что хранить в таблице лайков кроме FK?

Краткий итог

  • 1:N — FK в таблице «многие»; самый распространённый тип связи
  • 1:1 — FK + PRIMARY KEY в «расширяющей» таблице; для разделения данных
  • M:N — промежуточная таблица с двумя FK; составной PK предотвращает дубли
  • Промежуточная таблица может содержать атрибуты связи (дата, статус, количество)
  • Рекурсивная связь: FK ссылается на PK той же таблицы (parent_id)
  • Обязательность связи определяет NOT NULL на FK

Что дальше

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

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

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

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