skip to content

SQL Dialect & Functions

ClickHouse SQL adds array and lambda functions, Map and Tuple types, and aggregate combinators like -If and -State/-Merge, while departing from ANSI on JOIN strictness and the FINAL modifier. Interviewers check these because they are how you write efficient ClickHouse rather than portable SQL.

part ofClickHouseoverview, primer and where to startread it →
on this pageshow

questions

7

In ClickHouse, what do the -If and -Array aggregate combinators do to a function like sum?

level: middleimportance: must knowfreq 65%

answer

  1. suffixes on the function name, not extra clauses
  2. one scan, several differently filtered metrics
  3. the condition is the last argument
  4. arrays can be consumed element-wise
  5. Array comes before If when you stack them

basics

~20 s

Combinators 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 s

ClickHouse 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
sql
-- 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context

open as a page

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

level: seniorimportance: must knowfreq 62%

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.

open as a page

In ClickHouse, how do arrayMap and arrayFilter with lambdas transform an Array column?

level: juniorimportance: should knowfreq 72%

basics

~20 s

They apply a lambda written as x -> expression to every element of an array inside a single row. arrayMap returns a new array of transformed elements, arrayFilter returns only the elements where the lambda is true. Neither changes the row count.

open as a page

In ClickHouse, what does ARRAY JOIN do to a row, and how does LEFT ARRAY JOIN differ?

level: middleimportance: should knowfreq 55%

basics

~20 s

ARRAY JOIN expands each row into one row per element of the named array, so a row with five elements becomes five rows. Rows whose array is empty disappear. LEFT ARRAY JOIN keeps them, emitting one row with the element type's default value.

open as a page

In ClickHouse, what do the ANY, ALL and ASOF JOIN strictness modes mean?

level: middleimportance: should knowfreq 55%

basics

~20 s

ALL is the ANSI behaviour and the default: every matching right row produces a result row. ANY returns at most one right-hand match per left row, capping fan-out. ASOF joins on equality plus one inequality, returning the closest preceding row — the time-series as-of lookup.

open as a page

In ClickHouse, what does uniqState return, and how do you turn stored states back into a number?

level: seniorimportance: should knowfreq 45%

basics

~20 s

uniqState returns an opaque intermediate aggregation state of type AggregateFunction(uniq, ...), not a count. You store those states and finalize them later with the matching -Merge function, uniqMerge, or with finalizeAggregation for a single state.

open as a page

In ClickHouse, when should attributes live in a Map column rather than a Tuple or separate columns?

level: middleimportance: nice to knowfreq 38%

basics

~20 s

Use Map only for genuinely dynamic, sparse keys: it is stored as parallel key and value arrays, so reading one key reads them all. Use a Tuple for a fixed small group of related fields, and plain columns for any attribute you filter or group on regularly.

open as a page