In ClickHouse, what do the -If and -Array aggregate combinators do to a function like sum?
answer
- suffixes on the function name, not extra clauses
- one scan, several differently filtered metrics
- the condition is the last argument
- arrays can be consumed element-wise
- Array comes before If when you stack them
basics
~20 sCombinators are suffixes appended to any aggregate function name. -If adds a final condition argument so only rows where it is true are aggregated, as in sumIf(amount, status = 'paid'). -Array aggregates over the elements of an array column as if each element were a row.
solid answer
~40 sClickHouse lets you suffix nearly any aggregate function with a **combinator** that changes how it consumes input. `-If` appends a `UInt8` condition as the last argument and aggregates only the rows where it is 1: `countIf(status = 'error')`, `sumIf(bytes, status = 'ok')`, `uniqIf(user_id, is_purchase)`. That lets one scan produce several differently-filtered metrics, which a `WHERE` clause cannot do because `WHERE` filters rows for every aggregate at once. `-Array` treats each element of an `Array` column as a separate input value, so `sumArray(vals)` totals every element of every row and `uniqArray(tags)` counts distinct tags without any `ARRAY JOIN` expansion. The two compose, but **`-Array` must come before `-If`**: `uniqArrayIf(tags, country = 'DE')`. The same suffix grammar carries the other combinators — `-State`, `-Merge`, `-OrNull`, `-OrDefault`, `-Distinct`, `-ForEach`, `-Map`.
code
sql · 9 lines-- one scan, five differently filtered metrics
SELECT
count() AS requests,
countIf(status >= 500) AS server_errors,
sumIf(bytes, status = 200) AS ok_bytes,
uniqIf(user_id, path = '/checkout') AS checkout_users,
avgIf(latency_ms, region = 'eu') AS eu_latency
FROM requests
WHERE day = today()go deeper
Recall that countIf and sumIf exist and take the condition as the final argument, and that they let one query report several differently filtered numbers.
Explain why -If differs from WHERE, how -Array consumes array elements without ARRAY JOIN, and the fixed Array-then-If ordering when the two are stacked.
Demonstrate the sum(if(...)) versus sumIf distinction for count-sensitive aggregates, and cost reasoning about when one filtered scan beats multiple queries or a WHERE clause.
Frame it as a query-cost policy: consolidating many dashboard metrics into a single combinator-based scan is often the cheapest available change on a high-concurrency cluster.
## The idea Most SQL dialects give you a fixed catalogue of aggregate functions. ClickHouse instead gives you a set of base aggregates and a set of **combinators** — suffixes you append to the function's name to change its input or output behaviour. The combination is resolved at parse time, so `uniqArrayIf` is a real function, not a macro. This is why ClickHouse SQL looks unfamiliar: there is no separate `FILTER` ceremony or unnesting step, the behaviour is baked into the function name. ## The -If combinator `-If` appends one extra argument to the function: a condition evaluated per row. Rows where it is 0 are skipped by that aggregate only. ```sql SELECT count() AS total, countIf(status = 'error') AS errors, sumIf(bytes, status = 'ok') AS ok_bytes, uniqIf(user_id, event = 'purchase') AS buyers, avgIf(latency_ms, region = 'eu') AS eu_latency FROM requests WHERE day = today() ``` One scan, five metrics, each with its own predicate. Pushing any of those predicates into `WHERE` would apply it to all five. That is the whole point of the combinator: `WHERE` is a row filter for the query, `-If` is a row filter for a single aggregate. When every aggregate uses the same predicate, `WHERE` is strictly better — it filters earlier, prunes more, and reads less. `-If` earns its keep when the predicates differ. Note the argument order: the condition is always **last**. `countIf` is the special case where the condition is the only argument, since `count` has none. ## The -Array combinator `-Array` makes an aggregate consume an `Array` column element-wise: every element of every row's array is fed to the aggregate as if it were an ordinary input value. ```sql SELECT sumArray(item_prices) AS revenue, -- every element of every array uniqArray(tags) AS distinct_tags, maxArray(item_prices) AS priciest_item FROM baskets ``` The alternative would be `ARRAY JOIN` followed by a normal aggregate. The combinator avoids materializing the expanded rows, so it is usually both shorter and cheaper. `groupArrayArray(arr)` is the idiom for flattening arrays across rows into one array. ## Composing combinators Combinators stack, and the order is fixed by the grammar: **`-Array` first, then `-If`**. ```sql SELECT uniqArrayIf(tags, country = 'DE') AS de_tags FROM events ``` Read it right to left: `-If` selects the rows (`country = 'DE'`), `-Array` feeds the elements of those rows' arrays to `uniq`. Writing `uniqIfArray` is a parse error. Parametric aggregates keep their parameter list in front: `quantilesTimingArrayIf(0.5, 0.99)(durations, is_slow)`. ## The rest of the family The same suffix grammar carries combinators you should be able to name: - `-State` / `-Merge` — return and consume an intermediate aggregation state instead of a final value, which is what makes pre-aggregation mergeable. - `-OrNull` / `-OrDefault` — return `NULL`, or the type default, when the aggregate received no input rows at all, instead of the function's normal empty-input result. - `-Distinct` — deduplicate input values before aggregating, e.g. `sumDistinct`. - `-ForEach` — apply the aggregate element-wise across arrays, producing an array of aggregates (positional), which is different from `-Array` flattening everything into one value. - `-Map` — aggregate values grouped by map key. - `-SimpleState` — produce a state for the cheaper `SimpleAggregateFunction` type. ## Pitfalls - **`sum(if(cond, x, 0))` is not always the same as `sumIf(x, cond)`.** The `if` form contributes a zero for non-matching rows, which matters for `avg`, `min`, `count` and anything sensitive to how many values were seen. `avgIf(x, cond)` averages only matching rows; `avg(if(cond, x, 0))` drags the mean toward zero. - **Nothing is free.** `-If` still reads and evaluates every row that survives `WHERE`; it saves a second scan, not the scan. - **`countIf(x)` needs a condition, not a column.** `countIf(user_id)` counts rows where `user_id` is non-zero, which is rarely what was meant. - **`-Array` is not `-ForEach`.** `sumArray` returns one number over all elements; `sumForEach` returns an array of per-position sums. ## What interviewers probe Expect "compute paid, refunded and total revenue per customer in one query" and expect the answer to be three `sumIf` expressions in one `SELECT`, with an explanation of why that beats three subqueries or three scans. The follow-up is usually the `-Array`/`-If` ordering rule, or when a plain `WHERE` is the better tool.
- When is a plain WHERE clause better than the -If combinator?When every aggregate in the query needs the same predicate. `WHERE` filters before the aggregates run, prunes parts and granules, and reads less data. `-If` evaluates its condition per surviving row and only exists so that different aggregates in one scan can apply different filters. Using `-If` where `WHERE` would do just makes the query read more data for the same answer.
- Why is sumIf(amount, cond) not identical to sum(if(cond, amount, 0))?For `sum` the totals happen to match, because the non-matching rows add zero. For anything that counts inputs it diverges: `avgIf(x, cond)` averages only the matching rows, while `avg(if(cond, x, 0))` includes a zero for every non-matching row and pulls the mean down. `minIf` and `countIf` break the same way. The combinator skips rows; `if` substitutes a value.
- What is the difference between the -Array and -ForEach combinators in ClickHouse?`-Array` flattens: every element of every row's array becomes one input value, so `sumArray(v)` returns a single number. `-ForEach` aggregates positionally: it treats the arrays as vectors and returns an array where element *i* is the aggregate of all the rows' element *i*, so `sumForEach(v)` returns an array. Use `-Array` for totals, `-ForEach` for per-bucket vectors.
saying these in an interview costs you the question
- Puts the -If predicate in WHERE and expects per-metric filtering
- Writes uniqIfArray instead of uniqArrayIf
- Claims sumIf and sum(if(...)) are always interchangeable
- Thinks combinators avoid scanning the rows they filter out
- Uses countIf with a column instead of a condition