Conditional and NULL functions
Choose a value with CASE, replace NULL with coalesce, and turn a value into NULL with nullif.
- Category: Functions — conditional and NULL
- Keywords: if, if then else, else, switch, decode, iif, ifnull, nvl, isnull, zeroifnull, default value, empty text, missing value, three-valued logic, unknown, NULL
- See also: WHERE, Operators, Conversion functions, Aggregate functions (sum), GROUP BY and HAVING
These tools choose a value for each row.
CASEis SQL's "if ... then ... else". It gives the value of the first condition that is true.coalescegives the first value that is notNULL. Use it to show0or'-'instead of an empty cell.nullifdoes the opposite: it turns one value (like0or'') intoNULL.
NULL means "no value" or "not known". The last entry, NULL rules, lists how every part of KDB treats it.
Example: Label each CITY day as up, down or flat (sample data: sample)
SELECT time, open, close,
CASE WHEN close > open THEN 'up'
WHEN close < open THEN 'down'
ELSE 'flat' END AS direction
FROM ochl_day
WHERE symbol = 'CITY'
LIMIT 5;
Result:
| time (TIMESTAMP) | open (DOUBLE) | close (DOUBLE) | direction (STRING) |
|---|---|---|---|
| 2025-12-28T00:00:00.000Z | 540.3 | 542.2 | up |
| 2025-12-29T00:00:00.000Z | 540.6 | 539.7 | down |
| 2025-12-30T00:00:00.000Z | 538.5 | 539.3 | up |
| 2025-12-31T00:00:00.000Z | 540.4 | 542.4 | up |
| 2026-08-23T00:00:00.000Z | 505 | 507.9 | up |
(5 rows)
The examples below also use two small tables: ledger (an accounting book with money in DECIMAL(2)) and app_users (people who signed up in an app).
Setup used by the examples below:
CREATE TABLE ledger (
id INT PRIMARY KEY,
day DATE,
account STRING,
debit DECIMAL(2),
credit DECIMAL(2),
note STRING
);
INSERT INTO ledger VALUES
(1, '2026-08-05', 'Cash', 5000.00, NULL, 'Opening balance'),
(2, '2026-08-20', 'Rent', 1200.50, NULL, NULL),
(3, '2026-09-01', 'Cash', NULL, 750.25, 'Shop sale'),
(4, '2026-09-03', 'Bank', 300.00, NULL, '');
CREATE TABLE app_users (
id INT PRIMARY KEY,
name STRING NOT NULL,
phone STRING,
email STRING,
plan STRING,
signed_up TIMESTAMP
);
INSERT INTO app_users VALUES
(1, 'Sita', '9841000001', 'sita@example.com', 'pro', '2026-09-08T10:00:00+05:45'),
(2, 'Ram', NULL, 'ram@example.com', 'free', '2026-09-08T18:30:00+05:45'),
(3, 'Hari', '9851000003', NULL, NULL, '2026-09-09T09:15:00+05:45'),
(4, 'Gita', NULL, NULL, 'trial', '2026-09-10T12:00:00+05:45');
CASE WHEN
Syntax:
CASE WHEN condition THEN result
[WHEN condition THEN result ...]
[ELSE other_result]
END
KDB checks the conditions from top to bottom. The result of the first true condition is the answer. When no condition is true, the answer is the ELSE value, or NULL when there is no ELSE.
| Part | Type | Meaning |
|---|---|---|
condition | true/false | a test like close > open or plan IS NULL |
result | any | the value when this condition is true |
other_result | any | the value when no condition is true (optional) |
- Returns: the value of the chosen branch.
- NULL: a condition that is
NULLcounts as not true, so KDB goes on to the nextWHEN. WithoutELSE, no match givesNULL. - Notes:
- A condition must be true, false or
NULL.CASE WHEN 1 THEN ...is an error (writeWHEN x = 1). - Only the chosen branch is worked out. The other results are not computed.
- You can use
CASEeverywhere a value can go:SELECT,WHERE,ORDER BY,GROUP BY, insidesum(...), and inside anotherCASE. - KDB does not make the branches one type.
CASE WHEN ... THEN 1 ELSE 'low' ENDgives a number in some rows and text in others. Give every branch the same type. GROUP BYcannot use the name afterAS. Repeat the wholeCASEinGROUP BY(see the example).
- A condition must be true, false or
Example: Live price moves in five groups (sample data: sample)
SELECT symbol, ltp, pct_change,
CASE WHEN pct_change >= 2 THEN 'strong up'
WHEN pct_change > 0 THEN 'up'
WHEN pct_change = 0 THEN 'flat'
WHEN pct_change > -2 THEN 'down'
ELSE 'strong down' END AS move
FROM live_data
LATEST ON timestamp PARTITION BY symbol;
Result:
| symbol (SYMBOL) | ltp (DOUBLE) | pct_change (DOUBLE) | move (STRING) |
|---|---|---|---|
| CITY | 527.1 | 2.71 | strong up |
| COAST | 401.9 | 0.7 | up |
| TELCO | 897.7 | 1.9 | up |
| VOLT | 184 | -0.86 | down |
(4 rows)
Example: Count up days and down days per symbol (CASE inside sum) (sample data: sample)
SELECT symbol,
sum(CASE WHEN close > open THEN 1 ELSE 0 END) AS up_days,
sum(CASE WHEN close < open THEN 1 ELSE 0 END) AS down_days,
count(*) AS days
FROM ochl_day
GROUP BY symbol
ORDER BY symbol;
Result:
| symbol (SYMBOL) | up_days (LONG) | down_days (LONG) | days (LONG) |
|---|---|---|---|
| CITY | 12 | 7 | 19 |
| COAST | 8 | 11 | 19 |
| VOLT | 10 | 9 | 19 |
(3 rows)
Example: Group rows by a CASE: repeat it in GROUP BY
SELECT CASE WHEN phone IS NULL THEN 'no phone' ELSE 'has phone' END AS phone_status,
count(*) AS users
FROM app_users
GROUP BY CASE WHEN phone IS NULL THEN 'no phone' ELSE 'has phone' END
ORDER BY phone_status;
Result:
| phone_status (STRING) | users (LONG) |
|---|---|
| has phone | 2 |
| no phone | 2 |
(2 rows)
Example: Or group by the AS name, which is shorter
SELECT CASE WHEN phone IS NULL THEN 'no phone' ELSE 'has phone' END AS phone_status,
count(*) AS users
FROM app_users
GROUP BY phone_status
ORDER BY phone_status;
Result:
| phone_status (STRING) | users (LONG) |
|---|---|
| has phone | 2 |
| no phone | 2 |
(2 rows)
Example: The condition must be true or false (shows an error)
SELECT CASE WHEN 1 THEN 'yes' ELSE 'no' END;
Result:
Error: line 1, column 18: a condition must be true or false, got '1'
CASE value WHEN
Syntax:
CASE value
WHEN match THEN result
[WHEN match THEN result ...]
[ELSE other_result]
END
The short form. KDB compares value with each match, like value = match, and gives the result of the first one that is equal.
| Part | Type | Meaning |
|---|---|---|
value | any | the value to look at, worked out once |
match | any | a value to compare with |
result, other_result | any | as in CASE WHEN |
- Returns: the value of the chosen branch.
- NULL:
NULLnever matches, not evenWHEN NULL. UseCASE WHEN value IS NULL THEN ...for that. - Notes:
- Numbers match numeric text:
CASE 5 WHEN '5' THEN ...matches. - Values that cannot be compared (a number and
'abc') simply do not match. There is no error, unlike=.
- Numbers match numeric text:
Example
SELECT CASE 'COAST' WHEN 'CITY' THEN 1 WHEN 'COAST' THEN 2 ELSE 0 END AS a,
CASE 5 WHEN '5' THEN 'text five matches' END AS b,
CASE NULL WHEN NULL THEN 'matched' ELSE 'no match' END AS c;
Result:
| a (LONG) | b (STRING) | c (STRING) |
|---|---|---|
| 2 | text five matches | no match |
(1 row)
Example: Sort sectors in your own order (sample data: sample)
SELECT symbol, sector
FROM securities
ORDER BY CASE sector WHEN 'Banking' THEN 1
WHEN 'Energy' THEN 2
ELSE 3 END,
symbol;
Result:
| symbol (SYMBOL) | sector (STRING) |
|---|---|
| CITY | Banking |
| COAST | Banking |
| VOLT | Energy |
| ACME | Investment |
| TELCO | Telecom |
(5 rows)
Example: Plan names for an app report
SELECT name,
CASE plan WHEN 'pro' THEN 'Paid'
WHEN 'trial' THEN 'Trial (14 days)'
WHEN 'free' THEN 'Free'
ELSE 'Not chosen' END AS plan_name
FROM app_users
ORDER BY id;
Result:
| name (STRING) | plan_name (STRING) |
|---|---|
| Sita | Paid |
| Ram | Free |
| Hari | Not chosen |
| Gita | Trial (14 days) |
(4 rows)
Example: END is missing (shows an error)
SELECT CASE plan WHEN 'pro' THEN 'Paid' FROM app_users;
Result:
Error: line 1, column 41: expected an operator (AND, OR, =, +, ...), WHEN, ELSE or END to close the CASE that starts at line 1, column 8 but found 'FROM'
coalesce
Syntax:
coalesce(value, value, ...)
Gives the first value that is not NULL. coalesce(phone, 'no phone') shows the phone, or 'no phone' when there is none.
| Argument | Type | Meaning |
|---|---|---|
value | any | 1 to 64 values, checked from left to right |
- Returns: the first value that is not
NULL, with its own type. - NULL:
NULLonly when every value isNULL. - Notes:
- KDB stops at the first value that is not
NULL. The values after it are not worked out. - The values keep their types.
coalesce(debit, 0)gives aDECIMAL(2)in some rows and the whole number0in others. Thensumof it becomes aDOUBLE. For exact money writecoalesce(debit, 0::DECIMAL(2)). - Other databases call this
ifnull,nvlorisnull. KDB has onlycoalesce.
- KDB stops at the first value that is not
Example
SELECT coalesce(NULL, 'n/a') AS a, coalesce(NULL, NULL, 3) AS b, coalesce(7, 8) AS c, coalesce(NULL, NULL) AS d;
Result:
| a (STRING) | b (LONG) | c (LONG) | d (STRING) |
|---|---|---|---|
| n/a | 3 | 7 | NULL |
(1 row)
Example: Show something instead of an empty cell
SELECT name,
coalesce(phone, email, 'no contact') AS contact,
coalesce(plan, 'free') AS plan
FROM app_users
ORDER BY id;
Result:
| name (STRING) | contact (STRING) | plan (STRING) |
|---|---|---|
| Sita | 9841000001 | pro |
| Ram | ram@example.com | free |
| Hari | 9851000003 | free |
| Gita | no contact | trial |
(4 rows)
Example: Ledger totals per account: keep DECIMAL(2) exact
SELECT account,
sum(coalesce(debit, 0::DECIMAL(2))) AS total_debit,
sum(coalesce(credit, 0::DECIMAL(2))) AS total_credit,
sum(coalesce(debit, 0)) AS mixed_types
FROM ledger
GROUP BY account
ORDER BY account;
Result:
| account (STRING) | total_debit (DECIMAL(2)) | total_credit (DECIMAL(2)) | mixed_types (DECIMAL(2)) |
|---|---|---|---|
| Bank | 300.00 | 0.00 | 300.00 |
| Cash | 5000.00 | 750.25 | 5000 |
| Rent | 1200.50 | 0.00 | 1200.50 |
(3 rows)
In the Cash row, mixed_types is 5000 (a DOUBLE), not 5000.00: one row added the DECIMAL(2) value 5000.00 and the other row added the whole number 0.
Example: coalesce needs at least one value (shows an error)
SELECT coalesce();
Result:
Error: line 1, column 8: function 'coalesce' takes 1 to 64 argument(s), got 0
nullif
Syntax:
nullif(value, other)
Gives NULL when value equals other. Otherwise it gives value. It is a short way to write CASE WHEN value = other THEN NULL ELSE value END.
| Argument | Type | Meaning |
|---|---|---|
value | any | the value to give back |
other | any | when value equals this, the answer is NULL |
- Returns:
value(with its type) orNULL. - NULL:
nullif(NULL, x)isNULL.nullif(x, NULL)isx. - Notes:
- Equal means the same as
=:nullif(5, 5.0)andnullif(5, '5')areNULL. Text is compared exactly ('a'is not'A'). - Values that cannot be compared give back
value, without an error. - Division by zero is an error, as in PostgreSQL, so
x / nullif(y, 0)is the way to getNULLfor it.nullifis also useful for''or a "no data" marker like-1.
- Equal means the same as
Example
SELECT nullif(5, 5) AS a, nullif(10, 0) AS b, nullif(5, '5') AS c, nullif('a', 'A') AS d, nullif(NULL, 1) AS e;
Result:
| a (STRING) | b (LONG) | c (STRING) | d (STRING) | e (STRING) |
|---|---|---|---|---|
| NULL | 10 | NULL | a | NULL |
(1 row)
Example: Empty notes count as no note
SELECT id, note,
nullif(note, '') AS clean_note,
coalesce(nullif(note, ''), '(no note)') AS shown
FROM ledger
ORDER BY id;
Result:
| id (INT) | note (STRING) | clean_note (STRING) | shown (STRING) |
|---|---|---|---|
| 1 | Opening balance | Opening balance | Opening balance |
| 2 | NULL | NULL | (no note) |
| 3 | Shop sale | Shop sale | Shop sale |
| 4 | NULL | (no note) |
(4 rows)
Example: Average only over rows that really have a value (sample data: sample)
SELECT symbol,
round(avg(quantity), 1) AS avg_all,
round(avg(nullif(quantity, 1000)), 1) AS avg_without_1000_lots,
count(nullif(quantity, 1000)) AS trades_not_1000
FROM trade_log
GROUP BY symbol
ORDER BY symbol;
Result:
| symbol (SYMBOL) | avg_all (DOUBLE) | avg_without_1000_lots (DOUBLE) | trades_not_1000 (LONG) |
|---|---|---|---|
| CITY | 220 | 220 | 11 |
| COAST | 279.1 | 118.9 | 9 |
| TELCO | 461.7 | 192.5 | 4 |
| VOLT | 100 | 100 | 4 |
(4 rows)
NULL rules
Syntax:
value IS NULL value IS NOT NULL
coalesce(value, default) nullif(value, other)
NULL means "no value". Almost everything that touches NULL gives NULL, because the answer is not known. Use IS NULL to test for it: x = NULL is never true.
| Where | What NULL does | Example | Result |
|---|---|---|---|
+ - * / % | gives NULL | NULL + 1 | NULL |
= <> < > <= >= | gives NULL (not true, not false) | NULL = NULL | NULL |
IS NULL, IS NOT NULL | the only safe test | NULL IS NULL | true |
AND | false wins, else NULL | FALSE AND NULL, TRUE AND NULL | false, NULL |
OR | true wins, else NULL | TRUE OR NULL, FALSE OR NULL | true, NULL |
NOT | gives NULL | NOT NULL | NULL |
IN (...) | NULL when nothing matched and the list has a NULL | 3 IN (1, NULL) | NULL |
BETWEEN, LIKE | gives NULL | NULL LIKE 'a%' | NULL |
WHERE, HAVING, CASE WHEN | NULL counts as not true: the row is left out, the next WHEN is tried | ||
|| | gives NULL; concat skips NULL instead | 'a' || NULL | NULL |
| most functions | NULL in, NULL out | abs(NULL) | NULL |
coalesce, greatest, least, concat, concat_ws | skip NULL values | coalesce(NULL, 2) | 2 |
aggregates (sum, avg, min, count(x), ...) | ignore NULL rows; count(*) counts every row | ||
ORDER BY | NULL counts as the biggest value: last with ASC, first with DESC (change with NULLS FIRST / NULLS LAST) | ||
CAST(NULL AS type) | stays NULL | NULL::INT | NULL |
- Notes:
3 NOT IN (1, NULL)isNULL, nottrue. ANOT INlist with aNULLin it never keeps a row. See also aggregates.
Example
SELECT NULL + 1 AS plus, NULL = NULL AS equal, NULL IS NULL AS is_null,
FALSE AND NULL AS and_false, TRUE OR NULL AS or_true,
3 IN (1, NULL) AS in_list, 3 NOT IN (1, NULL) AS not_in_list, 'a' || NULL AS joined;
Result:
| plus (STRING) | equal (STRING) | is_null (BOOL) | and_false (BOOL) | or_true (BOOL) | in_list (STRING) | not_in_list (STRING) | joined (STRING) |
|---|---|---|---|---|---|---|---|
| NULL | NULL | true | false | true | NULL | NULL | NULL |
(1 row)
Example: = NULL finds nothing; IS NULL finds the rows
SELECT (SELECT count(*) FROM app_users WHERE phone = NULL) AS with_equals_null,
(SELECT count(*) FROM app_users WHERE phone IS NULL) AS with_is_null;
Result:
| with_equals_null (LONG) | with_is_null (LONG) |
|---|---|
| 0 | 2 |
(1 row)
Example: count(*) counts rows, count(x) skips NULL
SELECT count(*) AS users, count(phone) AS with_phone, count(email) AS with_email, count(plan) AS with_plan
FROM app_users;
Result:
| users (LONG) | with_phone (LONG) | with_email (LONG) | with_plan (LONG) |
|---|---|---|---|
| 4 | 2 | 2 | 3 |
(1 row)