Всем привет! На связи Михаил Поливаха, технический лидер проекта Axelix.

Я уже выпускал статью по мотивам вопросов в рамках Spring Айо Академии. Так вот, когда мы разгребали отзывы с первого потока, мы также получили вопрос (это не прямая цитата, я лишь передаю посыл в том виде, как я его понял):

А что насчет натуральных ключей в БД? Если, допустим, у меня есть поле, по которому я могу явно идентифицировать запись, стоит ли его использовать как Primary Key?

И я за время дизайна enterprise систем, и за время дизайна Axelix пришёл к выводу: Просто не используйте натуральные ключи вообще никогда. Когда у вас появляется такое желание - выйдите на свежий воздух, пройдитесь, прогуляйтесь, и вас отпустит.

Я понимаю, что ответ категоричный, я дам к нему пару пояснений, что делать, если всё же есть уникальная колонка-дискриминант, по которой вы, как вам кажется, можете уникально идентифицировать запись в табличке в бд. Полный и развернутый ответ понятное дело сложнее, но если давать прямо TL;DR:

В дизайне новых систем, на мой взгляд, стоит всегда использовать суррогатные первичные ключи.

Теперь давайте к тому, почему я так считаю (опять же, это во многом мой опыт).

Правила, написанные кровью

Подобного рода правила выше, как правило, рождаются от того, что ты несколько раз обжигаешься на реальных проектах. У меня есть прямо идеальная история из Open Source Axelix (опять же, source code на GitHub, при желании, можете поизучать).

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

Axelix по сути состоит из двух компонентов - Master, это независимое приложение, которое представляет собой “мозг” системы. Оно разворачивается либо в K8S кластере, либо запускается как отдельный docker контейнер, либо вообще как просто JAR запускается.

Master агрегирует информацию из ваших Spring Boot сервисов и кладет к себе в БД. Эта информация нужна чтобы потом на основании нее понимать некоторую техническую зрелость вашей экосистемы, распределение версий каких-то ключевых компонентов (например, версий Spring Boot или Java), контроля известных проблем тех. долга и т.д.

Axelix Architecture
Axelix Architecture

Если мы представим себе типичную компанию, то у них, как правило, есть кластер K8S/OpenShift, в котором крутится их production. Практически всегда то или иное приложение на production задеплоено не в одном экземпляре, а в качестве некоторого набора Instance-ов (K8S Deployment + настроенный HPA и т.д.). То есть, фактически у нас есть логически одно приложение, физически представляющее собой набор разных контейнеров.

Теперь, я думаю, нам достаточно контекста для обсуждения проблемы.

Начало. Natural Keys - вполне себе!

Как я уже сказал, Axelix хранит данные в своей БД для понимания общего состояния вашего приложения. Назовём эту табличку “Application” (В реальности в Axelix эта абстракция называется по-другому, но опять же, я сильно упрощаю). И вот в ней хранятся данные на уровне приложения.

Master умеет получать данные из Spring Boot микросервисов посредством как push так и pull модели, но независимо от модели - он получает их на уровне Instance, а не на уровне Application, то есть всего приложения. То есть Master опрашивает каждый Instance, и это уже задача Master-а каким-то образом понять, что все те Instance-ы относятся к одному приложению.

Axelix Instances Discovery
Axelix Instances Discovery

Как Master-у это сделать? Как ему понять, что вот эти Instance-ы относятся к одному приложению? (не забывайте, Axelix не всегда разворачивается в K8S, и у нас есть пилотные команды, кто работает без K8S вообще. Надеяться на ClusterIP сервисы и т.п нельзя)

На самом деле, если немного подумать, то решение на поверхности - можно просто агрегировать информацию на уровне пары GroupID/ArtifactID из GAV координат (стандартный формат Maven дистрибутива). Все же instance-ы обязаны иметь один и тот же GroupID/ArtifactID, ведь так?

Axelix Instances Grouping
Axelix Instances Grouping

