Оригинал статьи Ants Aasma

Существует множество причин использования универсального уникального идентификатора (UUID) в качестве первичного ключа таблиц баз данных. Например:

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

  • перенос наборов связанных записей из одной базы в другую без необходимости перенумерации.

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

Самый распространенный способ получения UUID — случайный выбор 128-битного числа. (Если вас беспокоит возможность повтора случайных значений: выиграть джекпот в лотерее два раза подряд является гораздо более вероятным событием, чем повтор двух случайных 128-битных чисел). Стандартом генерации случайного 128-битного числа сейчас является UUID v4. В нем заданы фиксированные значения шести бит, что оставляет 122 бита для случайного выбора, но и этого вполне достаточно для всех практических целей. В PostgreSQL для получения UUID по стандарту v4 используется функция gen_random_uuid().

Проблемы случайных чисел

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

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

Эксперимент

Для начала создадим таблицу записей с тремя идентификаторами: 1) из обычной последовательности, 2) случайный UUID, 3) последовательный UUID (используя метод UUID v7, который скоро будет стандартизирован). Добавим 100 байт заполнителя для каждой строки для реалистичности. В таблице будет 10 миллионов записей.

locality=# create table records (id int8 not null, uuid_v4 uuid not null, uuid_v7 uuid not null, filler text);
CREATE TABLE
Time: 98.515 ms
locality=# insert into records select id, gen_random_uuid(), uuid_generate_v7(), repeat(' ', 100) from generate_series(1, 10000000) id;
INSERT 0 10000000
Time: 28050.965 ms (00:28.051)

Добавим уникальный индекс для каждого из столбцов id, как в обычной таблице, и запустим VACUUM ANALYZE.

locality=# create index on records (id);
CREATE INDEX
Time: 1689.437 ms (00:01.689)
locality=# create index on records (uuid_v4);
CREATE INDEX
Time: 2378.215 ms (00:02.378)
locality=# create index on records (uuid_v7);
CREATE INDEX
Time: 1956.945 ms (00:01.957)
locality=# vacuum analyze records;
VACUUM
Time: 281.858 ms

Проверим размеры таблицы и индексов, убедимся, что они кэшированы:

locality=# select relname, pg_size_pretty(pg_relation_size(oid)), pg_prewarm(oid) from pg_class where relname like 'records%';
       relname       | pg_size_pretty | pg_prewarm 
---------------------+----------------+------------
 records             | 1662 MB        |     212766
 records_id_idx      | 214 MB         |      27422
 records_uuid_v4_idx | 301 MB         |      38506
 records_uuid_v7_idx | 301 MB         |      38506
(4 rows)
 
Time: 283.849 ms

Выполним пару index‑only запросов, чтобы посмотреть как работают созданные индексы:

locality=# SELECT COUNT(id) FROM records;
  count  
----------
 10000000
(1 row)
 
Time: 526.486 ms
locality=# SELECT COUNT(uuid_v4) FROM records;
  count  
----------
 10000000
(1 row)
 
Time: 1213.813 ms (00:01.214)

Запрос по индексу с UUID примерно в два раза медленнее, но и размер самого UUID вдвое больше чем int8, так что пока все бьется, верно?

Теперь посмотрим на запрос по последовательному индексу по UUID (v7).

locality=# SELECT COUNT(uuid_v7) FROM records;
  count  
----------
 10000000
(1 row)
 
Time: 541.322 ms

Вот тут уже интересно. Похоже, что снижение скорости выполнения запроса вызвано использованием случайной последовательности, а не размером идентификатора. Смотрим планы запросов.

locality=# EXPLAIN (BUFFERS, ANALYZE, TIMING OFF) SELECT COUNT(id) FROM records;
                                                             QUERY PLAN                                                             
