uuid и uuidv7
Brew решил выдавать клиентам ссылку на их регистрацию в программе лояльности и не хотел светить в URL последовательный числовой id (/signup/42 — видно, что регистраций всего 42, и легко перебрать соседние). Перешли на uuid — и тут же словили вторую проблему: новые регистрации, ключом которых стал случайный uuid, начали вставляться «вразнобой» по индексу, страница «последние регистрации» перестала сортироваться по ключу, а вставки стали чуть медленнее. Так Brew познакомился с разницей между случайным uuid (версия 4) и временным uuidv7 (версия 7).
Цель юнита — выбрать ключ осознанно. Числовой IDENTITY (его мы видели на базовых таблицах) хорош, но раскрывает порядок и количество. Случайный gen_random_uuid() (v4) непредсказуем, но не сортируется и фрагментирует индекс. PG18 добавил uuidv7() — uuid, в начало которого зашит timestamp: он так же непредсказуем в хвосте, но монотонно растёт во времени, поэтому годится как сортируемый по времени первичный ключ. Демонстрируем его на новой таблице — схема Brew байт-совместима с kafka-cookbook (customers.id там BIGINT), и трогать её нельзя.
v4 против v7: что в них зашито
gen_random_uuid() возвращает uuid версии 4 — 122 случайных бита. Его плюс — непредсказуемость; минус — полная случайность: соседние по времени значения раскиданы по всему диапазону, поэтому по B-tree-индексу они вставляются в случайные места (фрагментация страниц), а отсортировать по такому ключу «в порядке создания» нельзя.
uuidv7() (PG18) кладёт в старшие биты Unix-время в миллисекундах, а в остаток — случайность. Версия — 7, и у него есть встроенный timestamp, который можно достать функцией uuid_extract_timestamp() (у v4 она вернёт NULL — времени там нет). Главное свойство: значения, сгенерированные позже, численно больше — ключ монотонен, вставки идут «вправо» по индексу, а сортировка по ключу совпадает с порядком создания.
Локальность B-tree и выбор ключа
Почему «вразнобой» дороже, чем «в хвост»: индекс — это B-tree, и место новой строки определяет её ключ.
B-tree по ключу — куда ложится НОВАЯ вставка?
uuid v4 (122 случайных бита) uuidv7 (время в старших битах)
┌──┬──┬──┬──┬──┐ ┌──┬──┬──┬──┬──┐
│ │ │ │ │ │ │ │ │ │ │▓▓│ ← все новые сюда
└──┴──┴──┴──┴──┘ └──┴──┴──┴──┴──┘
↑ ↑ ↑ ↑ ↑
бьют по всем листам один «горячий» правый лист
→ фрагментация страниц → плотная упаковка, рост «в хвост»| Ключ | Предсказуемость | Локальность B-tree | Сортируется по времени | Когда брать |
|---|---|---|---|---|
IDENTITY (BIGINT) | низкая: раскрывает порядок и объём | отличная (рост «в хвост») | да | узкие внутренние таблицы; компактный и быстрый |
uuid v4 | высокая | плохая (фрагментирует) | нет | публичный id, где сортировка по ключу не нужна |
uuidv7 | высокая в хвосте, но светит время | отличная (рост «в хвост») | да | распределённые вставки + публичный сортируемый id |
Почему мы показываем свойства, а не значения
uuid по своей природе случаен в хвосте — конкретные значения в каждом прогоне разные, и вставить их в README дословно нельзя. Поэтому демо проверяет свойства, которые детерминированы: номер версии (4 vs 7), наличие встроенного времени (NULL у v4, не-NULL у v7) и монотонность — совпадает ли порядок строк по uuidv7-ключу с порядком вставки.
Что показывает наш код
Своя таблица loyalty_signups (DDL в schema.sql) с ключом на uuidv7() и независимым счётчиком seq (IDENTITY) для проверки порядка:
CREATE TABLE IF NOT EXISTS loyalty_signups (
id UUID NOT NULL DEFAULT uuidv7() PRIMARY KEY,
seq BIGINT GENERATED ALWAYS AS IDENTITY,
...
);Первый запрос — детерминированные факты о версиях; третий — проверка монотонности:
SELECT uuid_extract_version(gen_random_uuid())::int AS v4_version, -- 4
uuid_extract_version(uuidv7())::int AS v7_version, -- 7
(uuid_extract_timestamp(gen_random_uuid()) IS NULL)::boolean AS v4_has_no_timestamp,
(uuid_extract_timestamp(uuidv7()) IS NOT NULL)::boolean AS v7_has_timestamp;
SELECT bool_and(id_rank = seq_rank)::boolean AS ids_match_insertion_order -- SignupsTimeOrdered
FROM (SELECT row_number() OVER (ORDER BY id) AS id_rank,
row_number() OVER (ORDER BY seq) AS seq_rank FROM loyalty_signups) t;main.go чистит таблицу, вставляет три регистрации (id присваивает БД из DEFAULT uuidv7()) и спрашивает: совпал ли порядок по id с порядком вставки? Для v7 — да. В Go uuid приезжает как pgtype.UUID; конкретные значения мы не печатаем (они случайны).
Кстати, schema.sql этого юнита — первый в курсе, что добавляет свою таблицу: make db-reset накатывает её через brew.Apply(ctx, pool, ddl) (схема Brew → DDL юнита → seed), а сам DDL main.go читает рядом с собой через runtime.Caller.
Запуск
docker compose up -d
make lecture L=01-data-types/01-04-uuid-and-uuidv7 T=db-reset
make lecture L=01-data-types/01-04-uuid-and-uuidv7Вывод:
1) gen_random_uuid() (v4) против uuidv7() — проверяемые свойства:
версия: v4 = 4, v7 = 7
встроено время? v4: нет (timestamp = NULL) = true; v7: да = true
2) Вставили строк с ключом uuidv7: 3. Порядок по id = порядку вставки? true
→ uuidv7 монотонен во времени: годится как сортируемый по времени PK.
(v4 случаен — такой порядок был бы лишь совпадением.)Версии — 4 и 7; у v4 нет встроенного времени, у v7 есть. И главное: три строки, вставленные подряд, упорядочены по uuidv7-ключу ровно так же, как по счётчику вставки, — uuidv7 монотонен. Страница «последние регистрации» снова сортируется по ключу, а вставки идут «вправо» по индексу.
Заборчик
Что мы упростили:
- Монотонность — про порядок, а не про безопасность.
uuidv7не прячет время создания (его легко достать) — если важно не раскрывать «когда», это не тот инструмент. И наоборот, v4 раскрывает меньше, но проигрывает в локальности индекса. - Выбор ключа — компромисс (см. таблицу выше). Для распределённых вставок и публичных идентификаторов
uuidv7— хороший дефолт; для узких внутренних таблицIDENTITYчасто всё ещё лучше — компактнее, быстрее, хотя и раскрывает порядок/объём и хуже мерджится между базами. - Базовые таблицы остаются на
BIGINT. Намеренно, ради байт-совместимости с kafka-cookbook — новые идеи мы пробуем на новых таблицах, не на базовых.
Что забрать с собой
gen_random_uuid()— v4, случайный: непредсказуем, но не сортируется и фрагментирует B-tree.- PG18
uuidv7()— v7, со встроенным временем: монотонен, годится как сортируемый по времени первичный ключ; время достаётсяuuid_extract_timestamp(). - Значения
uuidслучайны — проверяй и показывай свойства (версия, монотонность), а не конкретные значения. - Современные идиомы (
uuidv7) демонстрируй на новых таблицах; базовые таблицы Brew (*.id BIGINT) не трогай — это держит handoff с kafka-cookbook.
Дальше — юнит 01-05 «enum, массивы и intro в jsonb»: контейнерные типы — упорядоченный enum, массивы (text[] с оператором @>) и первое знакомство с jsonb; когда какой контейнер уместен, а когда пора нормализовать.