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)


  1. nin-jin
    22.12.2025 10:00

    Опять эта сортировка по id. Сортировка по суррогатному идентификатору нужна примерно никогда.


    1. seekerhan
      22.12.2025 10:00

      Я не автор, но кажется это чтобы просто показать случайную природу uuidv4. И далее в статье раскрыть последствия. А так может merge join использовать...


      1. nin-jin
        22.12.2025 10:00

        Ну так джойн таблиц по их первичным суррогатным ключам тоже не имеет смысла. А идентификаторы, полученные из вторичных не будут последовательными в любом случае.


        1. seekerhan
          22.12.2025 10:00

          Не очень понял ответ. Есть 2 таблицы пользователь и его пост в соц сетях. У пользователя есть первичный суррогатный ключ user_id, у поста есть внешний ключ, который ссылается на user_id. Ну и дальше запрос найти посты пользователей, у которых (пользователей) дата регистрации больше 01012010 и посты начинались на "Глубокий взгляд". Есть и join по суррогатному первичному ключу и для соединения может быть выбран merge join в определенных ситуациях. А merge join использует сортировку


          1. nin-jin
            22.12.2025 10:00

            Речь о том, что выборка по вторичному ключу даст сортировку по этому ключу, а не по первичному. А это значит, что потребуется пересортировка и чтение записей не последовательно.


  1. norguhtar
    22.12.2025 10:00

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


  1. GarfieldX
    22.12.2025 10:00

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


    1. des1roer
      22.12.2025 10:00

      Типизация. Неподбираемость


    1. JajaComp
      22.12.2025 10:00

      Например когда ключ генерируется на клиенте, а потом копируется в бд бека


  1. FlamyXD
    22.12.2025 10:00

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


  1. Tzimie
    22.12.2025 10:00

    Причины использования guid другие - bigint не хватить не может. Даже если запись 8 байт (сам bigint) то чтобы его переполнить не хватит всех хранилищ земли


  1. kemsky
    22.12.2025 10:00

    Не хватает сути, почему вставка быстрее?


    1. nin-jin
      22.12.2025 10:00

      За счёт меньшего числа промахов кеша и изменения меньшего числа страниц в среднем.