Модуль 9. Миграция в ClickHouse без простоев
Вы уже научились проектировать витрины (М2), строить конвейеры (М3), ускорять отчёты (М4) и держать качество (М5–М6). Следующий вызов — перенести существующие витрины/дашборды из классических DWH (PostgreSQL/Greenplum/Vertica/Redshift/BigQuery и т. п.) в ClickHouse, не сломав цифры и не «уронив» пользователей. Мы разберём:
- инвентаризацию и приоритезацию объектов для переноса;
- картирование моделей данных и SQL-конструкций (что в CH «так же», что «иначе»);
- стратегию dual-run (двойной прогон) и «склейку» инкрементов/ретро-окна;
- проверку эквивалентности (баланс, допуски, регрессы v1→v2);
- миграцию дашбордов и обучение пользователей;
- эксплуатационные вопросы: параллельный доступ, стоимость, DR.
С чего начать: инвентаризация и «критический путь»
Список объектов
Снимите каталог текущего DWH:
- факты/витрины (физические таблицы/материализованные представления);
- представления/семантика (бизнес-вьюхи);
- дашборды и SQL-источники BI;
- расписания загрузки и SLA.
Для каждого объекта зафиксируйте:
- объём (строк/байт) и темп прироста;
- топовые запросы (read_bytes/read_rows/duration из логов);
- критичность (kpi-дашборды/ежедневки/NRT).
Приоритезация
Начните с узкой, но критичной вертикали (например, «розничные продажи дневные»). Критерии «первоочередности»:
- высокая выгода от колоночного чтения (тяжёлые GROUP BY, «широкие» факты);
- понятная семантика метрик (есть «паспорт»);
- понятные инкременты (есть ключ/ts, можно построить ретро-окно).
Целевые SLO на миграцию
Заранее договоритесь с бизнесом:
- точность: «расхождение ≤ 0.2% по Net Sales на окне 30 дней»;
- свежесть: «не хуже текущей»;
- времена отклика: «плитка ≤ N секунд, отчёт ≤ M секунд».
Картирование моделей: от «того, как было», к «как надо в ClickHouse»
Модель хранения
- Если в исходном DWH — «звезда», в ClickHouse используйте гибрид: факт + часто используемые атрибуты (wide), редкие — через pre-join в процессе загрузки (М2 §6).
- Для «снимков» (балансы/остатки) — дневные снепшоты; для событий — append-only факты.
Партиционирование и ORDER BY
- Партиции по времени (месяц/день), чтобы ретро-операции шли по партициям.
-
ORDER BY под реальные WHERE (обычно: дата → разрез → id).
Примеры:- продажи: (day, shop_id, sku_id);
- события: (event_time, user_id).
Движки таблиц
- MergeTree — «вставил и забыл» без переигровок;
- ReplacingMergeTree(version) — если поступают обновления по ключу (без FINAL в прод-чтении);
- AggregatingMergeTree — «устойчивые» агрегаты-состояния (sumState/uniq*/quantile*).
Антипаттерн: SummingMergeTree на данных с ретро-правками → «накрутка» сумм.
Правильнее: AggregatingMergeTree (состояния) + …Merge во VIEW.
Совместимость SQL: что совпадает, что менять
Ниже — «карта местности» с типичными заменами.
Агрегации и DISTINCT
-
COUNT(DISTINCT x) → либо точный uniqExact(x), либо быстрые uniqCombined(x)/uniqHLL12(x).
В витринах лучше хранить состояния:
-- запись sumState(amount), uniqCombinedState(user_id) -- чтение sumMerge(amount_state), uniqCombinedMerge(users_state)
Оконные функции
ClickHouse поддерживает оконные: row_number, rank, sum()/avg() OVER, lag/lead, фреймы ROWS BETWEEN ….
Разница: оконные на «дырявых» календарях дают странности — плотните ряд (М8 §1.1).
Временные бакеты
Не используйте функции в условии фильтра по времени:
-- плохо WHERE toDate(ts) BETWEEN '2025-07-01' AND '2025-07-31' -- хорошо WHERE ts >= '2025-07-01' AND ts < '2025-08-01'
SCD и «значение на момент»
- Вместо сложных RANGE BETWEEN-джойнов используйте ASOF JOIN или argMax(value, valid_from) с фильтром по дате.
- Для SCD2 — valid_from/valid_to, сортировка (id, valid_from) (М8 §1.3).
Upsert/merge
В классических DWH — MERGE INTO. В ClickHouse:
- ingest как append + ReplacingMergeTree(version);
- либо ночной overlay окна (пересборка партиции/окна).
Строки/JSON/массивы
- Часто используемые ключи — в отдельные столбцы (типизированные), не ищите всё время по JSON.
- Для «ключ-значение» — Map(String, T) или Array.
- Для поиска по длинным IN/LIKE — bloom-индексы (при доказанной пользе).
FULL TEXT / поиск
Если есть полнотекстовые отчёты — переосмыслите под фильтрацию по атрибутам + префикс/подстрока с индексами-скипами. Чистый «FTS» — не профиль ClickHouse.
Семантика валют/календарей
Разведите вьюхи:
- «на дату операции» vs «на дату отчёта» (разные задачи — разные правила);
- обычный календарь vs финансовый 4-5-4 (М1/М4).
Стратегия переноса: side-by-side, dual-run, ретро-окна
Общая схема
- Собрать в ClickHouse «сырые» факты (через batch/CDC/stream).
- Сконструировать целевую витрину (модель, партиции, ORDER BY, движок).
- Наполнить историю (backfill) — по партициям, с проверкой сумм/уникальности.
- Запустить dual-run: писать свежие данные и в старый DWH, и в CH; сверять отчёты на окне N дней.
- Переключить BI (алиас/VIEW-swap) после стабилизации.
- Держать v1 (старый путь) в read-only 30–60 дней.
Инкремент и ретро
- Оперативная фаза: каждые 5–15 минут дозаливаем «последние 1–2 часа».
- Ночной ретро: пересчитываем окно задержек (7–30 дней в зависимости от домена).
- Глубокий ретро (редко): по тикету, в выходные.
Согласование цифр (допуски)
- Для денежных метрик задайте «допуск» (например, 0.2%) и фиксируйте в YAML-тестах.
- Для uniq — используйте одинаковую методику в v1 и v2, или сразу договоритесь о приближённой (с описанной погрешностью).
Проверка эквивалентности: DQ-панель миграции
Базовые тесты (ежедневно)
- Баланс MARTS(v2) vs v1 на окне 7–30 дней (по ключевым срезам).
- Дубли зерна в целевых витринах CH.
- Плотность рядов (нет дыр по датам/разрезам).
- Инварианты: GM ≤ NetSales, rate ∈ [0,1].
Регресс-сравнение v1 vs v2
WITH v1 AS (
SELECT day, shop_id, sum(net_sales) s
FROM v1_net_sales_daily
WHERE day BETWEEN today()-30 AND today()-1
GROUP BY day, shop_id
),
v2 AS (
SELECT day, shop_id, sum(net_sales) s
FROM vw_net_sales_daily -- ClickHouse
WHERE day BETWEEN today()-30 AND today()-1
GROUP BY day, shop_id
)
SELECT day, shop_id,
v2.s - v1.s AS diff,
100.0*(v2.s - v1.s)/NULLIF(v1.s,0) AS diff_pct
FROM v1 FULL OUTER JOIN v2 USING (day, shop_id)
HAVING abs(diff_pct) <= 0.2 -- допустимая дельта, ‰
ORDER BY day, shop_id;
Производительность
Снимайте из system.query_log:
- топовые запросы/плитки по read_bytes/read_rows/duration;
- долю FINAL (должно быть 0 в прод);
- респонс-тайм на реальных нагрузках (replay).
Перенос дашбордов и UX-гвардейлы
Слой семантики
Создайте идентичные по полям VIEW с прежними именами метрик. Если меняете логику — версионируйте (vw_*_v2), добавьте «паспорт» и аннотацию «что изменится».
Подключение BI
- Connnector → vw_* (нет прямого доступа к таблицам).
- Дефолтные фильтры (последние 28/90 дней), лимиты строк, Top-N.
- Отдельные источники под NRT (минутные/часовые rollup-вьюхи).
Обучение пользователей
Короткий гайд (1–2 страницы):
«как читать новые VIEW», «чем отличается uniq/quantile», «почему стало быстрее», «где смотреть свежесть и баланс».
Типовые «разрывы совместимости» и их решения
COUNT(DISTINCT) «едет»
Причина: в старом DWH — точный, в CH — приближённый.
Решение:
- либо uniqExact (дороже),
- либо стандартизовать uniqCombined/HLL12 в обоих слоях (с описанной погрешностью),
- либо хранить uniq*State и читать …Merge.
Перцентили
Причина: разные алгоритмы/квантайзеры.
Решение: зафиксировать в паспорте quantileTDigest (или иной) как метод, хранить состояния.
Выборка по времени «через функцию»
Причина: toDate(ts) = … в WHERE ломает skip-индексы.
Решение: фильтруйте по интервалу ts >= … AND ts < …, бакет делайте в SELECT.
JOIN на SCD «по интервалу»
Причина: тяжёлые интервал-джойны без индексов.
Решение: ASOF JOIN или pre-join при записи (материализация атрибутов «на дату факта»).
Upsert’ы
Причина: привычка к MERGE INTO.
Решение: ReplacingMergeTree(version) + дисциплина версий и без FINAL в чтении; либо overlay окна ночью.
Валюты/календари смешаны
Причина: в старом отчёте «как-то работало».
Решение: разнести вьюхи, указать в паспорте метрики «какая логика где».
Кейс-шаблоны (конкретные сценарии)
Retail: дневные продажи (из Greenplum/Vertica)
- Было: факт чек-позиции, дневные аггрегаты, COUNT(DISTINCT buyers), возвраты «задним числом».
-
Стало (CH):
- mart_sales_wide (партиция месяц, ORDER BY (day, shop_id, sku_id, tx_id)),
- agg_sales_daily_state (sumState(amount), sumState(qty), uniqCombinedState(buyer_id)),
- VIEW vw_retail_daily (…Merge, GM%, календарь 4-5-4),
- ретро-окно 14 дней.
- Эффект: −50–80% latency плиток, устойчивость к повторным заливкам, согласованный uniq.
Маркетинг: события+атрибуция (из BigQuery)
- Было: COUNT DISTINCT, перцентили и пути считались «на лету».
-
Стало:
- ingest → events_wide,
- agg_events_minute_state (DAU, p95, topK),
- вьюхи для last-touch/position-based атрибуции,
- дашборды NRT на минутных rollup’ах.
- Эффект: NRT < 5 сек на 24 часа окна; понятные правила атрибуции.
Финансы: остатки/комиссии (из PostgreSQL + ночные выгрузки)
- Было: раз в сутки, долгая пересборка, ручные сверки.
- Стало: CDC → STAGE (нормализация upsert), факты в ReplacingMergeTree(version), агрегаты-состояния по дням, nightly сверка vs «главная книга», две вьюхи «операция/отчёт».
- Эффект: частичное NRT, устойчивость к коррекциям, автоматические балансы.
Эксплуатация в период миграции
Параллельный доступ
- BI переводится на vw_* после зелёной панели DQ на окне (например, 14 дней).
- Старые источники → read-only, «красная кнопка» отката — alias назад.
Стоимость
- «Горячее окно» (90 дней) на NVMe, остальное — TTL MOVE TO 'cold' (объектное).
- Бейджик «дорогих запросов» из query_log (read_bytes/duration) — работают с владельцами дашбордов.
DR-готовность
- Бэкапы партиций и метаданных ежедневно; раз в квартал — тест восстановления и быстрый smoke-тест «главных» вьюх.
Чек-листы
Перед стартом переноса витрины
- Есть «паспорт» метрик (формулы/календарь/валюта/зерно).
- Определены партиции и ORDER BY под реальные WHERE.
- Решён движок: MergeTree / Replacing(version) / Aggregating.
- Спроектированы pre-join/агрегаты-состояния.
- Есть план инкремента и ретро-окна.
- Готовы DQ-тесты (баланс, дубли, плотность, инварианты).
- Подготовлены VIEW под BI (без FINAL, без SELECT *).
Перед переключением BI
- Dual-run зелёный ≥ 7–30 дней (в зависимости от домена).
- Разница в допуске (≤ X ‰) по ключевым срезам.
- Время плиток/отчётов ≤ целевых SLO.
- Пользователи обучены (1–2-стр. гайд).
- Есть откат (alias назад), v1 живёт 30–60 дней.
Риски и митигации (сводная таблица)
|
Риск |
Как проявляется |
Что делать |
|---|---|---|
|
Неправильный ORDER BY |
читаем «пол-таблицы» |
спрофилировать WHERE, переопределить ключ, v2-таблица |
|
Summing при ретро-правках |
«накрутка» сумм |
Aggregating (…State/…Merge) или пересборка окна |
|
COUNT DISTINCT расходится |
d1≠d2 |
унифицировать метод (uniqExact/Combined/HLL), хранить состояния |
|
Перцентили «прыгают» |
p95 нестабилен |
TDigest, состояния, достаточный объём |
|
JOIN по интервалу тяжёлый |
таймауты |
ASOF / pre-join на записи |
|
toDate(ts) в WHERE |
не скипаются партиции |
фильтр по интервалу, бакет в SELECT |
|
«Сырые» статусы |
Net Sales «едут» |
маппинг статусов + алерт «новые статусы» |
|
Смешаны валюты/календари |
отчёты «не бьются» |
отдельные VIEW, паспорт фиксирует правило |
|
FINAL в прод-вьюхах |
SLA проваливается |
запрет FINAL, дисциплина записи |
|
Part-explosion |
мерджи «задыхаются» |
крупные батчи, буферные таблицы, алерты parts/merges |
Приложение: шаблоны SQL и процедур
Каркас витрины (retail)
CREATE TABLE mart_sales_wide ( tx_id UInt64, tx_datetime DateTime, day Date MATERIALIZED toDate(tx_datetime), shop_id UInt32, sku_id UInt32, qty Int32, amount Decimal(12,2), currency FixedString(3), status_canon LowCardinality(String), buyer_id UInt64 ) ENGINE = MergeTree PARTITION BY toYYYYMM(day) ORDER BY (day, shop_id, sku_id, tx_id); CREATE TABLE agg_sales_daily_state ( day Date, shop_id UInt32, category_id UInt32, amount_state AggregateFunction(sum, Decimal(14,2)), qty_state AggregateFunction(sum, Int64), buyers_state AggregateFunction(uniqCombined, UInt64) ) ENGINE = AggregatingMergeTree PARTITION BY toYYYYMM(day) ORDER BY (day, shop_id, category_id); CREATE OR REPLACE VIEW vw_retail_daily AS SELECT day, shop_id, category_id, sumMerge(amount_state) AS net_sales, sumMerge(qty_state) AS qty, uniqCombinedMerge(buyers_state) AS buyers FROM agg_sales_daily_state GROUP BY day, shop_id, category_id;
DQ: баланс v1 vs v2
-- diff в ‰ по окну SELECT sum_v1, sum_v2, 1000.0*(sum_v2 - sum_v1)/NULLIF(sum_v1,0) AS diff_permille FROM (SELECT sum(net_sales) sum_v1 FROM v1_net_sales_daily WHERE day BETWEEN today()-30 AND today()-1), (SELECT sum(net_sales) sum_v2 FROM vw_retail_daily WHERE day BETWEEN today()-30 AND today()-1);
Ночной ретро-пересчёт окна (идея)
- построить tmp_agg_state на окне N дней из «чистого» источника;
- заменить партиции в agg_*_state атомарно;
- обновить «семантическую свежесть» (sem_meta.updated_at) для VIEW.
Итоги
Успешная миграция в ClickHouse — это не «переписать SQL как-нибудь», а спроектировать целевую модель под чтение, материализовать тяжёлые метрики (uniq/quantile/%) как состояния, обеспечить идемпотентность инкрементов и доказать эквивалентность цифр на окне. Следуйте опорным шагам:
- Инвентаризация → приоритеты → SLO.
- Модель под CH: партиции, ORDER BY, движки, pre-join/aggregates.
- Backfill по партициям → dual-run → DQ/регресс-панель.
- VIEW-совместимость, дефолт-фильтры, обучение пользователей.
- Переключение с откатом, наблюдаемость, DR и экономия (tiering/TTL).
Arenadata QuickMarts (ADQM) — корпоративная платформа на базе ClickHouse для быстрого слоя витрин и near-real-time аналитики. Решает задачи «быстрых» дашбордов и API с низкой латентностью и высокой конкуррентностью, работает поверх вашего DWH/лейкхауса как serving-уровень. Даёт предсказуемую производительность на терабайтно-петабайтных объёмах за счёт колоночного хранения, компрессии и предагрегатов (Materialized Views, AggregatingMergeTree), подключается к Kafka/S3 и стандартным BI-инструментам по SQL/HTTP. Для корпоративных ИТ ADQM предлагает поддержку и SLA, отказоустойчивые кластеры (HA/DR), безопасность (RBAC, LDAP/OIDC, шифрование трафика и данных), мониторинг и резервное копирование. Платформа хорошо ложится на методологию курса: семантика vw_*, роллап-слои, NRT-ингест, SLO/наблюдаемость и «гвардейки» для BI/API. Итог — быстрый запуск витрин за недели, снижённые риски в проде и предсказуемая стоимость владения.



