TL;DR
UUIDv4 в роли PK даёт случайные вставки → чаще приводит к page split → фрагментированный B-tree и более хаотичный доступ к heap, что бьёт по вставкам и чтению. UUIDv7 в PostgreSQL 18 (uuidv7()) содержит временной префикс, поэтому значения обычно упорядочены по времени: индекс плотнее, листовые страницы чаще лежат подряд, а ORDER BY id и массовые вставки в тестах автора быстрее. Компромисс — из UUIDv7 можно извлечь время создания (uuid_extract_timestamp()), что иногда считают утечкой метаданных.
В прошлом было много обсуждений того, как использовать UUID в качестве первичного ключа в PostgreSQL. Для некоторых приложений даже столбца BIGINT недостаточно по диапазону: это знаковое 8-байтовое целое число с диапазоном от −9 223 372 036 854 775 808 до +9 223 372 036 854 775 807. Хотя эти значения выглядят достаточно большими, если представить веб-сервисы, которые ежедневно собирают миллиарды и больше записей, этот запас уже не кажется таким впечатляющим. Простые целочисленные значения также могут приводить к конфликтам в распределённых системах, в Data Lakehouse при объединении данных из нескольких исходных баз и т. п.
Однако главная практическая проблема UUIDv4 как первичного ключа в PostgreSQL заключалась не в недостатке диапазона, а в полной случайности значений. Эта случайность приводит к частым разделениям страниц B-дерева (page split), сильно фрагментированному индексу первичного ключа и, как следствие, к большому объёму случайного дискового ввода-вывода. Эту проблему уже описывали во многих статьях и докладах. Но чего во многих из этих материалов не хватало — так это глубокого погружения в структуры данных на диске. Именно это я и хочу здесь исследовать.

