Operators
Every operator of KDB SQL, which one is done first, and what NULL does to each of them.
- Category: Querying
- Keywords: operator, precedence, order of operations, parentheses, +, -, *, /, %, ||, concat, join text, ->, ->>, ::, cast, , !, <>, NULL rules, three-valued logic, unary minus, precedence, ||, ::, ->, ->>, int_division, integer division, integer division, int_division, 7 / 2
- See also: WHERE, Math functions (operators), Conversion functions, JSON functions, Conditional and NULL functions, Text functions
An operator is a sign or a word between values, like +, =, AND or ||.
When an expression has many operators, KDB does the tight ones first: 2 + 3 * 4 is 14, because * binds tighter than +.
Use ( ) to choose the order yourself, and to make long conditions easy to read.
Example: A trade log report line: value, commission and a flag (sample data: sample)
SELECT transaction_id, symbol,
quantity * rate AS value,
round(quantity * rate * 0.4 / 100, 2) AS commission,
quantity >= 500 AND rate > 500 AS big_bank_trade
FROM trade_log
WHERE date IN '2026-09-10' AND symbol IN ('CITY', 'COAST')
ORDER BY value DESC
LIMIT 4;
Result:
| transaction_id (LONG) | symbol (SYMBOL) | value (DOUBLE) | commission (DOUBLE) | big_bank_trade (BOOL) |
|---|---|---|---|---|
| 100031 | COAST | 402000 | 1608 | false |
| 100015 | COAST | 398000 | 1592 | false |
| 100018 | CITY | 258000 | 1032 | true |
| 100023 | COAST | 199500 | 798 | false |
(4 rows)
Operator precedence
Syntax:
OR
AND
NOT
= == != <> < <= > >= IS [NOT] NULL [NOT] IN [NOT] BETWEEN [NOT] LIKE [NOT] ILIKE
+ - ||
* / %
-value
:: -> ->>
From loose (done last) to tight (done first):
| Level | Operators | Example | Means |
|---|---|---|---|
| 1 | OR | a OR b AND c | a OR (b AND c) |
| 2 | AND | a AND b OR c | (a AND b) OR c |
| 3 | NOT | NOT a = b | NOT (a = b) |
| 4 | = == != <> < <= > >=, IS [NOT] NULL, [NOT] IN, [NOT] BETWEEN, [NOT] LIKE, [NOT] ILIKE | a + 1 = b * 2 | (a + 1) = (b * 2) |
| 5 | + - || | 'x' || 1 + 2 | ('x' || 1) + 2 (an error: write 'x' || (1 + 2)) |
| 6 | * / % | 2 + 3 * 4 | 2 + (3 * 4) |
| 7 | - before a value | -2 * 3 | (-2) * 3 |
| 8 | :: -> ->>, then values, columns, function calls, CASE, CAST, ( ) | '5'::INT * 2 | ('5'::INT) * 2 |
- Notes: operators of the same level are done from left to right:
10 - 4 - 3is3, anda = b IS NULLmeans(a = b) IS NULL. a = b = cis an error whencis not true or false: the first=givestrueorfalse, which is then compared withc.ANDandORstop early:false AND ...andtrue OR ...do not compute the right side.- Nesting is limited to 100 levels of
( )and operators.
Example (sample data: sample)
SELECT 2 + 3 * 4 AS a, (2 + 3) * 4 AS b, 10 - 4 - 3 AS c, -2 * 3 AS d,
1 = 1 OR 1 = 2 AND 1 = 3 AS e, NOT 1 = 2 AND 2 = 2 AS f, 2 + 3 BETWEEN 4 AND 6 AS g;
Result:
| a (LONG) | b (LONG) | c (LONG) | d (LONG) | e (BOOL) | f (BOOL) | g (BOOL) |
|---|---|---|---|---|---|---|
| 14 | 20 | 3 | -6 | true | true | true |
(1 row)
Example: Mixing AND and OR: ( ) change the answer (sample data: sample)
SELECT count(*) AS without_parens FROM trade_log WHERE symbol = 'TELCO' OR symbol = 'COAST' AND quantity >= 1000;
SELECT count(*) AS with_parens FROM trade_log WHERE (symbol = 'TELCO' OR symbol = 'COAST') AND quantity >= 1000;
Result:
SELECT count(*) AS without_parens FROM trade_log WHERE symbol = 'TELCO' OR symbol = 'COAST' AND quan
| without_parens (LONG) |
|---|
| 8 |
(1 row)
SELECT count(*) AS with_parens FROM trade_log WHERE (symbol = 'TELCO' OR symbol = 'COAST') AND quant
| with_parens (LONG) |
|---|
| 4 |
(1 row)
Example: || and + are on the same level (sample data: sample; shows an error)
SELECT 'Total: ' || 2 + 3 AS label;
Result:
Error: line 1, column 23: 'Total: 2' and '3' are not both numbers
Example: The fix: ( ) around the sum (sample data: sample)
SELECT 'Total: ' || (2 + 3) AS label;
Result:
| label (STRING) |
|---|
| Total: 5 |
(1 row)
Arithmetic: + - * / %
Syntax:
a + b a - b a * b a / b a % b -a
Add, subtract, multiply, divide and remainder. The full rules are in arithmetic operators.
| Operator | Result |
|---|---|
+ - * | LONG for whole numbers (an INT result past INT becomes a LONG), else DOUBLE. A result too big for LONG is an error: "bigint out of range" (SQLSTATE 22003) |
/ | DOUBLE: 7 / 2 is 3.5. After SET int_division = integer, whole number / whole number is a whole number, as in PostgreSQL: 7 / 2 is 3 (integer division) |
% | the remainder: 7 % 2 is 1 |
TIMESTAMP + number | the time plus that many milliseconds |
TIMESTAMP - TIMESTAMP | the milliseconds between the two times |
DATE + number | the date plus that many days |
- NULL:
NULLon either side givesNULL. - Notes: dividing by zero (
5 / 0,5.0 / 0,5 % 0) stops the query with "division by zero" (SQLSTATE22012), as in PostgreSQL. Writex / NULLIF(y, 0)to getNULLinstead. - Text is not a number:
'abc' + 1is an error.+does not join text; use||. TRUE + 1is an error:BOOLis not a number.
Example: Daily range and change in percent (sample data: sample)
SELECT symbol, round(high - low, 2) AS day_range, round((close - open) / open * 100, 2) AS change_pct
FROM ochl_day
WHERE time IN '2026-09-10';
Result:
| symbol (SYMBOL) | day_range (DOUBLE) | change_pct (DOUBLE) |
|---|---|---|
| CITY | 6.3 | 0.06 |
| COAST | 8.8 | -1.05 |
| VOLT | 7.9 | 3.02 |
(3 rows)
Example: Nepal clock hour: add 5 h 45 min in milliseconds (sample data: sample)
SELECT hour(time + 20700000) AS npt_hour, sum(volume) AS volume
FROM ochl_minutes
WHERE symbol = 'CITY' AND time IN '2026-09-10'
GROUP BY hour(time + 20700000)
ORDER BY npt_hour;
Result:
| npt_hour (LONG) | volume (DOUBLE) |
|---|---|
| 11 | 47020 |
| 12 | 41230 |
| 13 | 46180 |
| 14 | 48610 |
(4 rows)
Example (sample data: sample)
SELECT 7 / 2 AS a, 7 % 2 AS b, 5 / NULLIF(0, 0) AS c, 9223372036854775807 * 2.0 AS big_as_double, CAST('2026-09-10' AS DATE) + 1 AS next_day;
Result:
| a (DOUBLE) | b (LONG) | c (STRING) | big_as_double (DOUBLE) | next_day (DATE) |
|---|---|---|---|---|
| 3.5 | 1 | NULL | 1.84467440737096e+19 | 2026-09-11 |
(1 row)
Example: Dividing by zero is an error (sample data: sample; shows an error)
SELECT 5 / 0;
Result:
Error: line 1, column 10: division by zero
Example: + does not join text (sample data: sample; shows an error)
SELECT 'CITY' + 1;
Result:
Error: line 1, column 15: 'CITY' and '1' are not both numbers
Integer division: SET int_division
Syntax:
SET int_division = integer | float | default
In KDB, / gives a DOUBLE: 7 / 2 is 3.5. PostgreSQL gives 3: a whole number divided by a whole number stays a whole number there. An app written for PostgreSQL may count on that, for example to read sum(ms) / 1000 into an integer.
| Setting | 7 / 2 | -7 / 2 | 7 / 2.0 |
|---|---|---|---|
float (the default) | 3.5 | -3.5 | 3.5 |
integer | 3 | -3 | 3.5 |
- Returns: with
integer,LONG/LONGis aLONG, cut toward zero. When either side is aDOUBLE, the result is still aDOUBLE; aDECIMALdivided by aDECIMALor a whole number is an exactDECIMALin both settings (DECIMAL). - Scope: the setting is for the session: the rest of one
/execrequest, a shell, or one Postgres connection. Triggers and procedures that its statements run use it too.defaultgoes back to what the session started with. - Postgres port:
kdb serve --pg-int-division on(orKDB_PG_INT_DIVISION=on) makesintegerthe start value of every Postgres connection. A connection can also ask for it in its connection string (the startup parameterint_division=integer), or sendSET int_division = integer.SHOW int_divisiongives the value. - Table expressions: a table's own
CHECK,DEFAULT, computed columns, index expressions and materialized view queries always use the defaultDOUBLEdivision, whatever the session that writes or reads. They are shared by every session, so the answer must not depend on who asked. A query inintegermode does not use an index on an expression with/; it reads the table instead. - Notes: division by zero is an error in both modes. To get a
DOUBLEinintegermode, cast one side:CAST(x AS DOUBLE) / y. To get a whole number infloatmode, usetrunc(x / y).
Example: The same division in both modes (sample data: sample)
SELECT 7 / 2 AS a, -7 / 2 AS b, 7 / 2.0 AS c;
SET int_division = integer;
SELECT 7 / 2 AS a, -7 / 2 AS b, 7 / 2.0 AS c, 7 / NULLIF(0, 0) AS d;
Result:
SELECT 7 / 2 AS a, -7 / 2 AS b, 7 / 2.0 AS c
| a (DOUBLE) | b (DOUBLE) | c (DOUBLE) |
|---|---|---|
| 3.5 | -3.5 | 3.5 |
(1 row)
SET int_division = 'integer'
whole number / whole number gives a whole number for this session (7 / 2 = 3)
SELECT 7 / 2 AS a, -7 / 2 AS b, 7 / 2.0 AS c, 7 / nullif(0, 0) AS d
| a (LONG) | b (LONG) | c (DOUBLE) | d (STRING) |
|---|---|---|---|
| 3 | -3 | 3.5 | NULL |
(1 row)
Example: An average in whole shares, as a PostgreSQL app expects (sample data: sample)
SET int_division = integer;
SELECT symbol, sum(quantity) / count(*) AS avg_whole_shares FROM trade_log WHERE date IN '2026-09-10' GROUP BY symbol ORDER BY symbol LIMIT 3;
Result:
SET int_division = 'integer'
whole number / whole number gives a whole number for this session (7 / 2 = 3)
SELECT symbol, sum(quantity) / count(*) AS avg_whole_shares FROM trade_log WHERE date IN '2026-09-10
| symbol (SYMBOL) | avg_whole_shares (LONG) |
|---|---|
| CITY | 130 |
| COAST | 316 |
| TELCO | 192 |
(3 rows)
Example: Only integer and float (sample data: sample; shows an error)
SET int_division = half;
Result:
Error: int_division must be integer or float, not 'half'
Comparison operators
Syntax:
a = b a == b a != b a <> b a < b a <= b a > b a >= b
Compare two values and give true or false. They are mostly used in WHERE, but a comparison is a normal value: you can put it in the select list.
- Returns:
BOOL. - NULL:
NULLon either side givesNULL. - Notes: a text compared with a
TIMESTAMPorDATEis read as a time. A number compared with a text that holds a number is compared as numbers. Other mixes (text with a number, a BOOL with a number) are an error. IS NULL,IN,BETWEEN,LIKEandILIKEare on the same level; see WHERE.
Example: Up day or down day, as a true/false column (sample data: sample)
SELECT time, close, close > open AS up_day, close = high AS closed_at_high
FROM ochl_day
WHERE symbol = 'VOLT' AND time >= '2026-09-06';
Result:
| time (TIMESTAMP) | close (DOUBLE) | up_day (BOOL) | closed_at_high (BOOL) |
|---|---|---|---|
| 2026-09-06T00:00:00.000Z | 180.1 | true | false |
| 2026-09-07T00:00:00.000Z | 176.9 | false | false |
| 2026-09-08T00:00:00.000Z | 182.4 | true | false |
| 2026-09-09T00:00:00.000Z | 187.3 | true | false |
| 2026-09-10T00:00:00.000Z | 191.2 | true | false |
(5 rows)
Example: A BOOL cannot be compared with a number (sample data: sample; shows an error)
SELECT 1 = 1 = 1;
Result:
Error: line 1, column 14: cannot compare BOOL 'true' with LONG '1'
|| : join text
Syntax:
value || value
Joins two values into one text. Numbers, times and TRUE/FALSE are turned into text first.
- Returns:
STRING. TwoBYTESvalues giveBYTES. - NULL:
NULLon either side givesNULL. To skip NULL values, useconcat(...)orcoalesce(x, '')(see text functions). - Notes:
||is on the same level as+and-. Put( )around calculations:'Total: ' || (a + b).
Example: A label for a chart legend (sample data: sample)
SELECT symbol || ' - ' || name AS label FROM securities WHERE sector = 'Banking';
Result:
| label (STRING) |
|---|
| CITY - City Bank Limited |
| COAST - Coastal Bank Limited |
(2 rows)
Example: Full name with city; NULL makes the whole text NULL (sample data: query-app)
SELECT username, full_name || ' (' || city || ')' AS shown,
full_name || ' (' || coalesce(city, 'no city') || ')' AS shown_safe
FROM app_users
WHERE id IN (1, 6, 8);
Result:
| username (STRING) | shown (STRING) | shown_safe (STRING) |
|---|---|---|
| ram | Ram Sharma (Kathmandu) | Ram Sharma (Kathmandu) |
| anita | NULL | Anita Magar (no city) |
| test_user | NULL | NULL |
(3 rows)
Example (sample data: sample)
SELECT 'CITY' || '-' || 1 AS a, 5.5 || '%' AS b, 'x' || NULL AS c;
Result:
| a (STRING) | b (STRING) | c (STRING) |
|---|---|---|
| CITY-1 | 5.5% | NULL |
(1 row)
-> and ->> : parts of JSON
Syntax:
json -> 'key' json -> index
json ->> 'key' json ->> index
-> takes a part of a JSON value and gives it as JSON. ->> gives the part as text. An index counts from 0; -1 is the last element. The JSON functions are explained in JSON functions.
- Returns:
->givesJSON,->>givesSTRING. - NULL:
NULLwhen the key or index is not there, or when the value isNULL. - Notes: you can chain them:
doc -> 'a' -> 'b' ->> 0. They bind as tight as::, from left to right, sodoc ->> 'n'::INTmeans(doc ->> 'n')::INT.
Example (sample data: sample)
SELECT '{"symbol": "CITY", "prices": [512, 515.5, 509]}' ->> 'symbol' AS symbol,
'{"symbol": "CITY", "prices": [512, 515.5, 509]}' -> 'prices' AS prices,
'{"symbol": "CITY", "prices": [512, 515.5, 509]}' -> 'prices' ->> -1 AS last_price;
Result:
| symbol (STRING) | prices (JSON) | last_price (STRING) |
|---|---|---|
| CITY | [512,515.5,509] | 509 |
(1 row)
:: : change the type
Syntax:
value::TYPE
value::DECIMAL(scale)
x::TYPE is the short form of CAST(x AS TYPE). See conversion functions for every type and rule.
- Returns: the value in the new type.
- NULL:
NULLstaysNULL. - Notes:
::binds tighter than every other operator:'5'::INT * 2is10, and-5::STRINGis the text-5. - A value that cannot be changed is an error, for example
'abc'::INT, or512.456::DECIMAL(2)(too many decimal places: useround(x, 2)first).
Example (sample data: sample)
SELECT '42'::INT + 1 AS a, 512.45::DECIMAL(2) AS b, '2026-09-10T11:00:00+05:45'::TIMESTAMP AS c, 7::DOUBLE / 2 AS d;
Result:
| a (LONG) | b (DECIMAL(2)) | c (TIMESTAMP) | d (DOUBLE) |
|---|---|---|---|
| 43 | 512.45 | 2026-09-10T05:15:00.000Z | 3.5 |
(1 row)
Example: Volume as a whole number (sample data: sample)
SELECT time, symbol, volume::LONG AS volume FROM ochl_day WHERE time IN '2026-09-10';
Result:
| time (TIMESTAMP) | symbol (SYMBOL) | volume (LONG) |
|---|---|---|
| 2026-09-10T00:00:00.000Z | CITY | 183040 |
| 2026-09-10T00:00:00.000Z | COAST | 186200 |
| 2026-09-10T00:00:00.000Z | VOLT | 41470 |
(3 rows)
Example: Not a number (sample data: sample; shows an error)
SELECT 'abc'::INT;
Result:
Error: line 1, column 13: cannot convert 'abc' to INT
NULL rules
NULL means "no value" or "unknown". Most operators give NULL when a value is NULL, because the answer is unknown too.
| Operator | With NULL | Example | Result |
|---|---|---|---|
+ - * / % | NULL | NULL + 1 | NULL |
|| | NULL | 'a' || NULL | NULL |
= != < ... | NULL | NULL = NULL | NULL |
AND | false wins, else NULL | FALSE AND NULL | false |
OR | true wins, else NULL | TRUE OR NULL | true |
NOT | NULL | NOT NULL | NULL |
IN (list) | NULL when the value is NULL, or when it is not found and the list has a NULL | 2 IN (1, NULL) | NULL |
BETWEEN, LIKE, ILIKE | NULL | NULL LIKE '%' | NULL |
IS NULL, IS NOT NULL | never NULL | NULL IS NULL | true |
::, CAST | NULL | NULL::INT | NULL |
- Notes:
WHERE,HAVINGand joinONkeep a row only when the condition istrue;NULLcounts as false (see NULL in WHERE). - Aggregates like
sum,avg,minandcount(x)skipNULLvalues.count(*)counts every row. - Use
coalesce(x, 0)to use a default instead ofNULL(see conditional functions).
Example (sample data: sample)
SELECT NULL + 1 AS plus, NULL = NULL AS equals, FALSE AND NULL AS and_false, TRUE OR NULL AS or_true,
NOT NULL AS not_null, NULL IS NULL AS is_null, coalesce(NULL, 0) + 1 AS with_default;
Result:
| plus (STRING) | equals (STRING) | and_false (BOOL) | or_true (BOOL) | not_null (STRING) | is_null (BOOL) | with_default (LONG) |
|---|---|---|---|---|---|---|
| NULL | NULL | false | true | NULL | true | 1 |
(1 row)
Example: Balance of each account: NULL debits and credits need coalesce (sample data: query-app)
SELECT account,
sum(debit) - sum(credit) AS wrong_when_one_side_is_empty,
coalesce(sum(debit), 0) - coalesce(sum(credit), 0) AS balance
FROM ledger
GROUP BY account
ORDER BY account;
Result:
| account (INT) | wrong_when_one_side_is_empty (DECIMAL(2)) | balance (DECIMAL(2)) |
|---|---|---|
| 1000 | 5000.00 | 5000.00 |
| 1100 | 230320.00 | 230320.00 |
| 1200 | 247200.00 | 247200.00 |
| 2000 | 0.00 | 0.00 |
| 3000 | NULL | -500000.00 |
| 4000 | NULL | -6250.00 |
| 4100 | NULL | -2300.00 |
| 5000 | NULL | 1030.00 |
| 5100 | NULL | 25000.00 |
(9 rows)