В целом конечно, это так. Возможно, кому-то может показаться, что можно ориентироваться на другие вещи, например, на spring.application.name или т.п - на самом деле, к сожалению, так не получится по ряду причин, но это другая история. Она нам сейчас не важна.

И вот представь себе, мы с тобой дизайним такое отношение в БД. У меня к тебе вопрос - какой первичный ключ ты бы хотел сделать для такой таблички? Когда мы дизайнили сущность “Application”, казалось правильным сделать первичным ключом как раз пару {groupId/artifactId}, т.е. Natural Composite Key.

И это же очень удобно! Когда в Axelix Master приходит информация по тому или иному Instance (неважно, посредством push или pull модели):

@Transactional
public void reloadCurrentState(BasicRegistrationMetadata metadata) {

    Application application = converter.currentSnapshot(metadata);

    jdbcAggregateTemplate.upsert(application);
}

А для front-end-а как замечательно получается! И тут мы приходим к тому, что сам по себе натуральный ключ имеет бизнес значение, и иногда нам этого значения достаточно, чтобы принять решение в логике!. Это кстати одно из действительно хороших свойств натуральных ключей.

Что я имею в виду? Например, в ситуации, когда нам нужно просто отобразить имя нашего “Application”, и нам достаточно только имени, то можно же использовать artifactId, т.е часть составного натурального ключа. Не надо ничего “дозапрашивать” и т.д.

Ну где же, в чём же тогда проблема? Неужели с учетом всего того, что я сказал ранее, эта проблема настолько критичная, что я утверждаю, что вообще не стоит использовать натуральные ключи? Да, настолько серьезная. И вот почему.

Так в чём же дело? Немного Философии

Чем старше человек становится, тем более ему свойственно сомневаться в тех или иных вещах (например, в моём утверждении в этой статье, кстати! И это нормально). Это связано с тем, что у людей появляется опыт.

Я буквально знал системы, где в специальной битовой int-овой маске хранился 1 бит, который представлял собой информацию о поле человека - мужчина или женщина. Не сложно догадаться, что случилось потом. И вот люди, кто уже довольно долго занимаются инженерией, накапливают опыт и понимают, насколько всё меняется, и насколько много они ещё не знают (опытные инженеры меня 100% сейчас понимают). Вендор приходит и уходит. Сотрудник тоже. Уникальность натурального ключа…

Рей Далио (потрясающий человек и макро-инвестор, очень рекомендую его почитать) в своей книжке “Principles” говорил:

Sincerely believe that you might not know the best possible path and recognize that your ability to deal well with “not knowing” is more important than whatever it is you do know

Это невероятная мудрость. Идея в том, чтобы принять тот факт, что Ваши знания о внешнем ограничены и они всегда будут кратно меньше чем множество тех вещей, которые вы не знаете, но которые влияют на вашу жизнь/систему и т.д. И самое важное в такой ситуации это уметь работать СО СВОИМ НЕЗНАНИЕМ чего-то, хеджировать риски.

Как это относится к натуральным ключам?

А очень просто: если тот или иной дискриминант тебе в моменте кажется очевидным ключом, просто вспомни о том, что область твоих знаний она несопоставимо мала с тем, чего ты не знаешь. И этот “инвариант”, на который ты возлагаешь надежды, который ты думаешь, что будет уникален - он очень просто через полгода уже может таким не быть.

Более того, область твоих знаний она ещё и будет расти. Со временем, ты (да именно ты, дружище) растёшь как инженер. Спустя время ты посмотришь на этот код, или на дизайн этой системы и скажешь:

Ну кааак!? Как я мог это сделать? Такое дерьмо ну просто атас, это же очевидно было, что такой ключ сломает уникальность в кейсе Х!

И это тебе будет очевидно. Но потом. Когда ты станешь мудрее. Кстати, если у тебя вот такие моменты “прозрения” в карьере не происходят, когда ты журишь себя за свои же решения в прошлом - это очень сильный звоночек о том, что ты остановился в своём развитии как специалист.

