Pull to refresh

Comments 36

удобно в случае шардинга БД: у всех записей свои уникальные айдишники (можно перешардировать, можно смерджить две базы) — это основной плюс такого вида id

У меня всегда был глупый вопрос про uuid - точно уникальные? Что обеспечивает уникальность? Строка то конечная. Кажется, что коллизия вопрос времени. Проверяете ли вы существование uuid в базе после его генерации?

Спасибо за проявленный интерес!

Если говорить про UUIDv4, то в нем 122 бита отводится на случайную часть, что при хорошем генератора дает высокую вероятность уникальности. Согласно википедии:for a total of 2^122, or 5.3×10^36 (5.3 undecillion) possible version-4 variant-1 UUIDs.

В статье про спецификацию UUIDv7, описаны несколько вещей, которые используется для достижения уникальности у него.

Когда UUID является ключом, то БД сама проверяет при вставке на коллизию. Ситуаций с коллизиями пока не было.

Если это PK база сама проверит. И да, это все равно займёт время.

Насчёт уникальности. Ну это как 640кБ хватит всем. Проблему возможно мы просто отложили для будущих поколений. Потом это всплывёт как проблема 2000 года. Но это же потом и возможно без нас?

Вы совершенно верно отметили, что нет универсального решения.

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

Вероятность совпадения uuid-ов зависит от алгоритма генерации, но в худшем случае что-то порядка 1 раза в 30 лет, при условии, что генерируешь по 1 млн uuid-ов в секунду (пруфа не нашел). Т.е. специально проверять совпадение не нужно.

Много лет использую (по умолчанию) UUIDv7 и если честно забыл про INT в качестве PK, не вижу ни одного реального аргумента использовать INT поскольку он максимально неудобен.

  • Написать приложение в стиле DDD где сущность валидирует свое состояние в момент инстанцирования не получится (тк id будет null)

  • Нужно перенести данные с одной бд в другую? Тоже танцы с бубном

  • Шардинг, master-master и прочие вещи где нужно согласованность, последовательность и уникальность INT PK между различными экземплярами БД - головная боль.

  • Перебрать int иды злоумышленнику? Да не вопрос.

Вот реально не вижу ни одного преимущества (за исключением размера ну это дело наживное) у INT в виде PK перед UUIDv7, много лет юзаю UUIDv7, это очень удобно знать PK и уметь генерировать PK в приложении.

Бывает в некоторых сущностях бизнес просит идентификатор все же в виде INT потому что им так удобнее и читабельней, тут уже приходится либо действительно делать INT в виде PK либо оставлять UUIDv7, но делать рядом INT алиас, в общем по ситуации.

:-) В bigint ID (так же как и в символьных ID) при желании вы можете "впихнуть" что угодно... Номера шардов, дату время (с наносекундами) и все что необходимо базовой логике работы вашего приложения (зачастую "экономя" на дополнительных полях объекта)...
Но "по факту" выбор конечно за архитектором прикладного ПО в каждом конкретном случае... "Костыли" потом уже можно будет "подставить".... :-)

Согласна, тут получается составной тип id. Например, известный SnowflakeID.

"впихнуть" что угодно... Номера шардов, дату время (с наносекундами) и все что необходимо базовой логике

Это нарушение первой требований первой нормальной формы. В будущем такое решение может привести к весьма забавным спецэффектам.

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

Это что за шардирование где первичный ключ меняется?

Аргумент «а если разработчик тупит» я, простите, аргументом считать не буду, мне бы не пришло в голову сортировать по id как по дате, если контракта на содержание даты нет. Если есть как в MongoDB ObjectId — можно и использовать.

Ну там же комментатор пишет «впихнуть в ключ номер шарда». Так-то да, обычно шардирование двухуровневое: хеш-сумма — номер фрагмента, а потом консистентным хешированием или рандеву — номер шарда. Но иногда встречается и вот такое :))

➖ при вставке в БД сильно перестраивается индекс (происходит чтение и запись в разные страницы индекса), что замедляет вставку

Это полная ерунда. Как раз наоборот проблема возрастающего числового идентификатора в том, что при вставке куча транзакций конкурирует за единственный листовой блок. В PostgreSQL придумали какую-то специальную оптимизацию для этого случая, а Oracle позволяет создавать индексы с ключевым словом REVERSE, когда индексируется не число, а другое число, полученное перестановкой цифр в обратном порядке.

➖ запросы на чтение выполняются дольше, так как мало используется буферный кэш: нет "горячих частей", таких, как компактное хранение всех id текущего дня в ограниченном количестве блоков индекса.

И это ерунда. id текущего дня ищется по другому индексу — по дате.

Есть такая характеристика индекса как фактор кластеризации. Индекс по дате хорошо кластеризован, а по id — плохо. Если сделать возрастающие числовые идентификаторы, то индекс по id тоже будет хорошо кластеризован, но от необходимости индекса по дате это не избавит.

Полная ерунда это не различать две разные проблемы: замусоривание кэша БД/ФС неактуальными данными и запись в горячий участок индексного дерева.

