CollapsingMergeTree и VersionedCollapsingMergeTree
CollapsingMergeTree и VersionedCollapsingMergeTree реализуют sign-based мутации: вместо обновления строки in-place вы вставляете “отменяющую” строку (-1) и новую строку (+1). При фоновом merge пары cancel/state коллапсируются, оставляя только актуальные данные.
CollapsingMergeTree: базовый механизм
CollapsingMergeTree добавляет к таблице специальный sign-столбец типа Int8. Каждая строка помечается:
+1(state) — актуальная строка-1(cancel) — отмена предыдущей строки с тем же ORDER BY ключом
CREATE TABLE orders (
order_id UInt32,
status String,
amount Decimal(10, 2),
Sign Int8
) ENGINE = CollapsingMergeTree(Sign)
ORDER BY order_id;
Жизненный цикл: state -> cancel -> new state
Допустим, заказ #100 переходит из статуса “new” в “paid”:
-- 1. Вставляем исходное состояние (state, +1)
INSERT INTO orders VALUES (100, 'new', 1500.00, 1);
-- 2. Отменяем старое состояние (cancel, -1) -- ТОЧНАЯ копия с Sign=-1
INSERT INTO orders VALUES (100, 'new', 1500.00, -1);
-- 3. Вставляем новое состояние (state, +1)
INSERT INTO orders VALUES (100, 'paid', 1500.00, 1);
До merge в таблице 3 строки. После merge:
- Пара (state +1, cancel -1) с одинаковым ORDER BY ключом и значениями коллапсируется
- Остаётся одна строка:
(100, 'paid', 1500.00, 1)
Критическое ограничение: строгий порядок вставки
CollapsingMergeTree требует, чтобы cancel строка (-1) была вставлена после соответствующей state строки (+1). Если порядок нарушен (cancel приходит раньше state), коллапсирование не сработает — обе строки останутся.
Это ограничение делает CollapsingMergeTree пригодным только для однопоточной последовательной вставки, где порядок событий гарантирован.
Паттерн запроса: HAVING sum(Sign) > 0
До merge в таблице могут быть несколлапсированные строки. Правильный запрос всегда агрегирует по Sign:
-- ПРАВИЛЬНО: фильтрация по Sign
SELECT
order_id,
anyIf(status, Sign = 1) AS current_status,
sum(amount * Sign) AS net_amount
FROM orders
GROUP BY order_id
HAVING sum(Sign) > 0;
-- НЕПРАВИЛЬНО: чтение без агрегации Sign
-- SELECT * FROM orders -- покажет cancel и state строки
HAVING sum(Sign) > 0 отсеивает полностью отменённые записи (где cancel и state уже вставлены, но merge ещё не произошёл).
VersionedCollapsingMergeTree: out-of-order вставка
VersionedCollapsingMergeTree расширяет CollapsingMergeTree, добавляя столбец Version. Это снимает ограничение на порядок вставки:
CREATE TABLE orders_v (
order_id UInt32,
status String,
amount Decimal(10, 2),
Sign Int8,
Version UInt32
) ENGINE = VersionedCollapsingMergeTree(Sign, Version)
ORDER BY order_id;
При merge движок удаляет пары строк с одинаковым ORDER BY ключом, одинаковым Version, но разным Sign. Порядок вставки не имеет значения — Version столбец определяет, какие строки являются парой.
-- Порядок вставки НЕ важен
INSERT INTO orders_v VALUES (100, 'new', 1500.00, 1, 1); -- state v1
INSERT INTO orders_v VALUES (100, 'paid', 1500.00, 1, 2); -- state v2
INSERT INTO orders_v VALUES (100, 'new', 1500.00, -1, 1); -- cancel v1 (может прийти позже!)
После merge: пара (state v1, cancel v1) коллапсируется. Остаётся state v2.
Сравнение: Collapsing vs Versioned
| Аспект | CollapsingMergeTree | VersionedCollapsingMergeTree |
|---|---|---|
| Столбец Sign | Int8 (+1/-1) | Int8 (+1/-1) |
| Столбец Version | Нет | UInt* (обязательный) |
| Порядок INSERT | Строго последовательный | Произвольный |
| Cancel/state matching | По ORDER BY ключу + все значения | По ORDER BY ключу + Version |
| Однопоточная вставка | Подходит | Подходит |
| Многопоточная/distributed вставка | Не подходит | Предпочтительный выбор |
Когда использовать какой:
- CollapsingMergeTree — однопоточная вставка, один источник данных, порядок событий гарантирован. Проще (нет Version столбца).
- VersionedCollapsingMergeTree — многопоточная вставка, distributed INSERT, несколько источников данных. Всегда предпочтительнее в production-системах с несколькими клиентами.
Оба движка сложнее ReplacingMergeTree. Используйте только когда нужна точная отмена отдельных строк (не полная замена по ключу). Если задача — просто “оставить последнюю версию строки”, ReplacingMergeTree проще и надёжнее.
Когда Collapsing, а не Replacing
ReplacingMergeTree заменяет всю строку по ORDER BY ключу — старые данные теряются. Collapsing-движки позволяют:
- Аддитивную агрегацию с корректной отменой:
sum(amount * Sign)корректно вычитает отменённые строки. ReplacingMergeTree не поддерживает такую семантику. - Групповую отмену: можно отменить несколько строк одного заказа (позиции, платежи) с разными ORDER BY ключами.
- Точную коррекцию: ошибочно вставленный event можно отменить без потери других events с тем же ключом.
Ключевые выводы
- CollapsingMergeTree использует Sign столбец (Int8: +1 state, -1 cancel). Пары с одинаковым ORDER BY ключом коллапсируются при merge.
- Порядок INSERT критичен для CollapsingMergeTree: cancel (-1) должен быть вставлен строго после state (+1). Нарушение порядка ломает коллапсирование.
- VersionedCollapsingMergeTree добавляет Version столбец и снимает ограничение на порядок вставки. Предпочтительнее для distributed и многопоточных систем.
- Паттерн запроса: всегда используйте
HAVING sum(Sign) > 0или агрегируйте с Sign для корректных результатов до merge. - Collapsing vs Replacing: используйте Collapsing, когда нужна аддитивная агрегация с корректной отменой (
sum(amount * Sign)), а не простая замена последней версии строки.