skip to content

In ClickHouse, what does adding FINAL to a SELECT do, and what does it cost?

level: seniorimportance: must knowfreq 62%

answer

  1. background merges are eventual, not guaranteed
  2. the engine's own logic, applied at read time
  3. overlapping parts must be read together
  4. deduplication never crosses a partition
  5. argMax can replace it on hot paths

basics

~20 s

FINAL makes ClickHouse apply the table engine's merge logic at query time, so a ReplacingMergeTree returns one row per sorting key and a CollapsingMergeTree returns collapsed rows. It pays for that by merging overlapping parts on every query, costing extra CPU, memory and time.

solid answer

~50 s

On a MergeTree-family table whose engine transforms rows during merges — `ReplacingMergeTree`, `Collapsing`, `Summing`, `Aggregating` — background merges are **eventual**: until they run you can still see the pre-merge rows. `SELECT ... FROM t FINAL` forces that merge logic to be applied on the fly for the query, giving the deduplicated or collapsed view immediately. The cost is real: the engine must read the overlapping parts of each partition together and merge-sort them in sorting-key order rather than streaming parts independently, which burns CPU and memory and gets worse as the number of parts grows. Deduplication is per partition and per `ORDER BY` key, so two rows with the same key in different partitions both survive `FINAL`. Common alternatives are `GROUP BY key` with `argMax(col, version)`, or `LIMIT 1 BY key` with an ordering — often much cheaper on wide scans.

code

sql · 5 lines
sql
-- may include superseded versions still awaiting a merge
SELECT count() FROM orders;

-- engine's merge logic applied at read time
SELECT count() FROM orders FINAL;

go deeper

for a junior

Recall that some ClickHouse engines deduplicate or collapse rows only during background merges, and that FINAL asks for that result now, at a cost.

for a middle

Explain why overlapping parts must be merge-sorted at read time, that the effect is per partition on the ORDER BY key, and what happens without FINAL.

for a senior

Diagnose the real cases: duplicates surviving FINAL because of the partition key, part counts driving the cost, and rewriting to argMax or LIMIT 1 BY on hot paths.

for a principal

Own the correctness contract — decide whether dashboards read an eventually-deduplicated view, a rewritten aggregation, or a downstream rollup, and what staleness the business has agreed to.

