Найти в Дзене

UUID v4: тихий убийца производительности PostgreSQL

В последние годы UUID версии 4 стали почти стандартом «по умолчанию» для первичных ключей. Их любят за универсальность, распределённость и мнимую «безопасность». Но свежий и очень подробный разбор Эндрю Аткинсона заставляет взглянуть на эту практику без розовых очков — и цифры там, мягко говоря, пугающие. Я бы сказал так: UUID v4 — это отличный пример того, как удобство на уровне API может незаметно превратиться в инфраструктурную проблему на уровне базы данных. Где именно всё ломается На бумаге UUID v4 выглядит красиво: 128 бит, почти нулевая вероятность коллизий, можно генерировать где угодно — хоть в браузере, хоть в микросервисе. Но PostgreSQL живёт по своим физическим законам. B-Tree индекс — это упорядоченная структура. А UUID v4 по своей природе случаен. В результате: 🧱 Каждая вставка летит в случайное место индекса
🧠 Кэш забивается фрагментированными страницами
🔀 Page split происходят чаще, чем хотелось бы
🪵 WAL раздувается сильнее обычного В тесте с 10 млн строк разница м
Оглавление

В последние годы UUID версии 4 стали почти стандартом «по умолчанию» для первичных ключей. Их любят за универсальность, распределённость и мнимую «безопасность». Но свежий и очень подробный разбор Эндрю Аткинсона заставляет взглянуть на эту практику без розовых очков — и цифры там, мягко говоря, пугающие.

Я бы сказал так: UUID v4 — это отличный пример того, как удобство на уровне API может незаметно превратиться в инфраструктурную проблему на уровне базы данных.

Где именно всё ломается

На бумаге UUID v4 выглядит красиво: 128 бит, почти нулевая вероятность коллизий, можно генерировать где угодно — хоть в браузере, хоть в микросервисе. Но PostgreSQL живёт по своим физическим законам.

B-Tree индекс — это упорядоченная структура. А UUID v4 по своей природе случаен. В результате:

🧱 Каждая вставка летит в случайное место индекса
🧠
Кэш забивается фрагментированными страницами
🔀
Page split происходят чаще, чем хотелось бы
🪵
WAL раздувается сильнее обычного

В тесте с 10 млн строк разница между bigint и UUID v4 оказалась не «чуть хуже», а катастрофической: более 30 000% разницы по количеству обращений к памяти. Это не микробенчмарк ради галочки — это секунды дополнительной задержки на реальных запросах.

Почему это ощущается как «вдруг всё стало медленно»

Самый коварный момент — UUID v4 редко убивает систему сразу. Всё начинается незаметно:

🚶‍♂️ база растёт
📈 индексы пухнут
🧩 кэш-хиты падают
⏱️ latency медленно ползёт вверх

И в какой-то момент ты смотришь на pg_stat_statements и не понимаешь, почему простой UPDATE ... WHERE id = ? внезапно стоит секунды. А причина банальна: PostgreSQL вынужден трогать на миллионы страниц больше, чем при последовательных ключах.

«Но UUID же безопасные?» — нет

Это ещё один популярный миф. UUID не являются токенами безопасности. Об этом прямо говорит RFC. Они лишь неудобны для угадывания, но не предназначены для защиты.

Если цель — скрыть реальные идентификаторы от пользователя, есть куда более изящные решения:

🧮 integer / bigint внутри базы
🔐 псевдослучайный публичный ID поверх
🔤 base62 / hashids / XOR-схемы

Такой подход даёт и производительность, и контроль, и читаемую архитектуру.

UUID v7 — компромисс, а не панацея

Хорошая новость: проблема не в UUID как классе, а в версии 4. UUID v7 включает временную компоненту и естественно упорядочен, что резко снижает:

📉 фрагментацию
📦 размер индекса
⚙️ количество page split
🔥 нагрузку на WAL

PostgreSQL 18 должен принести поддержку UUID v7 «из коробки», а пока можно жить с расширениями. Это разумный вариант для распределённых систем, где генерация ID вне БД действительно необходима.

Моё практическое правило

Если упростить всё до инженерного здравого смысла:

🧠 Одна база, один сервис → bigint
🌍
Много сервисов, много источников → UUID v7
🚫
UUID v4 как PK → только если вы осознанно принимаете издержки

UUID v4 — это не «плохо», это дорого. И цена растёт нелинейно вместе с объёмом данных.

Итог

UUID v4 — пример того, как абстракция высокого уровня конфликтует с физикой хранения данных. PostgreSQL — не магия, а очень эффективная машина, которая любит порядок. И если кормить её случайностью, она обязательно возьмёт своё — задержкой (latency), IO и памятью.

Иногда самый современный выбор — это старый добрый bigint.

Источники и материалы

🔗 Основная статья:
https://andyatkinson.com/avoid-uuid-version-4-primary-keys

🔗 RFC UUID (безопасность):
https://datatracker.ietf.org/doc/html/rfc4122

🔗 UUID v7 и временная сортировка:
https://www.thenile.dev/blog/uuidv7

🔗 Неожиданные минусы UUID в PostgreSQL (Cybertec):
https://www.cybertec-postgresql.com/en/uuid-serial-or-identity-columns-for-postgresql-auto-generated-primary-keys/

🔗 Page splits и индексы (PlanetScale):
https://planetscale.com/blog/the-problem-with-using-a-uuid-primary-key-in-mysql

🔗 Буферный кэш PostgreSQL:
https://stringintech.github.io/blog/p/postgresql-buffer-cache-a-practical-guide/