Совет дня №17
Наверное, вы много слышали про UUID версии 7. Дело в том, что недавно его утвердили окончательно, и он появился в Postgres 18. До этого люди писали свой UUID на коленке, но теперь все официально.
В целом UUID – это контейнер из 128 бит. Первые несколько содержат информацию о версии, остальное заполняется в зависимости от нее. Самый популярный стандарт сегодня – UUID 4, который полностью случаен. Есть и другие алгоритмы: например, 1 и 2 использовали MAC-адрес машины, на которой генерировались, и таким образом раскрывали системную инфу.
Чем хороша версия 7? Ее первые 48 битов содержат время Unix в миллисекундах. Это значит, такие ключи сохраняют порядок в разрезе тысячной секунды. Кроме того, из него можно извлечь дату и время, а также получить UUID из времени, зная, как заполнить хвост. Уникальность плюс время, два в одном.
Существует три подмножества UUID v7. Они различаются тем, как заполняется хвост. Версия a) хвост заполняется рандомом. Версия b) используется возрастающий счетчик из 12 бит, остальное – рандом. Версия c) используется два счетчика: 12 и 10 бит (могу быть неточным, читал спеку давно).
Счетчики в хвосте важны для супер-точных систем, например трейдинга. Лично я не сталкивался с ситуацией, когда порядка в рамках тысячной секунды недостаточно.
Как я уже рассказывал, UUID v7 отлично подходит на роль первичного ключа. Во-первых, если у вас числовые айдишки, вы становитесь заложником базы. Только она назначает айдишки сущностям. Это становится проблемой в распределенных системах, очередях задач. Пока сущность не вставилась в базу, можно наначить ей временный айди, но это лишнее усложнение.
Наоборот, в случае UUID можно присвоить сущности ключ до вставки в базу, например:
# python
user_id = generate_uuid()
profile_id = generate_uuid()
# sql
insert into users (id) values (user_id)
insert into profiles (id, user_id)
values (profile_id, user_id)
Оба запроса можно выполнить, не обращаясь к полю generated_id результата.
Говорят: UUID длиней числовой айдишки, это раздувание таблицы. Вспомним, что UUIDv7 = id + created_at. Так или иначе нужно хранить время создания сущности. Типы timestamp(tz) занимают 8 байт, тип biginteger/bigserial – тоже 8 байт. В сумме 16, а это столько же, сколько занимает UUID. Все сходится.
Главное: UUIDv4 (полностью случайный) не очень подходит для индексов btree. Если добавлять в него значения возрастанию, они будут наполнять бакеты последовательно. Примерно как фигурки в Тетрисе, когда профессиональные игроки укладывают их слева направо. Если данные случайны, фигурки падают в разные бакеты. Из-за этого выше фрагментация, а кроме того, часто срабатывает перераспределение узлов.
Похожая беда с обходом индекса. Когда мы обходим btree-индекс с числами, то порядок блоков почти совпадает с порядком ключей, например:
id block
1 -> 1001
2 -> 1001
3 -> 1002
Если у нас уиды, маппинг будет такой:
id block
b7b0547b-...-148889ec0602 -> 51451
d1897a03-...-520566c5c9a7 -> 13
ef0093d3-...-26e923a66fe4 -> 7232
Будет много случайного доступа к файлу, и обход такого индекса медленней, чем с числовыми айдишками.
К счастью, седьмой уид сводит проблему на нет, так что пользуйтесь!
Нашли ошибку? Выделите мышкой и нажмите Ctrl/⌘+Enter