> Как организовать таблицы и шардирование для хранения исторических данных (Go)

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

Компании: BrightPattern

Стек: Go

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

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

Для хранения исторических данных оптимальна схема с партиционированием по времени (time-based partitioning) и шардированием по ключу доступа (например, user_id). Таблицы организуются как append-only с компрессией старых партиций. В Go это реализуется через миграции (golang-migrate) и пул соединений с балансировкой по шардам. Ключевой trade-off - горячие партиции (текущий день) против холодных (архив), что решается разными storage engine или настройками compaction.

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

Исторические данные имеют два паттерна доступа: запись текущих событий и чтение за период. Поэтому схема должна разделять hot и cold данные. Партиционирование по времени (daily/monthly) даёт дешёвое удаление старых данных (DROP PARTITION) и ускоряет запросы с фильтром по времени. Шардирование по user_id или account_id обеспечивает горизонтальное масштабирование записи и чтения по конкретному клиенту.

Для Go важно учитывать: connection pool должен быть настроен на каждый шард отдельно (или через прокси типа pgpool). Миграции выполняются идемпотентно на всех шардах. Индексы создаются только на ключевые колонки (user_id, timestamp), потому что полное сканирование партиций дешевле, чем поддержка тяжёлых индексов.

На практике

Типичная схема: таблица events с колонками user_id, event_type, payload JSONB, created_at. Партиционирование по created_at (например, по неделям). Шардирование по user_id через hash (например, 64 шарда). В Go используется паттерн repository с интерфейсом, где метод Insert выбирает шард через hash(user_id) % shardCount. Для чтения за период - запросы параллельно по всем шардам с fan-out и merge результатов.

Для холодных данных включается compression (ZSTD) и отключаются вторичные индексы. Горячие партиции держатся на SSD, холодные - на HDD или в object storage (S3) через foreign data wrapper.

Пример кода

GO
type ShardManager struct {
shards []*sql.DB
}
func (sm *ShardManager) Insert(ctx context.Context, e Event) error {
shardIdx := fnv32(e.UserID) % len(sm.shards)
_, err := sm.shards[shardIdx].ExecContext(ctx,
`INSERT INTO events (user_id, event_type, payload, created_at)
VALUES ($1, $2, $3, $4)`,
e.UserID, e.EventType, e.Payload, e.CreatedAt)
return err
}
func (sm *ShardManager) QueryRange(ctx context.Context, userID int64, from, to time.Time) ([]Event, error) {
shardIdx := fnv32(userID) % len(sm.shards)
rows, err := sm.shards[shardIdx].QueryContext(ctx,
`SELECT user_id, event_type, payload, created_at
FROM events
WHERE user_id = $1 AND created_at BETWEEN $2 AND $3`,
userID, from, to)
// ...
}

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

Начни с ключевого trade-off: партиционирование решает проблему размера таблицы, шардирование - проблему параллельной записи. Упомяни, что для исторических данных важнее append-only паттерн и компрессия, чем update. Покажи понимание, что выбор ключа шардирования зависит от паттерна запросов: если читают по user_id - шардируй по нему, если по времени - партиционируй по времени. Обязательно скажи про горячие/холодные данные и разные storage. В Go подчеркни управление пулами соединений и идемпотентные миграции.

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

  • Понимание разницы между партиционированием и шардированием.
  • Умение выбирать ключ шардирования под паттерн доступа.
  • Знание особенностей хранения временных рядов (append-only, retention).
  • Практический опыт работы с БД из Go (pool, миграции, ошибки при шардировании).
  • Способность аргументировать trade-off между сложностью шардирования и производительностью.

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

  • Шардирование по времени вместо партиционирования - приводит к неравномерной нагрузке на шарды.
  • Создание индексов на все колонки - убивает производительность записи.
  • Игнорирование hot partition problem: без партиционирования текущий день становится узким местом.
  • Использование одного connection pool для всех шардов - Go-приложение не сможет балансировать нагрузку.
  • Не учитывать, что JOIN между шардами невозможен - схема должна быть денормализована.

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

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