Pull to refresh

Comments 25

Сгенерированные UUIDv7 имеют все преимущества UUID и при этом упорядочены по дате и времени создания.

В пределах одной миллисекунды за счет random-ной части можно получить неупорядоченные ключи для UUIDv7. Это надо учитывать. Они, безусловно, лучше подходят для кластеризации и т.п., но если нужен возрастающий ключ на базе v7 - надо постараться.

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

Да, но это надо знать и уметь. Так-то, если просто генерировать, в пределах ms будут не монотонно-возрастающие значения, по крайней мере, в библиотеке Uuid в Rust, если просто генерировать.

Надо глянуть на крейт Uuid7, потому что в крейте Uuid монотонности нет.

По самому 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 в статье).

Спасибо за комментарий. По поднятым в нем вопросам я внес уточнения в текст статьи.

Нативная функция uuidv7() со счетчиком и с монотонностью внутри миллисекунды появится в 18 версии PostgreSQL предположительно в сентябре 2025 года. Сейчас для PostgreSQL практически есть только https://github.com/fboulnois/pg_uuidv7, но в этой реализации монотонности внутри миллисекунды нет и не будет. Ещё вариант - генерить UUIDv7 не в БД, а в приложении, и тогда возможно обеспечить монотонность внутри миллисекунды ценой компромиссов.

И зачем?

Если нужно генерировать меньше чем очень много id в одну миллисекунду, то производительности обычных монотонных последовательностей вашей любимой БД будет за глаза. Их и надо использовать в таком случае. Они понятны и удобны.

Значит есть смысл рассматривать только генерацию множества id в одну миллисекунду. А там нет ни монотоннсти, ни хороших индексов БД.

И в итоге оно опять не нужно. Я лучше сделаю время в миллисекундах + номер_генерирующего_шарда + счетчик_нужной_длины. Монотонности тоже нет, зато есть читаемость и понятность. Рестарт шарда точно дольше 1 миллисекунды в любых случаях. Повторов не будет. Производительности моей схемы хватит вообще для всего.

Для случайных значений которые должны быть уникальны и которые генерятся неизвестно где, но по которым в целом не очень надо искать хватит любых uuid. Это что-то вроде ray id cloudflare.

Одна из причин использовать UUID - ID на основе монотонно возрастающих последовательностей легко перебирать, из-за чего можно искать "уязвимые" ресурсы (через REST API, например)

Ну и пускай перебирают. Жалко что ли?

Безопасность это про ACL, а не про перебор. Нагрузка это про рейт лимитеры, а опять не про перебор.

>Я лучше сделаю время в миллисекундах + номер_генерирующего_шарда + счетчик_нужной_длины

вы как будто монговский objectid переизобрели )

Не надо изобретать то что знаешь )

Хорошие решения они известны и везде одинаковы.

Если работаете в рамках одного инстанса БД, то конечно смысла нет. Если у вас несколько источников генерации, то тогда решение помогает избежать коллизий.

Опциональный сегмент длиной 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.

Что еще следовало бы добавить:

  1. Опциональный формальный параметр seed (такой же, как параметр функции generateUUIDv7 в ClickHouse), который позволяет получить одинаковые значения UUIDv7 при нескольких вызовах функции, если это необходимо в SQL-запросе.

  1. Единый генератор 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

Sign up to leave a comment.

Articles