> Почему использовать UUID вместо int для идентификаторов (Go)

Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: Ozon

Стек: Go

> Пример ответа

Короткий ответ

UUID выбирают вместо int для глобальной уникальности без централизованной координации, что критично в распределённых системах, микросервисах и при миграциях/слияниях данных. Int удобен для внутренних ключей с автоинкрементом, но создаёт проблемы с предсказуемостью, коллизиями при шардировании и раскрытием объёма данных. UUID (особенно v7) даёт приемлемый компромисс между уникальностью, сортируемостью и производительностью индексов. Выбор зависит от контекста: OLTP с одним инстансом - int, распределённая система - UUID.

Подробное объяснение

Основные причины использования UUID:

  • Глобальная уникальность без координации - генерация на клиенте или в любом сервисе не требует обращения к центральному счётчику, что устраняет bottleneck и упрощает офлайн-режимы.
  • Безопасность и недетерминизм - int-идентификаторы предсказуемы (id=100500 → можно дёргать API), UUID v4 содержит 122 бита случайности, что затрудняет перебор.
  • Миграции и слияния - при объединении баз данных или переносе записей между окружениями int-ключи конфликтуют, UUID - нет.
  • Шардирование - распределение по шардам на основе int-ключа требует сложной логики (range, hash), UUID можно генерировать с учётом шарда (например, в v7 первые биты - timestamp, дальше - шард).
  • Кэширование и интеграции - внешние системы (клиенты, партнёры) получают стабильный идентификатор, не зависящий от внутренней нумерации.

Минусы UUID:

  • Размер - 16 байт против 4-8 у int, что увеличивает размер индексов и памяти.
  • Случайность - v4 ломает кластеризацию в B-tree, вызывая page splits и фрагментацию индексов. Решается UUID v7 (time-ordered) или ULID.
  • Читаемость - строковое представление (36 символов) неудобно в логах и отладке.

Сравнение с int:

КритерийintUUID
Размер4-8 байт16 байт
Генерацияцентрализованнаялокальная
Предсказуемостьданет (кроме v7)
Сортировкаестественнаяv7 - да, v4 - нет
Индекскомпактныйтяжёлый

В Go типичные подходы: uuid.New() из github.com/google/uuid (v4) или github.com/gofrs/uuid с поддержкой v7. Для PostgreSQL - тип uuid нативно, для MySQL - BINARY(16) или CHAR(36).

На практике

  • Когда int оправдан: внутренние сущности с высокой частотой вставки (логи, события), где нет внешнего доступа и шардирования. Автоинкремент даёт компактность и предсказуемость порядка.
  • Когда UUID обязателен: публичные API, мультитенантные системы, синхронизация между сервисами, event sourcing, CQRS.
  • Гибридный подход: int как внутренний ключ (PK), UUID как внешний public_id с уникальным индексом. Так делают, когда нужна скорость join'ов и безопасность внешних ссылок.
  • Производительность: для UUID v7 в PostgreSQL индекс работает почти как для bigint, но всё равно больше по размеру. Для v4 - ожидайте деградацию на больших таблицах (миллионы строк).
  • Генерация на клиенте - плюс для офлайн-режимов, минус - нельзя доверять порядку без timestamp.

Пример кода

GO
package main
import (
"fmt"
"github.com/google/uuid"
"time"
)
// UUID v7 - time-ordered, сортируемый
func newUUIDv7() uuid.UUID {
// в google/uuid нет v7, используем gofrs/uuid
// пример для gofrs/uuid:
// id, _ := uuid.NewV7()
// return id
return uuid.New() // v4 для демонстрации
}
type User struct {
ID uuid.UUID `db:"id"`
Name string `db:"name"`
}
func main() {
id := uuid.New()
fmt.Println(id.String()) // 6ba7b810-9dad-11d1-80b4-00c04fd430c8
// для сортируемых идентификаторов в PostgreSQL:
// CREATE EXTENSION IF NOT EXISTS pgcrypto;
// CREATE TABLE users (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), ...)
// для v7: CREATE TABLE users (id uuid PRIMARY KEY DEFAULT uuidv7(), ...)
}

Как отвечать на собеседовании

Начните с главного trade-off: глобальная уникальность без координации против размера и производительности индексов. Затем уточните контекст - спросите, о какой системе речь (распределённая или монолит). Если спрашивают абстрактно, структурируйте ответ:

  1. Назовите ключевые причины: распределённость, безопасность, миграции.
  2. Признайте минусы: размер, фрагментация индексов для v4.
  3. Упомяните современные решения: UUID v7, ULID, Snowflake - они решают проблему сортировки.
  4. Дайте практическую рекомендацию: для публичных API - UUID, для внутренних высоконагруженных таблиц - int или bigint.
  5. Приведите пример из Go: github.com/google/uuid или gofrs/uuid, тип uuid в PostgreSQL.

Не углубляйтесь в детали реализации, если не просят. Держите ответ в рамках 2-3 минут, затем предложите обсудить конкретный сценарий.

Что проверяет интервьюер

  • Понимание trade-off между уникальностью, производительностью и сложностью.
  • Знание особенностей индексов в БД (кластеризация, page splits).
  • Осведомлённость о современных форматах (v7, ULID) - признак актуальности знаний.
  • Умение адаптировать ответ под конкретный стек (Go + PostgreSQL/MySQL).
  • Способность аргументировать выбор, а не просто заучивать "UUID - это хорошо".

Типичные ошибки

  • Однобокость: "UUID всегда лучше" или "int всегда лучше" - нет, всё зависит от сценария.
  • Игнорирование v7: упоминание только v4 без сортируемости выглядит устаревшим.
  • Путаница с безопасностью: UUID не шифрует данные, он лишь недетерминирован.
  • Забывают про размер индекса: на таблицах с миллиардами строк разница между 4 и 16 байтами существенна.
  • Предлагают UUID для внутренних join'ов без причины - это увеличивает память и время запросов без пользы.
  • Не упоминают гибридный подход (int + public_id) - это сильный ответ, показывающий практический опыт.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью