SummingMergeTree: агрегация счётчиков при merge
SummingMergeTree автоматически суммирует числовые столбцы при фоновом merge. Строки с одинаковым ORDER BY ключом объединяются: числовые значения складываются, нечисловые — берутся произвольно. Это идеальный движок для счётчиков и инкрементальных метрик.
Базовый пример
CREATE TABLE page_views (
page_url String,
day Date,
views UInt32,
unique_visitors UInt32
) ENGINE = SummingMergeTree()
ORDER BY (page_url, day);
-- Два INSERT для одного и того же ключа (page_url, day)
INSERT INTO page_views VALUES ('/', '2024-01-01', 100, 50);
INSERT INTO page_views VALUES ('/', '2024-01-01', 200, 75);
После merge двух строк с ключом ('/', '2024-01-01'):
| page_url | day | views | unique_visitors |
|---|---|---|---|
/ | 2024-01-01 | 300 | 125 |
Оба числовых столбца (views, unique_visitors) суммированы автоматически. Не нужно писать GROUP BY в материализованных представлениях или ETL-пайплайнах — движок делает это при merge.
Параметр columns: выборочное суммирование
По умолчанию SummingMergeTree суммирует все числовые столбцы, не входящие в ORDER BY. Если нужно суммировать только конкретные столбцы:
CREATE TABLE metrics (
host String,
ts DateTime,
cpu_usage Float32,
requests UInt64,
errors UInt64
) ENGINE = SummingMergeTree((requests, errors))
ORDER BY (host, toStartOfMinute(ts));
Здесь суммируются только requests и errors. Столбец cpu_usage — произвольное значение из одной из объединяемых строк (не среднее, не максимум — именно произвольное).
Поведение нечисловых столбцов
При merge строк с одинаковым ORDER BY ключом:
- Числовые столбцы (Int*, UInt*, Float*, Decimal*) — суммируются
- Нечисловые столбцы (String, Date, DateTime, UUID, etc.) — произвольное значение из одной из строк
“Произвольное” означает: ClickHouse не гарантирует, какое значение останется. Может быть первое, может быть последнее, может меняться между версиями. Не полагайтесь на значение нечисловых столбцов после merge.
Удаление нулевых строк
Если после суммирования все числовые столбцы равны 0, строка удаляется при merge. Это полезно для “отмены” ранее вставленных данных:
-- Вставили 100 views
INSERT INTO page_views VALUES ('/old-page', '2024-01-01', 100, 50);
-- "Отменяем" -- вставляем отрицательные значения
INSERT INTO page_views VALUES ('/old-page', '2024-01-01', -100, -50);
-- После merge: views=0, unique_visitors=0 -> строка удалена
Строки удаляются только если ВСЕ суммируемые столбцы равны 0. Если хотя бы один столбец ненулевой — строка остаётся.
Почему SELECT без sum() опасен
Merge — фоновый процесс. В любой момент времени часть строк может быть ещё не объединена:
-- НЕПРАВИЛЬНО: может показать несуммированные промежуточные строки
SELECT page_url, day, views, unique_visitors
FROM page_views
WHERE page_url = '/';
-- ПРАВИЛЬНО: всегда агрегируйте в запросе
SELECT page_url, day, sum(views) AS views, sum(unique_visitors) AS visitors
FROM page_views
GROUP BY page_url, day;
SummingMergeTree гарантирует, что после merge значения будут суммированы. Но merge может быть неполным — несуммированные строки из разных parts остаются видимыми. Всегда используйте sum() + GROUP BY в запросах.
Не полагайтесь на SELECT без sum() — merge может быть неполным. Всегда агрегируйте в запросе. SummingMergeTree уменьшает объём хранения, но не освобождает от GROUP BY при чтении.
SimpleAggregateFunction: не только sum
SummingMergeTree по умолчанию только суммирует. Но через SimpleAggregateFunction можно использовать другие простые агрегатные функции: min, max, any, anyLast, sum:
CREATE TABLE user_activity (
user_id UInt32,
day Date,
total_actions SimpleAggregateFunction(sum, UInt64),
first_seen SimpleAggregateFunction(min, DateTime),
last_seen SimpleAggregateFunction(max, DateTime),
latest_page SimpleAggregateFunction(anyLast, String)
) ENGINE = SummingMergeTree()
ORDER BY (user_id, day);
INSERT INTO user_activity VALUES
(1, '2024-01-15', 5, '2024-01-15 08:00:00', '2024-01-15 12:00:00', '/docs');
INSERT INTO user_activity VALUES
(1, '2024-01-15', 3, '2024-01-15 14:00:00', '2024-01-15 18:00:00', '/pricing');
После merge:
| user_id | day | total_actions | first_seen | last_seen | latest_page |
|---|---|---|---|---|---|
| 1 | 2024-01-15 | 8 (sum) | 08:00 (min) | 18:00 (max) | произвольная (anyLast) |
SimpleAggregateFunction хранит только значение результата (число, дата, строка) — не бинарное состояние агрегатной функции. Это отличает его от AggregateFunction, который хранит бинарный blob промежуточного состояния и требует -State/-Merge combinator.
SummingMergeTree vs GROUP BY на MergeTree
| Аспект | SummingMergeTree | MergeTree + GROUP BY |
|---|---|---|
| Хранение | Меньше — строки объединяются при merge | Больше — все исходные строки на диске |
| Запрос | sum() + GROUP BY (обязательно!) | sum() + GROUP BY |
| Результат | Идентичный | Идентичный |
| Вставка | Обычный INSERT | Обычный INSERT |
| Гибкость | Только суммирование (+ SimpleAggregateFunction) | Любая агрегация при запросе |
Оба подхода дают одинаковый результат запроса. Разница — в объёме хранения. SummingMergeTree физически уменьшает число строк на диске, что ускоряет запросы на больших таблицах с высокой кардинальностью ключей.
Ключевые выводы
- SummingMergeTree суммирует числовые столбцы (не в ORDER BY) при фоновом merge. Нечисловые столбцы — произвольное значение.
- Параметр columns позволяет указать конкретные столбцы для суммирования. Без него — все числовые.
- Строки с нулевыми суммами удаляются при merge — можно “отменить” данные через отрицательные значения.
- Всегда используйте sum() + GROUP BY при чтении. Merge может быть неполным — необъединённые строки видны в SELECT.
- SimpleAggregateFunction расширяет SummingMergeTree за рамки суммирования: min, max, any, anyLast. Хранит результат, не бинарное состояние.