Что такое UUID
UUID — это 16-байтовое целочисленное значение (128 бит), для которого возможно 2^128 комбинаций (примерно 3,4 × 10^38). Этот диапазон настолько велик, что для большинства приложений вероятность появления дубликата UUID практически равна нулю. В Wikipedia приведён расчёт, показывающий, что вероятность найти дубликат среди 103 триллионов UUID версии 4 составляет примерно один шанс на миллиард. Ещё одно часто цитируемое «правило большого пальца»: чтобы получить 50% вероятность хотя бы одного совпадения, нужно генерировать примерно 1 миллиард UUID каждую секунду на протяжении около 86 лет.
Обычно значения представляют в виде строки длиной 36 символов, содержащей шестнадцатеричные цифры и дефисы, например: f47ac10b-58cc-4372-a567-0e02b2c3d479. Канонический формат — 8-4-4-4-12 символов. Первый символ третьего блока и первый символ четвёртого блока несут особый смысл: xxxxxxxx-xxxx-Vxxx-Wxxx-xxxxxxxxxxxx — V обозначает версию UUID (4 для UUIDv4, 7 для UUIDv7 и т. д.), а W кодирует вариант в своих старших 2 или 3 битах (семейство формата UUID).
До PostgreSQL 18 самым распространённым способом генерации UUID в PostgreSQL была версия 4 (например, через gen_random_uuid() или uuid_generate_v4() из расширений). В PostgreSQL 18 появилась нативная поддержка нового, упорядоченного по времени UUIDv7 через функцию uuidv7(), а также добавлена uuidv4() как встроенный алиас для прежней функции gen_random_uuid(). UUID версии 4 генерируется полностью случайно (за исключением фиксированных битов версии и варианта), поэтому в значениях нет внутренней последовательности. UUID версии 7, напротив, генерирует значения, упорядоченные по времени, потому что первые 48 бит содержат метку времени Unix-эпохи в формате big-endian с точностью примерно до миллисекунд, а далее идут дополнительные субмиллисекундные биты и случайная составляющая.
Тестовая схема в PostgreSQL 18
Я покажу конкретные результаты на простой тестовой схеме: две разные таблицы со столбцом «id», в котором хранится сгенерированный UUID (либо v4, либо v7), используемый как первичный ключ, и столбцом «ord» с последовательно генерируемым bigint, сохраняющим порядок создания строк.
-- UUIDv4 (полностью случайные ключи)
CREATE TABLE uuidv4_demo (
id uuid PRIMARY KEY DEFAULT uuidv4(), -- алиас для gen_random_uuid()
ord bigint GENERATED ALWAYS AS IDENTITY
);
-- UUIDv7 (ключи, упорядоченные по времени)
CREATE TABLE uuidv7_demo (
id uuid PRIMARY KEY DEFAULT uuidv7(),
ord bigint GENERATED ALWAYS AS IDENTITY
);
-- 1 млн строк с UUIDv4
INSERT INTO uuidv4_demo (id) SELECT uuidv4() FROM generate_series(1, 1000000);
-- 1 млн строк с UUIDv7
INSERT INTO uuidv7_demo (id) SELECT uuidv7() FROM generate_series(1, 1000000);
VACUUM ANALYZE uuidv4_demo;
VACUUM ANALYZE uuidv7_demo;
Производительность на уровне запросов: EXPLAIN ANALYZE
В качестве первого шага сравним стоимость сортировки по UUID для двух таблиц:
-- UUIDv4
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM uuidv4_demo ORDER BY id;
Index Scan using uuidv4_demo_pkey on uuidv4_demo (cost=0.42..60024.31 rows=1000000 width=24) (actual time=0.031..301.163 rows=1000000.00 loops=1)
Index Searches: 1
Buffers: shared hit=1004700 read=30
Planning Time: 0.109 ms
Execution Time: 318.005 ms
-- UUIDv7
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM uuidv7_demo ORDER BY id;
Index Scan using uuidv7_demo_pkey on uuidv7_demo (cost=0.42..36785.43 rows=1000000 width=24) (actual time=0.013..96.177 rows=1000000.00 loops=1)
Index Searches: 1
Buffers: shared hit=2821 read=7383
Planning Time: 0.040 ms
Execution Time: 113.305 ms
Точные показатели по буферам зависят от эффектов кэширования, но в этом прогоне ясно одно: сканирование индекса по UUIDv7 требует примерно в 100 раз меньше попаданий в shared buffers (shared hit) и выполняется примерно в три раза быстрее (113 мс против 318 мс) для одного и того же ORDER BY id по миллиону строк. Это первый признак того, что UUIDv7 — вполне жизнеспособное решение для первичного ключа, когда нужно заменить столбец BIGINT на что-то с гораздо большим пространством идентификаторов (и пренебрежимо малой вероятностью коллизий), но при этом сохраняющее поведение «последовательного» ключа с точки зрения индекса.
Скорость вставок — простой бенчмарк
Изначально я хотел сделать более продвинутые тесты, но даже самый базовый наивный бенчмарк показал огромную разницу в скорости вставок. Я сравнил время, необходимое для вставки 50 миллионов строк в пустую таблицу, а затем — в таблицу, где уже есть 50 миллионов строк.
INSERT INTO uuidv4_demo (id) SELECT uuidv4() FROM generate_series(1, 50000000);
INSERT INTO uuidv7_demo (id) SELECT uuidv7() FROM generate_series(1, 50000000);
-- UUID v4 -- UUID v7
Пустая таблица
Время вставки: 1239839.702 ms (20:39.840) Время вставки: 106343.314 ms (01:46.343)
Размер таблицы: 2489 MB Размер таблицы: 2489 MB
Размер индекса: 1981 MB Размер индекса: 1504 MB
Таблица с 50 млн строк
Время вставки: 2776880.790 ms (46:16.881) Время вставки: 100354.087 ms (01:40.354)
Размер таблицы: 4978 MB Размер таблицы: 4978 MB
Размер индекса: 3956 MB Размер индекса: 3008 MB
Как видим, скорость вставок отличается радикально. Вставка первых 50 миллионов строк в пустую таблицу заняла всего 1:46 минуты для UUIDv7, но уже 20 минут для UUIDv4. Во второй партии разница оказалась ещё больше — почти вдвое.
Как значения распределены в таблице
Эти результаты указывают на огромные различия в индексах. Давайте разберёмся. Сначала посмотрим, как значения распределены в таблице. Я использую следующий запрос для обеих таблиц (просто меняя имя таблицы):
SELECT
row_number() OVER () AS seq_in_uuid_order,
id,
ord,
ctid
FROM uuidv4_demo
ORDER BY id
LIMIT 20;
Столбец seq_in_uuid_order — это просто номер строки в порядке UUID, ord — порядок вставки, а ctid показывает физическое расположение каждого кортежа в heap-таблице: (номер_блока, смещение_в_блоке).
UUIDv4: случайный порядок UUID ⇒ случайный доступ к страницам heap-таблицы
Как выглядят результаты для UUIDv4?
seq_in_uuid_order | id | ord | ctid
-------------------+--------------------------------------+--------+------------
1 | 00000abf-cc8e-4cb2-a91a-701a3c96bd36 | 673969 | (4292,125)
2 | 00001827-16fe-4aee-9bce-d30ca49ceb1d | 477118 | (3038,152)
3 | 00001a84-6d30-492f-866d-72c3b4e1edff | 815025 | (5191,38)
4 | 00002759-21d1-4889-9874-4a0099c75286 | 879671 | (5602,157)
5 | 00002b44-b1b5-473f-b63f-7554fa88018d | 729197 | (4644,89)
6 | 00002ceb-5332-44f4-a83b-fb8e9ba73599 | 797950 | (5082,76)
7 | 000040e2-f6ac-4b5e-870a-63ab04a5fa39 | 160314 | (1021,17)
8 | 000053d7-8450-4255-b320-fee8d6246c5b | 369644 | (2354,66)
9 | 00009c78-6eac-4210-baa9-45b835749838 | 463430 | (2951,123)
10 | 0000a118-f98e-4e4a-acb3-392006bcabb8 | 96325 | (613,84)
11 | 0000be99-344b-4529-aa4c-579104439b38 | 454804 | (2896,132)
12 | 00010300-fcc1-4ec4-ae16-110f93023068 | 52423 | (333,142)
13 | 00010c33-a4c9-4612-ba9a-6c5612fe44e6 | 82935 | (528,39)
14 | 00011fa2-32ce-4ee0-904a-13991d451934 | 988370 | (6295,55)
15 | 00012920-38c7-4371-bd15-72e2996af84d | 960556 | (6118,30)
16 | 00014240-7228-4998-87c1-e8b23b01194a | 66048 | (420,108)
17 | 00014423-15fc-42ca-89bd-1d0acf3e5ad2 | 250698 | (1596,126)
18 | 000160b9-a1d8-4ef0-8979-8640025c0406 | 106463 | (678,17)
19 | 0001711a-9656-4628-9d0c-1fb40620ba41 | 920459 | (5862,125)
20 | 000181d5-ee13-42c7-a9e7-0f2c52faeadb | 513817 | (3272,113)
Значения распределены полностью случайно. Сканировать по индексу в порядке UUID здесь почти не имеет смысла: для запросов, использующих индекс первичного ключа, это напрямую приводит к случайному доступу к heap-таблице.
UUIDv7: порядок UUID следует порядку вставки
А вот значения UUIDv7 генерируются в явной последовательности:
seq_in_uuid_order | id | ord | ctid
-------------------+--------------------------------------+-----+--------
1 | 019ad94d-0127-7aba-b9f6-18620afdea4a | 1 | (0,1)
2 | 019ad94d-0131-72b9-823e-89e41d1fad73 | 2 | (0,2)
3 | 019ad94d-0131-7384-b03d-8820be60f88e | 3 | (0,3)
4 | 019ad94d-0131-738b-b3c0-3f91a0b223a8 | 4 | (0,4)
5 | 019ad94d-0131-7391-ab84-a719ca98accf | 5 | (0,5)
6 | 019ad94d-0131-7396-b41d-7f9f27a179c4 | 6 | (0,6)
7 | 019ad94d-0131-739b-bdb3-4659aeaafbdd | 7 | (0,7)
8 | 019ad94d-0131-73a0-b271-7dba06512231 | 8 | (0,8)
9 | 019ad94d-0131-73a5-8911-5ec5d446c8a9 | 9 | (0,9)
10 | 019ad94d-0131-73aa-a4a3-0e5c14f09374 | 10 | (0,10)
11 | 019ad94d-0131-73af-ac4b-3710e221390e | 11 | (0,11)
12 | 019ad94d-0131-73b4-85d6-ed575d11e9cf | 12 | (0,12)
13 | 019ad94d-0131-73b9-b802-d5695f5bf781 | 13 | (0,13)
14 | 019ad94d-0131-73be-bcb0-b0775dab6dd4 | 14 | (0,14)
15 | 019ad94d-0131-73c3-9ec8-c7400b5c8983 | 15 | (0,15)
16 | 019ad94d-0131-73c8-b067-435258087b3a | 16 | (0,16)
17 | 019ad94d-0131-73cd-a03f-a28092604fb1 | 17 | (0,17)
18 | 019ad94d-0131-73d3-b4d5-02516d5667b5 | 18 | (0,18)
19 | 019ad94d-0131-73d8-9c41-86fa79f74673 | 19 | (0,19)
20 | 019ad94d-0131-73dd-b9f1-dcd07598c35d | 20 | (0,20)
Здесь seq_in_uuid_order, ord и ctid красиво идут рука об руку: ord увеличивается на 1 для каждой строки, ctid последовательно перемещается по первой странице heap-таблицы, а сами UUID монотонны благодаря префиксу с временной меткой. Для сканирования по индексу первичного ключа это означает, что Postgres может проходить heap-таблицу гораздо более последовательно, чем в случае UUIDv4.
Насколько эти значения последовательны статистически?
После VACUUM ANALYZE я спрашиваю у планировщика, что он «думает» о корреляции между id и физическим порядком:
SELECT
tablename,
attname,
correlation
FROM pg_stats
WHERE tablename IN ('uuidv4_demo', 'uuidv7_demo')
AND attname = 'id'
ORDER BY tablename, attname;
Результат
tablename | attname | correlation
-------------+---------+---------------
uuidv4_demo | id | -0.0024808696
uuidv7_demo | id | 1
Статистика подтверждает то, что мы только что увидели:
Для
uuidv4_demo.idкорреляция по сути равна 0 ⇒ значения случайны относительно порядка в heap-таблице.Для
uuidv7_demo.idкорреляция равна 1 ⇒ в этом тестовом прогоне порядок UUID идеально совпадает с физическим порядком строк.
Именно из-за такой высокой корреляции UUIDv7 так привлекателен в качестве первичного ключа для B-деревьев.
Индексы первичного ключа: размер, листовые страницы, плотность, фрагментация
Далее я смотрю на индексы первичного ключа — их размер, число листовых страниц, плотность и фрагментацию — используя pgstatindex:
SELECT 'uuidv4_demo_pkey' AS index_name, (pgstatindex('uuidv4_demo_pkey')).*;
index_name | uuidv4_demo_pkey
version | 4
tree_level | 2
index_size | 40026112
root_block_no | 295
internal_pages | 24
leaf_pages | 4861
empty_pages | 0
deleted_pages | 0
avg_leaf_density | 71
leaf_fragmentation | 49.99
SELECT 'uuidv7_demo_pkey' AS index_name, (pgstatindex('uuidv7_demo_pkey')).*;
index_name | uuidv7_demo_pkey
version | 4
tree_level | 2
index_size | 31563776
root_block_no | 295
internal_pages | 20
leaf_pages | 3832
empty_pages | 0
deleted_pages | 0
avg_leaf_density | 89.98 -- т. е. стандартный fillfactor 90%
leaf_fragmentation | 0
Сразу видно, что индекс первичного ключа на UUIDv4 примерно на 26–27% больше:
index_size— около 40 МБ против ~31,6 МБleaf_pages— 4861 против 3832 (опять же примерно на 26–27% больше)листовые страницы в индексе v4 имеют более низкую среднюю плотность (71 против ~90)
leaf_fragmentation для v4 — около 50%, тогда как для v7 — 0
То есть UUIDv4 вынуждает B-дерево выделять больше страниц и держать их менее заполненными, а листовой уровень фрагментирует гораздо сильнее.
Более глубокий анализ индекса с bt_multi_page_stats
Чтобы копнуть глубже, я рассмотрел B-деревья постранично и собрал статистику. Для обоих индексов я использовал один и тот же запрос (просто меняя имя индекса в CTE). Запрос считает минимальное, максимальное и среднее количество кортежей на листовой странице индекса, а также проверяет, насколько часто логически соседние листовые страницы B-дерева оказываются физически смежными в файле индекса:
WITH leaf AS (
SELECT *
FROM bt_multi_page_stats('uuidv4_demo_pkey', 1, -1) -- с блока 1 до конца
WHERE type = 'l'
)
SELECT
count(*) AS leaf_pages,
min(blkno) AS first_leaf_blk,
max(blkno) AS last_leaf_blk,
max(blkno) - min(blkno) + 1 AS leaf_span,
round( count(*)::numeric / (max(blkno) - min(blkno) + 1), 3) AS leaf_density_by_span,
min(live_items) AS min_tuples_per_page,
max(live_items) AS max_tuples_per_page,
avg(live_items)::numeric(10,2) AS avg_tuples_per_page,
sum(CASE WHEN btpo_next = blkno + 1 THEN 1 ELSE 0 END) AS contiguous_links,
sum(CASE WHEN btpo_next <> 0 AND btpo_next <> blkno + 1 THEN 1 ELSE 0 END) AS non_contiguous_links
FROM leaf;
Результаты для UUIDv4:
-- uuidv4_demo_pkey
leaf_pages | 4861
first_leaf_blk | 1
last_leaf_blk | 4885
leaf_span | 4885
leaf_density_by_span | 0.995
min_tuples_per_page | 146
max_tuples_per_page | 291
avg_tuples_per_page | 206.72
contiguous_links | 0
non_contiguous_links | 4860
Результаты для UUIDv7:
-- uuidv7_demo_pkey
leaf_pages | 3832
first_leaf_blk | 1
last_leaf_blk | 3852
leaf_span | 3852
leaf_density_by_span | 0.995
min_tuples_per_page | 109
max_tuples_per_page | 262
avg_tuples_per_page | 261.96
contiguous_links | 3812
non_contiguous_links | 19
Как видим, у индекса UUIDv4 больше листовых страниц, распределённых по более широкому диапазону блоков, и хотя минимальное и максимальное число кортежей на страницу у него выше, среднее число кортежей на листовую страницу (206,72) заметно ниже, чем у UUIDv7 (261,96).
Но такие числа могут скрывать общую картину. Поэтому давайте посмотрим на гистограммы, показывающие распределение количества кортежей по листовым страницам. Для этого я использую следующий запрос с «корзинами» (bucket) от 100 до 300 и выведу только непустые результаты:
WITH leaf AS (
SELECT live_items
FROM bt_multi_page_stats('uuidv4_demo_pkey', 1, -1)
WHERE type = 'l'
),
buckets AS (
-- нижние границы корзин: 100, 110, ..., 290
SELECT generate_series(100, 290, 10) AS bucket_min
)
SELECT
b.bucket_min AS bucket_from,
b.bucket_min + 9 AS bucket_to,
COUNT(l.live_items) AS page_count
FROM buckets b
LEFT JOIN leaf l
ON l.live_items BETWEEN b.bucket_min AND b.bucket_min + 9
GROUP BY b.bucket_min HAVING count(l.live_items) > 0
ORDER BY b.bucket_min;
Результат для UUIDv4:
bucket_from | bucket_to | page_count
-------------+-----------+------------
140 | 149 | 159
150 | 159 | 435
160 | 169 | 388
170 | 179 | 390
180 | 189 | 427
190 | 199 | 466
200 | 209 | 430
210 | 219 | 387
220 | 229 | 416
230 | 239 | 293
240 | 249 | 296
250 | 259 | 228
260 | 269 | 214
270 | 279 | 171
280 | 289 | 140
290 | 299 | 21
Результат для UUIDv7:
bucket_from | bucket_to | page_count
-------------+-----------+------------
100 | 109 | 1
260 | 269 | 3831
Эти результаты наглядно демонстрируют огромную фрагментацию индекса UUIDv4 и стабильную, компактную структуру индекса UUIDv7. Самые «нижние» корзины в гистограмме UUIDv4 показывают случаи наполовину пустых листовых страниц индекса. С другой стороны, страницы с количеством кортежей больше 270 превышают fillfactor 90%, что является типичным следствием случайных вставок после разделений страниц; fillfactor задаёт целевую заполненность при split, а не жёсткий верхний предел. В индексе UUIDv7 все листовые страницы, кроме одной (самой последней в дереве), заполнены до стандартного fillfactor 90%.
Ещё один важный результат — в двух последних столбцах статистики индекса:
Для UUIDv4:
contiguous_links = 0, non_contiguous_links = 4860Для UUIDv7:
contiguous_links = 3812, non_contiguous_links = 19
btpo_next = blkno + 1 означает, что следующая листовая страница в логическом порядке B-дерева расположена в следующем физическом блоке файла индекса. В этом тесте для UUIDv4 такого не происходит ни разу — листовые страницы полностью фрагментированы и случайно распределены по структуре индекса. Для UUIDv7 почти все листовые страницы идут подряд, то есть аккуратно следуют одна за другой.
Кроме того, если посмотреть на реальное содержимое листовых страниц, сразу видно отличие случайного поведения UUIDv4 от последовательного поведения UUIDv7: листовые страницы UUIDv4 указывают на кортежи в heap-таблице, разбросанные по всей таблице, тогда как листовые страницы UUIDv7 обычно указывают на плотные диапазоны страниц heap-таблицы. Это тот же самый паттерн, который мы уже видели ранее, глядя на ctid напрямую в таблице, поэтому «сырые» дампы здесь повторять не буду.
Небольшой нюанс: встроенная временная метка в UUIDv7
У значений UUIDv7 есть один небольшой нюанс: они «светят» время создания. В PostgreSQL 18 это явно доступно через uuid_extract_timestamp():
SELECT
id,
uuid_extract_timestamp(id) AS created_at_from_uuid
FROM uuidv7_demo
ORDER BY ord
LIMIT 5;
Пример результатов
id | created_at_from_uuid
--------------------------------------+----------------------------
019ad94d-0127-7aba-b9f6-18620afdea4a | 2025-12-01 09:44:53.799+00
019ad94d-0131-72b9-823e-89e41d1fad73 | 2025-12-01 09:44:53.809+00
019ad94d-0131-7384-b03d-8820be60f88e | 2025-12-01 09:44:53.809+00
019ad94d-0131-738b-b3c0-3f91a0b223a8 | 2025-12-01 09:44:53.809+00
019ad94d-0131-7391-ab84-a719ca98accf | 2025-12-01 09:44:53.809+00
Если посмотреть на всю последовательность значений, то можно анализировать временные интервалы между созданием записей прямо по UUID — без отдельного столбца с временной меткой. Для некоторых приложений это может считаться потенциальной утечкой информации (например, раскрывать приблизительное время создания или интенсивность запросов), тогда как многим другим, скорее всего, это будет безразлично.
Итоги
UUID дают огромный простор идентификаторов (128 бит, ~3,4 × 10^38 значений), где вероятность коллизий для реальных нагрузок ничтожно мала.
Традиционные ключи UUIDv4 полностью случайны. При использовании в качестве первичных ключей в PostgreSQL они обычно:
фрагментируют индексы B-дерева
снижают плотность листовых страниц
вызывают крайне случайные паттерны доступа к heap-таблице и больше случайного ввода-вывода
UUIDv7, нативно добавленный в PostgreSQL 18 как uuidv7(), сохраняет 128-битное пространство, но переупорядочивает биты так, что:
в старших битах находится метка времени Unix (Unix timestamp) с миллисекундной точностью (плюс субмиллисекундная доля)
оставшиеся биты остаются случайными
В практических тестах на 1 млн строк в каждой таблице:
Индекс первичного ключа на UUIDv7 оказался примерно на 26–27% меньше, с меньшим числом листовых страниц и существенно более высокой средней плотностью листовых страниц;
Листовые страницы в индексе UUIDv7 в подавляющем большинстве располагались физически подряд, тогда как листовые страницы UUIDv4 были полностью фрагментированы;
Запрос ORDER BY id по UUIDv7 в моём прогоне выполнялся примерно в три раза быстрее, чем тот же запрос по UUIDv4 — благодаря лучшей локальности индекса и более последовательному доступу к heap-таблице.
Компромисс в том, что UUIDv7 содержит временную метку, которая может раскрывать приблизительное время создания, но для большинства сценариев это приемлемо или даже полезно. Таким образом, UUIDv7 заметно улучшает производительность и физическую организацию первичных ключей UUID в PostgreSQL — не отказываясь от случайности, а добавляя упорядоченный по времени префикс. В PostgreSQL 18 это даёт лучшее из двух миров: огромное пространство идентификаторов и преимущества распределённой генерации UUID — при поведении индекса, куда более близком к классическому последовательному первичному ключу на BIGINT.
Если после чтения хочется не просто выбрать «правильный» UUID, а понимать, как Postgres ведёт себя на диске и под нагрузкой, обратите внимание на курс «PostgreSQL для администраторов баз данных и разработчиков». Внутри — настройка кластера, бэкапы, разбор блокировок/deadlock, индексы, join’ы и статистика для оптимизации на больших объёмах. Пройдите вступительный тест, чтобы узнать, подойдет ли вам программа курса.
А чтобы узнать больше о формате обучения и задать вопросы экспертам, приходите на бесплатные демо-уроки:
24 декабря. PostgreSQL FDW: швейцарский нож для интеграций или как я перестал бояться разнородных данных. Записаться
14 января. Улица разбитых кластеров: про бэкапы и реплики в PostgreSQL. Записаться
22 января. Безопасность в PostgreSQL: защита данных, управление доступом и аудит. Записаться
Комментарии (13)

