In ClickHouse, how do arrayMap and arrayFilter with lambdas transform an Array column?
answer
- arrays are per-row values, not rows
- the arrow is the lambda
- one output value per row, not per element
- arrayMap transforms, arrayFilter selects
- x -> expression, only inside a higher-order function
basics
~20 sThey 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.
solid answer
~40 sClickHouse treats `Array(T)` as an ordinary column type, and higher-order functions operate on the array **within one row**. The lambda syntax is `x -> expression`, and a lambda is only legal in the argument slot of a higher-order function — you cannot store one in a column or an alias. `arrayMap(x -> x * 2, arr)` returns a same-length transformed array; `arrayFilter(x -> x > 10, arr)` returns the matching subset; `arrayCount`, `arraySum`, `arrayExists`, `arrayAll`, `arrayFirst` and `arraySort` also accept an optional lambda. A lambda may reference other columns of the same row, so `arrayFilter(x -> x > threshold, arr)` works. Multi-array forms zip in parallel — `arrayMap((x, y) -> x + y, a, b)` requires equal lengths per row or it throws. Arrays are 1-indexed and `length(arr)` gives the size.
code
sql · 6 linesSELECT
arrayMap(x -> x * 2, [1, 2, 3]) AS doubled,
arrayFilter(x -> x > 1, [1, 2, 3]) AS kept,
arrayCount(x -> x > 1, [1, 2, 3]) AS n_big,
arraySum(x -> x * x, [1, 2, 3]) AS sum_squares,
arrayMap((x, y) -> concat(x, y), ['a','b'], ['1','2']) AS zippedgo deeper
Recall the arrow syntax x -> expression and what arrayMap and arrayFilter each return. Be ready to say that arrays are 1-indexed and that these functions work inside a single row.
Explain the parallel multi-array form, why mismatched lengths throw, and how a lambda closes over other columns. Distinguish array functions from aggregates and from ARRAY JOIN.
Show cost awareness: single-pass arrayCount and arrayExists over filter-then-measure, and when an array column is the wrong model compared with real columns that prune and compress.
Own the schema question behind it. Decide when nested arrays are the right representation for variable-arity data versus real columns or a child table, and what that choice costs the team in query readability and compression.
## Arrays are a first-class column type In ClickHouse, `Array(T)` is a normal column type you declare in a `MergeTree` table, not an escape hatch. Physically it is stored as a flat column of element values plus an offsets column marking where each row's array ends, so arrays read and compress well. Arrays are **1-indexed**: `arr[1]` is the first element and `length(arr)` is the size. Because an array is a per-row value, the idiomatic way to work with it is the family of **higher-order array functions**, which take a lambda and apply it element-wise inside each row. ## The lambda A lambda is written with an arrow: `x -> x * 2`, or with several parameters, `(x, y) -> x + y`. Three rules matter: 1. A lambda is only valid **in the argument position of a higher-order function**. There is no lambda value type, so you cannot alias one, store it in a column, or pass it around. 2. A lambda closes over the surrounding row: it can reference other columns and constants, e.g. `arrayFilter(x -> x >= min_value, prices)` where `min_value` is a column. 3. With several array arguments, the lambda takes one parameter per array and the arrays are traversed **in parallel**, so they must all have the same length in that row; otherwise ClickHouse throws. ## The common functions ```sql SELECT arrayMap(x -> x * 2, [1, 2, 3]) AS doubled, -- [2,4,6] arrayFilter(x -> x > 1, [1, 2, 3]) AS kept, -- [2,3] arrayCount(x -> x > 1, [1, 2, 3]) AS n_big, -- 2 arraySum(x -> x * x, [1, 2, 3]) AS sum_squares, -- 14 arrayExists(x -> x = 2, [1, 2, 3]) AS any_two, -- 1 arrayAll(x -> x > 0, [1, 2, 3]) AS all_positive -- 1 ``` Without a lambda, several of these degrade to the obvious aggregate over the array: `arraySum(arr)`, `arrayMin(arr)`, `arrayMax(arr)`. `arraySort(x -> -x, arr)` sorts by the lambda's result rather than by the element itself, and `arraySort((x, y) -> y, arr, keys)` sorts `arr` by a parallel key array. For membership you usually want `has(arr, value)` or `indexOf(arr, value)` rather than a lambda. ## The mental model that matters There are three different "loops" in ClickHouse SQL and candidates confuse them: - **Higher-order array functions** loop over elements *inside one row* and return a value for that row. Row count is unchanged. - **Aggregate functions** loop over *rows* and collapse them into one value per group. - **`ARRAY JOIN`** (and the `arrayJoin` function) *expands* one row into many, one per element. If your report needs one output row per tag, that is `ARRAY JOIN`. If it needs one output row per event with a computed property of its tags, that is a higher-order function. Choosing the array-function path keeps the query on one row per event, which is usually far cheaper than expanding and re-grouping. ## Performance and pitfalls Higher-order functions execute vectorized over the flat element storage, so they are fast, but each one that returns an array (`arrayMap`, `arrayFilter`) allocates a new array. Prefer a single pass: `arrayCount(x -> x > 10, arr)` rather than `length(arrayFilter(x -> x > 10, arr))`, and `arrayExists(...)` rather than `arrayCount(...) > 0`. Other things that bite: - Empty arrays are legal and common. `arraySum([])` is 0, `arrayFirst` returns the element type's default, and index access past the end returns a default rather than raising. - Mismatched lengths in a multi-array lambda raise an exception at runtime, not at parse time, so it can appear only for a subset of rows. - `Array(Nullable(T))` propagates NULLs through the lambda; if you want NULLs skipped you must filter them explicitly. - Arrays are not a substitute for a real column when the set of attributes is fixed and known — a real column prunes and compresses better. ## What interviewers are checking They want to see that you write ClickHouse rather than portable SQL: that your first instinct for "how many of this row's items cost more than 100" is `arrayCount(x -> x > 100, prices)` and not an unnest-and-regroup. Being able to say plainly that lambdas run per row, are only legal as function arguments, and can close over other columns is the whole answer at this level.
- Can a ClickHouse lambda reference a column of the row it is running on?Yes. A lambda closes over the surrounding row, so `arrayFilter(x -> x > min_price, prices)` compares each element against another column's value in the same row. What you cannot do is treat the lambda itself as a value: there is no lambda type, so it must appear directly in a higher-order function's argument list, never in an alias, a column, or a variable.
- How do you sort a ClickHouse array by a computed key rather than by the element itself?Pass a lambda to `arraySort`: `arraySort(x -> -x, arr)` sorts descending by the negated value, and `arraySort((x, y) -> y, arr, keys)` sorts `arr` by a parallel array of keys, traversing both in lockstep. `arrayReverseSort` takes the same forms. The parallel arrays must be the same length in every row or the query throws.
- Why prefer arrayCount over length(arrayFilter(...)) in ClickHouse?`arrayFilter` materializes a new array per row before you measure it; `arrayCount(x -> cond, arr)` counts in a single pass with no allocation. Same result, less work and less memory. The equivalent one-pass forms are `arrayExists` instead of `arrayCount(...) > 0` and `arraySum(x -> expr, arr)` instead of summing a mapped array.
saying these in an interview costs you the question
- Thinks arrayMap expands the array into one row per element
- Expects to store a lambda in a column or alias
- Assumes ClickHouse arrays are zero-indexed
- Uses length(arrayFilter(...)) where arrayCount does one pass
- Believes higher-order functions aggregate across rows