Перейти к содержанию
Learning Platform
Глоссарий Troubleshooting
Урок 08.02 · 20 мин
Средний
Time-SeriesCODECDeltaGorillaDoubleDeltaZSTDLZ4DateTime64Metrics

Моделирование временных рядов

Метрики инфраструктуры, IoT-телеметрия, финансовые котировки — все эти данные представляют собой временные ряды: последовательности значений, привязанных к метке времени. ClickHouse создавался именно для этого класса нагрузки: высокая скорость записи, эффективное сжатие монотонных последовательностей, быстрые range-сканы по временному диапазону.

Правильный выбор схемы определяет разницу между 10x и 100x сжатием, между секундами и миллисекундами на запрос.


Базовая схема time-series

CREATE TABLE metrics (
    ts DateTime64(3) CODEC(Delta, ZSTD(1)),
    metric_name LowCardinality(String),
    host LowCardinality(String),
    value Float64 CODEC(Gorilla, LZ4),
    tags Map(String, String)
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(ts)
ORDER BY (metric_name, host, ts);

Каждое решение в этой схеме имеет конкретную причину. Разберём по порядку.


Codec chains: сжатие для каждого типа столбца

ClickHouse позволяет назначить цепочку кодеков (codec chain) каждому столбцу индивидуально. Первый кодек — препроцессор (Delta, DoubleDelta, Gorilla), второй — компрессор (ZSTD, LZ4). Препроцессор трансформирует данные так, чтобы компрессор работал эффективнее.

Рекомендации по codec chain для типов столбцов
Timestamp: Delta + ZSTDTimestamp (DateTime/DateTime64): Delta кодирует разницу между соседними значениями. Для монотонно растущих timestamps разница = константа (например, 1 секунда). ZSTD эффективно сжимает последовательность одинаковых delta. Результат: 10-20x сжатие.
Float: Gorilla + LZ4Float значения (метрики, температура, CPU%): Gorilla -- XOR-кодирование из Facebook Gorilla paper. Для медленно меняющихся float-значений XOR между соседними = почти нули. LZ4 для скорости декомпрессии (метрики читаются часто). Результат: 5-15x сжатие.
Counter: DoubleDelta + ZSTDМонотонные счётчики (bytes_sent, requests_total): DoubleDelta кодирует разницу разниц. Для линейно растущих счётчиков вторая производная = 0. ZSTD сжимает последовательность нулей почти бесплатно. Результат: 20-50x сжатие.
String: LowCardinality + ZSTDLowCardinality/String (имена метрик, хосты): словарное кодирование LowCardinality уже обеспечивает компрессию. Дополнительный кодек ZSTD(1) для дальнейшего сжатия словаря. Delta не применим к строковым данным.

Как работает Delta

Delta кодирует разницу между соседними значениями вместо абсолютных значений:

Исходные timestamps: [1000, 1001, 1002, 1003, 1004]
После Delta:         [1000,    1,    1,    1,    1]

Для регулярных временных рядов (метрика каждую секунду) delta — константа. ZSTD сжимает последовательность одинаковых значений в несколько байт.

Как работает Gorilla

Gorilla (из Facebook Gorilla paper, 2015) XOR-кодирует float-значения:

Исходные значения: [72.5, 72.6, 72.5, 72.7, 72.6]
XOR с предыдущим:  [72.5, 0x...small, 0x...small, ...]

Для медленно меняющихся метрик (CPU utilization, temperature) XOR между соседними значениями — малое число с большим количеством нулевых бит. LZ4 сжимает это эффективно, при этом обеспечивая быструю декомпрессию.

Как работает DoubleDelta

DoubleDelta — разница разниц (вторая производная):

Исходные значения:  [100, 200, 300, 400, 500]
Delta:              [100, 100, 100, 100, 100]
DoubleDelta:        [100,   0,   0,   0,   0]

Идеален для монотонно растущих счётчиков с постоянным приростом.


ORDER BY: metric first, timestamp second

ORDER BY (metric_name, host, ts)

Почему metric_name первым, а не timestamp?

Кардинальность и фильтрация: типичный запрос к time-series — “покажи CPU utilization хоста X за последний час”. Первый столбец ORDER BY (metric_name) отсекает все остальные метрики на уровне гранул. Второй столбец (host) сужает до конкретного хоста. Третий (ts) обеспечивает range-scan внутри отфильтрованной группы.

Если поставить timestamp первым:

-- Плохой ORDER BY для time-series
ORDER BY (ts, metric_name, host)

Гранулы будут содержать все метрики всех хостов за один временной интервал. Фильтрация по конкретной метрике потребует чтения всех гранул — sparse index бесполезен.

TIP

Правило для time-series ORDER BY: dimension-столбцы (metric, host, region) перед timestamp. Timestamp всегда последним для range-сканов внутри группы.


PARTITION BY: lifecycle, не оптимизация запросов

PARTITION BY toYYYYMM(ts)

Партиционирование в ClickHouse — инструмент управления жизненным циклом данных, а не оптимизации запросов (для этого есть ORDER BY и sparse index).

Месячные партиции для метрик:

  • TTL: ALTER TABLE metrics MODIFY TTL ts + INTERVAL 90 DAY — автоматическое удаление старых партиций
  • DROP PARTITION: ALTER TABLE metrics DROP PARTITION '202401' — моментальное удаление целого месяца
  • Detach/Attach: перенос холодных данных на S3 через tiered storage

Дневные партиции подходят только при очень высоком объёме записи (более 100 миллионов строк в день), когда месячная партиция становится слишком крупной для одного merge.

WARNING

Избыточное партиционирование (по часу, по минуте) создаёт тысячи мелких parts, которые не успевают сливаться. Это замедляет SELECT и увеличивает потребление ZooKeeper/Keeper ресурсов в распределённых конфигурациях.


DateTime vs DateTime64: выбор гранулярности

ТипГранулярностьХранениеКогда использовать
DateTime1 секунда4 байтаМетрики инфраструктуры (Prometheus-style, scrape interval более 1 секунды)
DateTime64(3)1 миллисекунда8 байтТрейсинг, финансовые данные, IoT с sub-second записью
DateTime64(6)1 микросекунда8 байтHigh-frequency trading, научные измерения
DateTime64(9)1 наносекунда8 байтОчень специализированные сценарии (network packet timing)

DateTime64 занимает 8 байт вместо 4 для DateTime. При миллиардах строк это удваивает размер timestamp-столбца. Выбирайте минимальную достаточную гранулярность.

-- Prometheus-style метрики: секундной гранулярности достаточно
CREATE TABLE infra_metrics (
    ts DateTime CODEC(Delta, ZSTD(1)),
    ...
);

-- Distributed tracing: нужны миллисекунды для span ordering
CREATE TABLE traces (
    ts DateTime64(3) CODEC(Delta, ZSTD(1)),
    ...
);

Пример: полная схема метрик с TTL

CREATE TABLE system_metrics (
    ts DateTime64(3) CODEC(Delta, ZSTD(1)),
    metric_name LowCardinality(String),
    host LowCardinality(String),
    dc LowCardinality(String),
    value Float64 CODEC(Gorilla, LZ4),
    min_1m Float64 CODEC(Gorilla, LZ4),
    max_1m Float64 CODEC(Gorilla, LZ4),
    count_1m UInt64 CODEC(DoubleDelta, ZSTD(1)),
    tags Map(String, String)
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(ts)
ORDER BY (metric_name, dc, host, ts)
TTL ts + INTERVAL 90 DAY;

-- Запрос: CPU за последний час для конкретного хоста
SELECT
    toStartOfMinute(ts) AS minute,
    avg(value) AS avg_cpu,
    max(max_1m) AS peak_cpu
FROM system_metrics
WHERE metric_name = 'cpu.usage'
  AND host = 'web-01'
  AND ts >= now() - INTERVAL 1 HOUR
GROUP BY minute
ORDER BY minute;

ORDER BY (metric_name, dc, host, ts) обеспечивает: sparse index отсекает все метрики кроме cpu.usage, затем dc и host сужают до конкретного сервера, ts обеспечивает range-scan по последнему часу.


Ключевые выводы

  1. Codec chains — назначайте индивидуально: Delta+ZSTD для timestamps, Gorilla+LZ4 для float, DoubleDelta+ZSTD для счётчиков.
  2. ORDER BY — dimension-столбцы (metric, host) перед timestamp. Timestamp всегда последним.
  3. PARTITION BY toYYYYMM — для lifecycle management (TTL, DROP PARTITION), не для оптимизации запросов.
  4. DateTime vs DateTime64 — выбирайте минимальную достаточную гранулярность. DateTime (4 байта) вдвое компактнее DateTime64 (8 байт).
  5. Типичное сжатие time-series в ClickHouse: 10-50x по сравнению с несжатыми данными, благодаря комбинации codec chains и столбцового хранения.
Внутренности алгоритмов компрессии: LZ77, Huffman, ANS Streaming: event time, watermarks, windows

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 7