------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=202422.47..202422.48 rows=1 width=8) (actual rows=1 loops=1)
   Buffers: shared hit=27332
   ->  Index Only Scan using records_id_idx on records  (cost=0.43..177422.46 rows=10000002 width=8) (actual rows=10000000 loops=1)
         Heap Fetches: 0
         Buffers: shared hit=27332
 Planning Time: 0.056 ms
 Execution Time: 777.764 ms
(7 rows)
locality=# EXPLAIN (BUFFERS, ANALYZE, TIMING OFF) SELECT COUNT(uuid_v4) FROM records;
                                                                QUERY PLAN                                                                
------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=213506.47..213506.48 rows=1 width=8) (actual rows=1 loops=1)
   Buffers: shared hit=8562960
   ->  Index Only Scan using records_uuid_v4_idx on records  (cost=0.43..188506.46 rows=10000002 width=16) (actual rows=10000000 loops=1)
         Heap Fetches: 0
         Buffers: shared hit=8562960
 Planning Time: 0.058 ms
 Execution Time: 1430.684 ms
(7 rows)

Ну и ну. Что тут происходит с обращениями к общим буферам в запросе со случайным UUID? Чтобы понять это, придется разобраться с тем как работает index‑only сканирование в PostgreSQL.

Как работает index-only сканирование

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

Оптимизация

В PostgreSQL необходимость чтения данных индексируемой таблицы учтена и оптимизирована. При очистке (vacuum) PostgreSQL отслеживает, какие страницы (блоки) полностью доступны для всех — это называется «видимость страницы». Данные по видимости страниц доступны в карте видимости (файл с суффиксом _vm, который хранится в том же каталоге что и данные таблицы). Для каждой страницы таблицы БД размером 8Кб карта видимости использует два бита (первый бит отмечает доступность для всех) — то есть карта видимости занимает в 32 768 раз меньше места, чем сама таблица, и поэтому легко кэшируется. При выполнении index‑only выборки если карта видимости говорит, что страница, на которой находится запись, видна всем, то чтение записи из таблицы можно пропустить и сэкономить кучу времени.

Но ведь обе наших выборки обращаются к карте видимости, откуда же разница в результатах? Ответ заключается в том, что доступ к странице в общих буферах не является бесплатным — он включает блокировку, поиск в довольно большой хэш‑таблице и запись отметки об уже просмотренных страницах. Если следующая запись в индексе ссылается на ту же самую страницу таблицы, что и предыдущая, все эти действия можно пропустить. Ну, вы поняли куда я клоню…

В чем проблема

Каждая страница карты видимости содержит данные о 8 192*8/2 = 32 768 страницах таблицы или 256 Мб. В случае с последовательным индексом близкие по значению идентификаторы практически всегда относятся к одной и той же странице карты видимости, поэтому оптимизация работает отлично. Однако, при случайных значениях UUID вероятность того, что два последовательных идентификатора ссылаются на одну и ту же страницу, составляет всего 1 из 7 (256 Mb / 1662 Mb – размер таблицы). Это приводит к необходимости выполнения около 6/7*10M ~ 8,5M дополнительных обращений к буферу для проверки видимости, что и отражено в плане запроса. Обращение к буферу стоит недорого, но оно не бесплатно, а уж если повторить его 8,5 миллионов раз, то разница в расходе ресурсов становится очевидной.

Для проверки посмотрим на план запроса с использованием индекса по последовательному UUID.

locality=# EXPLAIN (BUFFERS, ANALYZE, TIMING OFF) SELECT COUNT(uuid_v7) FROM records;
                                                                QUERY PLAN                                                                
------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=213506.47..213506.48 rows=1 width=8) (actual rows=1 loops=1)
   Buffers: shared hit=39126
   ->  Index Only Scan using records_uuid_v7_idx on records  (cost=0.43..188506.46 rows=10000002 width=16) (actual rows=10000000 loops=1)
         Heap Fetches: 0
         Buffers: shared hit=39126
 Planning Time: 0.058 ms
 Execution Time: 790.872 ms
(7 rows)

Как видим, количество обращений к общим буферам для индекса по UUID увеличилось относительно индекса по int8 пропорционально размеру данных в индексе (16 байт UUID / 8 байт int8).

