Комментарии 25
Сгенерированные UUIDv7 имеют все преимущества UUID и при этом упорядочены по дате и времени создания.
В пределах одной миллисекунды за счет random-ной части можно получить неупорядоченные ключи для UUIDv7. Это надо учитывать. Они, безусловно, лучше подходят для кластеризации и т.п., но если нужен возрастающий ключ на базе v7 - надо постараться.
Уже постарались. Возрастание ключа обеспечивается счетчиком между таймстемпом и случайной частью
Да, но это надо знать и уметь. Так-то, если просто генерировать, в пределах ms будут не монотонно-возрастающие значения, по крайней мере, в библиотеке Uuid в Rust, если просто генерировать.
Надо глянуть на крейт Uuid7, потому что в крейте Uuid монотонности нет.
https://crates.io/crates/uuid7 Это очень хорошая реализация от одного из самых активных контрибьюторов RFC9562
По самому RFC - отличные новости, давно этого ждал. Теперь можно будет добавить реализации в стандартные библиотеки языков и в СУБД, и выкинуть UUIDv4 со всеми его проблемами.
По статье - в кучу свалены и полезные заметки по поводу реализации (устойчивость к переводу времени, наличие счётчика после миллисекунд и.т.п.), так и какая-то странная отсебятина (цвета выделения в интерфейсе, какие-то идентиконы, слияние дубликатов). К UUID отношения не имеет, в RFC ничего такого нет, и непонятно, зачем это здесь.
"Странная отсебятина", как Вы выразились (а от кого ещё должен писать автор, как не от себя?), - это ответные меры на гораздо более странные претензии к UUID, с которыми постоянно сталкиваются те, кто пытается использовать UUID в информационных системах. Бизнесу, программистам, тимлидам и прочим участникам разработки непривычны и неудобны многие аспекты UUID, и эти люди очень успешно противодействуют использованию UUID. И RFC9562 действительно ничего не говорит о том, как избежать такого противодействия, а посвящен только очень узкому вопросу генерации UUID. Так что в статье вполне уместно осветить меры по "продвижению" UUID.
Чтобы "продать" это разработчикам, нужно описать более понятные для них преимущества:
Т.к. UUIDv7 значения монотонно возрастают (по крайней мене первые 48 бит), БД при построении статистики по таблице могут увидеть корреляцию со значениями других столбцов (с датами, со другими числовыми значениями), и генерировать более оптимальные планы выполнения запросов.
Из-за все тех же возрастающих значений B-tree индексы перестает раздувать, и вставка выполняется быстрее.
UUIDv7 можно будет использовать как primary key для партиционированной таблицы - ключом партиционирования можно сделать вшитый в него timestamp. При доступе к конкретной записи вместо фуллскана всех партиций будет выполняться поиск только в той, которой принадлежит этот timestamp. С UUIDv4 же нужно дополнительное поле с timestamp, которое придется явно добавлять во все фильтры, и в случае PRIMARY KEY добавляет головной боли.
Выделение индексированных и неиндексированных столбцов в интерфейсе разными цветами и без UUID можно реализовать. Да и фича выглядит крайне сомнительной, давать пользователям в руки фильтрацию по полю без индекса - прямой путь к тормозящим запросам на стороне БД.
Слияние дубликатов - дубликаты же не по одному id определяются, а по комбинации других полей. UUID тут ничего нового не даст, дубликаты вообще не обязательно прилетают в один и тот же промежуток времени, чтобы его внутренний timestamp тут чем-то помогл.
Сквозной поиск объекта по его UUID во всей базе/API - без вшитого в идентификатор типа записи это довольно проблематично реализовать, нужен кастомный генератор значений + функция для извлечения типа из id. Возможно проще использовать идентификаторы вида {type}:{uuid}
, как это делают например в GraphQL Relay.
Фильтрация или выделение имен столбцов с типом данных UUID и UUID_192 цветом шрифта - это не для пользователей, а для системных аналитиков. Сильно облегчило бы работу. Сразу стало бы видно, какие столбцы могут быть первичными или внешними ключами при массовом использовании UUID в качестве ключей. В таблицах с сотнями полей искать потенциально ключевые поля глазами очень долго.
Автоматическое слияние дубликатов UUID - это, например, для тех случаев, когда один и тот же клиент был зарегистрирован в системе дважды под разными ID. Речь идет не о дубликатах записей, как Вы подумали, а о том, что разными ID обозначен один и тот же объект. Возможно, термин "дубликат" в данном случае не очень удачный. В "исторических" таблицах с версионостью записей один и тот же ID может встречаться в десятках записей. Исправление ID в десятках записей вручную трудоемко (в смысле организационной работы, а не написания примитивного SQL-запроса), может спровоцировать ошибки и сомнительно с точки зрения информационной безопасности.
"Сквозной поиск объекта по его UUID во всей базе/API - без вшитого в идентификатор типа записи это довольно проблематично реализовать" - я долго пользовался именно таким сервисом в спецдепозитарии. Очень удобно и экономит уйму времени. Как именно это было там реализовано, я не знаю. Тип записи, который для этого действительно желателен, - это метаданные, для которых предусмотрен опциональный сегмент составного идентификатора справа от UUID в столбцах БД (пункт 6 в статье).
Спасибо за комментарий. По поднятым в нем вопросам я внес уточнения в текст статьи.
Я последние годы использую это https://www.2ndquadrant.com/en/blog/sequential-uuid-generators/ , конечно появление uuid7 в базе предпочтительнее... Еще отметил для себя https://github.com/fboulnois/pg_uuidv7 - но не использовал ибо 1й вариант устраивает
Нативная функция uuidv7() со счетчиком и с монотонностью внутри миллисекунды появится в 18 версии PostgreSQL предположительно в сентябре 2025 года. Сейчас для PostgreSQL практически есть только https://github.com/fboulnois/pg_uuidv7, но в этой реализации монотонности внутри миллисекунды нет и не будет. Ещё вариант - генерить UUIDv7 не в БД, а в приложении, и тогда возможно обеспечить монотонность внутри миллисекунды ценой компромиссов.
И зачем?
Если нужно генерировать меньше чем очень много id в одну миллисекунду, то производительности обычных монотонных последовательностей вашей любимой БД будет за глаза. Их и надо использовать в таком случае. Они понятны и удобны.
Значит есть смысл рассматривать только генерацию множества id в одну миллисекунду. А там нет ни монотоннсти, ни хороших индексов БД.
И в итоге оно опять не нужно. Я лучше сделаю время в миллисекундах + номер_генерирующего_шарда + счетчик_нужной_длины. Монотонности тоже нет, зато есть читаемость и понятность. Рестарт шарда точно дольше 1 миллисекунды в любых случаях. Повторов не будет. Производительности моей схемы хватит вообще для всего.
Для случайных значений которые должны быть уникальны и которые генерятся неизвестно где, но по которым в целом не очень надо искать хватит любых uuid. Это что-то вроде ray id cloudflare.
Одна из причин использовать UUID - ID на основе монотонно возрастающих последовательностей легко перебирать, из-за чего можно искать "уязвимые" ресурсы (через REST API, например)
Если работаете в рамках одного инстанса БД, то конечно смысла нет. Если у вас несколько источников генерации, то тогда решение помогает избежать коллизий.
Опциональный сегмент длиной 64 бита (из-за выравнивания данных) справа от UUID в ключевых столбцах БД с новым типом данных UUID_192.
Очень странное решение. Получается, оверхед по сравнению с ID из семейства snowlake - 128 бит, а ёмкость ID увеличивается только на 27 бит (+16 бит счётчика, которые не занимает метаинформация, перенесённая в UUID_192, и +11 бит монотонного счётчика по спекам).
Получается, что на каждую запись добавляется дополнительно ~12.5 байт.
Это приведёт к тому, что UUIDv7 будут пихать везде, просто потому что везде пишут что это супер удобно. А потом "окажется" что без 192-битовых UUID шардирование невозможно по спекам. В итоге эти 12 байт на запись будут попадать в кеш, оперативки будет нужно больше и диска нужно будет больше, поэтому выиграют как обычно производители лопат (т.е. железок для серверов).
При том что объективно этот мусор нужен только там, где нежелателен перебор последовательностей. А это нужно далеко не везде.
Сегмент опциональный, то есть, по желанию - когда действительно есть метаданные, которые лучше хранить вместе с UUID, а не в других полях таблицы БД. Этот сегмент является не частью UUID, а частью поля в таблице БД, в котором также есть и UUID.
Шардирование в соответствии с RFC9562 возможно и с 128-битовым UUID (посмотрите пункты 1 и 9 в статье), поскольку RFC9562 позволяет практически любые манипуляции с таймстемпом, кроме использования произвольного значения таймстемпа, никак не зависящего от текущего времени.
Недостатки Snowflake ID не ограничиваются возможностью атаки перебором. У UUIDv7 стойкость к коллизиям гораздо выше, чем у Snowflake ID, особенно при слиянии данных из нескольких таблиц или БД, которое может внезапно потребоваться, и учитывая возможность совпадения ID генераторов. Да и сами ID генераторов могут быть чувствительной информацией.
Счетчик у Snowflake ID слишком короткий (12 бит) по современным меркам, что значительно ограничивает производительность генерации. В существующих реализациях UUIDv7 длина счетчика от 18 до 42 бит. Кроме того, для UUIDv7 может быть столько генераторов, сколько таблиц в БД (а при некотором пренебрежении монотонностью - вообще сколько угодно), а Snowflake ID генерятся централизованно, и это "узкое место" производительности.
И наконец, ни одному разгильдяю не удастся нагенерить дубликатов UUIDv7, чего нельзя сказать со всей уверенностью про Snowflake ID.
Кто-то быстренько опубликовал перевод статьи на английский
Что еще следовало бы добавить:
Опциональный формальный параметр seed (такой же, как параметр функции generateUUIDv7 в ClickHouse), который позволяет получить одинаковые значения UUIDv7 при нескольких вызовах функции, если это необходимо в SQL-запросе.
Единый генератор UUIDv7 на каждый сервер, что обеспечит монотонность генерируемых на сервере UUIDv7 внутри миллисекунды при параллельной записи в таблицу базы данных (как функция generateUUIDv7 в СУБД ClickHouse). Монотонность при многопоточности необходима, например, если нескольким микросервисам разрешено делать записи в общей таблице.
Из обязательных функциональных требований к ХОРОШЕЙ функции генерации UUIDv7 можно отметить:
1) Наличие (между таймстемпом и случайной частью) счетчика длиной от 18 до 42 бит, инициализируемого каждую миллисекунду случайным значением, кроме старшего бита, иницилизируемого нулем (этим обеспечивается монотонность внутри миллисекунды и обеспечивается защита от переполнения счетчика)
2) Наличие формального параметра timestamp_offset сдвига таймстемпа по времени (позволяет скрыть истинные дату и время создания записи)
3) Гарантия монотонности генерируемых UUIDv7 внутри миллисекунды при параллельной работе нескольких микросервисов с одной и той же таблицей — благодаря единому генератору UUIDv7 на сервер (реализовано в ClickHouse: https://clickhouse.com/docs/en/sql-reference/functions/uuid-functions#generateUUIDv7 )
4) Возможность получать одни и те же значения UUIDv7 для нескольких вызовов функций, если это необходимо в SQL-запросе (это интересно реализовано в ClickHouse с помощью параметра: https://github.com/ClickHouse/ClickHouse/pull/62852#issuecomment-2150127227 )
Особенности хорошей реализации UUIDv7:
Binary, including UUID type
Timestamp offset
Incremented timestamp on overflow
Short counter segment initialized to 0 (the most significant, leftmost bit)
Long enough counter segment initialized with random data
Global counter or microsecond precision
Can generate the same UUIDs at function calls
Спецификация уникальных идентификаторов UUIDv7 для ключей баз данных и распределенных систем по новому стандарту RFC9562