norguhtar
22.12.2025 10:00До выхода v7 можно было использовать uuid_generate_v1mc() он тоже дает достаточную равномерность.

GarfieldX
22.12.2025 10:00Не понимаю проблемы. И зачем использовать UUID в качестве первичного ключа? Как уникальное значения - ок. Но первичный ключ - странно. Не хватает bigint - сделайте составной из двух таких полей.

FlamyXD
22.12.2025 10:00Очень странно сортировать по полю uuidv4 и потом говорить что это медленно, поскольку в такой сортировке нет никакого смысла.
Результаты будут в случайном порядке.

Tzimie
22.12.2025 10:00Причины использования guid другие - bigint не хватить не может. Даже если запись 8 байт (сам bigint) то чтобы его переполнить не хватит всех хранилищ земли
nin-jin
Опять эта сортировка по id. Сортировка по суррогатному идентификатору нужна примерно никогда.
seekerhan
Я не автор, но кажется это чтобы просто показать случайную природу uuidv4. И далее в статье раскрыть последствия. А так может merge join использовать...
nin-jin
Ну так джойн таблиц по их первичным суррогатным ключам тоже не имеет смысла. А идентификаторы, полученные из вторичных не будут последовательными в любом случае.
seekerhan
Не очень понял ответ. Есть 2 таблицы пользователь и его пост в соц сетях. У пользователя есть первичный суррогатный ключ user_id, у поста есть внешний ключ, который ссылается на user_id. Ну и дальше запрос найти посты пользователей, у которых (пользователей) дата регистрации больше 01012010 и посты начинались на "Глубокий взгляд". Есть и join по суррогатному первичному ключу и для соединения может быть выбран merge join в определенных ситуациях. А merge join использует сортировку
nin-jin
Речь о том, что выборка по вторичному ключу даст сортировку по этому ключу, а не по первичному. А это значит, что потребуется пересортировка и чтение записей не последовательно.