MV + AggregatingMergeTree: канонический паттерн
Канонический паттерн pre-aggregation в ClickHouse — связка MV + AggregatingMergeTree. MV триггерится на каждый INSERT, вычисляет частичные агрегаты с -State функциями и записывает их в AggregatingMergeTree. При чтении -Merge функции объединяют частичные состояния в финальный результат.
Этот паттерн решает задачу, где raw-таблица хранит миллиарды строк, а dashboard-запросы должны выполняться за миллисекунды.
Почему SummingMergeTree недостаточно
SummingMergeTree складывает числовые столбцы при merge. Это работает для sum() и count(). Но для uniq(user_id) (HyperLogLog) или quantile(0.99)(latency) (t-digest) — сложить два числа недостаточно. Нужен движок, который понимает внутреннее представление сложных агрегатных функций.
AggregatingMergeTree хранит бинарные состояния агрегатных функций и объединяет их при merge. Это единственный движок, поддерживающий корректное объединение HyperLogLog, t-digest и других нетривиальных агрегатов.
AggregateFunction: тип столбца
AggregateFunction(func, arg_type) — специальный тип столбца, хранящий бинарный blob промежуточного состояния агрегатной функции:
CREATE TABLE daily_stats (
date Date,
visits AggregateFunction(sum, UInt32),
users AggregateFunction(uniq, UInt64),
avg_dur AggregateFunction(avg, Float64)
) ENGINE = AggregatingMergeTree()
ORDER BY date;
Что хранит каждый столбец:
visits— blob состоянияsum()(фактически UInt64-аккумулятор, обёрнутый в blob)users— blob состоянияuniq()(HyperLogLog sketch, ~16 КБ)avg_dur— blob состоянияavg()(пара: сумма + count)
Прямой SELECT visits FROM daily_stats вернёт бессмысленные бинарные данные. Для финализации нужен комбинатор -Merge.
-State: запись агрегатного состояния
Комбинатор -State добавляется к имени агрегатной функции. Он вычисляет промежуточное состояние, но не финализирует его:
-- MV использует -State для записи blob в target
CREATE MATERIALIZED VIEW daily_stats_mv TO daily_stats AS
SELECT
toDate(timestamp) AS date,
sumState(1) AS visits,
uniqState(user_id) AS users,
avgState(duration_ms) AS avg_dur
FROM raw_events
GROUP BY date;
sumState(1) не возвращает число — возвращает бинарный blob, содержащий частичную сумму блока. uniqState(user_id) возвращает HyperLogLog sketch для user_id из текущего INSERT block.
-Merge: чтение и финализация
Комбинатор -Merge объединяет промежуточные состояния и возвращает финальный результат:
SELECT
date,
sumMerge(visits) AS total_visits,
uniqMerge(users) AS unique_users,
avgMerge(avg_dur) AS average_duration
FROM daily_stats
GROUP BY date
ORDER BY date;
sumMerge(visits) берёт все blob из столбца visits (по группе), объединяет их и возвращает число. uniqMerge(users) объединяет HyperLogLog sketches и возвращает приблизительный count distinct.
Полный канонический паттерн
Полный SQL-пример:
-- 1. Source: все события
CREATE TABLE raw_events (
timestamp DateTime,
user_id UInt64,
page String,
duration_ms UInt32
) ENGINE = MergeTree()
ORDER BY (timestamp, user_id);
-- 2. Target: агрегированные состояния
CREATE TABLE daily_stats (
date Date,
visits AggregateFunction(sum, UInt32),
users AggregateFunction(uniq, UInt64)
) ENGINE = AggregatingMergeTree()
ORDER BY date;
-- 3. MV: автоматическая агрегация при INSERT
CREATE MATERIALIZED VIEW daily_stats_mv TO daily_stats AS
SELECT
toDate(timestamp) AS date,
sumState(1) AS visits,
uniqState(user_id) AS users
FROM raw_events
GROUP BY date;
-- 4. Вставка данных -- MV автоматически обновляет daily_stats
INSERT INTO raw_events VALUES
('2024-01-15 10:00:00', 1, '/home', 250),
('2024-01-15 10:05:00', 2, '/docs', 1200),
('2024-01-15 10:10:00', 1, '/pricing', 800);
-- 5. Чтение через -Merge
SELECT
date,
sumMerge(visits) AS total_visits,
uniqMerge(users) AS unique_users
FROM daily_stats
GROUP BY date;
-- Результат: date=2024-01-15, total_visits=3, unique_users=2
SimpleAggregateFunction: лёгкая альтернатива
SimpleAggregateFunction(func, type) хранит результат агрегации как обычное значение — не бинарный blob. Работает только для простых функций: sum, min, max, any, anyLast.
CREATE TABLE counters (
key String,
total SimpleAggregateFunction(sum, UInt64),
first_seen SimpleAggregateFunction(min, DateTime),
last_seen SimpleAggregateFunction(max, DateTime)
) ENGINE = AggregatingMergeTree()
ORDER BY key;
-- INSERT обычными значениями (НЕ -State)
INSERT INTO counters VALUES ('page_a', 100, '2024-01-15 08:00:00', '2024-01-15 12:00:00');
INSERT INTO counters VALUES ('page_a', 200, '2024-01-15 14:00:00', '2024-01-15 18:00:00');
-- SELECT обычными функциями (НЕ -Merge)
SELECT key, sum(total), min(first_seen), max(last_seen)
FROM counters GROUP BY key;
-- total=300, first_seen=08:00, last_seen=18:00
Преимущества SimpleAggregateFunction:
- INSERT обычными значениями — не нужен -State
- SELECT обычными функциями — не нужен -Merge
- Значение читаемо напрямую (число, дата) — не blob
Ограничения: не поддерживает uniq, quantile, avg и другие функции с нетривиальным промежуточным состоянием.
Типичные ошибки
Забыли GROUP BY при чтении:
-- Неверно: без GROUP BY, -Merge объединит ВСЕ строки
SELECT sumMerge(visits) FROM daily_stats; -- одно число за все дни
-- Верно: GROUP BY для разбивки по дням
SELECT date, sumMerge(visits) FROM daily_stats GROUP BY date;
Прямой SELECT без -Merge:
-- Неверно: бессмысленные бинарные данные
SELECT visits FROM daily_stats;
-- Верно: финализация через -Merge
SELECT sumMerge(visits) FROM daily_stats GROUP BY date;
SimpleAggregateFunction для uniq:
-- Неверно: uniq не поддерживается SimpleAggregateFunction
SimpleAggregateFunction(uniq, UInt64) -- ошибка
-- Верно: используйте AggregateFunction
AggregateFunction(uniq, UInt64)
Dictionary Encoding: кросс-форматный анализ
Streaming fundamentals: event time, watermarks, windows