UUID как id — вроде идеально: генерится на клиенте, не угадать, не конфликтует между сервисами
Все привыкли лепить его как первичный ключ и не думать
А потом база на паре миллионов строк начинает необъяснимо тупить на вставках
Разберём, почему так и что с этим делать
🎲 Корень зла — случайность Обычный UUID (v4) полностью случайный
Выглядит безобидно, но вот в чём засада: первичный ключ в базе хранится не кучей, а в отсортированном виде (B-tree индекс)
Когда id идёт по порядку (обычный автоинкремент 1, 2, 3...), каждая новая строка ложится в конец
А случайный UUID лезет каждый раз в новое случайное место в середину индекса
Базе приходится раздвигать уже забитые страницы, чтобы впихнуть строку между существующими
На потоке вставок таких разрывов тысячи, индекс пухнет и фрагментируется
💾 Мимо кэша Второй удар — по кэшу.
База держит в оперативке горячие страницы (buffer pool)
Когда вставки идут по порядку, ты всё время работаешь с одним и тем же «хвостом» индекса, он всегда в памяти
А со случайным UUID каждая вставка дёргает случайную страницу — сегодня эту, через миг вон ту
В память всё не влезает, и база начинает гонять страницы с диска
На больших таблицах это разница между «мгновенно» и «чего оно висит»
📏 И просто жирнее UUID — это 16 байт, а bigint — 8
Вроде мелочь, но первичный ключ тащится во все индексы и во все внешние ключи, что на него ссылаются Помножь на миллионы строк и десяток ссылающихся таблиц — набегает ощутимо
А если ты ещё и хранишь UUID строкой (
varchar(36)) вместо родного типа — это уже 36+ байт, и вообще больТак делать не надо.
🛠 Что делать Хорошая новость: отказываться от UUID не нужно, надо взять правильный
UUIDv7 — это UUID, у которого в начале зашито время создания
Он всё так же уникален и генерится на клиенте, но при этом растёт по порядку — новые id всегда больше старых Для базы он ложится так же аккуратно, как автоинкремент: в конец, без разрывов страниц и без прыжков по памяти
Забираешь все плюсы UUID и убираешь главный минус
-- вместо случайного uuid_generate_v4()
-- берём time-ordered v7 (в свежих версиях уже из коробки)
Если UUIDv7 под рукой нет — есть ULID, работает по тому же принципу (время + случайность, растёт по порядку)
А если тебе вообще не нужна генерация на клиенте и распределённость — старый добрый bigint-автоинкремент по скорости не переплюнуть
И маленькое, но важное: храни UUID родным типом (
uuid в постгресе, binary(16) в mysql), а не строкойИначе ты сверху к случайности добавляешь ещё и лишний вес
⚖️ Когда UUID реально оправдан
Чтобы не подумали, что UUID — это плохо
Он отлично заходит, когда id нужно сгенерить ДО похода в базу: например, клиент создаёт запись офлайн, или несколько сервисов пишут в одну таблицу и не должны драться за общий счётчик
Ещё UUID не даёт угадать соседние записи по id (с автоинкрементом любой видит, что до него было
/order/1041, а значит есть и /order/1040)Вопрос не в том, брать UUID или нет, а в том, чтобы он был упорядоченный, а не случайный
А вы уже переехали на v7? 🤔
#database #postgres #backend #performance #sql #dev