Преимущества функции uuidv7()

В 18 версии PostgreSQL появится функция uuidv7(). Она разработана для замены последовательных автоинкрементных идентификаторов SERIAL, BIGSERIAL и IDENTITY, которые могут привести к катастрофическому дублированию ключей при слиянии данных, и для замены более медленных UUIDv4.

Функция uuidv7() в PostgreSQL имеет следующие преимущества:

  • Безопасное слияние данных из нескольких источников без необходимости перегенерации ключей

  • Генерация ключей одновременно в нескольких процессах без ущерба для уникальности и порядка ключей

  • Возможность искажения даты и времени создания записи для конфиденциальной информации (в отличие от почти всех других аналогичных функций)

Функция uuidv7() также полезна для приложений, требующих хронологического извлечения данных, таких как системы логирования и базы данных временных рядов.

Таким образом, использование функции uuidv7() позволит упростить архитектуру информационных систем, упростить SQL-запросы, избежать некоторых ошибок, облегчить внесение изменений и благодаря этому повысить надежность и снизить стоимость разработки и сопровождения информационных систем.

Необходимость официального бенчмарка

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

Кроме того, такой бенчмарк должен облегчить выбор типа идентификатора с учетом оборудования, задач и условий конкретного клиента.

Еще одна причина для разработки официального бенчмарка – это абсурдно низкое качество самодельных бенчмарков.

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

Другие самодельные бенчмарки доказывают и без того очевидный факт, что UUIDv7 гораздо быстрее, чем UUIDv4.

Некоторые без бенчмарков сравнивают только длину UUIDv7 и целочисленных идентификаторов, не обращая внимания на остальные обстоятельства. Но «скупой платит дважды» (см. выше пункт «Преимущества uuidv7()»).

На самом деле критически важен темп однопоточной и многопоточной вставки записей (в том числе, с учетом партиционирования) с ключами, сгенерированными функцией uuidv7() – по сравнению с автоинкрементом. Но для некоторых клиентов могут оказаться важными и другие параметры, в частности, скорость работы SQL-запросов различных типов.

Кроме того, наличие официального бенчмарка вообще избавило бы клиентов от необходимости разработки самодельных бенчмарков. Ведь в абсолютном большинстве случаев клиенты нуждаются в одних и тех же сценариях работы бенчмарков, а различия в потребностях могут быть учтены изменением значений входных параметров (количество строк в таблице, объем строки).

Хорошо было бы включить бенчмарк в 18 версию PostgreSQL, отдельно от патча для собственно функции uuidv7(). Время для этого еще есть, так как заморозка фич произойдет в апреле 2025 года.

Входные параметры бенчмарка

Бенчмарк должен запускаться как функция с одним обязательным параметром «тип идентификатора» (по умолчанию «uuidv7() без намеренного сдвига таймстемпа») и двумя опциональными параметрами:

  • Количество записей в тестовой таблице, по умолчанию 1 миллион

  • Объем данных в поле типа BYTEA, по умолчанию 2048 байтов

Шаги бенчмарка

До запуска шагов бенчмарка создается тестовая таблица mock_table:

CREATE TABLE mock_table (id UUID PRIMARY KEY DEFAULT uuidv7(), payload BYTEA);

На каждом своем шаге бенчмарк должен выполнять SQL-запросы различных типов, поскольку именно это в конечном итоге требуется клиентам, выбирающим тип идентификатора. Однако в бенчмарке не нужно, чтобы запросы выводили какие-то осмысленные результаты, а важно лишь время исполнения запросов.

  1. INSERT INTO mock_table (id, payload) VALUES (uuidv7(), (decode(repeat('FF', payload_size_b), 'hex'))); --здесь вместо uuidv7() может быть подставлена другая функция, генерирующая идентификаторы

  2. Параллельная (многопоточная) вставка записей. Это не выразить на SQL

  3. SELECT COUNT(*) FROM mock_table a LEFT JOIN mock_table b ON b.id = a.id WHERE b.id IS NULL;

  4. SELECT COUNT(*) FROM mock_table a INNER JOIN mock_table b ON b.id = a.id WHERE b.id IS NULL;

  5. SELECT id, COUNT(*) FROM mock_table GROUP BY id HAVING COUNT(*) > 1;

  6. DELETE FROM mock_table a USING mock_table b WHERE b.id = a.id;

Таблица результатов бенчмарка

