Подходы к генерации айдишников

Итак, у вас есть есть ваше приложеньицо и ваша бд.

Задача: вставить в бд данные и показать пользователю ссылку на вставленные данные. Типичный подход при таком сценарии: использовать автоинкрементные айдишники, которые автоматически возвращаются из бд и передаются на клиент. Этот подход заслуженно популярен, потому что в большинстве случаев его хватает с головой и думать не надо. Но у такого подхода есть некоторые ограничения, которые очень больно вас бьют время от времени.

Цель этой статьи показать альтернативные варианты и рассмотреть их достоинства и недостатки.


Пожалуй, начнем с истоков. В книжках и курсах по SQL вы скорее всего узнаете о понятиях “натуральный ключ” (“естественный ключ”) и “суррогатный ключ” (“искусственный ключ”). Это интересный подход, но у него есть некоторые ограничения:

  1. Не у всего есть натуральный ключ, а поддерживать две схемы ключей без лишней на то необходимости может показаться унылой тратой времени.
  2. То, что раньше могло быть натуральным ключом может перестать таковым быть. Причем это даже не обязательно должно быть проёбом краткосрочного планирования, тупо может измениться внешняя ситуация.
  3. Нельзя в качестве натурального ключа выбрать данные, которые могут измениться. И наоборот : если у вас что-то выбрано в качестве натурального ключа, вы не можете теперь это взять и легко поменять.
  4. На большом наборе данных операции с поиском/добавлением/удалением сущностей станут работать медленнее, потому что индексы в современных бд заточены под автоинкрементные последовательные ключи (дальше будет более подробное объяснение в рамках GUID-ключей).
  5. В реляционной таблице скорее всего на вашу таблицу будет ссылка хотя-бы с одной другой таблицы (и индекс для этой ссылки). А еще у вас с большой вероятностью будут вспомогательные индексы. Если ваш натуральный ключ занимает больше суррогатного ключа в байтах, то у вас любая операция на индексах будет работать чутка медленнее.

Поэтому натуральные ключи в практике не используются и даже не рассматриваются. Ну я не видел такого в продакшене, но я слишком юн и наивен.


Возвращаем к автоинкрементным ключам.

Если у вас работа с бд основана на хранимых процедурах (или чем-то наиболее близким по уровню), то общая схема работы вашего кода такова:

  1. С клиента приходит какая-то сущность, которую нужно создать.
  2. Вы передаете в хранимую процедуру данные.
  3. Хранимка создает данные.
  4. Хранимка получает от бд последний сгенерированный айдишник в этой транзакции.
  5. Хранимка возвращает либо айдишник, либо всю созданную сущность (мол, на случай если в бд что-то заполняется дефолтными значениями, отличными от null и 0). Вы скорее всего выберете что-то одно и не сможете быстро меняться между этими вариантами
  6. Вы генерируете и возвращаете ссылку/вьюху сущности.

Если у вас ORM, то схема такова:

  1. С клиента приходит какая-то сущность, которую нужно создать.
  2. Вы просите ORM принять факт создания сущности
  3. Вы просите ORM сохранить данные
  4. У сущности, которую вы хотели создать обновляется свойство-айдишник
  5. Вы генерируете и возвращаете ссылку/вьюху сущности.

А теперь о проблемах:

  1. Этот подход очень плохо работает, если вам нужно создавать сразу много данных транзакционно. Если вкратце, вы вынуждены на каждую сущность делать roundtrip между приложением и бд. А чем дольше вы держите открытой транзакцию, тем хуже вашей бд и вам. Возможно какая-нибудь ORM может решать эту проблему, но я знаком только с EF относительно давних времен, который не умеет решать эту проблему. Стоит заметить, что необходимость создавать сразу много транзакционно записей может быть вызвана необходимостью в фоне выполнять какие-то операции с максимальной производительностью .
  2. Этот подход плохо работает, если вам нужно создавать данные, которые зависят друг от друга. Т.е. если вам приходит сложная модель с фронта и вам нужно создать записи в таблице A и таблице B, так чтобы запись в таблице B ссылалась на свежесозданную запись в таблице А, то у вас проблема. На хранимках вам придется как-то это разруливать (а вариантов немного: либо вы получаете дополнительный roundtrip на каждую созданную сущность, либо вы пишите сложную хранимку, что моветон). ORM-мапперы чисто теоретически могли бы разруливать эту ситуацию, но EF не умеет.
  3. Если у вас совмещаются пункты 1 и 2, то вам дополнительно придется делать такую забавную вещь как сопоставлять айдишники созданных сущностей с сущностями, которые вы хотели создать. Это нетривиальная задача и это захламляет код.
  4. Если у вас есть необходимость создавать циклические данные, то у вас опять же проблемы, но чуток повеселее, чем в пункте 2.
  5. Offline first, Immutability, CQRS и прочие полезные концепты идут лесом. Вы ограничены в том, как вы можете писать код.
  6. Шардинг бд идет лесом.

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