Обратно в Инженерию

Давайте вернемся чуть ближе к технической части.

Основная мысль прошлой секции в том, что кажущаяся сегодня уникальность натурального ключа, на деле, спустя время, легко может перестать ей быть. Теперь мыслим как инженеры - насколько это вообще плохо? Насколько плохо то, что мы ошибемся в том, что наш natural key (составной или нет - сейчас не так важно) - что вот он будет не уникален?

На самом деле у первичного ключа записи всегда (!) должны быть (помимо прочих) следующие две отличительные особенности:

1. Он должен быть immutable

Когда мы записи даем тот или иной ключ, по которому мы её идентифицируем - мы потом не имеем права его менять. Почему? Потому что внешний мир, который от нашей системы зависит, он хранит именно этот самый ID, первичный ключ, чтобы идентифицировать запись. Он хранит ссылку, а не саму запись.

Например, представь себе, что у тебя есть сторонний сервис, который хранит профили пользователей: user-service. И вот там решили использовать email в качестве natural key. Вы пишите сервис, который оркестрирует подписки пользователя на те или иные сервисы в рамках экосистемы. И вот вам надо для своих операций получить профиль пользователя из вот этой смежной вам системы. Каким образом вы его получать будете? Конечно же, по email! Это же “уникальный ключ”.

А теперь представим себе, что бизнес такой приходит и говорит: мы в нашем сервисе хотим дать возможность пользователю менять email, привязанный к аккаунту. То есть, по сути, у уже существующей записи в БД будет меняться identity. Поменяв ID у записи в этом user-service, любая другая система, в том числе ваша, не может теперь найти нужный ей профиль. Это будет массовый инцидент, причём тестами это отловить будет ой как непросто. Поэтому, ID должен быть immutable всегда.

2. Он должен уникально идентифицировать запись в любой момент времени t

А теперь представь себе, что вдруг парни, из вашей смежной команды, которая заведует user-service, получают требование. Им говорят:

Ребят, у нас иногда возникает такая ситуация, что пользователь когда-то создал какой-то аккаунт, привязал к нему свой email. И вот сейчас он хочет как-то старый аккаунт (который он создавал лет 10 назад) удалить, и создать новый, и привязать к нему ту же самую почту. Старый аккаунт мы удалять бы не хотели (В Enterprise по разного рода причинам Hard delete делают нечасто). Ну что, сделаем?

Тут проблема ещё более очевидная. Мало того, что теперь ваша система, зависящая от user-service не найдёт нужный профиль пользователя (их же может быть несколько!), все существующие контракты сломаются, и чтобы их “починить”, придётся дорабатывать ID (он ведь больше не уникален, теперь нельзя только на ID смотреть). А если нам придётся менять ID, то смотрите на пункт выше.

Причина смерти: Natural Id

Ошибки бывают разной степени тяжести. Бывают такие ошибки, которые имеют локальный эффект, и исправить которые можно относительно быстро и просто.

Но ключи, которые идентифицируют данные в распределенны системах - это то, что распространяется по всей распределённой системе в самые разные её уголки. Поэтому в момент, когда вы вдруг с ужасом осознаете, что natural id больше не уникален, так называемый blast radius будет фантастическим. Особенно в современной микросервисной архитектуре.

Поэтому, друзья, причины смерти у людей бывают разные. Кто-то умер от рака, кто-то умер от сердечной недостаточности. А кто-то просто выбрал Natural Id в качестве первичного ключа, а потом получил на почту email, или вдруг на daily услышал о том, что предположение уникальности этого ключа может пошатнуться.

Я предлагаю сделать минуту молчания перед чтением дальше, в память о тех инженерах, кто поплатился, выбрав Natural ID в качестве первичного ключа…

Спасибо.

Кейс Axelix

Вернемся к реальному кейсу, который был у нас в Axelix.

