In ClickHouse, what do the ANY, ALL and ASOF JOIN strictness modes mean?
answer
- a keyword before JOIN, not after
- one of them caps fan-out at one row
- one of them is built for time-series lookups
- the inequality has a required position
- unmatched rows are not NULL by default
basics
~20 sALL 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.
solid answer
~50 sClickHouse puts a **strictness** keyword before `JOIN`. `ALL JOIN` is standard SQL semantics and the default (governed by the `join_default_strictness` setting): each left row is repeated once per matching right row. `ANY JOIN` returns **at most one** matching right row per left row, and which one is unspecified — it is the cheap dictionary-style lookup that guarantees no fan-out. `ASOF JOIN` matches on equality of some columns plus **one inequality that must be the last condition in `ON`**, e.g. `ON t.symbol = q.symbol AND t.ts >= q.ts`, returning the nearest row satisfying it — the classic "price as of the moment of the trade" query. `SEMI` and `ANTI` also exist as strictness modes. Separately, ClickHouse's `join_use_nulls` defaults to 0, so unmatched columns in a `LEFT JOIN` come back as **type defaults** — `0`, `''` — rather than `NULL`, which is the dialect's biggest join surprise.
code
sql · 5 lines-- most recent quote at or before each trade
SELECT t.ts, t.symbol, t.qty, q.price
FROM trades AS t
ASOF LEFT JOIN quotes AS q
ON t.symbol = q.symbol AND t.ts >= q.tsgo deeper
Recall that ClickHouse writes a strictness keyword before JOIN and that ALL is the default meaning ordinary SQL matching.
Explain how ANY caps fan-out, what ASOF matches and where its inequality must sit, and the join_use_nulls default that returns 0 instead of NULL.
Show production judgment: which table belongs on the right, when ASOF replaces an expensive correlated subquery, and how the NULL default silently corrupts a migrated report.
Own the cluster-wide convention — whether join_use_nulls is on, whether strictness is always written explicitly, and how those defaults are enforced so that queries mean the same thing everywhere.
## Strictness is a keyword, not a hint In ANSI SQL a join's type (`INNER`, `LEFT`, …) is all you specify, and matching is always all-to-all. ClickHouse adds an orthogonal **strictness** keyword that controls how many right-hand matches a left row may consume: ```sql SELECT ... FROM left ANY LEFT JOIN right ON ... SELECT ... FROM left ALL INNER JOIN right ON ... SELECT ... FROM left ASOF LEFT JOIN right ON ... ``` Omitting it uses the `join_default_strictness` setting, whose default is `ALL` — ANSI behaviour. Older deployments occasionally set it to `ANY`, so an unqualified join can mean different things on different clusters; writing the strictness explicitly is a good habit. ## ALL `ALL` is ordinary SQL: a left row matching three right rows produces three result rows. Fan-out is possible and, when the right side is not unique on the join key, this is where duplicated sums come from. Nothing surprising here — it is the baseline the other modes deviate from. ## ANY `ANY` stops at the first match: one left row yields at most one result row. Which right row you get is **unspecified** — do not build logic on it. It is meant for lookups where the right side is effectively unique (a dimension keyed by id, a config table) and you want a guarantee that the join cannot multiply rows. It is cheaper than `ALL` because the join can stop probing after the first hit. The risk is exactly the same as its benefit: if the right side is *not* unique, `ANY` silently picks a row and your result is non-deterministic between runs. Use it when uniqueness is a property of the data you can state, not as a shortcut for deduplicating a messy right side. ## ASOF `ASOF JOIN` is the mode with no ANSI equivalent and the one interviews love. It matches on zero or more equality conditions **plus exactly one inequality, which must come last in the `ON` clause**: ```sql SELECT t.ts, t.symbol, t.qty, q.price FROM trades AS t ASOF LEFT JOIN quotes AS q ON t.symbol = q.symbol AND t.ts >= q.ts ``` For each trade this returns the single most recent quote for that symbol at or before the trade's timestamp. The inequality may be `>=`, `>`, `<=` or `<`; the direction decides whether you get the closest preceding or the closest following row. `ASOF LEFT JOIN` keeps trades with no qualifying quote; plain `ASOF JOIN` drops them. Writing the same thing portably means a correlated subquery or a window function over a union of both tables, both of which are dramatically more expensive. This is the query shape that makes ClickHouse attractive for time-series joins. ## SEMI and ANTI `SEMI LEFT JOIN` returns left rows that have at least one match, once each, without right-hand columns fanning out; `ANTI LEFT JOIN` returns left rows with no match. They are the explicit forms of `EXISTS` / `NOT EXISTS` semantics as a join strictness. ## The NULL surprise Separate from strictness, and the thing that actually breaks people's queries: `join_use_nulls` defaults to **0**. With that default, a `LEFT JOIN` whose right side does not match fills the right-hand columns with the **type's default value** — `0` for numbers, `''` for strings, `[]` for arrays — not `NULL`. ```sql -- with defaults, unmatched right-hand amount is 0, not NULL SELECT u.id, o.amount FROM users AS u ALL LEFT JOIN orders AS o ON o.user_id = u.id; ``` So `WHERE o.amount IS NULL` finds nothing, and `count(o.amount)` counts unmatched rows too. Setting `join_use_nulls = 1` restores ANSI behaviour by making the right-hand columns `Nullable`, at the cost of the `Nullable` wrapper's extra byte per value and some function-compatibility friction. Either choice is defensible; not knowing which one is in force is not. ## Practical notes ClickHouse builds its hash table from the **right** table, so the right side should be the smaller one — there is no automatic reordering to rescue you from putting a billion-row table on the right. In distributed queries, `GLOBAL JOIN` sends the right side to every shard once instead of each shard joining against its local slice. ## What interviewers probe They want the three modes distinguished cleanly, the `ASOF` inequality-must-be-last rule, and ideally the `join_use_nulls` default — because it is the reason a migrated ANSI query returns wrong numbers rather than an error.
- With ClickHouse defaults, what does a LEFT JOIN put in unmatched right-hand columns?The column type's default value — `0` for numeric types, `''` for `String`, `[]` for arrays — because `join_use_nulls` defaults to 0. `IS NULL` therefore finds nothing and aggregates silently include the unmatched rows. Setting `join_use_nulls = 1` makes the right-hand columns `Nullable` and restores ANSI semantics, at the cost of the Nullable representation.
- Which side of a ClickHouse join should hold the smaller table, and why?The right side. ClickHouse builds its hash table from the right table and streams the left one through it, and it does not reorder joins for you the way a cost-based optimizer would. Putting the large table on the right means materializing it in memory, which is the usual cause of a join blowing the memory limit.
- When is ANY JOIN dangerous?When the right side is not actually unique on the join key. `ANY` picks one matching row and which one is unspecified, so the result can differ between runs and between versions. It is safe as a stated-uniqueness lookup against a dimension table and unsafe as an ad-hoc way to suppress duplicates coming from a messy right side.
saying these in an interview costs you the question
- Assumes an unqualified JOIN always means ANSI ALL semantics
- Expects NULL for unmatched LEFT JOIN columns by default
- Puts the ASOF inequality first in the ON clause
- Uses ANY JOIN to deduplicate a non-unique right side
- Believes ClickHouse reorders joins to put the small table on the right