Conditional Expressions
Three functions, and between them they cover what every other database spells with half a dozen
names. coalesce fills in a blank, nullif creates one, and case decides between values.
Why there is no if
Every database offers a ternary — if, iff, iif, nvl2, decode — and no two agree on the
name, the argument order, or how many arguments there are. case says the same thing and says it
once:
case(o.freight > 100, 'high', 'low')
The conditions are tested in order and the first one that holds wins. A trailing argument without a condition is the fallback; leave it off and a row that matches nothing simply comes out blank. That is one name to learn instead of five, and it means the same thing on every database.
coalesce and nullif are opposites
coalesce(c.region, c.country) the first value that is not blank
nullif(p.reorder_level, 0) blank, when the value is that placeholder
coalesce is how you stop a blank from reaching a report. nullif is how you get one in —
because data is full of placeholders that mean "unknown" without being blank: a 0 that stands for
"no threshold set", an empty string that was never filled. Written together they clean a column in
one pass: coalesce(nullif(c.region, ''), c.country).
Only what is needed gets evaluated. coalesce reads left to right and stops at the first
non-blank argument; case stops at the first condition that holds. Nothing to the right of that is
evaluated, which is why coalesce(x, 1/0) is safe. This is a guarantee, not an optimisation that
might not happen.
One type for the whole column
A case produces one column, so all its results must be able to meet in a single type — decided
once for the expression, not per row. Branches that return text in one arm and a number in another
are refused outright, rather than yielding a column whose type depends on the data.
The same rule reaches nullif, for a less obvious reason: its two arguments are compared, so they
too must meet in one type — and the result then carries that common type rather than the first
argument's own. nullif(quantity, 0.5) comes out decimal, not integer.
coalesce
coalesce(value: any, ...) → argument-dependent
The first argument that is not blank; blank only if every argument is.
The arguments are read left to right and evaluation stops at the first non-blank one — what stands to its right is never evaluated. That is a guarantee, not an optimisation: it is what makes coalesce(x, 1/0) safe.
Operands: any type.
Sample query:
// coalesce: region, falling back to country.
FIND customers c
FETCH coalesce(c.region, c.country) region_or_country
Generated SQL
all dialects
-- coalesce: region, falling back to country.
SELECT
coalesce(c.region, c.country) AS region_or_country
FROM
customers c
nullif
nullif(value: any, when: any) → argument-dependent
value, or blank when it equals when — the inverse of coalesce, and the way to turn a placeholder such as 0 or an empty string into a real blank.
The two are compared, so they must be able to meet in one type, and the result takes that common type rather than value's own. nullif(quantity, 0.5) therefore comes out decimal, not integer — the comparison promotes value before it is returned. Comparing across type groups, say text with a number, is an error.
Operands: any type.
Sample query:
// nullif: treat a reorder level of 0 as "not set" rather than as a real threshold.
FIND products p
FETCH p.product_id ASC, nullif(p.reorder_level, 0) reorder_level_or_blank
Generated SQL
all dialects
-- nullif: treat a reorder level of 0 as "not set" rather than as a real threshold.
SELECT
p.product_id
, nullif(p.reorder_level, 0) AS reorder_level_or_blank
FROM
products p
ORDER BY
p.product_id ASC
case
case(condition: BOOLEAN, result: any, ...) → argument-dependent
Tests each condition in order and returns the result paired with the first one that holds. A trailing argument without a condition is the fallback; without it, a row that matches nothing comes out blank.
All the results must be able to meet in one type — that is what the column will hold, and it is decided once for the whole expression, not per row. Mixing text and numbers across the branches is therefore an error, not a per-row surprise.
Like coalesce, only the branches that are needed get evaluated: testing stops at the first condition that holds.
| Argument | Type | Description |
|---|---|---|
| condition | BOOLEAN | a WHEN test evaluated in order until one is true |
| result | any | the value returned when its paired condition is true |
Sample query:
// case: classify orders by freight.
FIND orders o
FETCH case(o.freight > 100, 'high', 'low') freight_tier
Generated SQL
all dialects
-- case: classify orders by freight.
SELECT
CASE WHEN o.freight > 100 THEN 'high' ELSE 'low' END AS freight_tier
FROM
orders o