# 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](where.md), [Math functions (operators)](functions-math.md#operators), [Conversion functions](functions-conversion.md), [JSON functions](functions-json.md), [Conditional and NULL functions](functions-conditional.md), [Text functions](functions-text.md)
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)
```sql
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:
```sql
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 - 3` is `3`, and `a = b IS NULL` means `(a = b) IS NULL`.
- `a = b = c` is an error when `c` is not true or false: the first `=` gives `true` or `false`, which is then compared with `c`.
- `AND` and `OR` stop early: `false AND ...` and `true OR ...` do not compute the right side.
- Nesting is limited to 100 levels of `( )` and operators.
**Example** (sample data: sample)
```sql
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)
```sql
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)
```sql
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)
```sql
SELECT 'Total: ' || (2 + 3) AS label;
```
Result:
| label (STRING) |
|---|
| Total: 5 |
(1 row)
## Arithmetic: + - * / %
Syntax:
```sql
a + b a - b a * b a / b a % b -a
```
Add, subtract, multiply, divide and remainder. The full rules are in [arithmetic operators](functions-math.md#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](operators.md#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:** `NULL` on either side gives `NULL`.
- **Notes:** dividing by zero (`5 / 0`, `5.0 / 0`, `5 % 0`) stops the query with "division by zero" (SQLSTATE `22012`), as in PostgreSQL. Write `x / NULLIF(y, 0)` to get `NULL` instead.
- Text is not a number: `'abc' + 1` is an error. `+` does **not** join text; use `||`.
- `TRUE + 1` is an error: `BOOL` is not a number.
**Example: Daily range and change in percent** (sample data: sample)
```sql
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)
```sql
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)
```sql
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)
```sql
SELECT 5 / 0;
```
Result:
Error: `line 1, column 10: division by zero`
**Example: + does not join text** (sample data: sample; shows an error)
```sql
SELECT 'CITY' + 1;
```
Result:
Error: `line 1, column 15: 'CITY' and '1' are not both numbers`
## Integer division: SET int_division
Syntax:
```sql
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` / `LONG` is a `LONG`, cut toward zero. When either side is a `DOUBLE`, the result is still a `DOUBLE`; a `DECIMAL` divided by a `DECIMAL` or a whole number is an exact `DECIMAL` in both settings ([DECIMAL](column-types.md#decimal)).
- **Scope:** the setting is for the **session**: the rest of one `/exec` request, a shell, or one Postgres connection. Triggers and procedures that its statements run use it too. `default` goes back to what the session started with.
- **Postgres port:** `kdb serve --pg-int-division on` (or `KDB_PG_INT_DIVISION=on`) makes `integer` the start value of every Postgres connection. A connection can also ask for it in its connection string (the startup parameter `int_division=integer`), or send `SET int_division = integer`. `SHOW int_division` gives the value.
- **Table expressions:** a table's own `CHECK`, `DEFAULT`, computed columns, index expressions and materialized view queries always use the default `DOUBLE` division, whatever the session that writes or reads. They are shared by every session, so the answer must not depend on who asked. A query in `integer` mode 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 `DOUBLE` in `integer` mode, cast one side: `CAST(x AS DOUBLE) / y`. To get a whole number in `float` mode, use `trunc(x / y)`.
**Example: The same division in both modes** (sample data: sample)
```sql
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'`
```text
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)
```sql
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'`
```text
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)
```sql
SET int_division = half;
```
Result:
Error: `int_division must be integer or float, not 'half'`
## Comparison operators
Syntax:
```sql
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](where.md#comparison), but a comparison is a normal value: you can put it in the select list.
- **Returns:** `BOOL`.
- **NULL:** `NULL` on either side gives `NULL`.
- **Notes:** a text compared with a `TIMESTAMP` or `DATE` is 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`, `LIKE` and `ILIKE` are on the same level; see [WHERE](where.md).
**Example: Up day or down day, as a true/false column** (sample data: sample)
```sql
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)
```sql
SELECT 1 = 1 = 1;
```
Result:
Error: `line 1, column 14: cannot compare BOOL 'true' with LONG '1'`
## || : join text
Syntax:
```sql
value || value
```
Joins two values into one text. Numbers, times and `TRUE`/`FALSE` are turned into text first.
- **Returns:** `STRING`. Two `BYTES` values give `BYTES`.
- **NULL:** `NULL` on either side gives `NULL`. To skip NULL values, use `concat(...)` or `coalesce(x, '')` (see [text functions](functions-text.md)).
- **Notes:** `||` is on the same level as `+` and `-`. Put `( )` around calculations: `'Total: ' || (a + b)`.
**Example: A label for a chart legend** (sample data: sample)
```sql
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)
```sql
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)
```sql
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:
```sql
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](functions-json.md).
- **Returns:** `->` gives `JSON`, `->>` gives `STRING`.
- **NULL:** `NULL` when the key or index is not there, or when the value is `NULL`.
- **Notes:** you can chain them: `doc -> 'a' -> 'b' ->> 0`. They bind as tight as `::`, from left to right, so `doc ->> 'n'::INT` means `(doc ->> 'n')::INT`.
**Example** (sample data: sample)
```sql
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:
```sql
value::TYPE
value::DECIMAL(scale)
```
`x::TYPE` is the short form of `CAST(x AS TYPE)`. See [conversion functions](functions-conversion.md) for every type and rule.
- **Returns:** the value in the new type.
- **NULL:** `NULL` stays `NULL`.
- **Notes:** `::` binds tighter than every other operator: `'5'::INT * 2` is `10`, and `-5::STRING` is the text `-5`.
- A value that cannot be changed is an error, for example `'abc'::INT`, or `512.456::DECIMAL(2)` (too many decimal places: use `round(x, 2)` first).
**Example** (sample data: sample)
```sql
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)
```sql
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)
```sql
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`, `HAVING` and join `ON` keep a row only when the condition is `true`; `NULL` counts as false (see [NULL in WHERE](where.md#null-in-where)).
- Aggregates like `sum`, `avg`, `min` and `count(x)` skip `NULL` values. `count(*)` counts every row.
- Use `coalesce(x, 0)` to use a default instead of `NULL` (see [conditional functions](functions-conditional.md)).
**Example** (sample data: sample)
```sql
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)
```sql
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)