Модуль 5. Data Quality, тестирование, управление изменениями и надёжность витрин ClickHouse
Когда у вас есть модель (Модуль 2), семантика (Модуль 1), пайплайны (Модуль 3) и дашборды (Модуль 4), главная угроза — тихие ошибки: «вчера цифры были одни, сегодня другие», «метрика не совпала с финансовой», «новый статус сломал Net Sales». В этом модуле мы:
- формализуем контракты данных (семантика + схема + SLA);
- разложим DQ-тесты по уровням (ингест → витрины → семантика → BI);
- настроим наблюдаемость (freshness, parts/merges, репликация, “здоровье” метрик);
- опишем CI/CD для данных и процедуры v1→v2;
- разберём инциденты и постмортемы;
- закроем вопросы безопасности/PII, стоимости, ёмкости.
Контракты данных (Data Contracts): что именно фиксируем
Контракт — это документ + проверяемые правила, которые защищают вас от «неожиданностей».
Что входит:
- Схема: имена/типы колонок, домены значений, обязательность (NOT NULL), ключ уникальности (зерно).
- Семантика: определения метрик (паспорт), календарь/TZ, валюта и правила пересчёта.
- SLA: свежесть (например, «NRT: ≤5 минут, дневная: ≤30 минут»), доступность, окно ретро-пересчёта («корректируем последние 14 дней»).
- События и статусы: список канонических статусов и соответствия источников.
- Управление изменениями: какие изменения аддитивные (безопасно), какие ломающие (только через v2), процесс согласования.
Где жить контрактам:
- YAML в Git (для схем/метрик/тестов),
- SQL-тесты в /tests,
- CI валидирует, что изменения кода сопровождаются изменениями контрактов.
Риск: «немая» смена схемы/типа в источнике.
Митигация: STAGE-валидация, карантин «грязных» строк, фейл фазы ingest при нарушении контракта.
Таксономия DQ-тестов: уровни, примеры, SQL
Ингест/RAW/STAGE
Цель: не пустить «грязь» дальше.
- Схема и типы: проверка, что колонка amount — Decimal, day — Date; нежёстко — через CAST … DEFAULT NULL + подсчёт ошибок.
- Домены: статус ∈ {paid, captured, refund, cancelled}.
- Обязательность: event_id не NULL.
- Дедуп ключа: по (event_id) или (business_key, version).
Шаблон STAGE-таблиц для карантина:
CREATE TABLE stage_sales_quarantine ( ingest_ts DateTime, raw String, error LowCardinality(String) ) ENGINE = MergeTree ORDER BY ingest_ts;
Паттерн: при нарушении — строка уходит в quarantine с причиной, метрики «здоровья» растут, алерт.
Витрины (MARTS)
Цель: физическое качество и согласование с источниками (CORE).
- Уникальность зерна (нет дублей):
SELECT day, shop_id, sku_id, count() c FROM mart_sales_wide WHERE day BETWEEN today()-7 AND today()-1 GROUP BY day, shop_id, sku_id HAVING c > 1;
- Плотность рядов (нет «дыр» в днях по ключевым срезам).
- Баланс vs CORE (допуск, например 0.2%):
WITH m AS (
SELECT sum(amount_base) s FROM mart_sales_wide WHERE day=yesterday()
),
c AS (
SELECT sum(amount_base) s FROM core_sales WHERE toDate(tx_datetime)=yesterday()
AND status_canon='PAID'
)
SELECT abs(m.s - c.s)/NULLIF(c.s,0) AS rel_diff
HAVING rel_diff <= 0.002;- Новые статусы → отчёт «на разбор».
Семантика/VIEW
Цель: бизнес-инварианты и непротиворечивость.
- Инварианты: GM <= NetSales, NRR >= 0, SuccessRate ∈ [0,1].
SELECT 'gm_le_net' AS test_name
FROM (SELECT sum(gm) gm, sum(net_sales) ns FROM vw_retail_daily
WHERE day BETWEEN today()-14 AND today()-1)
HAVING gm <= ns;- Консистентность агрегирования (родитель = сумма детей по иерархии категорий).
- Версионность: v1 vs v2 на окне → расхождения в допустимых пределах (описаны в changelog).
BI/выдача
Цель: поведенческие тесты.
- Границы: запрос не «тащит» без фильтра по времени; нет FINAL, нет SELECT *.
- Стабильность KPI: «большие» отклонения (Z-score/процентиль) по NetSales/DAU → не фантомные ли.
Аномалии, дрейф распределений и «раннее предупреждение»
В дополнение к «жёстким» тестам нужны «мягкие» — ловить неожиданные пики/провалы.
Пороговые/статистические сигналы
- YoY/WoW отклонения > X%.
- Z-score по скользящему окну.
- Квантили (p1/p99) «уехали» дальше порога.
-- Z-score для Net Sales по 28-дневному окну
WITH base AS (
SELECT day, sum(net_sales) s
FROM vw_net_sales_daily
WHERE day BETWEEN today()-90 AND today()-1
GROUP BY day
),
agg AS (
SELECT
day, s,
avg(s) OVER (ORDER BY day ROWS BETWEEN 27 PRECEDING AND CURRENT ROW) AS m,
stddevPop(s) OVER (ORDER BY day ROWS BETWEEN 27 PRECEDING AND CURRENT ROW) AS sd
FROM base
)
SELECT day, s, (s - m)/NULLIF(sd,0) AS z
FROM agg
WHERE day = today()-1 AND abs(z) > 3; -- алерт
Дрейф доменов/категорий
Количество уникальных src_status вне маппинга → алерт и задача на обновление d_status_map.
“Ложные” аномалии
Праздники, распродажи, релизы — ожидаемые пики. Введите календарь событий и исключения в алерт-правилах.
Оркестрация качества: когда и где запускать проверки
NRT (каждые 5–15 мин): свежесть витрин, лаг ingest (Kafka offsets), доля quarantine, parts/merges backlog, репликация.
Ночь (1 раз/сутки): балансы с CORE, дубли зерна, плотность, инварианты, v1 vs v2, распределения (квантили).
Еженедельно: DR-тест восстановления, линейка производительности топ-запросов, аудит прав.
Технически: храните результаты тестов в таблице dq_results(test_name, scope, ts, status, value, threshold, details) + дашборд «Здоровье».
Наблюдаемость: метрики ClickHouse и «здоровье метрик»
Что мониторить в СH
- system.query_log — топ дорогих запросов (read_bytes, duration).
- system.parts/system.part_log — part-explosion, время мерджей.
- system.merges — зависания.
- system.replication_queue — лаг и ошибки репликации.
- system.asynchronous_metrics — общие счётчики.
Семантическая свежесть
Таблица sem_meta(view_name, updated_at) (обновляйте в конце джоба) → алерт, если now() - updated_at > SLA.
KPI «здоровья» витрины
- Freshness (мин)
- Balance vs CORE (%)
- Duplicate grain (шт)
- Unknown statuses (шт)
- Parts per partition (шт)
- Share of FINAL requests (%)
CI/CD для данных и семантики
Репозиторий
/sql/tables/*.sql -- DDL (v2 отдельно) /sql/views/*.sql -- VIEW (CREATE OR REPLACE) /metrics/*.yaml -- паспорта метрик (контракты) /tests/*.sql -- DQ / регрессия /ci/* -- скрипты деплоя и проверок
Линтеры и “сторожа”
- Запрет FINAL в VIEW.
- Запрет SELECT * в источниках BI (если используете SQL-источники).
- Проверка «есть ли changelog/version-bump» при изменении формулы метрики.
Стейдж-прогон
- Применить DDL, наполнить тестовое окно (например, последние 7–30 дней).
- Прогнать /tests.
- Сравнить v1 vs v2 (агрегаты на окнах) и зафиксировать ожидаемую дельту (в YAML).
- Replay топ-запросов из query_log на стейдже → сравнить время/байты.
Прод-деплой
- Side-by-side (v2 рядом с v1), VIEW-алиас переключается в конце.
- Пост-мониторинг: freshness, DQ, нагрузка.
- Откат = вернуть алиас на v1, держать v1 30–60 дней.
Безопасность и соответствие (RBAC, RLS, PII)
- BI имеет только SELECT на vw_*, прямые таблицы закрыты.
- RLS (политики строк) на базовых таблицах; вьюхи наследуют.
- PII-маскирование во VIEW (хеш/обрезка), аудит запросов.
- Квоты: лимиты по памяти/времени/чтению для ролей BI/аналитиков.
- Retention/TTL: сроки хранения и «legal hold» (исключение TTL для заблокированных партиций).
Риск: «в отчёт попала PII».
Митигация: автоматический скан VIEW на запрещённые колонки (линтер), доступ BI только к схеме db_marts/vw_*.
Стоимость и ёмкость: как не переплачивать и не упираться
- Партиции по месяцу/дню в зависимости от профиля запросов (см. Модуль 2).
- Крупные батчи вставок → меньше parts → меньше мерджей.
- Холодные слои: TTL MOVE TO volume 'cold' на S3/объектное; горячее окно — NVMe.
- Кодеки/типы: деньги — Decimal, домены — LowCardinality(String).
- Агрегаты-состояния вместо «COUNT DISTINCT на лету» — уменьшают read_bytes ×10–100.
- Распределение нагрузки: лимит на одновременные тяжёлые джобы, расписание ретро-пересчёта ночью.
Надёжность пайплайнов: идемпотентность, поздние события, бэкфиллы
Идемпотентность
- Ключ event_id или (business_key, version);
- ReplacingMergeTree(version) для апдейтов (BI не читает с FINAL);
- Для агрегатов — …State/…Merge (повторная заливка не «накрутит»).
Поздние события и ретро-окно
- Измерьте распределение задержек → возьмите P95/P99 как окно.
- Две фазы: NRT-инкремент «последние 2 часа», nightly ретро «последние 14–30 дней».
- «Глубокий» ретро (редко) — по тикету, в выходные окна.
Бэкфиллы (история)
- Делайте в отдельные v2-таблицы и переключайте алиас после сверок.
- Никогда не «подливайте» старую историю в боевую таблицу без проверки дубликатов и конкаррентных версий.
Кейсы
Retail: новые статусы сломали Net Sales
Симптом: “Net Sales вчера упали на 8%”, в CORE всё стабильно.
Разбор: в источнике появился статус paid_ok, которого не было в d_status_map. Строки ушли в UNKNOWN и не попали в метрику.
Решение: алерт «новые статусы» сработал; добавили маппинг, ретро-пересчёт 14 дней, v1 vs v2 дельта +7.9% (ожидаемо).
Профилактика: правило — без маппинга новые статусы не считаем, но сигналим в Slack/почту.
FinTech: расхождение с главной книгой
Симптом: дашборд комиссий не сходится на 0.6%.
Разбор: апдейт курсов валют «задним числом» для 3 дней.
Решение: в факте фиксируем курс на дату операции (в словаре), nightly ретро-пересчёт за 30 дней; добавили вторую вьюху «на дату отчёта».
Профилактика: в паспорте метрики — явное правило пересчёта и компромисс «управленка vs FP&A».
Events/Telecom: NRT-дашборд тормозит
Симптом: плитка «p95 latency, последние 2 часа» грузится 25–40 с.
Разбор: считали перцентили «на лету» по сырым событиям; insert мелкими батчами → тысячи parts.
Решение: agg_minute_state с quantileTDigestState, ingest микробатчами (MV), vw_perf_minute читает …Merge. Время запроса < 2 с.
Профилактика: алерт «parts per partition > порога» и борд «дурные запросы».
Шаблоны YAML/SQL для включения в ваш репозиторий
DQ-правила (YAML)
id: NET_SALES_BALANCE
scope: vw_net_sales_daily
schedule: "daily 01:10"
owner: dwh-architect
checks:
- name: freshness_sla
type: freshness
view: vw_net_sales_daily
sla_minutes: 60
- name: balance_vs_core
type: compare_sql
threshold_rel: 0.002
sql: |
WITH m AS (SELECT sum(net_sales) s FROM vw_net_sales_daily WHERE day=yesterday()),
c AS (SELECT sum(amount_base) s FROM core.sales WHERE toDate(tx_datetime)=yesterday() AND status_canon='PAID')
SELECT abs(m.s - c.s)/NULLIF(c.s,0) AS rel_diff FROM m,c;
- name: no_duplicate_grain
type: uniqueness
key: [day, shop_id, category_id]
table: vw_retail_daily
- name: invariant_gm_le_net
type: sql
sql: |
SELECT 1 WHERE (
SELECT sum(gm) <= sum(net_sales) FROM vw_retail_daily WHERE day BETWEEN today()-14 AND today()-1
);
Регресс-сравнение v1 vs v2 (SQL)
WITH v1 AS (
SELECT day, shop_id, sum(net_sales) s
FROM vw_net_sales_daily_v1
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_v2
WHERE day BETWEEN today()-30 AND today()-1
GROUP BY day, shop_id
)
SELECT coalesce(v1.day,v2.day) day,
coalesce(v1.shop_id,v2.shop_id) 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) <= 3.0 -- ожидаемая дельта, зафиксированная в changelog
ORDER BY day, shop_id;
Runbooks (краткие инструкции на инциденты)
A. Свежесть просела
- Проверить lag Kafka/MV, parts/merges, replication_queue.
- Временно отключить «тяжёлые» ретро/overlay.
- Запустить OPTIMIZE проблемных партиций.
- Согласовать SLA-отклонение с бизнесом (ETA), записать в пост-инцидент.
B. Расхождение с CORE
- Баланс-тест на окне 7–14 дней.
- Новые статусы? валюты? календарь?
- Ретро-пересчёт окна; если “ломающее” изменение — v2 и согласование.
C. BI «лежит» (медленные запросы)
- Топ из query_log; есть ли FINAL/SELECT *.
- Сверить WHERE vs ORDER BY; добавить/подкрутить skip-индексы.
- Вынести тяжёлые метрики в агрегаты-состояния; ограничить период по умолчанию.
Антипаттерны и как их не допустить
|
Антипаттерн |
К чему приводит |
Как избежать |
|---|---|---|
|
Summing на данных с ретро-правками |
«накрутка» сумм |
Aggregating (…State/…Merge), rebuild окна |
|
FINAL в продуктивных VIEW |
провалы SLA, рост read_bytes |
дисциплина записи, линтер «запрет FINAL» |
|
Семантика в BI, а не во VIEW |
разные формулы у команд |
метрики как код (VIEW + паспорт) |
|
Смешанные валюты/календари в одной вьюхе |
«не бьются» отчёты |
отдельные VIEW, правило в паспорте |
|
Мелкие вставки → тысячи parts |
merges «задыхаются», лаг свежести |
микробатчи, буферные таблицы |
|
Отсутствие quarantine |
«грязь» в витринах |
STAGE-валидация, карантин-таблицы |
|
Нет регрессионных тестов v1→v2 |
«тихий» излом истории |
side-by-side + сравнение на окне |
Итог
Надёжные витрины ClickHouse зависят не столько от «быстрых запросов», сколько от контрактов, тестов, наблюдаемости и управляемых изменений. Держите правила простыми и проверяемыми:
- Контракты: схема, семантика, SLA, окно ретро.
- Тесты: от STAGE до VIEW (балансы, дубли, инварианты, дрейф).
- Наблюдаемость: freshness, parts/merges, репликация, DQ-дашборд.
- CI/CD: линтеры, стейдж-прогон, v1→v2 side-by-side.
- Безопасность: vw_* для BI, RLS/маскирование, аудит.
- Надёжность: идемпотентность, ретро-окна, бэкфиллы через v2.
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. Итог — быстрый запуск витрин за недели, снижённые риски в проде и предсказуемая стоимость владения.



