How does ClickHouse's CollapsingMergeTree use its sign column to retract a row?
answer
- retract by writing the row again, negated
- one column of plus and minus ones
- the pair annihilates during a background merge
- multiply the measure by that column before summing
- out-of-order arrival needs the versioned variant
basics
~20 sEach row is written twice: once with sign = 1 as a state row, later with sign = -1 as an identical cancel row. A background merge cancels the pair. Queries must use sum(metric * sign) because merges are eventual.
solid answer
~50 s`CollapsingMergeTree(sign)` requires an `Int8` column holding `1` for a *state* row and `-1` for a *cancel* row. To retract or change a fact, you insert a copy of the old row with `sign = -1` and, if it is an update, the new version with `sign = 1`. During a background merge, rows with the same sorting key and opposite signs annihilate each other, so the table shrinks to the surviving state. Because merges are asynchronous, queries cannot assume cancellation has happened. The standard pattern is to make the sign do the arithmetic: `SELECT sum(amount * sign) ... GROUP BY key HAVING sum(sign) > 0`. Uncancelled pairs then contribute zero. The engine assumes the cancel row is written **after** the state row it retracts. If your producer can deliver out of order, use `VersionedCollapsingMergeTree(sign, version)`, which uses the version column to pair rows correctly regardless of arrival order.
code
sql · 20 linesCREATE TABLE order_totals
(
order_id UInt64,
amount Decimal(18, 2),
sign Int8
)
ENGINE = CollapsingMergeTree(sign)
ORDER BY order_id;
-- Original fact
INSERT INTO order_totals VALUES (42, 100.00, 1);
-- Correction: cancel the old row exactly, then state the new one
INSERT INTO order_totals VALUES (42, 100.00, -1), (42, 120.00, 1);
-- Correct whether or not the merge has run yet
SELECT order_id, sum(amount * sign) AS amount
FROM order_totals
GROUP BY order_id
HAVING sum(sign) > 0;go deeper
Recall the shape: an Int8 column where 1 means the fact holds and -1 retracts it, and background merges cancel matching pairs. Detailed use is rarely expected this early.
Explain the two-row write protocol and why queries multiply the measure by the sign — that expression is correct whether or not the merge has run.
Show that you know the ordering assumption and the versioned variant that lifts it, and that a cancel row must reproduce the original values exactly or aggregates are silently corrupted.
Own the modelling call: this engine pushes a before-image obligation onto every producer. Weigh that coupling against replacing-plus-version, and say when the operational simplicity is worth more than the exact-aggregate property.
## The problem it solves ClickHouse has no cheap row update. `CollapsingMergeTree` offers an insert-only way to express "this fact is no longer true", which is useful for mutable aggregates: an order whose amount changed, a session whose duration was corrected, a running total maintained from a change stream. ```sql CREATE TABLE order_totals ( order_id UInt64, amount Decimal(18, 2), sign Int8 ) ENGINE = CollapsingMergeTree(sign) ORDER BY order_id; ``` ## The write protocol Every row carries a sign. `sign = 1` marks a **state** row — a fact that is currently true. `sign = -1` marks a **cancel** row — an exact copy of a previously written state row, same sorting key and same measure values, that retracts it. To change an order's amount from 100 to 120 you insert two rows: `(order_id, 100, -1)` and `(order_id, 120, 1)`. To delete it entirely you insert only the cancel row. Nothing is ever modified in place; the table is append-only and the *net* of the signs is the truth. This puts a real obligation on the writer: to cancel a row you must know its previous values. In practice that means either your source of change events includes the before-image (many change-data-capture streams do) or you keep the last-known state in the application. If you cannot produce an accurate before-image, this engine is the wrong tool — `ReplacingMergeTree` with a version is far more forgiving. ## What a merge does When the background merge processes a run of rows sharing a sorting key, it cancels out state and cancel pairs and writes only what survives. In the ordinary case — one state row and one cancel row — both vanish; where state rows outnumber cancel rows, the latest state survives. What the merge can only do correctly is pair rows it sees in the right relative order: `CollapsingMergeTree` assumes the cancel row was inserted after the state row it retracts. Rows delivered out of order can leave residue that never collapses, and in awkward cases produce values that are simply wrong rather than merely uncollapsed. As with every MergeTree variant, collapsing is **eventual** and confined to a partition. Nothing happens at insert time, and rows for the same key that land in different partitions never meet. ## Querying a collapsing table Because you cannot know whether a pair has collapsed yet, you never read the rows directly. You let the sign carry the arithmetic: ```sql SELECT order_id, sum(amount * sign) AS amount FROM order_totals GROUP BY order_id HAVING sum(sign) > 0; ``` An uncollapsed `+1 / -1` pair contributes `amount - amount = 0` to the sum and `1 - 1 = 0` to the sign total, so the `HAVING` clause drops fully cancelled keys. This expression is correct whether or not the merge has run — that is the elegance of the design and the reason the pattern is so idiomatic. `FINAL` also works and is simpler to write, at query cost. Note what this implies for non-additive columns: multiplying by the sign works for sums and counts. For a value you merely want to carry — a status string, a latest label — collapsing is awkward and a replacing table is the better model. ## VersionedCollapsingMergeTree `VersionedCollapsingMergeTree(sign, version)` adds a version column to the engine parameters. The version identifies which state a cancel row belongs to, so pairs collapse correctly even when rows arrive out of order — the usual situation when a partitioned message stream feeds several concurrent inserters. If your ingestion cannot guarantee ordering per key, this is the variant to reach for. ## When to use it at all Honestly: less often than it used to be. `ReplacingMergeTree` with a version column covers most "latest state per key" needs, requires no before-image, and is much easier to reason about. Collapsing earns its place when you maintain **additive aggregates over mutable facts** and want the sign trick to keep sums correct without resolving the latest row per key first — a running total over a change stream is the canonical case. Knowing the model, and knowing when *not* to choose it, is what an interviewer is actually probing. ## How to answer Describe the two-row protocol, say that the merge annihilates pairs eventually, show the `sum(metric * sign)` and `HAVING sum(sign) > 0` query, and name the ordering assumption plus the versioned variant that removes it. Close by contrasting it with `ReplacingMergeTree` so the interviewer sees you would not reach for it reflexively.
- What breaks if the cancel row does not exactly match the state row it retracts?The pair no longer annihilates cleanly. The sorting key must match for the rows to meet during a merge, and the measure columns must match for `sum(metric * sign)` to net to zero. A cancel row carrying the wrong amount leaves a permanent residual in every aggregate — a silent, hard-to-trace corruption, since nothing errors. This is why the writer needs a reliable before-image.
- When would you pick ReplacingMergeTree instead of CollapsingMergeTree?Almost whenever you only need the latest state per key. Replacing needs no before-image, tolerates out-of-order arrival if you supply a version, and reads naturally with `FINAL` or `argMax`. Collapsing earns its keep when you maintain additive aggregates over mutable facts and want sums to stay correct without first resolving the latest row per key.
- How does VersionedCollapsingMergeTree change the ordering requirement?Plain collapsing assumes the cancel row is written after the state row it retracts, which a partitioned stream with concurrent writers cannot guarantee. `VersionedCollapsingMergeTree(sign, version)` uses the extra version column to identify which state a cancel belongs to, so pairs collapse correctly regardless of arrival order. The query pattern with the sign column stays the same.
It is double-entry bookkeeping: you never erase a line, you post an equal and opposite line, and the balance is the sum of everything on the page.
saying these in an interview costs you the question
- Thinks inserting sign = -1 deletes the row immediately
- Writes a cancel row with different measure values than the original
- Reads the table directly instead of summing measure times sign
- Assumes collapsing works when events arrive out of order
- Uses it for latest-state-per-key where replacing is simpler