У нас хоть пока и не вышел GA (выйдет этим летом, мы активно над этим работаем), но у нас уже есть несколько Milestone релизов. Мы встаем к различным компаниям в контур, чтобы пособирать фидбек, возможные баги, проблемы и т.д.

И вот одна комания нам говорит:

А у нас знаете, у нас так получилось, что есть по сути два сервиса: сервис А и сервис B. Они по большому счёту одинаковые, просто задеплоены в разных сегментах сети. Там один и тот же groupId и artifactId. Тем не менее, сервис A сопровождает вот эта команда, а сервис B - вот эта.

Я все детали упростил, но заметьте, как много деталей выясняется уже потом, после того, как релиз встал в контур. Мы уже не можем идентифицировать приложение как мы хотели через пару artifactId/groupId. Вспоминаем Рея Далио!

… What exists within the area of “not knowing” is so much greater and more exciting than anything any one of us knows

Именно в такой ситуации нужно просить пользователей самим предоставить Axelix информацию о том, какой уникальный ID у того или иного приложения (например, в application.yaml, что в общем-то Axelix и делает).

Но ведь у Natural ID есть преимущества…

На моем опыте тот факт, что у Natural ID есть бизнес значение, которое можно где-то использовать (например, на UI отображать имя application в качестве artifactId, как я уже приводил пример из Axelix) - эта проблема решается просто проектированием API.

Иными словами, даже с суррогатными ключами, можно проектировать API таким образом, чтобы не пришлось дозапрашивать данные с backend-а, это не большая проблема (например, получить некоторую метаинформацию, положить в state manager на фронте и т.д. Там способов много).

Из того, что действительно важно в натуральных ключах - они заставляют вас думать об инвариантах ваших данных. То есть, например, по логике, если ваш email уникальный, то логично создать индекс как раз по нему (что, например, и сделает Postgres, когда вы попросите создать Primary Key). И чтобы не иметь два разных индекса, почему бы не сделать email primary key, ведь в таком случае, будет всего один индекс - лишь на email?

Это в целом валидный аргумент, но я скажу так - он того не стоит. Если вы не будете использовать email в качестве Natural ID, создавать ли вам уникальный индекс на email или нет - это уже решать надо в каждом конкретном случае. Я бы сказал, что для 95%+ кейсов ответ точно стоит и никаких проблем с этим не будет. Тем не менее, для больших write heavy систем, с большим количеством данных, это может создать определённый оверхед - но опять же, как правило, незначительный в масштабах системы.

Ну и финально, по поводу операций MERGE/INSERT ON CONFLICT. Их можно спокойно делать и не на основе первичного ключа, а на основе любого constraint, например, на основе UNIQUE constraint, который вы явно определите в миграции. Другой вопрос, что не все ORM в это умеют, но технически это возможно.

Выводы

На основе своего опыта могу вам сказать одно - помните, что область вашего незнания гораздо больше по природе, чем область вашего “знания”. Поэтому очень опасно строить предположение о том, что Natural ID, который вам в моменте кажется уникальным для той или иной записи, будет хорошим Primary Key.

Тем не менее, стоит признать, что основное преимущество Natural ID в том, что оно заставляет тебя думать о том, какие инварианты у твоих данных в целом есть. И эти инварианты должны давать тебе инсайты на тему того, как моделировать паттерны доступа и хранения данных, например - определять уникальные b+tree индексы для колонки email.

Помни - индексы и подобные вещи можно будет потом убрать без последствий для всей системы. Изменять же первичные ключи это dead end.