Без анализа паттернов записи, схемы обновления индекса, устройства concurrency control и оценки необходимости кэша и влияния замусоривания на его эффективность сложно надёжно сказать что из этого хуже. А вот что «ерунда» видимо легко :)

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

Вы правы, это разные проблемы, и первая с алгоритмом формирования первичного ключа никак не связана. «Ерунда» — объединять две проблемы в одну.

Если бизнес-логика не требует поиска по дате, то второй индекс, конечно, не нужен. А вот если требует, то надо создать второй индекс, а не запихивать неявно дату в ключ. Ну элементарная же гигиена.

Какой тип id используется у вас на проекте?

У меня много "проектов" и везде разные подходы, как раз, по целесообразности. Поэтому опрос с единственным выбором не подходит.

uuid длинный и содержит дефисы, сложнее быстро копировать для разборов

nanoid - топ https://github.com/ai/nanoid/blob/HEAD/README.ru.md с переменной длиной, с кастомным алфавитом

плюс использовать в id префиксы, а-ля msg_, usr_ для разных сущностей, для относительно небольшого множества, например длину в 8 рандомных символов, а для ожидаемого большго множества - длину в 16, 24, 32 и т.д. рандомного символа

т.е. в данных, в логах будут id типа usr_ewndf32d отправил msg_835erjeiofdsf909 - читаемо и понимаемо

Хранится в базе скорее всего как строка, следовательно для больших данных менее пригоден чем 64-битные целые или UUID. После того как я прочитал что размер кода генератора в 130 байт против 423 байт для UUID подаётся как значительное преимущество, я заплакал и закрыл страницу.

все только и говорят большие данные, большие данные... а многие 2 млрд записей ни разу и не видели в БД

То что кто-то не видел это же не аргумент что так не бывает?

Это не так. RFC 9562 рекомендует хранить в бинарном формате, а в PostgreSQL хранится в специальном бинарном формате UUID - всего 128 бит.

Рекомендую пользоваться оптимизированными функциями-генераторами UUIDv7, встроенными в ядро СУБД. В PostgreSQL появится в 18 версии (сейчас на ревью), в MySQL уже есть, а в Clickhouse степень готовности непонятна. При отсутствии можно пользоваться реализациями в разных языках программирования или даже просто генерировать в SQL.

Что именно "это" не так?

Я не говорил что UUID не хранится в бинарном формате. Я говорил что nanoid скорее всего хранится как строка.

Nanoid это не UUID, это другой формат идентификатора. По количеству бит он помещается в 128 бит, но я в репозитории не вижу рекомендаций упаковке в UUID для хранения, то есть для пользователя это получается просто строка в 21 символ.

Да, используя знание о том, что алфавит этой строки -- URL-safe base64 можно сделать конвертер в массив байт (последние 2 бита видимо придётся добивать фиксированным значением, потому что 21*6=126), который можно хранить как UUID, но я не думаю что это стандартный паттерн использования. Я могу ошибаться, но документация мне не оставляет другого выбора =)

Прошу прощения, не понял, к чему относится Ваш комментарий

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

использует всего 64 бита (для bigint)

Увидев две цитаты выше вспомнил кейс из практики. У нас в одном легаси-приложение, попавшем к нам на редизайн, Id/PK справочников делали bigint, но значений там было обычно не более 100, а обычно и нескольких десятков. Но проблема в том что они связывались с таблицей типа "item" или "entity" у которых были внешние ключи на десятки справочников. И эти внешние ключи были тоже bigint разумеется.

А таблицы "item" или "entity" содержали миллионы записей. Как следствие они все содержали десятки bigint колонок с внешними ключами на справочники. То есть если одна из таблиц "item" содержит хотя б 10 внешних ключей на такие справочники, то мы получим 10 * 8 байт * 1000000 (число записей) = 8 * 10 ^ 7 = 80 МБайт информации. У нас таких таблиц было штук 25. Но если предположить что их хотя бы 5, то мы получим 5 * 80 Мбайт = 400 Мбайт в одной базе подобных внешних ключей. С учетом что мы использовали полные бэкапы раз в сутки и они хранились иногда до полугода, то набегал приличный бесполезный объем данных. За месяц могло набежать 12 ГБ. Конечно если вы в облаке как SaaS то проблем почти нет, а если как IaaS или у вас свои сервера, то надо внимательно следить за свободным местом на диске. А об этом как обычно забывают)) честно скажу забывали и мы и наши опсы )

Хотя если б для Id/PK справочников использовать хотя бы int (4байта), то можно итоговые 12ГБ "перегруза" перевести в 6ГБ, а если smallint (2 байта) то в 3ГБ. Tinyint использовать наверное уже рискованно, но тоже можно рассмотреть.

Это я все к чему ?) К тому что bigint способ хранения PK наверное самый популярный но далеко не самый эффективный. По хорошему нужно проводить мини анализ при его использование от таблицы к таблице. И не слушать крики коллег из разряда "у нас все Id это bigint".

Давным давно хотел провести анализ с списком рекомендаций, когда использовать bigint когда int когда smallint и так далее. Но так и сделал. Если об этом подумаете вы, раз уж взялись за эту тему, то будет замечательно )

