Как организовать таблицы и шардирование для хранения исторических данных
Что проверяют
Отличаете ли вы партиционирование от шардирования и выбираете ли ключ под реальные запросы. Историю почти всегда пишут в хвост и читают по диапазону времени, это надо учесть. Следом спрашивают, как удалять данные старше года и что делать, когда шард переполнится.
Как отвечать
Начните с требований: объём в сутки, срок хранения, типичные запросы (история одного пользователя или агрегаты по всем за период).
Внутри одной базы первое средство это партиционирование:
- по времени: в PostgreSQL
PARTITION BY RANGE (created_at), партиция на месяц или неделю. Запрос с условием по времени читает только нужные партиции (partition pruning), а старые удаляются черезDROPилиDETACH PARTITIONвместо тяжёлогоDELETE. Новые партиции создаётpg_partmanили свой крон; - схема: только вставка без обновлений, узкая строка, редкие атрибуты в
jsonb. Первичный ключ обязан включать колонку партиционирования; - индексы: составной
(user_id, created_at)под историю пользователя и BRIN по времени для больших диапазонов, он крошечный и хорош на данных, вставленных по порядку.
Шардирование нужно, когда не хватает одного сервера. Ключ обычно user_id через хэш: нагрузка распределяется ровно, а история пользователя лежит на одном шарде. В Go маршрутизацию делают в слое репозитория: хэш ключа и выбор pgxpool.Pool нужного шарда. Готовые решения: Citus, Vitess для MySQL. Аналитику по всей истории лучше отдать колоночной базе вроде ClickHouse с PARTITION BY и TTL.
Где спотыкаются
Шардируют по времени. Тогда вся запись идёт в один «текущий» шард, а остальные простаивают.
Выбирают ключ шардирования, не глядя на запросы, и получают запросы по всем шардам со склейкой в приложении.
Чистят историю через DELETE ... WHERE created_at < ... на сотнях миллионов строк. Таблица раздувается, автовакуум захлёбывается, а партиция удалилась бы одной командой.