Составные ключи в базе данных: когда они оправданы
Пара колонок вместо одного id может точно отражать уникальность данных, но увеличивает индексы и усложняет внешние ключи. Разбираем, когда составной ключ полезен, а когда лучше оставить его ограничением уникальности.

В таблице товаров продавца артикул может повторяться у разных продавцов, но пара «продавец + артикул» — уже нет. Составной ключ описывает такую уникальность точно, однако разносит эту пару по внешним ключам, индексам и запросам. В PostgreSQL 16 он оправдан, когда несколько колонок действительно составляют устойчивую идентичность записи, а не просто вместе встречаются в фильтре.
Что именно считается составным ключом
Первичный ключ из нескольких колонок требует, чтобы их комбинация была уникальной и не содержала NULL. В PostgreSQL объявление первичного ключа также создаёт уникальный индекс. Например, в таблице позиций заказа сочетание номера заказа и номера позиции может идентифицировать строку: номер позиции 1 встречается в разных заказах, но не должен повторяться внутри одного.
Не путайте ключ с индексом для поиска. Если приложение часто ищет запись по двум полям, это ещё не повод объявлять их первичным ключом. Индекс может ускорить запрос, но сам по себе не выражает бизнес-правило уникальности. И наоборот: первичный ключ обеспечивает правило и даёт базе индекс, но его состав и порядок колонок влияют на другие запросы.
Сноска. Суррогатный ключ — технический идентификатор, например числовой id, который назначает база или приложение. Он не несёт бизнес-смысла и не заменяет проверку уникальности предметных данных.
Три варианта и цена каждого
| Подход | Когда уместен | Цена |
|---|---|---|
| Составной первичный ключ | Несколько колонок вместе задают устойчивую идентичность: заказ и номер позиции, арендатор и локальный номер записи | Каждая ссылка на запись должна хранить все части ключа; изменения бизнес-значений могут затронуть связи |
| Суррогатный первичный ключ и составное ограничение уникальности | Записи удобно адресовать коротким id, но комбинация полей всё равно обязана быть уникальной | В схеме появляется дополнительный индекс и нужно следить, чтобы правило уникальности не потерялось |
| Один суррогатный ключ без ограничения на пару полей | Комбинация не является правилом предметной области или допускает повторы | База не защитит от случайного дубля, если он недопустим по бизнес-правилам |
Часто практичен второй вариант. У строки есть короткий технический идентификатор для ссылок, а отдельное ограничение не позволяет создать два товара с одинаковой парой продавец–артикул. Это не избыточность ради порядка: две конструкции отвечают на разные вопросы. Первичный ключ отвечает, как однозначно сослаться на строку; уникальность пары — какие данные нельзя дублировать.
Если составной ключ неудобно передавать по системе, это ещё не доказывает, что он неверен. Но если его части меняются, а ссылки на запись разбросаны по десяткам таблиц, цена такого решения уже становится архитектурной.
Когда составной ключ действительно помогает
Уникальность локальна, а не глобальна
Так устроены многие данные с областью действия: номер строки уникален внутри заказа, имя файла — внутри каталога, локальный номер сотрудника — внутри организации. В таком случае комбинация области и локального значения отражает правило напрямую. Одной колонки недостаточно: номер позиции 4 может существовать в сотнях заказов.
Похожий случай — таблица связи между двумя сущностями. Если пара «пользователь + группа» может встретиться только один раз, составной первичный ключ или уникальное ограничение на эту пару не даст случайно добавить участника дважды. Выбор зависит от того, должны ли на конкретную строку ссылаться другие таблицы. Если должны, отдельный id может упростить такие ссылки; если нет, пара часто и есть естественная идентичность связи.
Ключ устойчив и не разрастается
Хороший кандидат состоит из значений, которые редко меняются и имеют разумный размер. Если первичный ключ состоит из двух коротких целочисленных идентификаторов, цена передачи пары обычно предсказуема. Если это длинные текстовые значения, ссылки и индексы могут стать заметно тяжелее. PostgreSQL хранит ключевые значения в индексах, а внешние таблицы должны хранить все колонки, нужные для ссылки.
В таблицах-сателлитах, которые полностью принадлежат родительской записи, общий ключ тоже может быть естественным выбором: значение идентификатора родителя одновременно служит первичным ключом дочерней строки и внешней ссылкой. Но если дочерних строк у одного родителя может быть несколько, одного родительского id уже недостаточно — понадобится локальная часть ключа или отдельный id.
Когда лучше оставить один id
Суррогатный первичный ключ обычно проще, если предметные поля меняются, длинные или неудобны для передачи между сервисами. Например, электронная почта может выглядеть как естественный идентификатор пользователя, но её можно изменить. Привязать к ней все ссылки — значит сделать каждую смену адреса потенциально каскадной операцией.
Ещё один повод не делать бизнес-комбинацию первичным ключом — разные таблицы и API используют только часть этой комбинации. Если для ссылки на товар потребуются и id продавца, и артикул, их придётся хранить и передавать вместе. Короткий id упростит ссылку, а уникальное ограничение на продавца и артикул сохранит правило предметной области.
Не добавляйте составной ключ и ради «ускорения» без проверки. В PostgreSQL 16 многостолбцовый B-tree индекс особенно эффективен, когда запрос задаёт ограничения по его ведущим колонкам. Порядок полей поэтому выбирают по правилам уникальности и реальным запросам, а не по предположению, что «больше колонок — быстрее». Индекс может помочь одному шаблону запросов и почти не помочь другому.
Что ломается на практике
- Забыли правило уникальности. В схеме есть отдельный id, но нет ограничения на комбинацию предметных полей. Приложение проверяет дубли до вставки, однако два параллельных запроса могут пройти проверку одновременно. Защитное ограничение должно быть в базе.
- Перепутали порядок колонок. Само правило уникальности может оставаться корректным, но индекс хуже подходит для запросов, которые фильтруют только по колонке, стоящей не первой. Сверьте порядок с частыми запросами и планами выполнения.
- Ключ меняется вместе с бизнес-данными. Изменение части первичного ключа требует обновить связанные ссылки. Каскадное обновление может быть допустимым, но его нужно проектировать и проверять, а не обнаруживать при изменении названия или номера.
- Слишком широкая комбинация размножилась по схеме. Внешние ключи, индексы и запросы начинают повторять длинные значения. Оцените фактический размер индексов и стоимость операций на реалистичном объёме данных.
- Ключ приняли за проверку доступа. Наличие tenant_id в первичном или внешнем ключе помогает связать данные с арендатором, но не заменяет авторизацию. Запрос всё равно должен проверять, имеет ли текущий пользователь право читать именно эти строки.
Как выбирать для конкретной таблицы
- Сформулируйте правило словами. Какая комбинация значений не должна повторяться и почему? Если на этот вопрос нет ясного ответа, не делайте набор часто используемых фильтров первичным ключом.
- Определите, кто будет ссылаться на запись. Проверьте, готовы ли дочерние таблицы и внешние потребители хранить все части составного ключа. Убедитесь, что выбранные поля доступны там, где нужна ссылка.
- Проверьте устойчивость и размер значений. Уточните, может ли каждая часть ключа измениться, быть пустой или оказаться длинной. Если ключ отражает изменяемые данные, сравните его с вариантом «id плюс уникальность предметной комбинации».
- Сверьте ключ и запросы. В PostgreSQL 16 изучите планы частых запросов и размер создаваемых индексов на репрезентативном наборе данных. Смотрите, помогают ли ведущие колонки индекса нужным фильтрам; не делайте вывод по одному тестовому запросу.
- Проверьте конкурентную запись и связи. Убедитесь, что база, а не только код приложения, запрещает дубли. Прогоните сценарии вставки, обновления и удаления связанных строк, включая параллельные операции.
Границы решения
Составной ключ начинает мешать, когда его части часто меняются, длинны или нужны как ссылка за пределами одной таблицы. Также он не решает задачу глобальной идентичности автоматически: пара может быть уникальна только в рамках одного арендатора, а для интеграции с другой системой потребуется отдельно определить, как адресовать запись.
Не существует универсального числа колонок, после которого составной ключ становится плохим. Решение зависит от распределения данных, схемы связей и запросов. Без измерений не стоит обещать, что один вариант быстрее: сравнивайте планы выполнения, размеры индексов и время операций на данных, похожих на рабочие.
Что делать дальше
Для спорной таблицы выпишите отдельно два правила: как база должна находить строку и какие предметные значения не должны дублироваться. Затем сравните две схемы — составной первичный ключ и суррогатный id с уникальностью комбинации. В PostgreSQL 16 проверьте внешние ссылки и планы главных запросов; если предметные поля могут меняться или состав неудобен потребителям, оставьте короткий id, не отказываясь от ограничения уникальности.
Контент-конвейер: Из одной идеи — текст, изображение, озвучка и ролик. Один заказ вместо четырёх.
Спросить за 60 ₽Источники: PostgreSQL 16: ограничения целостности, PostgreSQL 16: многостолбцовые индексы
Частые вопросы
Составной ключ всегда лучше суррогатного id?
Можно ли использовать составной ключ только для ускорения запросов?
Нужно ли дублировать составной ключ в таблице, которая ссылается на запись?
Что выбрать для таблицы связи многие-ко-многим?
Меняется ли это поведение в другой версии PostgreSQL?
Как понять, что составной индекс не помогает?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.