Комментарии (16)


  1. michael_v89
    24.07.2026 16:42

    Строго говоря, настоящих Natural ID не бывает. Любой объект может иметь такие же характеристики, как другой аналогичный объект. Natural ID это такой же искусственный ID, только придуманный для учета в другой системе или вообще не в компьютере.

    Кроме практических нюансов есть теоретическая причина, почему не стоит использовать Natural ID. Идентификатор объекта имеет внутреннюю природу, он нужен для самой системы, чтобы она могла различать объекты или распознавать один и тот же объект в разные моменты времени. Поэтому неправильно использовать для этого идентификаторы из внешних систем. Последовательные ID дают гарантированно разный идентификатор для новых объектов, и с ними проще работать с технической стороны, поэтому они удобны.

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


    1. anshdo
      24.07.2026 16:42

      Строго говоря, настоящих Natural ID не бывает. Любой объект может иметь такие же характеристики, как другой аналогичный объект. Natural ID это такой же искусственный ID, только придуманный для учета в другой системе или вообще не в компьютере.

      Именно! Всегда это говорил коллегам на поползновения использовать естественные первичные ключи. Испльзуя естественный ключ вы, на самом деле, используете искуственный ключ из другой учётной системы, которую вы НЕ КОНТРОЛИРУЕТЕ. И зачем вам такая зависимость?


  1. martin_wanderer
    24.07.2026 16:42

    С естественными ключами бывает ещё проще проблема: вот казалось бы куда уж естественнее ключ - серия/номер паспорта РФ. Даже оставляя за кадром как раз непостоянство законов человеческих, где сама система нумерации может поменяться в любой момент, что делать в том случае, если оператор ошибся при вводе этого самого естественного ключа? Редактировать ключ нельзя, он уже защит в какие-то ещё связи. Ничего нерешаемого, но морока


    1. imann
      24.07.2026 16:42

      Нельзя же.. меняем 20,45 лет, смена фамилии, утеря. И да, криво вносим или OCR.


      1. martin_wanderer
        24.07.2026 16:42

        Справедливости ради... Если это идентификатор именно паспорта, а не его владельца, то любые замены не проблема. А вот OCR дааа


        1. VladD-exrabbit
          24.07.2026 16:42

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


          1. miksoft
            24.07.2026 16:42

            В моей практике наоборот. Есть человек, а есть ДУЛ - документ, удостоверяющий личность. У одного человека может быть несколько ДУЛ, как одного типа (паспорт), так и разных. Каждый со своим временным интервалом действия.


  1. CEOCTO
    24.07.2026 16:42

    На мой взгляд, ключевая мысль статьи даже не про Natural vs Surrogate Key, а про то, что Primary Key — это контракт системы, а не просто способ обеспечить уникальность строки.

    Пока приложение живёт внутри одной БД, кажется, что изменить PK — не такая уж проблема. Но как только появляются внешние сервисы, кэш, Kafka, CDC, S3, аналитика или просто ссылки в других БД — PK становится частью публичного API.

    Именно поэтому стоимость ошибки здесь растёт не линейно, а практически экспоненциально.


    1. anshdo
      24.07.2026 16:42

      Есть, кстати, и такая школа мысли, что использовать первичный ключ в качестве ключа для внешних API — нехорошо. Это должно быть отдельное поле БД.


  1. Yankee2d
    24.07.2026 16:42

    Не стоит из своего ушибленного пальца делать религию с запретом на молотки.

    Мир намного больше, чем вы себе можете представить - кто-то может неплохо уметь обращаться с молотками.

    Overhead на реализацию и поддержку решения, где гвозди забиваются ватными тампонами, чтобы не ушибить ваш палец, может в итоге вырасти намного в больший размер за пределами вашей ответственности и так как вы это не видели, вам кажется что всё ок.

    Иногда заказчикам и эффективным менеджерам нужно говорить «нет». Когда вам говорят «а может мы можем как-то сделать, чтобы трактор перепрыгивал небольшие заборы высотой всего лишь до 3х метров?» - нужно сказать «нет», а не закладывать в каждый трактор изначально турбореактивный двигатель.

    Даже с вашим примером с емэйлом как идентификатором пользователя если вы начинаете любые хотелки удовлетворять, в итоге получается, что нужно городить интерфейсы по слиянию пользователей и разделению, сценарий подтверждения смены емэйла, так как все системы сейчас историчны и требуют siem и отслеживания, гораздо сложнее отслеживать потом действия. Из своей практики могу привести пример, как при инциденте было убито куча человеко-часов на выслеживание по системе пользователя, который несколько раз менял в учётке ФИО и логин и был футбол с ИБ и менеджментом «вы мне не то прислали, это про Петю, а нам про Васю надо!!!», «но он был тогда Вася, а сейчас Петя!».

    Иногда, если вас просят сменить айди экаунта заказчика, в котором три шестёрки, потому что заказчик суеверный, нужно говорить «нет». Или смириться с тем, что он создаст новый, а старый будет похоронен без связи с новым. Это дешевле, чем везде на будущее добавлять гибкость, DI, ключи в виде uuid, которые пользователи в выгруженных таблицах не могут глазами быстро сравнить и тд, бороться с ошибками и зализывать старые места, которые полагались на «on conflict do nothing”, а вы вдруг решили удалить индекс и сделать это поле не уникальным, бороться с join-ами и размерами бд и бэкапов, потому что «ну вот, трактор прыгает, но чёт теперь всё медленно работает» и тд.


    1. VladD-exrabbit
      24.07.2026 16:42

      С точки зрения юзеров, необоснованные констрейнты (не разрешим поменять емейл, потому что мы решили сделать из него primary key) выглядит банальным неосиляторством. Точно так же как запрет пробелов в имени пользователя, или запрет знаков <> в тексте комментария.


    1. SergeyProkhorenko
      24.07.2026 16:42

      Кодируйте UUIDv7 в выгузках в компактный текстовый формат. А вообще сравнивать идентификаторы глазами - это плохая идея. Пользователи вообще не должны видеть идентификаторы, ведь им нужны бизнес-данные. А для внешних потребителей нужно использовать стандартизованные классификаторы и преобразовывать суррогатные ключи в натуральные, действительные в отчетную дату.


  1. economist75
    24.07.2026 16:42

    Натуральным id не хватает натуральности и полноты. Это результат спешки, глупости, жадности айтишников и заказчиков стандартов.

    Вот возьмём ИНН. Чтобы он стал хорошим id, нужно было всего лишь чуть дольше думать, а затем утвердить стандарт в ООН, убрать регион, но добавить пару-тройку разрядов на код или координаты места рождения. И тогда ИНН стал бы уникальным. Его удалось бы присвоить даже всем умершим, а родившимся давать автоматически по месту рождения, полу, timestamp. С юр лицами - аналогично.

    Это сэкономило бы миллиарды долларов. Кто виноват в плохих, слабых стандартах, которые ломаются за несколько лет? - айтишники. Только они могут здраво оценить матем. ёмкость кодификаторов.

    Впрочем, есть примеры, когда стандарт пережил 45+ лет, не сломавшись. MIDI с невероятно медленным потоком 1500 бод до сих пор правдоподобно передает всю музыку, путем грамотного уплотнения и 16-ричной системы исчисления.


    1. Newbilius
      24.07.2026 16:42

      Насчёт midi - вы же в курсе наличия внутри разных версий стандарта и несовместимых расширений, из-за которой что-то может не играться или играться не так, как задумал автор? Хотели бы жить с ИНН, построенным по тому же принципу?)


  1. des1roer
    24.07.2026 16:42

    Когда то себе сохранял такую памятку https://des1roer.blogspot.com/2016/01/blog-post_95.html


  1. miksoft
    24.07.2026 16:42

    И вот там решили использовать email в качестве natural key. Вы пишите сервис, который оркестрирует подписки пользователя на те или иные сервисы в рамках экосистемы. И вот вам надо для своих операций получить профиль пользователя из вот этой смежной вам системы. Каким образом вы его получать будете? Конечно же, по email! Это же “уникальный ключ”.

    Лично сталкивался с тем, что на свежий ящик %.mail.ru начали приходить уведомления от одного регистратора домена, связанные с доменом другого их клиента, причем не самого завалящего в Москве в то время. Т.е. E-mail не такой уж и уникальный.