Колонка не може посилатися на кілька рядків іншої таблиці, тож зв'язок «багато-до-багатьох» (статті ↔ теги, студенти ↔ курси) реалізують проміжною таблицею з двома зовнішніми ключами.
CREATE TABLE posts (id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, title text NOT NULL);
CREATE TABLE tags (id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text NOT NULL UNIQUE);
CREATE TABLE post_tag (
post_id bigint NOT NULL REFERENCES posts (id) ON DELETE CASCADE,
tag_id bigint NOT NULL REFERENCES tags (id) ON DELETE CASCADE,
PRIMARY KEY (post_id, tag_id)
);
CREATE INDEX post_tag_tag_id_idx ON post_tag (tag_id);
Ключові деталі:
- Складений первинний ключ
(post_id, tag_id)- не дає прив'язати той самий тег двічі і водночас є індексом для «теги статті». - Окремий індекс на
tag_id- для зворотного напрямку «статті з тегом». Складений ключ починається зpost_id, тож для пошуку заtag_idвін не допоможе. ON DELETE CASCADE- видалили статтю, зник і зв'язок. Без цього видалення статті падатиме на зовнішньому ключі.
Атрибути зв'язку. Якщо в самого зв'язку є дані - коли тег додано, хто додав, роль користувача в команді, - вони живуть у проміжній таблиці:
CREATE TABLE team_user (
team_id bigint REFERENCES teams (id),
user_id bigint REFERENCES users (id),
role text NOT NULL DEFAULT 'member',
joined_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (team_id, user_id)
);
У Laravel: belongsToMany з таблицею post_tag (назви моделей в однині, за абеткою), withPivot('role') і withTimestamps() для атрибутів, attach/detach/sync для зміни зв'язків.
Коли проміжна таблиця стає сутністю: якщо в неї з'явилися власна поведінка й багато полів (членство з історією й статусами), варто зробити з неї повноцінну модель з власним id.