Перейти к содержанию
Innovialabs

Первичный и внешний ключ: как связываются таблицы

Основы4 мин

Коротко

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

Коротко о главном

  • Первичный ключ (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 NULLFK в дочерних строках становится NULL
SET DEFAULTFK получает значение по умолчанию
customer_id BIGINT REFERENCES customers (id) ON DELETE CASCADE

CASCADE подходит для данных, которые без родителя не имеют смысла, например позиции заказа без заказа. Для ценных записей его лучше избегать: каскад от клиентов к заказам и платежам может незаметно стереть финансовую историю. Здесь предпочтительнее RESTRICT или мягкое удаление (колонка is_deleted или deleted_at).

Serial или UUID

Serial / identityUUID
Размер4 или 8 байт16 байт
Читаемость1042 легко продиктовать и отлаживатьДлинная строка
Где генерируетсяВ базеВ базе или в приложении
Можно угадатьДа, ID идут подрядПрактически нет
Слияние данных, создание офлайнРиск конфликтовУникальны между системами
Дружелюбность к индексуПоследовательные, хорошая локальностьСлучайный UUIDv4 разбрасывает вставки

Практические советы:

  • Для большинства внутренних таблиц identity/serial — простой и эффективный вариант.
  • Выбирайте UUID, если ID создаются на клиентах или в нескольких сервисах, если вы сливаете данные из разных источников или не хотите раскрывать количество записей.
  • Если UUID — первичный ключ, присмотритесь к упорядоченным по времени вариантам, например UUIDv7: они держат вставки ближе друг к другу в индексе.
  • Не считайте неугадываемые ID защитой — права доступа проверяйте всегда.

Частые ошибки

  • Забыть индекс на колонке FK. PostgreSQL не создаёт его на ссылающейся колонке автоматически, а MySQL InnoDB создаёт. На больших таблицах JOIN и каскадное удаление становятся медленными.
  • Несовпадение типов, например INT в одной таблице и BIGINT в другой.
  • Отказ от FK «ради скорости» — в итоге появляются строки-сироты, ссылающиеся в никуда.
  • Изменяемое значение, например email, в роли первичного ключа.

Частые вопросы

Да. Заказ может одновременно ссылаться на клиента, адрес доставки и менеджера. Таблица может ссылаться и сама на себя, например employees.manager_id указывает на другого сотрудника.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.