Выводы

Мораль этой истории в том, что нужно учитывать физическую структуру данных: она может оказаться значимой в самых неожиданных местах. Использование случайных величин, как правило, является худшим из того что можно сделать для физической структуры, поэтому, если хочется непременно использовать UUID, попробуйте использовать последовательный вариант. UUID v7 — хороший вариант. Надеюсь, он появится в PostgreSQL 17. Your cache hit rates will thank you.

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


  1. 3ton
    12.07.2023 12:27
    +1

    (Если вас беспокоит возможность повтора случайных значений: выиграть джекпот в лотерее два раза подряд является гораздо более вероятным событием, чем повтор двух случайных 128-битных чисел)

    Дело в том, что если клиент выиграет 2 раза подряд в лотерею, те кто устраивают лотерею не разорятся, а вот ваш проект если будет иметь 2 строки с одинаковым идентификатором - может принести печальные результаты.


    1. ermadmi78
      12.07.2023 12:27
      +4

      Вероятность появления баги в коде, из за которого вы вставите 2 одинаковых значения, на несколько порядков выше, чем коллизия в генерации случайного uuid. Просто не забывайте создавать уникальные индексы на столбцы, которые должны содержать уникальные значения. Или объявляйте такие столбцы как primary key - тогда индекс создастся неявно.


    1. maximw
      12.07.2023 12:27
      +1

      Ага, вероятность коллизии uuid примерно такая же, как если высокоэнергетичная частица из космоса ударит в ячейку диска сервера и поменяет значение автоинкреметного id на уже существующее.


      1. ptr128
        12.07.2023 12:27

        Что касается диска, то СХД сам исправит ошибку. А что касается UUID я уже не раз нарвался на его дублирование. Например, из-за совпадений UUID node в двух контейнерах. Причем проблема известная https://stackoverflow.com/questions/27971464/uuid-generated-randomly-is-having-duplicates

        Естественно речь о генерации UUID на множестве хостов. С другой стороны, если уникальный ключ генерируется на одном хосте, то и UUID не нужен. Проще обычный serial использовать.


        1. iskorotkov
          12.07.2023 12:27
          +1

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

          Примеры:

          It could be that in your actual code, the UUIDs are getting mixed up at a later stage, e.g. due to a race condition somewhere in a higher-level layer.

          But, say you generated a set of random numbers with virtual machine A. Then took a snapshot of A. Then sometime later, stopped A, resumed from the snap shot, and resumed generating random numbers

          Instead, look for places where a UUID stored by one server could be clobbering one stored by another. Why does this only happen between 2 servers out of 50? That has something to do with the details of your environment and system that haven't been shared.

          As stated above, the chances of a legit collision are impossibly small. A more likely possibly is if the values are ever transferred between objects in an improper way. For languages like Java that behave as pass by reference...

          Можете поделиться своими историями про коллизии?

          Upd 1: вижу ниже подобное обсуждение, мой вопрос неактуален


          1. ptr128
            12.07.2023 12:27

            Как раз наоборот, видно, что из 50+ JVM автора, у двух одинаковый сегмент node. Вероятность такого при V4 близка к нулю. А при V1/V3 это обозначает лишь то, что у этих JVM совпали mac-адреса виртуальных сетевых адаптеров. Что, в принципе, обычное дело. Я даже на совпадение mac-адресов физических адаптеров нарывался.


          1. ptr128
            12.07.2023 12:27
            -1

            У меня похожая история, только много разных поставщиков сообщений, многие из которых явно генерируют V1/V3.

            V4, используемый в PotgreSQL по-умолчанию, сильно зависит от доступной энтропии. А с ней в контейнерах очень плохо. Особенно, если хостятся они тоже в VM. Поэтому вероятность поймать совпадающую энтропию при одновременном старте на разных ядрах двух контейнеров из одного и того же образа - не так уж мала.


  1. dsh2dsh
    12.07.2023 12:27

    Приводя в пример лотерею, ведь наверняка не учитывается, что попытка выиграть в неё будет делаться несколько миллионов раз в секунду? А вставка строки в таблицу, и соответственно, генерирование нового случайного ID, вполне может выполняться миллион раз в секунду. Так что мне кажется, некорректное это сравнение.


    1. ermadmi78
      12.07.2023 12:27
      +1

      Согласно википедии, вам надо генерировать 1 миллиард случайных UUID'ов каждую секунду в течении 86 лет, чтобы схлопотать коллизию. Боюсь, у вас база раньше треснет...


      1. ptr128
        12.07.2023 12:27
        -1

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


        1. ermadmi78
          12.07.2023 12:27
          -1

          Так как раз на разных хостах случайные uuid'ы и интересно генерировать - для горизонтального масштабирования хранилища. И в реальной практике их именно так и используют. Если бы вероятность коллизии из за этого существенно повышалась, от этой практики уже давным давно отказались бы. Но что то я об этом пока не слышал.

          Я, в своей практике, на дубликаты тоже нарывался. Но, как показывало расследование этих инцидентов, во всех случаях это были "рукотворные" дубликаты. Кто-то руками написал insert с uuid'ом, взятым с другого хоста. Или кто то случайно залил бекап не на тот хост. Был даже случай, когда программист сам пытался генерировать uuid'ы с помощью инкрементации.


          1. ptr128
            12.07.2023 12:27
            -1

            Вы не слышали, а я на это уже не раз нарывался. И не я один https://stackoverflow.com/questions/27971464/uuid-generated-randomly-is-having-duplicates


            1. ermadmi78
              12.07.2023 12:27

              Почитал. Практически во всех ответах автору поста советуют поискать багу в коде. Хотелось бы увидеть более убедительное исследование, желательно с математическим обоснованием и, в идеале, бенчмарки со стабильным воспроизведением проблемы.


              1. ptr128
                12.07.2023 12:27
                -1

                Плохо читали. Речь именно о том, что из множества JVM у автора в какой то паре формировался совпадающий node. А уже совпадение времени генерации сообщения на разных хостах - дело обычное.

                Математическое обоснование - не придумали еще механизм формирования гарантированно разного node на разных виртуальных хостах. В пределах одного кластера k8s проблему решили. В пределах множества разных кластеров - нет.


                1. ermadmi78
                  12.07.2023 12:27
                  -1

                  И несколько раз в день стреляла коллизия? И всегда на одних и тех же 2х серваках? А на остальных не стреляла? Ну ну...

                  PS

                  Автору лучше посмотреть историю накатывания бекапов в базы. За сим откланяюсь :)))


                  1. ptr128
                    12.07.2023 12:27

                    Если речь об авторе топика по моей ссылке, то у него изначально совпали node на паре JVM, а он их и не пытался пересоздавать, отчего и получал коллизии постоянно.

                    Если же речь о моем опыте с множеством k8s кластеров, то не приписывайте мне слова кого-то постороннего. Нарвавшись на такие приколы несколько раз, я от использования только UUID, как уникального идентификатора просто отказался, добавляя к нему в составной уникальный ключ еще и мой внутренний уникальный идентификатор источника сообщений. Это проблему радикально решило и я доволен.


                    1. breninsul
                      12.07.2023 12:27

                      так вы можете написать свою функцию генерации uuid на основе своей ид ноды. Только вот при чем тут jvm к PostgreSQL, вы в приложении uuid генерили?


                      1. ptr128
                        12.07.2023 12:27
                        -1

                        вы можете написать свою функцию генерации uuid

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

                        Только вот при чем тут jvm к PostgreSQL

                        Потому что именно PostgreSQL выступает в качестве DWH.

                        В PostgreSQL есть смысл использовать UUID так же только в случае, когда он совсем не единственный поставщик информации для этой сущности. В противном случае serial всяко проще и удобней.


                    1. breninsul
                      12.07.2023 12:27

                      кстати, у mongo еще меньше байт в уникальном ObjectId(96). Удивительно, что нет постоянных дублей


                      1. ptr128
                        12.07.2023 12:27
                        -1

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


                  1. ptr128
                    12.07.2023 12:27

                    Специально проверил. За этот год уже два дубля UUID в БД. Хотя сообщений еще меньше 10 млрд. Один раз задублировались белорусы с казахами, другой раз - РФ с белорусами.


                    1. breninsul
                      12.07.2023 12:27
                      +1

                      у вас генератор случайных чисел сломался, почините


                      1. ptr128
                        12.07.2023 12:27
                        -1

                        А с чего это Вы решили что все мои поставщики информации используют исключительно V4-V5? По моим наблюдениями, там и V1 прилетает.


                      1. breninsul
                        12.07.2023 12:27
                        +1

                        какие на*уй постовщики информации, у вас uuid генерируют клиенты? Чё вы несете?!


                      1. ptr128
                        12.07.2023 12:27

                        Вы бы хоть почитали выше:

                        Если же кластеров несколько, то вероятность совпадения сегмента node в двух контейнерах разных кластеров не такая уж маленькая. Понятно, что тут сильная зависимость от версии UUID и способа генерации значения node. Но, в случае контейнеров из одного и того же образа, да еще и стартующих одновременно, например, каждый час, возможности тут довольно ограничены.

                        Откуда тут нормальная энтропия возьмется?


          1. ptr128
            12.07.2023 12:27
            +1

            Кстати, еще несколько лет назад, даже в пределах одного кластера k8s могли подняться два контейнера в которых в UUID сегмент node совпадал. Пару лет назад эту багу поправили, но только для одного кластера. Если же кластеров несколько, то вероятность совпадения сегмента node в двух контейнерах разных кластеров не такая уж маленькая. Понятно, что тут сильная зависимость от версии UUID и способа генерации значения node. Но, в случае контейнеров из одного и того же образа, да еще и стартующих одновременно, например, каждый час, возможности тут довольно ограничены.


          1. Didimus
            12.07.2023 12:27

            Погодите, если создать две абсолютно идентичные виртуальные машины, на них разве не могут генерироваться одинаковые случайные числа?


            1. ermadmi78
              12.07.2023 12:27
              -1

              Ну, если речь о случайных числах в Java, то там вам придется запустить процессы в одно и то же время вплоть до наносекунд:

                  /**
                   * Creates a new random number generator. This constructor sets
                   * the seed of the random number generator to a value very likely
                   * to be distinct from any other invocation of this constructor.
                   */
                  public Random() {
                      this(seedUniquifier() ^ System.nanoTime());
                  }

              Если речь о случайных UUID'ах в Java, то там все несколько хитрее, используется SecureRandom:

                  /**
                   * Constructs a secure random number generator (RNG) implementing the
                   * default random number algorithm.
                   *
                   * <p> This constructor traverses the list of registered security Providers,
                   * starting with the most preferred Provider.
                   * A new {@code SecureRandom} object encapsulating the
                   * {@code SecureRandomSpi} implementation from the first
                   * Provider that supports a {@code SecureRandom} (RNG) algorithm is returned.
                   * If none of the Providers support a RNG algorithm,
                   * then an implementation-specific default is returned.
                   *
                   * <p> Note that the list of registered providers may be retrieved via
                   * the {@link Security#getProviders() Security.getProviders()} method.
                   *
                   * <p> See the {@code SecureRandom} section in the <a href=
                   * "{@docRoot}/../specs/security/standard-names.html#securerandom-number-generation-algorithms">
                   * Java Security Standard Algorithm Names Specification</a>
                   * for information about standard RNG algorithm names.
                   */
                  public SecureRandom() {
                      /*
                       * This call to our superclass constructor will result in a call
                       * to our own {@code setSeed} method, which will return
                       * immediately when it is passed zero.
                       */
                      super(0);
                      getDefaultPRNG(false, null);
                      this.threadSafe = getThreadSafe();
                  }

              Там уже все будет зависеть от сконфигурированного для JVM алгоритма:

              https://docs.oracle.com/en/java/javase/11/docs/specs/security/standard-names.html#securerandom-number-generation-algorithms

              Ну а если речь идет о случайных UUID в PostgresSQL (а в этом треде речь идет именно о нём), то обычно на виртуалках его и не разворачивают. И уж тем более раз в час не перезапускают. Ну и тем более, если вы в качестве первичных ключей используете рандомные uuid'ы, то вполне можно (и даже нужно) выдвинуть соответствующие требования к развертыванию инфраструктуры.


              1. ermadmi78
                12.07.2023 12:27
                -1

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


              1. Didimus
                12.07.2023 12:27

                Так в виртуальной машине и время виртуальное?


                1. ermadmi78
                  12.07.2023 12:27

                  Ну да, можно настроить время так, чтобы часы все время отдавали 0. Это, пожалуй, хороший способ. Это я и называю - очень сильно постараться.


              1. ermadmi78
                12.07.2023 12:27

                По SecureRandom в Java вот интересную статью нашел:

                https://blog.progwards.ru/uchimsya-pravilno-rabotat/

                PS

                Мне почему то казалось, что алгоритм по умолчанию это SHA1PRNG. Но я этот вопрос изучал на этапе Java 8 - возможно с тех пор что то изменилось.


                1. ermadmi78
                  12.07.2023 12:27

                  Про настройку источника энтропии в виртуальных машинах можно почитать здесь. Но если в Linux алгоритм по умолчанию это NativePRNG, то "много" энтропии и не надо - только чтобы проинициализировать seed при старте java процесса.

                  Если сильно беспокоитесь насчет генерации уникальный UUID в виртуальной машине, можно принудительно сконфигурировать NativePRNG - этого должно быть достаточно. Если же паранойя спать не даёт, то настроить NativePRNGBlocking и поколдовать над источниками энтропии.


              1. ptr128
                12.07.2023 12:27
                -1

                запустить процессы в одно и то же время вплоть до наносекунд

                Так при одновременном запуске сразу нескольких контейнеров из одного образа именно это и происходит. Достаточно, чтобы на хосте k8s свободных ядер в этот момент было не меньше, чем контейнеров.

                Отсюда же и проблемы с энтропией в контейнерах.


                1. ermadmi78
                  12.07.2023 12:27

                  Вам пора церковь свидетелей uuid коллизии открывать! :)


                  1. ptr128
                    12.07.2023 12:27

                    А Вы не задумывались, для чего у UUID есть целых пять версий, причем четыре из них поддерживаются обсуждаемым PostgreSQL?


                    1. ermadmi78
                      12.07.2023 12:27
                      -1

                      Я не силён в теологических спорах. Я привык оперировать математическими доказательствами и бенчмарками. Образование, знаете ли, сказывается.

                      Так сколько вы говорите версий uuid'ов может танцевать на булавочной головке?


                      1. ptr128
                        12.07.2023 12:27

                        Мои соболезнования поклоннику сферических коней в вакууме )))


                1. ermadmi78
                  12.07.2023 12:27
                  -1

                  И если бы вы знали, сколько всего странного и интересного происходит при старте java процесса. И сколько есть нюансов в вопросах измерения времени (особенно с System.nanoTime()). То вам бы стыдно стало от той чуши, что вы написали. Благо знаете вы мало, поэтому не стыдитесь.


              1. ptr128
                12.07.2023 12:27

                UUID в PostgresSQL (а в этом треде речь идет именно о нём)

                PostgreSQL лишь БД в которой сохраняются сообщения с UUID из разных источников. Сам он источником UUID выступает не часто. Все же sequence явно производительней и удобней. Доказательство этого утверждения я приводил уже выше.


                1. ermadmi78
                  12.07.2023 12:27
                  -1

                  Сам он источником UUID выступает не часто.

                  У вас, может и не часто. А у меня практически всегда.


                  1. ermadmi78
                    12.07.2023 12:27
                    -1

                    Вот так примерно:

                    create extension if not exists "uuid-ossp" schema "public";
                    
                    create table my_relation
                    (
                        id uuid not null default uuid_generate_v4(),
                        primary key (id)
                    );


                    1. ptr128
                      12.07.2023 12:27

                      Вот только зачем подключать uuid-ossp, если родной get_random_uuid() и так генерирует V4?


                  1. ptr128
                    12.07.2023 12:27
                    -1

                    Не расстраивайтесь. У Вас еще будет возможность набраться опыта работы с распределенными гетерогенными системами и мультисервисными архитектурами.

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


                    1. ermadmi78
                      12.07.2023 12:27

                      Не учи дедушку кашлять...


                1. breninsul
                  12.07.2023 12:27
                  +1

                  Если для вас БД - чисто хранилище данных а uuid прилетают с клиентов - это чисто ваша проблема. Да, внешние клиенты id и захардкодить могут, кто ж им указ


  1. Ivan22
    12.07.2023 12:27
    +1

    при сильной OLTP нагрузке на запись - этот gen_random_uuid() будет узким местом.
    Я использую где возможно для генерации суррогатных id hash функции от бизнес ключа.
    Это еще и ооочень удобно тем что ты можешь зная бизнес ключ получить суррогатник не делая доп поиск по таблице.


    1. ermadmi78
      12.07.2023 12:27
      +2

      этот gen_random_uuid() будет узким местом

      ИМХО - запись на диск по любому дороже генерации UUID. Т.е. если вы дошли до того, что генерация uuid стала узким местом, то значит в вашем хранилище имеются более фундаментальные проблемы, чем генератор uuid. И, отказавшись от генерации uuid, вы с вероятностью 99% не уберете узкое место. Так как фундаментальные проблемы требуют фундаментальных решений - партиционирование, сегментирование, пересмотр структуры хранилища, переход на другую СУБД - в эту сторону вам придется посмотреть.


      1. maximw
        12.07.2023 12:27
        +1

        Зависит от диска и от способа генерации (псевдо)случайностей.


        1. ermadmi78
          12.07.2023 12:27

          Я, наверное, уже лет 5 использую случайные UUID'ы в качестве первичных ключей. Пока на проблемы с производительностью не нарывался :)

          ИМХО - вы ищите проблемы там, где их нет. Если для первичного ключа сравнивать производительность uuid с генерацией и bigserial, то bigserial гарантированно окажется дороже из за конкуренции за sequence. На проблемы с производительностью sequence'ов я пока тоже не нарывался, хотя sequence'ами пользуюсь более 20 лет ;)

          Главный бонус uuid в качестве первичного ключа - возможность практически бесплатного сегментирования хранилища. Случайные uuid'ы, сгенерированные в разных базах остаются уникальными. bigserial вам, к сожалению, такого бонуса не дает.

          PS

          Пора перестать бояться, и начать генерировать случайные UUID'ы :)


          1. ermadmi78
            12.07.2023 12:27
            -1

            Наверное единственный минус uuid - это размер.


          1. ptr128
            12.07.2023 12:27

            gen_random_uuid - это open("/dev/urandom", O_RDONLY, 0) и чтение из него. Есть подозрение, что три системных вызова open()/read()/close() все же более ресурсоёмки, чем манипуляции с sequence в userspace.

            Kогда же pg_strong_random() работает через OpenSSL RAND_bytes(), там то уж точно борьба за общий ресурс с evp_rand_lock()/evp_rand_unlock()


      1. Ivan22
        12.07.2023 12:27
        +1

        запись на диск дороже, но запись случается гораздо реже. Ну и все же главная причина - вторая.


        1. ermadmi78
          12.07.2023 12:27

          hash вы можете сгенерировать, если у вас уже есть бизнес ключ. А если бизнес ключ генерируется при insert'е (что бывает в 99% случаев) - то у вас есть только 2 разумные альтернативы - sequence или случайный uuid. Как я уже писал выше, в высоко-конкурентной среде sequence гарантированно окажется дороже, так как за него будут конкурировать разные потоки.

          PS

          Ребят, еще раз - не ищите проблем там, где их нет. Поверьте моему опыту - реальные проблемы сами вас найдут. Вот с ними и боритесь ;)


          1. breninsul
            12.07.2023 12:27
            +1

            зависит от сущности, это может быть, например, nft токен, который имеет адрес коллекции(20 байт), сеть(8 байт) и ид токена (256 байт). В таких случаях я предпочитаю создавать кастомный составной тип для ид и использовать его в качестве первичного ключа, но это может бить по производительности, если нам нужен суррогатный ключ - хэш хорошее решение.

            Другое дело, если в сущности не ща что зацепиться, все не уникально или может меняться, тогда да, только суррогатный ключ


          1. ptr128
            12.07.2023 12:27
            -1

            случайный uuid. Как я уже писал выше, в высоко-конкурентной среде sequence гарантированно окажется дороже

            Даже mutex() окажется дешевле, чем генерация случайного или псевдослучайного числа. Тем более futex(). А с учетом времени, необходимого для обновления sequence, futex() даже в высоконагруженной системе только изредка будет приводить к переключению контекста из userspace.

            Просто посмотрите исходники PostgreSQL, чтобы в этом убедиться.


          1. Didimus
            12.07.2023 12:27
            +1

            Можно выполнить предгенерацию последовательных ключей, если время их генерации вносит существенный вклад


            1. ptr128
              12.07.2023 12:27

              А так и делают, указывая CACHE больше единицы в CREATE SEQUENCE. Просто надо отдавать себе отчёт в том, что закешированные, но не использованные значения в сессии, буду утеряны и образуют пробел в нумерации.


    1. breninsul
      12.07.2023 12:27
      +1

      согласен+избавляет от возможности дубля через разные системы (если по какой-то причине запрос создания идет одновременно на 2х серверах)


  1. sshikov
    12.07.2023 12:27
    +4

    значения, которые соседствуют в индексе, будут вставлены в совершенно разное время и находиться в совершенно разных местах в индексируемой таблице.

    Так прямо и хочется спросить — ну и что? Вот возьмем для примера оракл. Там есть так называемые реверсивные индексы, в задачу которых входит разбросать похожие ключи по разным блокам на диске. Потому что последовательная их запись в таблицу приводит к блокировкам одних и тех же блоков и тормозам. Я упрощаю, но суть в том, что узкое место — вставка.


    Если же ключи разбросать — вставка производится быстрее. Иными словами — описанная автором ситуация вовсе не обязательно является проблемой, как тут описано, а наоборот, может быть решением. Все зависит от того, какого сорта у нас нагрузка, и что мы оптимизируем.


    1. tba Автор
      12.07.2023 12:27

      Статья именно про это: оптимизировать нужно под конкретные условия, при этом хорошо понимая как работает выбранная СУБД. Для меня сюрпризом оказалось, что при index-only scans Postgres все равно обращается к индексированной таблице.


      1. sshikov
        12.07.2023 12:27

        Для меня сюрпризом оказалось

        Это еще что. Вы можете себе представить, чтобы оракл полез за select константа… в партиционированный индекс, причем надолго? А он смог… причем где-то на несколько сотен экземпляров СУБД нашлась ровно одна такая, у которой оказался такой извращенный план запроса. Остальные выбирали константу за константное время.


        Статья именно про это:

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


        1. Ivan22
          12.07.2023 12:27

          когда статистика протухла и говорит что в таблице(индексе) 0 строк, оракл с планом такие чудеса творить может ....


          1. sshikov
            12.07.2023 12:27

            Да, тоже логично. База была не моя, не могу ничего про статистики сказать, но легко могу представить протухшие.