Первичный и внешний ключ: как связываются таблицы
Основы4 минРедакция Innovialabs
Коротко
Первичный ключ однозначно определяет каждую строку своей таблицы, а внешний ключ — колонка, ссылающаяся на первичный ключ другой таблицы; вместе они строят связи между таблицами и не дают базе сохранить ссылку на несуществующую строку.
Коротко о главном
- Первичный ключ (PK) — колонка или набор колонок, значение которых однозначно определяет строку в таблице. Он уникален, никогда не бывает
NULL, и в таблице он один. - Внешний ключ (FK) — колонка в одной таблице, которая ссылается на первичный ключ (или другую уникальную колонку) другой таблицы. Она говорит: «этот заказ принадлежит вот этому клиенту».
База следит за обоими. Нельзя вставить двух клиентов с одинаковым ID и нельзя создать заказ для несуществующего клиента. Это называется ссылочной целостностью.
CREATE TABLE customers (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE orders (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id BIGINT NOT NULL REFERENCES customers (id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
Естественные и суррогатные ключи
- Естественный ключ берётся из реального мира: номер паспорта, email, ИНН.
- Суррогатный ключ — искусственный ID, который генерирует база или приложение.
Для первичных ключей обычно выбирают суррогатные: значения из реального мира меняются — люди меняют почту, документы перевыпускают. Естественные идентификаторы храните обычными колонками с ограничением UNIQUE.
Типы связей
Один ко многим
Самый частый тип. У одного клиента много заказов, у каждого заказа один клиент. Внешний ключ ставится на стороне «многих»: orders.customer_id.
Один к одному
У одного пользователя один профиль. В таблицу профилей добавляют FK и делают его уникальным — часто он же становится первичным ключом:
CREATE TABLE profiles (
user_id BIGINT PRIMARY KEY REFERENCES users (id),
bio TEXT
);
Связь один к одному удобна, чтобы вынести редко используемые или чувствительные колонки в отдельную таблицу. Если данные всегда читаются вместе, одна таблица обычно проще.
Многие ко многим
Студент посещает много курсов, на курсе много студентов. Ни одна из таблиц не может хранить FK в одиночку, поэтому добавляют связующую таблицу (junction table):
CREATE TABLE enrollments (
student_id BIGINT REFERENCES students (id),
course_id BIGINT REFERENCES courses (id),
enrolled_at DATE NOT NULL,
PRIMARY KEY (student_id, course_id)
);
Составной первичный ключ не даёт записать одного студента на один курс дважды. В связующей таблице можно хранить и факты о самой связи, например дату записи.
Что происходит при удалении
Когда вы удаляете клиента, у которого есть заказы, поведение определяет внешний ключ:
| Вариант | Поведение |
|---|---|
RESTRICT / NO ACTION | Удаление не пройдёт, пока есть дочерние строки (по умолчанию) |
CASCADE | Дочерние строки удаляются вместе с родителем |
SET NULL | FK в дочерних строках становится NULL |
SET DEFAULT | FK получает значение по умолчанию |
customer_id BIGINT REFERENCES customers (id) ON DELETE CASCADE
CASCADE подходит для данных, которые без родителя не имеют смысла, например позиции заказа без заказа. Для ценных записей его лучше избегать: каскад от клиентов к заказам и платежам может незаметно стереть финансовую историю. Здесь предпочтительнее RESTRICT или мягкое удаление (колонка is_deleted или deleted_at).
Serial или UUID
| Serial / identity | UUID | |
|---|---|---|
| Размер | 4 или 8 байт | 16 байт |
| Читаемость | 1042 легко продиктовать и отлаживать | Длинная строка |
| Где генерируется | В базе | В базе или в приложении |
| Можно угадать | Да, ID идут подряд | Практически нет |
| Слияние данных, создание офлайн | Риск конфликтов | Уникальны между системами |
| Дружелюбность к индексу | Последовательные, хорошая локальность | Случайный UUIDv4 разбрасывает вставки |
Практические советы:
- Для большинства внутренних таблиц identity/serial — простой и эффективный вариант.
- Выбирайте UUID, если ID создаются на клиентах или в нескольких сервисах, если вы сливаете данные из разных источников или не хотите раскрывать количество записей.
- Если UUID — первичный ключ, присмотритесь к упорядоченным по времени вариантам, например UUIDv7: они держат вставки ближе друг к другу в индексе.
- Не считайте неугадываемые ID защитой — права доступа проверяйте всегда.
Частые ошибки
- Забыть индекс на колонке FK. PostgreSQL не создаёт его на ссылающейся колонке автоматически, а MySQL InnoDB создаёт. На больших таблицах JOIN и каскадное удаление становятся медленными.
- Несовпадение типов, например
INTв одной таблице иBIGINTв другой. - Отказ от FK «ради скорости» — в итоге появляются строки-сироты, ссылающиеся в никуда.
- Изменяемое значение, например email, в роли первичного ключа.