Очень часто рекомендуют отказаться от автоинкреметных ключей в пользу GUID-ов. Это решает практически все вышеописанные проблемы. GUIDы могут генерироваться на клиенте (offline-first), могут генерироваться на сервере. Вы можете даже пользоваться Immutability и CQRS. Вы можете вставлять данные в бд большими пачками, у вас не будет проблем с созданием зависимых записей. С циклическими записями придется повозиться, но даже это станет проще.

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

Еще поговаривают, что гуиды затрудняют последовательный перебор сущностей хакером, но это уже security through obsecurity.

Но за все нужно платить.

GUID — это 128-битное случайное число. Именно поэтому GUID можно генерировать и на клиенте, и на сервере без страха, что у вас совпадут айдишники. На самом деле, совпадение айдишников возможно, но очень маловероятно. Если сравнивать вероятность коллизий айдишников и вероятность того, что вас изнасилуют и убьют до конца проекта, то я поставлю на второе. Более детальный анализ вы можете найти в статье “Руководство по GUID. Часть 3”.

И именно из того, что GUID это 128-битное случайное число проистекают многие проблемы:

  1. Если у вас автоинкрементный ключ в 32 бита, то у вас получится 96 бит (12 байт) оверхеда на каждую запись (ага, и это число умножить на количество вспомогательных индексов). Ну и конечно же, если у вас есть ссылки между записями, то на каждую ссылку еще плюс 12 байт.
  2. Индексы в бд не заточены под случайные числа, с ними ваша бд будет работать медленнее.
  3. Объем трафика бд<->сервер и сервер<->клиент увеличивается.
  4. Ваше приложение жрет чуточку больше памяти по сравнению с автоинкрементными айдишниками. Это также может дополнительно вылезти в проблемы по производительности из-за того, как устроена работа с памятью.
  5. Если у вас нету практики генерации человекочитаемых ссылок, то вы можете испортить ссылку унылыми GUID-ами вместо чисел. А еще есть системы, в которых айдишники отображаются прямо на UI.
  6. В статье “Уникальный ключ в условиях распределенной БД” от 2011 года приводится дополнительный аргумент о том, что некоторые другие систему могут не поддерживать 128-битные айдишники. Дальнейшую цитату я не проверял:
Sphinx search поддерживает только целочисленные идентификаторы документов (64 бита в 64-битной версии).

Но помимо обычных случайных GUID-ов есть концепция “последовательных гуидов”. GUID делится на две части — уникальная (время от времени обновляющаяся) и последовательная. В итоге мы получаем все плюсы гуидов, и не сильно проседаем по производительности (но по прежнему серьезно увеличиваем расходы по памяти). Реализацию и бенчмарки на скорость создания вы можете увидеть в статье “ID — Sequential Guid (COMB) Vs. Int Identity, using Entity Framework”.


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

Таким образом вы избавляетесь от проблем с массовыми вставками, от проблем связанных со вставкой зависимых данных (и от проблемы их комбинирования), вам больше не нужно делать дурацкие раундтрипы сервер<->бд. Ну и у вас остается возможность оставаться в парадигме CQRS.

Но по-прежнему частично может страдать Immutability и сильно страдает Offline-first. И увы, но этот механизм требует от пользовательского кода чуточку более сложного кода, нежели при автоинкрементных ключах и гуидах. Самая главная сложность заключается в том, чтобы начать переписывать уже существующий код.

В принципе, как только у вас появятся механизм для резервации айдишников вы можете плавно начать выкидывать автоинкрементные айдишники. А вместо прямого вызова Guid.NewGuid вы можете просить айдишник у identity reservatorа. И в результате у вас не будет необходимости поддерживать две разные схемы генерации.

Если у вас в качестве бд используется MS SQL 2011+, то вы можете начать с использования Sequence Numbers. В дальнейшем эту схему можно усложнять сколь вашей душе угодно. Если у вас другая бд — вы с легкостью можете реализовать какой-либо другой механизм.

В качестве дальнейших улучшений можно сделать следующее:

  1. Можно отказаться от Sequence Numbers в пользу счетчика в рамках какого-то быстрого NoSQL-решения.
  2. Если у вас есть несколько экземпляров приложения или несколько шардов бд, то можно предварительно раскидать целые отрезки айдишников на разные шарды, чтобы у вас не было нужды каждый раз дергать единую точку генерации. Прим.: эта идея не была проверена ни в продакшене, ни даже в pet-projects.
  3. Можно даже попытаться реализовывать offline-first подход вводя на клиенте генератор фейковых айдишников. Дальше, генератор айдишников на сервере должен запоминать соответствие (фейковый айдишник -> реальный айдишник). При окончании транзакции на клиент просто возвращать все такие пары. Плюс еще можно в командах на вставку автомагически заменять фейковые айдишники на реальные. Тогда у вас получится организовать максимально простой код на уровне контроллеров, скрыв всю магию в дебрях вашего кода. Прим.: эта идея не была проверена ни в продакшене, ни даже в pet-projects.

Увы, я не знаю адекватного подхода, который избавлен от всех вышеперечисленных недостатков. Если кто-то сообщит мне такой— я проставлюсь пивом :-)