64-битные идентификаторы проще генерировать без координации между генераторами чем 32-битные. А стоит ли задаваться вопросом «какой тип идентификатора выбрать» — это увеличение когнитивной нагрузки на разработчика. Некоторые предпочитают вообще везде UUID (чтобы не думать), но и на каждую коллекцию данных обдумывать размер может быть непродуктивно, особенно если предсказать рост объёма проблематично. Сменить тип идентификатора может быть трудно в работающей системе и выбор слишком короткого идентификатора может привести к неприятным последствиям.

Часто используется смешанный подход: автоинкерментные айди для плюсов (индексы и запросы) плюс генерится случайный айди (не обязательно uuid) для публичной части фронтенда. Таким образом мы скрываем айди на паблике и используем все преимущества целочисленных айди для бэкенда.

Подтверждаю этот подход и жизненность решения:
- Для организации связей в базе использую автоинкрементный ID как первичный ключ,
- а для работы с записями из внешних клиентских приложений дополнительный uuid.

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

можно видеть порядок добавления записей без поля createDateНе очень частный кейс, но может быть актуально, например, для справочников.

Только в монопольной среде. В конкурентной же запросто можно получить набор, в котором сортировки по id и по timestamp не совпадают.

Если каждый шард базы генерирует свои идентификаторы, то они будут пересекаться с другими шардами.

Только если чей-то гениальный ум велел всем шардам генерировать идентификаторы с единицы. Если поделить всё пространство 64-битного целого между всеми (ну сколько их там у вас?) шардами на равномерные блоки, то порядок вероятности пересечения сравним с порядком вероятности переполнения. Ну то есть - плюнуть и растереть.

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

Хорошая тема добавлю и я 5 копеек.

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

Во вторых, не смотря на появившийся uuid7, далеко не во всех БД он поддерживаеться, а вставка uuid4 в качестве первичного ключа быстро приводит к высокой фрагментации индекса. Если кто не помнит, то я напомню, первичный ключ не только уникальный, но еще и кластерный, во всяком случае в большинстве систем. Кластерный индекс подразумевает следование записей в порядке возрастания или убывания индекса. Если вы каждый раз получаете уникальное значение, которое больше или меньше предидущего, то вы реально попадаете на лютую деградацию при чтении данных на больших объёмах.

Ну и в третьих, это операции сравнения.

Пока у нас 64 битные процессоры, поэтому на сравнение объекта 128 бит нужно как минимум две операции сравнения. Да казалось бы не существенно, но умножте это на количество этих операций в контексте одного запроса, а в контексте всех запросов к БД? А если БД весит не 100 Гб, а скажем 1 ПиБ и она постоянно находиться под нагрузкой?

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

Вот есть BIGINT. 64 бита, и формально все случайны (правда, случайные я только в MS Access помню). И есть UUID. 128 бит, из которых только часть случайные, а часть - информационные (хотя и не совпадающие). Замените UUID с его 36-символьным представлением на соответствующее 40-циферное числовое представление. Пойдите дальше, поделите отображение на две части - истинно-случайную и информационную, представьте информационную (идентификатор узла, метка времени) в соответствующем формате. Шагните ещё дальше, и поделите поле на два - истинно случайное и алгоритмически заполняемое. Получите в итоге случайное число, от 92 до 122 битов длиной, и дополнительную нашлёпку, которой в принципе-то можно и пренебречь, и назначение которой в основном - обеспечение уникальности при последовательном, а не истинно случайном, алгоритме генерации первой части за счёт добавления некоей аппаратной характеристики узла-генератора и/или момента генерации.

В общем, с моей личной точки зрения основная разница состоит в количестве битов. А всё остальное - незначащие бантики.

А почему в статье не раскрыть тему ULID?

  • лексикографически сортируемые

  • содержат метку времени (иногда удобно иметь вместо отдельных полей даты создания)

  • субъективно удобочитаемы (возможно дело в верхнем регистре символов, но смотреть на столбец ULID визуально значительно приятней, чем на столбец с UUID)

  • исключается немаловажный фактор, когда кто-то может воткнуть в MySQL v4 вместо v7. Притом не факт, что это будет замечено прежде, чем станет критически поздно.

Так же я бы рассмотрел еще Hashid. Это, правда, уже совсем "другая история". Но может быть крайне полезным в своей нише. Если главная задача в уникальности и/или например читаемости (возможности удобно передать строку, которая бы не содержала спорно-читаемых (0/О, 1/I) символов.

Как мне кажется набор из выше указанных двух идентификаторов + UUID v7 - могут закрыть почти любой кейс. А UUID v4 уже можно отправить "на покой", он свою задачу успешно выполнил, проторив дорогу для более новых версий.

Ну а история про "хаккинг" сайта застройщика тронула конечно до глубины души

Пагинация делается для UI, и там почти всегда будет сортировка не по id, в по чему-то еще. И ее можно делать без offset с любыми id, которые можно сравнивать (даже с guid 4):

select * from users
order by name
where (name > "last name")
-- и, для случая когда name может повторяться
or (name = "last name" and id > lastId) 

Sign up to leave a comment.

Articles