## Why FINAL exists The MergeTree family stores data as immutable parts and rewrites them in the background. Several engines attach a *transformation* to that rewrite: `ReplacingMergeTree` keeps one row per sorting key (the highest version, or the last inserted when no version column is declared), `CollapsingMergeTree` cancels matched pairs by sign, `SummingMergeTree` adds numeric columns, `AggregatingMergeTree` merges aggregate states. Critically, that transformation happens **only when a merge runs**, and merges are best-effort background work with no completion guarantee. So a plain `SELECT` from a `ReplacingMergeTree` can return several rows for the same key — the old version and the new one, sitting in different parts. The engine is not broken; deduplication is eventual by design. `FINAL` closes the gap for one query: it applies the engine's merge logic at read time, so the result matches what a fully merged table would return. ```sql SELECT count() FROM orders; -- may count superseded versions SELECT count() FROM orders FINAL; -- engine-correct view ``` ## What it costs Without `FINAL`, ClickHouse can read parts independently and in parallel, streaming granules as they come. With `FINAL`, rows for the same sorting key may live in several overlapping parts, so those parts must be read together and merge-sorted on the sorting key before the transformation can be applied. That means more CPU, more memory, and less freedom to short-circuit. The penalty scales with how many overlapping parts a partition has, so a table with heavy small-batch inserts suffers most — the same table that most needs deduplication. Modern versions parallelize `FINAL` processing across parts and threads, so it is far from the disaster it once was, but it remains strictly more work than the same query without it. One structural optimization matters: `FINAL` is applied **per partition**, and the setting `do_not_merge_across_partitions_select_final` lets the engine skip merging across partitions when the partition key is a prefix of the sorting key, which can cut the work substantially on time-partitioned tables. ## What FINAL does not fix - **Cross-partition duplicates.** Merges never span partitions, so two rows with the same key inserted into different partitions both survive `FINAL`. If your partition key is the month and a row's key can appear in two months, deduplication will not happen. This is the most common "FINAL didn't work" report. - **Keys that differ.** Deduplication is on the full `ORDER BY` tuple. If a column in that tuple changed between versions, they are different keys and both remain. - **Insert-time deduplication.** Retried identical inserted blocks are deduplicated by a separate mechanism entirely; `FINAL` has nothing to do with it. ## The alternatives Because `FINAL` is a per-query tax, production queries often rewrite it away: ```sql -- latest version per id, expressed as an aggregation SELECT id, argMax(status, updated_at) AS status, max(updated_at) AS updated_at FROM orders GROUP BY id; -- or: one row per id, chosen by an ordering SELECT * FROM orders ORDER BY updated_at DESC LIMIT 1 BY id; ``` `argMax` is often the cheapest when you need a handful of columns, because it is an ordinary aggregation with no merge-sort of parts. `LIMIT 1 BY` is ClickHouse's top-N-per-group clause and keeps all columns. For `AggregatingMergeTree`, the natural form is a `GROUP BY` with the `-Merge` combinator rather than `FINAL`. A further pattern is to not deduplicate at read time at all: write an append-only log, and let a downstream rollup or a scheduled rewrite produce the deduplicated table that dashboards actually query. `OPTIMIZE TABLE t FINAL` is a different thing — an operational command that forces a full merge now, rewriting each partition into a single part. It is expensive (it rewrites all the data), and new inserts immediately create new parts again, so it is a maintenance tool, not a substitute for query-time correctness. ## Making the choice Use `FINAL` when correctness must be exact, the table is not enormous, and the query is not on a hot dashboard path. Rewrite to `argMax`/`LIMIT 1 BY` when the query is frequent and touches many rows. Consider whether the duplicates matter at all: for counters and monitoring, reading a slightly stale un-deduplicated view is sometimes acceptable and is by far the cheapest option. ## What interviewers probe Expect "our ReplacingMergeTree returns duplicates — why, and what do you do?" A strong answer explains that merges are eventual, that `FINAL` applies the logic at read time and what that costs, that deduplication is per partition on the sorting key, and offers at least one rewrite that avoids `FINAL` on the hot path.

  • Why can two rows with the same key survive a ClickHouse FINAL query?
    Because merging is per partition. Rows with the same sorting key inserted into different partitions are never merged, and `FINAL` inherits that boundary. The other cause is that the key is the full `ORDER BY` tuple: if any column in it differs between the two rows, they are different keys and both are kept. Check the partition key and the sorting key before blaming the engine.
  • Does OPTIMIZE TABLE ... FINAL remove the need for FINAL in queries?
    No. It forces a merge now, collapsing each partition to a single part, but the next insert creates a new part and duplicates reappear. On a large table it also rewrites all the data, which is expensive and I/O-heavy. Treat it as an operational tool for a one-off cleanup, not as a way to make plain SELECTs correct.
  • How would you rewrite a FINAL query on a hot dashboard path?
    Express the deduplication as an aggregation: `SELECT id, argMax(status, updated_at) FROM orders GROUP BY id` when you need a few columns, or `SELECT * FROM orders ORDER BY updated_at DESC LIMIT 1 BY id` when you need them all. Both avoid merge-sorting overlapping parts. For an AggregatingMergeTree, use `GROUP BY` with the `-Merge` combinator instead.

saying these in an interview costs you the question

  • Believes background merges are guaranteed to have deduplicated already
  • Thinks FINAL is a free metadata flag
  • Expects FINAL to deduplicate across partitions
  • Runs OPTIMIZE TABLE FINAL routinely to avoid query-time FINAL
  • Assumes duplicates mean the ReplacingMergeTree is misconfigured

context