skip to content

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

level: juniorimportance: should knowfreq 72%

answer

  1. arrays are per-row values, not rows
  2. the arrow is the lambda
  3. one output value per row, not per element
  4. arrayMap transforms, arrayFilter selects
  5. x -> expression, only inside a higher-order function

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.

solid answer

~40 s

ClickHouse 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 lines
sql
SELECT
    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 zipped

go deeper

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context