Таблица результатов запущенного бенчмарка должна содержать одну строку значений для каждого шага бенчмарка, в том числе:

  • Дата и время (в формате по умолчанию) запуска бенчмарка

  • Значения каждого входного параметра (в том числе, взятые по умолчанию)

  • Имя шага бенчмарка (1_insert, 2_parallel_insert, 3_left_join, 4_inner_join, 5_group_by, 6_delete)

  • Темп обработки записей, штук в миллисекунду

  • Использование CPU, %

  • Использование памяти, МБ

  • Использование диска, МБ/с

Сравниваемые типы идентификаторов

Помимо UUIDv7 и BIGSERIAL имеет смысл запускать бенчмарк по меньшей мере с такими типами идентификаторов по выбору пользователя: UUIDv4, ULID, Snowflake ID.

Комментарии (12)


  1. vmalyutin
    11.01.2025 10:37

    Честно говоря ничего не понял. Во-первых, бенчмарк у pg есть. Не помню, как он называется, но он есть. Во-вторых, если вам надо проверить производительность вашего конкретного сценария, то почему бы вам его и не проверить? В конце концов не будут же разработчики pg делать отдельный бенчмарк на каждый тип данных в PG!?


    1. SergeyProkhorenko Автор
      11.01.2025 10:37

      Бенчмарка для функции uuidv7() в PostgreSQL нет, поэтому невозможно вспомнить его название.

      Добрая половина этой моей статьи посвящена объяснению, почему самодельные бенчмарки не выдерживают никакой критики.

      UUIDv7 - это не "каждый тип данных". Функцию uuidv7() или аналогичную в PostgreSQL ждали уже не меньше восьми лет. Она революционным образом меняет проектирование и сопровождение баз данных и систем логирования. Так что, она достойна отдельного бенчмарка.

      Разработчики PostgreSQL считают, что такой бенчмарк нужен. Но как всегда в open source, все ждут, что за них это сделает кто-то другой, а время уходит. Может быть Вы возьметесь?


      1. vmalyutin
        11.01.2025 10:37

        Спасибо за предложение, но я занят развитием pgTap. Да еще и работу работаю. И все же pgbanch не подходит? Там они пишут много-много потоков можно сделать. Или я не понимаю цель нового бенчмарка?


        1. SergeyProkhorenko Автор
          11.01.2025 10:37

          Ваша идея использовать pgbench действительно прекрасная, особенно с собственным файлом со скриптом. В этот скрипт вполне можно было бы вставить те SQL-запросы, которые я предложил в статье. Стандартный скрипт не очень подходит для сравнения производительности разных первичных ключей для разных типов запросов, так как сделан для другой цели - тестирования производительности PostgreSQL на определенном "железе".

          Но я ни разу не встречал предложение использовать pgbench для тестирования разных функций генерации первичного ключа. Видимо, этот инструмент недостаточно хорошо известен. К тому же, судя по пухлой документации, правильная настройка pgbench - нетривиальная вещь, и требует слишком высокой квалификации разработчика. Я думаю, что готовый шаблон теста на основе pgbench действительно был бы очень полезен. Было бы замечательно, если бы кто-нибудь опубликовал такой шаблон на Хабре.


  1. vmalyutin
    11.01.2025 10:37

    И ещё. Объясните мне, ещё не читавшему официальную документацию, каким образом функция генерирующая чего-то там не как старая последовательно поможет временным рядам и особо хронологическому чтению, плиз.


    1. SergeyProkhorenko Автор
      11.01.2025 10:37

      Перевод цитаты из статьи "Why UUIDv7 is Revolutionizing Time-Ordered Identifiers for Modern Systems":

      1. Улучшенная совместимость с временными метками (Timestamps)

      Поскольку UUIDv7 включает 48-битную временную метку, разработчики могут напрямую извлекать информацию о времени из самого UUID. Это полезно для любого анализа или отладки на основе времени, поскольку позволяет понять, когда был сгенерирован идентификатор, просто изучив его временную метку. В отличие от других типов UUID, которым требуются дополнительные метаданные для передачи временной информации, UUIDv7 сохраняет эффективность и автономность. Это делает UUIDv7 идеальным для регистрации событий, метрик и аналитических приложений, где понимание временного порядка событий имеет решающее значение.

      Временная метка также хорошо согласуется со стандартами времени Unix, что позволяет легко преобразовывать ее в обычные форматы даты и времени, что упрощает интеграцию с существующими рабочими процессами данных на основе времени.


    1. SergeyProkhorenko Автор
      11.01.2025 10:37

      Еще одна переведенная цитата оттуда же (в оригинале на английском, правда, понятнее):

      1. Простота временного разбиения (Partitioning)

      В системах, которые управляют большими наборами данных, разбиение по времени часто является наиболее эффективным способом обработки данных в масштабе. Поскольку UUIDv7 изначально упорядочены по времени, разбиение на основе времени становится простым. Например, журналы можно эффективно разбить на ежемесячные, ежедневные или даже часовые разделы без необходимости сканирования каждого UUID на предмет временных меток.

      Такая конструкция может значительно упростить управление данными в распределенных хранилищах данных, где доступ к большим объемам данных и их поддержка становятся проще, когда они разделены хронологически. UUIDv7 идеально соответствует этой стратегии, обеспечивая эффективное извлечение, удаление и архивацию старых данных в разделах на основе времени.


  1. leninD
    11.01.2025 10:37

    Если нету бенча и он вам нужен может вам его и написать?


    1. SergeyProkhorenko Автор
      11.01.2025 10:37

      Это не тот случай, когда спасение утопающих - дело рук самих утопающих. Бенчмарк нужен не мне лично, а очень многим компаниям самых разных отраслей. Эти компании скорее всего сляпают на коленке какой-нибудь примитивный и неправильный бенчмарк для внутренего употребления, будут ему слепо доверять, но не будут отдавать его в open source. А я не разработчик на языке C, а системный аналитик DWH. К тому же, далеко не всякий разработчик на языке C сможет написать такой бенчмарк - нужно глубокое понимание PostgreSQL. Качество бенчмарка должно быть таким высоким, чтобы кто-нибудь из коммитеров PostgreSQL согласился рискнуть своей репутацией и его закоммитить.


      1. vmalyutin
        11.01.2025 10:37

        Ну C тут явно не нужен. Подойдёт буквально любой язык. Даже интерпретатор Python. Тут скорее дело в том, что общую оценку производительности разработчики PG уже провели. Наверняка выявили сильные и слабые стороны, описали их. Теперь дело за разработчиками прикладных систем. Именно они должны тестировать свои решения с применением нового типа данных на их железе и в их среде выполнения. Тут серебряной пули быть не может. К примеру, и с последовательными типами данных можно так все устроить, что работать будет ужасно медленно.


        1. SergeyProkhorenko Автор
          11.01.2025 10:37

          Функции в ядре PostgreSQL пишутся на C.


  1. Ryav
    11.01.2025 10:37

    Кстати, а почему в 17 не завезли? Я на проектах использую такую реализацию (но хотелось бы что-то из коробки):

    CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
    create or replace function uuid_generate_v7()
    returns uuid
    as $$
    declare
      unix_ts_ms bytea;
      uuid_bytes bytea;
    begin
      unix_ts_ms = substring(int8send(floor(extract(epoch from clock_timestamp()) * 1000)::bigint) from 3);
    
      -- use random v4 uuid as starting point (which has the same variant we need)
      uuid_bytes = uuid_send(gen_random_uuid());
    
      -- overlay timestamp
      uuid_bytes = overlay(uuid_bytes placing unix_ts_ms from 1 for 6);
    
      -- set version 7
      uuid_bytes = set_byte(uuid_bytes, 6, (b'0111' || get_byte(uuid_bytes, 6)::bit(4))::bit(8)::int);
    
      return encode(uuid_bytes, 'hex')::uuid;
    end
    $$
    language plpgsql
    volatile;
    



    1. SergeyProkhorenko Автор
      11.01.2025 10:37

      Стандарт RFC 9562 еще не был утвержден к моменту "заморозки" функциональности 17 версии PostgreSQL. Поэтому никто не согласился закоммитить функцию по еще не утвержденному стандарту, хотя он был высокой степени готовности и после этого не изменялся. Если бы RFC 9562 утвердили на пару недель раньше, то функция попала бы в 17 версию PostgreSQL. Правда, после этого функция претерпела изменение: вместо прежнего метода 1 (используется счетчик) в ней теперь используется метод 3 (используется субмиллисекундный сегмент таймстемпа, но при этом весь таймстемп работает как счетчик в критических ситуациях), что почти всегда позволяет обеспечить монотонность даже при многопоточной генерации идентификаторов. То есть, мы получили более полезную функцию, но с задержкой еще на год.