# Aggregate functions
> Make one value from many rows: count, sum, avg, min, max, first, last, median, percentile, vwap, twap, conditional counts and sums, the row of the maximum, and lists of text.
- Category: Functions — aggregate
- Keywords: aggregate, group, total, average, mean, count rows, how many, number of rows, biggest, smallest, highest, lowest, open high low close, candle, turnover, count(), distinct count, uniq, quantile, p90, p95, volume weighted average price, implicit GROUP BY, HAVING, aggregate in WHERE, conditional count, countif, COUNT FILTER, sumif, SUM FILTER, averageif, multiply all, all true, any true, bool\_all, row of the maximum, day of the highest close, row of the minimum, group\_concat, join text, comma list, collect list, JSON array, jsonb\_agg, JSON object, jsonb\_object\_agg, weighted average, SUMPRODUCT, time weighted average price, TWAP
- See also: [GROUP BY and HAVING](group-by.md), [SAMPLE BY](sample-by.md), [Statistics functions](functions-statistics.md), [Window and series functions (aggregates)](functions-window.md#aggregates), [Conditional and NULL functions (coalesce)](functions-conditional.md#coalesce), [Candles cookbook](candles-cookbook.md), [PIVOT](pivot.md)
An **aggregate** function reads many rows and gives back one value. For example, `sum(quantity)` adds up the quantity of every row.
- With `GROUP BY` (or `SAMPLE BY`) you get one result row for each **group**. A group is all the rows with the same key, for example the same symbol.
- Without `GROUP BY`, all rows are one group, and you get one row.
- **Implicit GROUP BY:** like QuestDB, KDB groups by itself. Every select item that is **not** inside an aggregate becomes a group key. `SELECT symbol, max(high) FROM ochl_day` gives one row per symbol.
- Aggregates **skip NULL values**. `avg` of 10, NULL and 20 is 15. Only `count(*)` counts every row, and `first` and `last` take the first / last row's value even when it is NULL (as in QuestDB; `first_not_null` and `last_not_null` skip NULLs).
- On **no rows** (without GROUP BY) `count` gives `0` and the others give `NULL`. With GROUP BY, no rows give no groups.
- Aggregates can only be in the select list and in `HAVING`. To keep only some groups, use `HAVING` ([group-by](group-by.md)), not `WHERE`.
- `GROUP BY` can use a name from the select list (`GROUP BY band` when `band` is an alias) or a position: `GROUP BY 1` is the first column, as in PostgreSQL.
- Put `OVER (...)` after an aggregate to get a running total or a moving value for **every** row instead of one row per group ([window functions](functions-window.md#aggregates)).
- More aggregates have their own pages: spread and shape ([statistics](functions-statistics.md)), correlation and regression ([correlation](functions-correlation.md)), Sharpe ratio and drawdown ([returns and risk](functions-returns-risk.md)), and `xnpv`, `xirr` ([finance](functions-finance.md)).
**Example: A daily candle for each symbol, made from one-minute candles** (sample data: sample)
```sql
SELECT symbol,
first(open) AS open,
max(high) AS high,
min(low) AS low,
last(close) AS close,
sum(volume) AS volume,
count(*) AS minutes
FROM ochl_minutes
WHERE time IN '2026-09-10'
GROUP BY symbol
ORDER BY symbol;
```
Result:
| symbol (SYMBOL) | open (DOUBLE) | high (DOUBLE) | low (DOUBLE) | close (DOUBLE) | volume (DOUBLE) | minutes (LONG) |
|---|---|---|---|---|---|---|
| CITY | 504.6 | 507 | 500.7 | 504.9 | 183040 | 240 |
| COAST | 380.1 | 382 | 373.2 | 376.1 | 186200 | 236 |
(2 rows)
**Example: Aggregates cannot be used in WHERE** (sample data: sample; shows an error)
```sql
SELECT symbol FROM trade_log WHERE sum(quantity) > 1000;
```
Result:
Error: `line 1, column 36: 'sum' works on many rows and can only be used in a SELECT list or HAVING`
**Example: GROUP BY can use the AS name of a column** (sample data: sample)
```sql
SELECT floor(rate / 100) * 100 AS band, count(*) AS trades
FROM trade_log
GROUP BY band
ORDER BY band;
```
Result:
| band (DOUBLE) | trades (LONG) |
|---|---|
| 100 | 4 |
| 300 | 7 |
| 400 | 4 |
| 500 | 11 |
| 800 | 6 |
(5 rows)
## count(*)
Syntax:
```sql
count(*)
count()
```
The number of rows. NULL values do not matter here: every row counts. `count()` without `*` is the same (QuestDB style).
- **Returns:** `LONG`.
- **NULL:** rows with NULL values are counted too.
- **Notes:** on no rows, `count(*)` is `0` (without GROUP BY). With GROUP BY, a group with no rows does not exist, so you do not get a `0` row for it.
**Example: Trades per symbol on one day** (sample data: sample)
```sql
SELECT symbol, count(*) AS trades
FROM trade_log
WHERE date IN '2026-09-10'
GROUP BY symbol
ORDER BY trades DESC;
```
Result:
| symbol (SYMBOL) | trades (LONG) |
|---|---|
| COAST | 9 |
| CITY | 7 |
| TELCO | 4 |
| VOLT | 4 |
(4 rows)
**Example: New app users per Nepal day** (sample data: fn-dates)
```sql
SELECT to_date(created_at + 20700000) AS nepal_day, count(*) AS signups
FROM signups
GROUP BY to_date(created_at + 20700000)
ORDER BY nepal_day;
```
Result:
| nepal_day (DATE) | signups (LONG) |
|---|---|
| 2026-09-01 | 3 |
| 2026-09-02 | 2 |
| 2026-09-03 | 3 |
| 2026-09-04 | 1 |
| 2026-09-06 | 1 |
(5 rows)
**Example: No rows: count is 0, sum is NULL** (sample data: sample)
```sql
SELECT count(*) AS rows, count() AS rows_again, sum(close) AS total
FROM ochl_day
WHERE symbol = 'TELCO';
```
Result:
| rows (LONG) | rows_again (LONG) | total (STRING) |
|---|---|---|
| 0 | 0 | NULL |
(1 row)
## count(x)
Syntax:
```sql
count(x)
```
The number of rows where `x` is **not** NULL.
| Argument | Type | Meaning |
|---|---|---|
| `x` | any type | the value to look at |
- **Returns:** `LONG`.
- **NULL:** rows where `x` is NULL are not counted.
- **Notes:** `count(*) - count(x)` is the number of NULL values.
**Example: Users who gave their city, and users who never logged in** (sample data: query-app)
```sql
SELECT count(*) AS users,
count(city) AS with_city,
count(*) - count(last_login) AS never_logged_in
FROM app_users;
```
Result:
| users (LONG) | with_city (LONG) | never_logged_in (LONG) |
|---|---|---|
| 8 | 6 | 2 |
(1 row)
## count(DISTINCT x)
Syntax:
```sql
count(DISTINCT x)
```
The number of **different** values of `x`. The same value many times counts once.
- **Returns:** `LONG`.
- **NULL:** NULL is not counted as a value.
- **Notes:** numbers are compared by value, so `1` and `1.0` are one value. `count(DISTINCT x)` and [count_distinct](functions-aggregate.md#count_distinct) are the same. Only `count` accepts `DISTINCT` inside.
**Example: How many different brokers bought each symbol** (sample data: sample)
```sql
SELECT symbol, count(*) AS trades, count(DISTINCT buyer) AS buying_brokers
FROM trade_log
GROUP BY symbol
ORDER BY symbol;
```
Result:
| symbol (SYMBOL) | trades (LONG) | buying_brokers (LONG) |
|---|---|---|
| CITY | 11 | 4 |
| COAST | 11 | 5 |
| TELCO | 6 | 5 |
| VOLT | 4 | 3 |
(4 rows)
**Example: NULL is not a value; 1 and 1.0 are the same value**
```sql
SELECT count(*) AS rows, count(x) AS not_null, count(DISTINCT x) AS different
FROM (SELECT NULL AS x UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1.0 UNION ALL SELECT 2) t;
```
Result:
| rows (LONG) | not_null (LONG) | different (LONG) |
|---|---|---|
| 5 | 4 | 2 |
(1 row)
## count_distinct
Syntax:
```sql
count_distinct(x)
```
The number of different values of `x`, without NULL. The same as `count(DISTINCT x)`.
| Argument | Type | Meaning |
|---|---|---|
| `x` | any type | the value to look at |
- **Returns:** `LONG`.
- **NULL:** NULL is not counted.
**Example: Posts and different writers per forum topic** (sample data: fn-dates)
```sql
SELECT topic, count(*) AS posts, count_distinct(author) AS writers
FROM forum_posts
GROUP BY topic
ORDER BY posts DESC;
```
Result:
| topic (STRING) | posts (LONG) | writers (LONG) |
|---|---|---|
| Market today | 5 | 5 |
| CITY dividend | 3 | 3 |
| Hydro IPOs | 3 | 2 |
| COAST merger | 3 | 3 |
(4 rows)
## sum
Syntax:
```sql
sum(x)
```
The total of all values of `x`.
| Argument | Type | Meaning |
|---|---|---|
| `x` | any number | the value to add up |
- **Returns:** `LONG` when every value is a whole number, an exact `DECIMAL` when every value is a `DECIMAL` (with the largest scale of them), else `DOUBLE`.
- **NULL:** NULL values are skipped. When there are no values (or all are NULL), the result is `NULL`, not `0`. Write `coalesce(sum(x), 0)` to get `0` ([coalesce](functions-conditional.md#coalesce)); for a `DECIMAL(2)` column write `coalesce(sum(x), CAST(0 AS DECIMAL(2)))`, because a plain `0` is a `LONG`.
- **Notes:** a `LONG` total too big for 64 bits (more than about 9.2e18) is an error: `sum() is out of range` (SQLSTATE `22003`). A `DECIMAL` total is exact up to 38 digits, also past 64 bits; only past that is it an error. It never comes back rounded. For an approximate total add `DOUBLE` values: `sum(x::DOUBLE)`. Text gives an error (`sum needs numbers`). For money, use `DECIMAL(2)` columns: their sum is exact.
- **DOUBLE totals:** a `DOUBLE` total is **compensated** (the Neumaier method): the rounding error of each step is carried along and added back, so the total does not depend on the order of the rows and is as close to the exact total as a `DOUBLE` can be. QuestDB's plain `sum` adds without this, so its last digit can differ: five NEPSE volumes that add up to exactly 55128735 give 55128734.99999999 there. QuestDB's `ksum` and `nsum` give KDB's answer; KDB accepts both names as `sum`.
**Example: Broker buy totals on one day (trade log)** (sample data: sample)
```sql
SELECT buyer, b.name, count(*) AS trades, sum(quantity) AS shares, sum(amount) AS turnover
FROM trade_log f JOIN brokers b ON b.id = f.buyer
WHERE date IN '2026-09-10'
GROUP BY buyer, b.name
ORDER BY turnover DESC;
```
Result:
| buyer (INT) | name (STRING) | trades (LONG) | shares (LONG) | turnover (DOUBLE) |
|---|---|---|---|---|
| 49 | Online Securities | 5 | 1740 | 960370 |
| 45 | Imperial Securities | 5 | 1560 | 695950 |
| 58 | Northgate Securities | 4 | 760 | 260190 |
| 1 | Keystone Securities | 7 | 570 | 239420 |
| 34 | Vision Securities | 3 | 300 | 205450 |
(5 rows)
**Example: Accounting month report: debit and credit per account type (DECIMAL stays exact)** (sample data: query-app)
```sql
SELECT a.type, sum(l.debit) AS debit, sum(l.credit) AS credit
FROM ledger l JOIN accounts a ON a.code = l.account
WHERE l.entry_date IN '2026-09'
GROUP BY a.type
ORDER BY a.type;
```
Result:
| type (STRING) | debit (DECIMAL(2)) | credit (DECIMAL(2)) |
|---|---|---|
| asset | 806350.00 | 323830.00 |
| equity | NULL | 500000.00 |
| expense | 26030.00 | NULL |
| income | NULL | 8550.00 |
| liability | 257500.00 | 257500.00 |
(5 rows)
**Example: No values give NULL; coalesce makes it 0.00** (sample data: query-app)
```sql
SELECT sum(debit) AS debit,
coalesce(sum(debit), CAST(0 AS DECIMAL(2))) AS debit_or_zero
FROM ledger
WHERE account = 3000;
```
Result:
| debit (STRING) | debit_or_zero (DECIMAL(2)) |
|---|---|
| NULL | 0.00 |
(1 row)
**Example: Check the books: every journal entry must have debits = credits** (sample data: query-app)
```sql
SELECT entry_id, count(*) AS lines, sum(debit) AS debit, sum(credit) AS credit,
sum(debit) = sum(credit) AS balanced
FROM ledger
GROUP BY entry_id
ORDER BY entry_id;
```
Result:
| entry_id (INT) | lines (LONG) | debit (DECIMAL(2)) | credit (DECIMAL(2)) | balanced (BOOL) |
|---|---|---|---|---|
| 1 | 2 | 500000.00 | 500000.00 | true |
| 2 | 2 | 257500.00 | 257500.00 | true |
| 3 | 2 | 257500.00 | 257500.00 | true |
| 4 | 2 | 1030.00 | 1030.00 | true |
| 5 | 2 | 30000.00 | 30000.00 | true |
| 6 | 2 | 25000.00 | 25000.00 | true |
| 7 | 2 | 6250.00 | 6250.00 | true |
| 8 | 3 | 12600.00 | 12600.00 | true |
(8 rows)
**Example: A DOUBLE total is compensated: exactly 55128735, not 55128734.99999999**
```sql
CREATE TABLE vol (v DOUBLE);
INSERT INTO vol VALUES (15380798.2), (10594146.6), (12320992.8), (6619323.5), (10213473.9);
SELECT sum(v) = 55128735 AS exact, ksum(v) = sum(v) AS same_as_ksum FROM vol;
```
Result:
`CREATE TABLE vol (`
```text
table 'vol' created
```
`INSERT INTO vol VALUES (15380798.2), (10594146.6), (12320992.8), (6619323.5), (10213473.9)`
```text
5 row(s) inserted
```
`SELECT sum(v) = 55128735 AS exact, sum(v) = sum(v) AS same_as_ksum FROM vol`
| exact (BOOL) | same_as_ksum (BOOL) |
|---|---|
| true | true |
(1 row)
**Example: Text cannot be added** (sample data: sample; shows an error)
```sql
SELECT sum(symbol) FROM trade_log;
```
Result:
Error: `line 1, column 8: sum needs numbers, got 'CITY'`
## avg
Syntax:
```sql
avg(x)
```
The average (mean): the sum of the values divided by how many values there are.
| Argument | Type | Meaning |
|---|---|---|
| `x` | any number | the value |
- **Returns:** `DOUBLE`, also for `LONG` input. For `DECIMAL` input an exact `DECIMAL` with at least 16 significant digits, as in PostgreSQL: `avg` of `1.50` and `2.50` is `2.0000000000000000`. Use `round(avg(x), 2)` for a report ([round](functions-math.md#round)).
- **NULL:** NULL values are skipped: they do not count as 0. No values give `NULL`.
**Example: Average close and volume per symbol in September** (sample data: sample)
```sql
SELECT symbol, round(avg(close), 2) AS avg_close, round(avg(volume)) AS avg_volume
FROM ochl_day
WHERE time IN '2026-09'
GROUP BY symbol
ORDER BY symbol;
```
Result:
| symbol (SYMBOL) | avg_close (DOUBLE) | avg_volume (DOUBLE) |
|---|---|---|
| CITY | 500.18 | 109526 |
| COAST | 374.73 | 113784 |
| VOLT | 179.3 | 89164 |
(3 rows)
**Example: NULL is skipped, not counted as 0**
```sql
SELECT avg(x) AS avg, sum(x) / count(*) AS sum_by_all_rows
FROM (SELECT 10 AS x UNION ALL SELECT NULL UNION ALL SELECT 20) t;
```
Result:
| avg (DOUBLE) | sum_by_all_rows (DOUBLE) |
|---|---|
| 15 | 10 |
(1 row)
## min
Syntax:
```sql
min(x)
```
The smallest value. Works for numbers, text (A before B), times and dates, and `BOOL` (`false` before `true`).
- **Returns:** the type of `x`.
- **NULL:** NULL values are skipped. No values give `NULL`.
- **Notes:** values of different kinds (a number and a text) give an error.
**Example: Lowest price and first trade day of each symbol** (sample data: sample)
```sql
SELECT symbol, min(low) AS lowest, min(time) AS first_day
FROM ochl_day
GROUP BY symbol
ORDER BY symbol;
```
Result:
| symbol (SYMBOL) | lowest (DOUBLE) | first_day (TIMESTAMP) |
|---|---|---|
| CITY | 494.1 | 2025-12-28T00:00:00.000Z |
| COAST | 364.1 | 2025-12-28T00:00:00.000Z |
| VOLT | 167.3 | 2025-12-28T00:00:00.000Z |
(3 rows)
**Example: Text and dates** (sample data: query-app)
```sql
SELECT min(username) AS first_name_a_to_z, min(created_at) AS first_signup, min(last_login) AS oldest_login
FROM app_users;
```
Result:
| first_name_a_to_z (STRING) | first_signup (TIMESTAMP) | oldest_login (TIMESTAMP) |
|---|---|---|
| anita | 2026-01-01T03:15:00.000Z | 2026-08-30T06:15:00.000Z |
(1 row)
## max
Syntax:
```sql
max(x)
```
The biggest value. Works for numbers, text, times and dates, and `BOOL`.
- **Returns:** the type of `x`.
- **NULL:** NULL values are skipped. No values give `NULL`.
- **Notes:** values of different kinds give an error.
**Example: High, low and range of each symbol since 23 August** (sample data: sample)
```sql
SELECT symbol, max(high) AS highest, min(low) AS lowest, round(max(high) - min(low), 2) AS range
FROM ochl_day
WHERE time >= '2026-08-23'
GROUP BY symbol
ORDER BY range DESC;
```
Result:
| symbol (SYMBOL) | highest (DOUBLE) | lowest (DOUBLE) | range (DOUBLE) |
|---|---|---|---|
| COAST | 398.8 | 364.1 | 34.7 |
| VOLT | 192.1 | 167.3 | 24.8 |
| CITY | 517 | 494.1 | 22.9 |
(3 rows)
**Example: The last login of every plan** (sample data: query-app)
```sql
SELECT plan, count(*) AS users, max(last_login) AS latest_login
FROM app_users
GROUP BY plan
ORDER BY plan;
```
Result:
| plan (STRING) | users (LONG) | latest_login (TIMESTAMP) |
|---|---|---|
| admin | 1 | 2026-09-11T04:20:00.000Z |
| free | 4 | 2026-09-09T02:15:00.000Z |
| pro | 3 | 2026-09-11T00:45:00.000Z |
(3 rows)
## first
Syntax:
```sql
first(x)
```
The value of `x` in the **earliest** row. On a time-series table, earliest means the smallest time. `first(open)` is the open price of a candle.
| Argument | Type | Meaning |
|---|---|---|
| `x` | any type | the value to take |
- **Returns:** the type of `x`.
- **NULL:** the earliest row's value, **even when it is NULL**, as in QuestDB. To skip NULLs, use [first_not_null](#first_not_null). No rows give `NULL`.
- **Notes:** the order is the table's time column, not `ORDER BY`. On a relational table (no time column) it is the order of the primary key. When many rows have the **same** time, KDB takes the first in storage order; do not count on which one.
- **Changed 2026-09-25:** before, `first` and `last` skipped NULLs (what `first_not_null` and `last_not_null` do now).
**Example: Open, close and change of each symbol in September** (sample data: sample)
```sql
SELECT symbol, first(open) AS open, last(close) AS close,
round((last(close) - first(open)) / first(open) * 100, 2) AS change_pct
FROM ochl_day
WHERE time IN '2026-09'
GROUP BY symbol
ORDER BY change_pct DESC;
```
Result:
| symbol (SYMBOL) | open (DOUBLE) | close (DOUBLE) | change_pct (DOUBLE) |
|---|---|---|---|
| VOLT | 171.3 | 191.2 | 11.62 |
| COAST | 367 | 376.1 | 2.48 |
| CITY | 504.9 | 504.9 | 0 |
(3 rows)
**Example: The first post of each thread** (sample data: content-forum)
```sql
SELECT thread_id, first(author) AS started_by, first(created_at) AS started, count(*) AS posts
FROM posts
GROUP BY thread_id
ORDER BY thread_id;
```
Result:
| thread_id (LONG) | started_by (STRING) | started (TIMESTAMP) | posts (LONG) |
|---|---|---|---|
| 1 | ram | 2026-09-01T04:15:00.000Z | 6 |
| 2 | hari | 2026-09-02T03:15:00.000Z | 2 |
(2 rows)
## last
Syntax:
```sql
last(x)
```
The value of `x` in the **latest** row (the biggest time). `last(close)` is the close price of a candle.
| Argument | Type | Meaning |
|---|---|---|
| `x` | any type | the value to take |
- **Returns:** the type of `x`.
- **NULL:** the latest row's value, **even when it is NULL**, as in QuestDB. To skip NULLs, use [last_not_null](#first_not_null).
- **Notes:** same order rules as [first](functions-aggregate.md#first). For the newest **row** per symbol, [LATEST ON](latest-on.md) is faster.
**Example: The last price of every symbol in the live data** (sample data: sample)
```sql
SELECT symbol, last(ltp) AS ltp, last(timestamp) AS at, max(high) AS day_high
FROM live_data
GROUP BY symbol
ORDER BY symbol;
```
Result:
| symbol (SYMBOL) | ltp (DOUBLE) | at (TIMESTAMP) | day_high (DOUBLE) |
|---|---|---|---|
| CITY | 527.1 | 2026-09-10T09:14:00.000Z | 528.1 |
| COAST | 401.9 | 2026-09-10T09:14:00.000Z | 402.9 |
| TELCO | 897.7 | 2026-09-10T09:14:00.000Z | 898.7 |
| VOLT | 184 | 2026-09-10T09:14:00.000Z | 186.6 |
(4 rows)
**Example: 15-minute candles with SAMPLE BY** (sample data: sample)
```sql
SELECT time, first(open) AS open, max(high) AS high, min(low) AS low, last(close) AS close, sum(volume) AS volume
FROM ochl_minutes
WHERE symbol = 'CITY' AND time IN '2026-09-10'
SAMPLE BY 15m
LIMIT 4;
```
Result:
| time (TIMESTAMP) | open (DOUBLE) | high (DOUBLE) | low (DOUBLE) | close (DOUBLE) | volume (DOUBLE) |
|---|---|---|---|---|---|
| 2026-09-10T05:15:00.000Z | 504.6 | 505.3 | 503.9 | 504.4 | 13210 |
| 2026-09-10T05:30:00.000Z | 504.4 | 506.8 | 503.8 | 506.5 | 11690 |
| 2026-09-10T05:45:00.000Z | 506.5 | 507 | 505.3 | 506.4 | 11270 |
| 2026-09-10T06:00:00.000Z | 506.4 | 506.6 | 504.9 | 505.3 | 10850 |
(4 rows)
## first_not_null, last_not_null
Syntax:
```sql
first_not_null(x)
last_not_null(x)
```
The value of `x` in the earliest / latest row where `x` is **not** NULL (QuestDB). Use them when a missing value must not become the answer, for example a candle whose first minute has no open price yet.
| Argument | Type | Meaning |
|---|---|---|
| `x` | any type | the value to take |
- **Returns:** the type of `x`.
- **NULL:** rows where `x` is NULL are skipped. When every value is NULL (or there are no rows), the result is `NULL`.
- **Notes:** same order rules as [first](#first). They work with `GROUP BY`, `SAMPLE BY` and `OVER (...)`.
**Example: first keeps a NULL first row, first_not_null skips it**
Setup:
```sql
CREATE TABLE px (time TIMESTAMP, symbol SYMBOL, open DOUBLE) TIMESERIES(time) PARTITION BY DAY;
INSERT INTO px VALUES ('2026-09-10T05:00:00Z', 'CITY', NULL), ('2026-09-10T05:01:00Z', 'CITY', 512.5),
('2026-09-10T05:02:00Z', 'CITY', 513), ('2026-09-10T05:03:00Z', 'CITY', NULL);
```
```sql
SELECT symbol, first(open), first_not_null(open), last(open), last_not_null(open) FROM px GROUP BY symbol;
```
Result:
| symbol (SYMBOL) | first(open) (DOUBLE) | first_not_null(open) (DOUBLE) | last(open) (DOUBLE) | last_not_null(open) (DOUBLE) |
|---|---|---|---|---|
| CITY | NULL | 512.5 | NULL | 513 |
(1 row)
## median
Syntax:
```sql
median(x)
```
The middle value: half of the values are smaller, half are bigger. With an even number of values, it is the average of the two in the middle. Unlike `avg`, one very big trade does not move the median much.
- **Returns:** `DOUBLE`.
- **NULL:** NULL values are skipped. No values give `NULL`.
- **Notes:** the same as `percentile(x, 0.5)`. KDB keeps all values of a group in memory to find it.
**Example: Typical trade size: median against average** (sample data: sample)
```sql
SELECT symbol, count(*) AS trades, median(quantity) AS median_qty, round(avg(quantity), 1) AS avg_qty
FROM trade_log
GROUP BY symbol
ORDER BY symbol;
```
Result:
| symbol (SYMBOL) | trades (LONG) | median_qty (DOUBLE) | avg_qty (DOUBLE) |
|---|---|---|---|
| CITY | 11 | 100 | 220 |
| COAST | 11 | 50 | 279.1 |
| TELCO | 6 | 350 | 461.7 |
| VOLT | 4 | 75 | 100 |
(4 rows)
**Example: Even number of values: the average of the two in the middle**
```sql
SELECT median(x) AS median
FROM (SELECT 1 AS x UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 100) t;
```
Result:
| median (DOUBLE) |
|---|
| 2.5 |
(1 row)
## percentile
Syntax:
```sql
percentile(x, p)
```
The value below which a part `p` of the values lie. `percentile(x, 0.9)` is the value that 90% of the values are at or below. Between two values, KDB goes the right part of the way from one to the other (linear interpolation, like PostgreSQL `percentile_cont`).
| Argument | Type | Meaning |
|---|---|---|
| `x` | any number | the values |
| `p` | number | the part, from `0` to `1` (`0.9`). A number **above 1** is read as a percent (`90` means `0.9`) |
- **Returns:** `DOUBLE`.
- **NULL:** NULL values of `x` are skipped. No values give `NULL`.
- **Notes:** `p` below 0 is taken as 0 (the smallest value); a percent above 100 is taken as 100. Careful: `1` means 100%, but `1.5` means **1.5%**. `p` is read once, from the first row. A `p` that is not a number is an error.
**Example: Big trades: 90th percentile of the trade amount per symbol** (sample data: sample)
```sql
SELECT symbol, percentile(amount, 0.9) AS p90_amount, max(amount) AS biggest
FROM trade_log
GROUP BY symbol
ORDER BY symbol;
```
Result:
| symbol (SYMBOL) | p90_amount (DOUBLE) | biggest (DOUBLE) |
|---|---|---|
| CITY | 258000 | 258500 |
| COAST | 398000 | 402000 |
| TELCO | 880000 | 881000 |
| VOLT | 31370 | 36800 |
(4 rows)
**Example: 0.9 and 90 are the same; values in between are interpolated**
```sql
SELECT percentile(x, 0.9) AS p90, percentile(x, 90) AS p90_percent, percentile(x, 0.25) AS p25, percentile(x, 0) AS p0, percentile(x, 1) AS p100
FROM (SELECT 10 AS x UNION ALL SELECT 20 UNION ALL SELECT 30 UNION ALL SELECT 40 UNION ALL SELECT 50) t;
```
Result:
| p90 (DOUBLE) | p90_percent (DOUBLE) | p25 (DOUBLE) | p0 (DOUBLE) | p100 (DOUBLE) |
|---|---|---|---|---|
| 46 | 46 | 20 | 10 | 50 |
(1 row)
**Example: p must be a number** (sample data: sample; shows an error)
```sql
SELECT percentile(rate, 'high') FROM trade_log;
```
Result:
Error: `line 1, column 8: percentile needs a number like 0.95`
## vwap
Syntax:
```sql
vwap(price, volume)
```
The **volume-weighted average price**: `sum(price × volume) / sum(volume)`. A trade of 1,000 shares counts 100 times more than a trade of 10 shares. Traders use VWAP as the "fair" average price of a day.
| Argument | Type | Meaning |
|---|---|---|
| `price` | number | price of each trade or candle |
| `volume` | number | how many shares |
- **Returns:** `DOUBLE`.
- **NULL:** a row where `price` or `volume` is NULL is skipped. When the total volume is 0 (or there are no rows), the result is `NULL`.
**Example: VWAP per symbol from the trade log** (sample data: sample)
```sql
SELECT symbol, sum(quantity) AS shares, round(vwap(rate, quantity), 2) AS vwap, round(avg(rate), 2) AS plain_avg
FROM trade_log
WHERE date IN '2026-09-10'
GROUP BY symbol
ORDER BY symbol;
```
Result:
| symbol (SYMBOL) | shares (LONG) | vwap (DOUBLE) | plain_avg (DOUBLE) |
|---|---|---|---|
| CITY | 910 | 516.3 | 516 |
| COAST | 2850 | 399.77 | 399.28 |
| TELCO | 770 | 880.86 | 880.75 |
| VOLT | 400 | 184.88 | 185 |
(4 rows)
**Example: Hourly VWAP from one-minute candles (Nepal hours)** (sample data: sample)
```sql
SELECT time, round(vwap(close, volume), 2) AS vwap, sum(volume) AS volume
FROM ochl_minutes
WHERE symbol = 'COAST' AND time IN '2026-09-10'
SAMPLE BY 1h TIME ZONE 'Asia/Kathmandu';
```
Result:
| time (TIMESTAMP) | vwap (DOUBLE) | volume (DOUBLE) |
|---|---|---|
| 2026-09-10T05:15:00.000Z | 380.44 | 47660 |
| 2026-09-10T06:15:00.000Z | 379.71 | 45870 |
| 2026-09-10T07:15:00.000Z | 377.6 | 43950 |
| 2026-09-10T08:15:00.000Z | 375.67 | 48720 |
(4 rows)
**Example: Both arguments are needed** (sample data: sample; shows an error)
```sql
SELECT vwap(rate) FROM trade_log;
```
Result:
Error: `line 1, column 8: function 'vwap' takes 2 argument(s), got 1`
## twap
Syntax:
```sql
twap(price, time)
```
The **time-weighted average price**. Each price counts for the time until the next price. A price that stayed for 2 hours counts twice as much as a price that stayed for 1 hour. Traders use TWAP to judge a price when trades came at uneven times, for example live price snapshots.
| Argument | Type | Meaning |
|---|---|---|
| `price` | number | the price |
| `time` | `TIMESTAMP`, `DATE` or whole number (milliseconds) | when this price started |
- **Returns:** `DOUBLE`.
- **NULL:** a row where `price` or `time` is NULL is skipped. No rows give `NULL`.
- **Notes:**
- The rows are sorted by `time` first; the order in the table does not matter.
- The **last** price has no time after it, so it gets no weight (it is only used when it is the only price).
- When every price has the same time, the result is the plain average.
- Unlike [vwap](functions-aggregate.md#vwap), the volume does not matter. With prices at even steps (one-minute candles) TWAP is almost the plain average.
**Example: Live prices came at 11:05, 13:00 and 14:59: TWAP against the plain average** (sample data: sample)
```sql
SELECT symbol, round(twap(ltp, timestamp), 2) AS twap, round(avg(ltp), 2) AS plain_avg
FROM live_data
GROUP BY symbol
ORDER BY symbol;
```
Result:
| symbol (SYMBOL) | twap (DOUBLE) | plain_avg (DOUBLE) |
|---|---|---|
| CITY | 514.32 | 518.57 |
| COAST | 391.01 | 394.63 |
| TELCO | 864.82 | 875.73 |
| VOLT | 184.51 | 184.33 |
(4 rows)
**Example: How it works: 513 for 115 minutes and 515.6 for 119 minutes; 527.1 is the last price** (sample data: sample)
```sql
SELECT timestamp, ltp FROM live_data WHERE symbol = 'CITY' ORDER BY timestamp;
SELECT round((513 * 115 + 515.6 * 119) / 234.0, 2) AS by_hand, round(twap(ltp, timestamp), 2) AS twap
FROM live_data
WHERE symbol = 'CITY';
```
Result:
`SELECT timestamp, ltp FROM live_data WHERE symbol = 'CITY' ORDER BY timestamp`
| timestamp (TIMESTAMP) | ltp (DOUBLE) |
|---|---|
| 2026-09-10T05:20:00.000Z | 513 |
| 2026-09-10T07:15:00.000Z | 515.6 |
| 2026-09-10T09:14:00.000Z | 527.1 |
(3 rows)
`SELECT round((513 * 115 + 515.6 * 119) / 234.0, 2) AS by_hand, round(twap(ltp, timestamp), 2) AS twa`
| by_hand (DOUBLE) | twap (DOUBLE) |
|---|---|
| 514.32 | 514.32 |
(1 row)
**Example: Hourly TWAP of one-minute closes** (sample data: sample)
```sql
SELECT time, round(twap(close, time), 2) AS twap, round(avg(close), 2) AS plain_avg
FROM ochl_minutes
WHERE symbol = 'COAST' AND time IN '2026-09-10'
SAMPLE BY 1h
LIMIT 3;
```
Result:
| time (TIMESTAMP) | twap (DOUBLE) | plain_avg (DOUBLE) |
|---|---|---|
| 2026-09-10T05:00:00.000Z | 380.41 | 380.39 |
| 2026-09-10T06:00:00.000Z | 380.32 | 380.31 |
| 2026-09-10T07:00:00.000Z | 378.05 | 378.06 |
(3 rows)
## weighted_avg, wavg
Syntax:
```sql
weighted_avg(x, weight)
wavg(x, weight)
```
The **weighted average**: `sum(x × weight) / sum(weight)`. A row with weight 2 counts twice as much as a row with weight 1. `wavg` is the same function.
| Argument | Type | Meaning |
|---|---|---|
| `x` | number | the values |
| `weight` | number | how much each value counts |
- **Returns:** `DOUBLE`.
- **NULL:** a row where `x` or `weight` is NULL is skipped. When the total weight is 0 (or there are no rows), the result is `NULL`.
- **Notes:** with price and volume it is the same as [vwap](functions-aggregate.md#vwap). Use it for other weights: the average buy price of a portfolio (weight = shares), the average interest rate of loans (weight = loan amount).
**Example: Average buy rate of each broker, weighted by the shares bought** (sample data: sample)
```sql
SELECT buyer, count(*) AS trades, sum(quantity) AS shares,
round(weighted_avg(rate, quantity), 2) AS avg_rate_weighted,
round(avg(rate), 2) AS avg_rate_plain
FROM trade_log
WHERE symbol = 'CITY'
GROUP BY buyer
ORDER BY buyer;
```
Result:
| buyer (INT) | trades (LONG) | shares (LONG) | avg_rate_weighted (DOUBLE) | avg_rate_plain (DOUBLE) |
|---|---|---|---|---|
| 1 | 5 | 1170 | 515.06 | 515.5 |
| 34 | 1 | 500 | 514.5 | 514.5 |
| 45 | 3 | 530 | 515.94 | 515.33 |
| 49 | 2 | 220 | 517.77 | 516.75 |
(4 rows)
**Example: Average interest rate of three loans**
```sql
SELECT round(wavg(rate_pct, amount), 3) AS weighted_rate, round(avg(rate_pct), 3) AS plain_avg
FROM (SELECT 500000 AS amount, 9.5 AS rate_pct
UNION ALL SELECT 100000, 12.0
UNION ALL SELECT 50000, 14.0) loans;
```
Result:
| weighted_rate (DOUBLE) | plain_avg (DOUBLE) |
|---|---|
| 10.231 | 11.833 |
(1 row)
## count_if
Syntax:
```sql
count_if(condition)
```
Counts the rows where the condition is **true**. `count_if(plan = 'pro')` is the number of pro users. It is shorter than `sum(CASE WHEN plan = 'pro' THEN 1 ELSE 0 END)`.
| Argument | Type | Meaning |
|---|---|---|
| `condition` | true/false | a comparison or any true/false expression |
- **Returns:** `LONG`.
- **NULL:** rows where the condition is NULL are not counted. No rows give `0`, not NULL.
- **Notes:** the condition must be true/false: `count_if(plan)` is an error. PostgreSQL writes `count(*) FILTER (WHERE ...)`; KDB has no `FILTER`, use `count_if`.
**Example: App users per city: all, pro, and who logged in since 10 September** (sample data: query-app)
```sql
SELECT city, count(*) AS users,
count_if(plan = 'pro') AS pro_users,
count_if(last_login >= '2026-09-10') AS active_users
FROM app_users
GROUP BY city
ORDER BY users DESC, city;
```
Result:
| city (STRING) | users (LONG) | pro_users (LONG) | active_users (LONG) |
|---|---|---|---|
| Kathmandu | 2 | 1 | 1 |
| Pokhara | 2 | 1 | 1 |
| NULL | 2 | 0 | 0 |
| Dharan | 1 | 1 | 1 |
| Lalitpur | 1 | 0 | 1 |
(5 rows)
**Example: Up days of each symbol in the last 5 days (with OVER)** (sample data: sample)
```sql
SELECT time, symbol, open, close,
count_if(close > open) OVER (PARTITION BY symbol ORDER BY time ROWS 4 PRECEDING) AS up_days_of_5
FROM ochl_day
WHERE time >= '2026-08-30'
ORDER BY symbol, time
LIMIT 8;
```
Result:
| time (TIMESTAMP) | symbol (SYMBOL) | open (DOUBLE) | close (DOUBLE) | up_days_of_5 (LONG) |
|---|---|---|---|---|
| 2026-08-30T00:00:00.000Z | CITY | 507.3 | 503.1 | 0 |
| 2026-08-31T00:00:00.000Z | CITY | 502.5 | 505.7 | 1 |
| 2026-09-01T00:00:00.000Z | CITY | 504.9 | 502.4 | 1 |
| 2026-09-02T00:00:00.000Z | CITY | 502.1 | 496.4 | 1 |
| 2026-09-03T00:00:00.000Z | CITY | 497.7 | 497.9 | 2 |
| 2026-09-06T00:00:00.000Z | CITY | 497.4 | 499 | 3 |
| 2026-09-07T00:00:00.000Z | CITY | 497.3 | 498.7 | 3 |
| 2026-09-08T00:00:00.000Z | CITY | 498.2 | 497.5 | 3 |
(8 rows)
**Example: The argument must be a condition** (sample data: query-app; shows an error)
```sql
SELECT count_if(plan) FROM app_users;
```
Result:
Error: `line 1, column 8: count_if: needs a condition (true/false), got 'pro'`
## sum_if
Syntax:
```sql
sum_if(x, condition)
```
Adds up `x` only in the rows where the condition is **true**. Shorter than `sum(CASE WHEN condition THEN x END)`.
| Argument | Type | Meaning |
|---|---|---|
| `x` | any number | the value to add |
| `condition` | true/false | which rows to add |
- **Returns:** `LONG` when every added value is a whole number, `DECIMAL` when they are all `DECIMAL` of the same scale (money stays exact), else `DOUBLE`.
- **NULL:** rows where `x` is NULL or the condition is not true are skipped. When no row is added, the result is `NULL` (like `sum`); wrap it in `coalesce(..., 0)` for 0.
- **Notes:** a condition that is not true/false is an error. A whole-number total too big for 64 bits (a `DECIMAL` one past 38 digits) is an error, as for [sum](#sum).
**Example: Money into and out of the bank account, and only the CITY entries** (sample data: query-app)
```sql
SELECT sum(debit) AS money_in, sum(credit) AS money_out,
sum_if(debit, memo LIKE '%CITY%') AS city_in,
sum_if(credit, memo LIKE '%CITY%') AS city_out
FROM ledger
WHERE account = 1100;
```
Result:
| money_in (DECIMAL(2)) | money_out (DECIMAL(2)) | city_in (DECIMAL(2)) | city_out (DECIMAL(2)) |
|---|---|---|---|
| 518850.00 | 288530.00 | 18850.00 | 1030.00 |
(1 row)
**Example: Buy and sell turnover of broker 1 on each day** (sample data: sample)
```sql
SELECT date,
sum_if(amount, buyer = 1) AS bought,
sum_if(amount, seller = 1) AS sold,
coalesce(sum_if(amount, buyer = 1), 0) - coalesce(sum_if(amount, seller = 1), 0) AS net
FROM trade_log
GROUP BY date
ORDER BY date;
```
Result:
| date (TIMESTAMP) | bought (DOUBLE) | sold (DOUBLE) | net (DOUBLE) |
|---|---|---|---|
| 2026-09-09T00:00:00.000Z | 515000 | 5150 | 509850 |
| 2026-09-10T00:00:00.000Z | 239420 | 486650 | -247230 |
(2 rows)
## avg_if
Syntax:
```sql
avg_if(x, condition)
```
The average of `x` over only the rows where the condition is **true**.
| Argument | Type | Meaning |
|---|---|---|
| `x` | any number | the values |
| `condition` | true/false | which rows to use |
- **Returns:** `DOUBLE`.
- **NULL:** rows where `x` is NULL or the condition is not true are skipped. When no row is left, the result is `NULL`.
**Example: Average volume on up days and on down days** (sample data: sample)
```sql
SELECT symbol,
round(avg_if(volume, close > open)) AS avg_volume_up_days,
round(avg_if(volume, close < open)) AS avg_volume_down_days
FROM ochl_day
WHERE time >= '2026-08-23'
GROUP BY symbol
ORDER BY symbol;
```
Result:
| symbol (SYMBOL) | avg_volume_up_days (DOUBLE) | avg_volume_down_days (DOUBLE) |
|---|---|---|
| CITY | 123828 | 102885 |
| COAST | 95953 | 99090 |
| VOLT | 87421 | 97304 |
(3 rows)
## product
Syntax:
```sql
product(x)
```
All values multiplied together. Traders use it to **compound** daily returns: a rise of 1% and then 2% is `1.01 × 1.02 = 1.0302`, a growth of 3.02%.
| Argument | Type | Meaning |
|---|---|---|
| `x` | any number | the values |
- **Returns:** `DOUBLE`. A result too big for a `DOUBLE` is `NULL`.
- **NULL:** NULL values are skipped. No values give `NULL`.
- **Notes:** for a running product on every row, use [cumprod](functions-window.md#cumprod).
**Example: Growth in September: multiply the daily price ratios** (sample data: sample)
```sql
SELECT symbol, count(ratio) AS days, round(product(ratio), 4) AS growth
FROM (SELECT symbol, close / lag(close) OVER (PARTITION BY symbol ORDER BY time) AS ratio
FROM ochl_day
WHERE time >= '2026-09-01') t
GROUP BY symbol
ORDER BY symbol;
```
Result:
| symbol (SYMBOL) | days (LONG) | growth (DOUBLE) |
|---|---|---|
| CITY | 7 | 1.005 |
| COAST | 7 | 1.0237 |
| VOLT | 7 | 1.1314 |
(3 rows)
**Example**
```sql
SELECT product(x) AS product FROM (SELECT 2 AS x UNION ALL SELECT 3 UNION ALL SELECT NULL UNION ALL SELECT 4) t;
```
Result:
| product (DOUBLE) |
|---|
| 24 |
(1 row)
## bool_and, every
Syntax:
```sql
bool_and(condition)
every(condition)
```
`true` when the condition is true in **every** row, else `false`. `every` is the same function.
| Argument | Type | Meaning |
|---|---|---|
| `condition` | true/false | the check for each row |
- **Returns:** `BOOLEAN`.
- **NULL:** NULL values are skipped. No values give `NULL`.
- **Notes:** the argument must be true/false: `bool_and(plan)` is an error.
**Example: Did every user log in? Is every email good? Is there an admin?** (sample data: query-app)
```sql
SELECT bool_and(last_login IS NOT NULL) AS all_logged_in,
every(email LIKE '%@%') AS all_emails_have_at,
bool_or(plan = 'admin') AS has_admin
FROM app_users;
```
Result:
| all_logged_in (BOOL) | all_emails_have_at (BOOL) | has_admin (BOOL) |
|---|---|---|
| false | true | true |
(1 row)
**Example: Did the share close above its open on every day of the week?** (sample data: sample)
```sql
SELECT symbol, bool_and(close > open) AS up_every_day, bool_or(close > open) AS up_some_day
FROM ochl_day
WHERE time >= '2026-09-06'
GROUP BY symbol
ORDER BY symbol;
```
Result:
| symbol (SYMBOL) | up_every_day (BOOL) | up_some_day (BOOL) |
|---|---|---|
| CITY | false | true |
| COAST | false | true |
| VOLT | false | true |
(3 rows)
## bool_or
Syntax:
```sql
bool_or(condition)
```
`true` when the condition is true in **at least one** row, else `false`.
| Argument | Type | Meaning |
|---|---|---|
| `condition` | true/false | the check for each row |
- **Returns:** `BOOLEAN`.
- **NULL:** NULL values are skipped. No values give `NULL`.
- **Notes:** see the examples of [bool_and](functions-aggregate.md#bool_and).
**Example: Which brokers made a trade of 1,000 shares or more?** (sample data: sample)
```sql
SELECT buyer, count(*) AS trades, bool_or(quantity >= 1000) AS has_big_trade
FROM trade_log
GROUP BY buyer
ORDER BY buyer;
```
Result:
| buyer (INT) | trades (LONG) | has_big_trade (BOOL) |
|---|---|---|
| 1 | 9 | false |
| 34 | 5 | false |
| 45 | 6 | true |
| 49 | 6 | true |
| 58 | 6 | true |
(5 rows)
## arg_max, argmax, max_by
Syntax:
```sql
arg_max(value, by)
argmax(value, by)
max_by(value, by)
```
The `value` from the row where `by` is **biggest**. `arg_max(time, close)` answers "on which day was the close highest?". `max(close)` only gives the highest close, not its day. `argmax` and `max_by` are the same function.
| Argument | Type | Meaning |
|---|---|---|
| `value` | any type | what to give back |
| `by` | number, text or time | what to find the biggest of |
- **Returns:** the type of `value`.
- **NULL:** rows where `by` is NULL are skipped. When the best row's `value` is NULL, the result is NULL. No rows give `NULL`.
- **Notes:** when several rows share the biggest `by`, the first one in time order wins.
**Example: The day of the highest close and of the lowest close** (sample data: sample)
```sql
SELECT symbol,
max(close) AS highest_close, arg_max(time, close) AS highest_day,
min(close) AS lowest_close, arg_min(time, close) AS lowest_day
FROM ochl_day
WHERE time >= '2026-08-23'
GROUP BY symbol
ORDER BY symbol;
```
Result:
| symbol (SYMBOL) | highest_close (DOUBLE) | highest_day (TIMESTAMP) | lowest_close (DOUBLE) | lowest_day (TIMESTAMP) |
|---|---|---|---|---|
| CITY | 513.3 | 2026-08-25T00:00:00.000Z | 496.4 | 2026-09-02T00:00:00.000Z |
| COAST | 391 | 2026-08-23T00:00:00.000Z | 367.1 | 2026-08-27T00:00:00.000Z |
| VOLT | 191.2 | 2026-09-10T00:00:00.000Z | 169 | 2026-09-01T00:00:00.000Z |
(3 rows)
**Example: The user who logged in last, and the newest user's plan** (sample data: query-app)
```sql
SELECT max_by(username, last_login) AS last_seen_user,
argmax(plan, created_at) AS newest_users_plan
FROM app_users;
```
Result:
| last_seen_user (STRING) | newest_users_plan (STRING) |
|---|---|
| bikash | free |
(1 row)
## arg_min, argmin, min_by
Syntax:
```sql
arg_min(value, by)
argmin(value, by)
min_by(value, by)
```
The `value` from the row where `by` is **smallest**. `arg_min(username, created_at)` is the first user who signed up. `argmin` and `min_by` are the same function.
| Argument | Type | Meaning |
|---|---|---|
| `value` | any type | what to give back |
| `by` | number, text or time | what to find the smallest of |
- **Returns:** the type of `value`.
- **NULL:** rows where `by` is NULL are skipped. No rows give `NULL`.
- **Notes:** when several rows share the smallest `by`, the first one in time order wins. See also the example of [arg_max](functions-aggregate.md#arg_max).
**Example: First user of each plan, and the cheapest trade of each symbol** (sample data: sample, query-app)
```sql
SELECT plan, arg_min(username, created_at) AS first_user FROM app_users GROUP BY plan ORDER BY plan;
SELECT symbol, min(rate) AS lowest_rate, min_by(transaction_id, rate) AS trade, argmin(buyer, rate) AS buyer
FROM trade_log
GROUP BY symbol
ORDER BY symbol;
```
Result:
`SELECT plan, arg_min(username, created_at) AS first_user FROM app_users GROUP BY plan ORDER BY plan`
| plan (STRING) | first_user (STRING) |
|---|---|
| admin | bikash |
| free | sita |
| pro | ram |
(3 rows)
`SELECT symbol, min(rate) AS lowest_rate, min_by(transaction_id, rate) AS trade, argmin(buyer, rate) `
| symbol (SYMBOL) | lowest_rate (DOUBLE) | trade (LONG) | buyer (LONG) |
|---|---|---|---|
| CITY | 513 | 100003 | 1 |
| COAST | 398 | 100013 | 58 |
| TELCO | 878 | 100021 | 1 |
| VOLT | 184 | 100011 | 1 |
(4 rows)
## string_agg
Syntax:
```sql
string_agg(text, separator)
```
Joins the values of many rows into one text, with `separator` between them: `'ram, hari_99'`. Useful for a short list per group in a report.
| Argument | Type | Meaning |
|---|---|---|
| `text` | any type | the value of each row (numbers and times are written as text) |
| `separator` | text | what goes between two values, like `', '` |
- **Returns:** `STRING`.
- **NULL:** NULL values are skipped. No values give `NULL`. A NULL separator joins without anything between.
- **Notes:**
- The values come in the order of the rows: time order for a time-series table, else the order the rows come in. `string_agg(x, ',' ORDER BY x)` is not supported: sort the rows in a subquery first.
- Both arguments are needed; [listagg](functions-aggregate.md#listagg) has `','` as the default.
**Example: Usernames per city** (sample data: query-app)
```sql
SELECT city, count(*) AS users, string_agg(username, ', ') AS usernames
FROM app_users
GROUP BY city
ORDER BY city;
```
Result:
| city (STRING) | users (LONG) | usernames (STRING) |
|---|---|---|
| Dharan | 1 | gita |
| Kathmandu | 2 | ram, hari_99 |
| Lalitpur | 1 | bikash |
| Pokhara | 2 | sita, suman |
| NULL | 2 | anita, test_user |
(5 rows)
**Example: The trades of VOLT on each day, in one line** (sample data: sample)
```sql
SELECT date, string_agg(quantity || ' @ ' || rate, ' | ') AS trades
FROM trade_log
WHERE symbol = 'VOLT'
GROUP BY date
ORDER BY date;
```
Result:
| date (TIMESTAMP) | trades (STRING) |
|---|---|
| 2026-09-10T00:00:00.000Z | 50 @ 184 \| 200 @ 184 \| 50 @ 185 \| 100 @ 187 |
(1 row)
**Example: Sort the rows in a subquery to choose the order** (sample data: sample)
```sql
SELECT string_agg(symbol, ' > ') AS biggest_first
FROM (SELECT symbol, sum(amount) AS turnover FROM trade_log GROUP BY symbol ORDER BY turnover DESC) t;
```
Result:
| biggest_first (STRING) |
|---|
| TELCO > CITY > COAST > VOLT |
(1 row)
## listagg
Syntax:
```sql
listagg(text)
listagg(text, separator)
```
The same as [string_agg](functions-aggregate.md#string_agg), but the separator is optional: without it, the values are joined with `','`. The name comes from Oracle and Snowflake.
| Argument | Type | Meaning |
|---|---|---|
| `text` | any type | the value of each row |
| `separator` | text | what goes between two values; default `','` |
- **Returns:** `STRING`.
- **NULL:** NULL values are skipped. No values give `NULL`.
**Example: Symbols per sector** (sample data: sample)
```sql
SELECT sector, listagg(symbol) AS symbols, listagg(symbol, ' / ') AS with_slash
FROM securities
GROUP BY sector
ORDER BY sector;
```
Result:
| sector (STRING) | symbols (STRING) | with_slash (STRING) |
|---|---|---|
| Banking | CITY,COAST | CITY / COAST |
| Energy | VOLT | VOLT |
| Investment | ACME | ACME |
| Telecom | TELCO | TELCO |
(4 rows)
## array_agg, json_agg, jsonb_agg
Syntax:
```sql
array_agg(x)
json_agg(x)
jsonb_agg(x)
```
Collects the values of many rows into one **JSON array**: `[513,515.6,527.1]`. An app can read the array in one piece, for example the points of a small chart. `json_agg` and `jsonb_agg` are the same function; Postgres clients get `jsonb_agg` as PostgreSQL's `jsonb` type (`[513, 515.6, 527.1]`).
| Argument | Type | Meaning |
|---|---|---|
| `x` | any type | the value of each row |
- **Returns:** `JSON`. Numbers stay numbers, text and times become JSON text (`"..."`).
- **NULL:** NULL values are **kept** as `null` in the array. No rows give `NULL`.
- **Notes:** the values come in the order of the rows (time order for a time-series table). Read the array with the [JSON functions](functions-json.md).
**Example: The live prices of each symbol as a JSON array, for a sparkline chart** (sample data: sample)
```sql
SELECT symbol, array_agg(ltp) AS prices
FROM live_data
GROUP BY symbol
ORDER BY symbol;
```
Result:
| symbol (SYMBOL) | prices (DOUBLE[]) |
|---|---|
| CITY | [513,515.6,527.1] |
| COAST | [390.6,391.4,401.9] |
| TELCO | [860.5,869,897.7] |
| VOLT | [184,185,184] |
(4 rows)
**Example: NULL values stay in the array** (sample data: query-app)
```sql
SELECT plan, json_agg(full_name) AS names, array_agg(id) AS ids
FROM app_users
GROUP BY plan
ORDER BY plan;
```
Result:
| plan (STRING) | names (JSON) | ids (INT[]) |
|---|---|---|
| admin | ["Bikash Shrestha"] | [5] |
| free | ["Sita Karki","Hari Thapa","Anita Magar",null] | [2,3,6,8] |
| pro | ["Ram Sharma","Gita Rai","Suman Gurung"] | [1,4,7] |
(3 rows)
## json_object_agg, jsonb_object_agg
Syntax:
```sql
json_object_agg(key, value)
jsonb_object_agg(key, value)
```
Collects the rows into one **JSON object**: the key of each row, and its value. `{"CITY":513,"COAST":287.5}`.
| Argument | Type | Meaning |
|---|---|---|
| `key` | any type but JSON | the key; a number becomes text (`1` gives `"1"`) |
| `value` | any type | its value; `JSON` stays JSON |
- **Returns:** `JSON`. `jsonb_object_agg` keeps each key once, with the **last** value given; `json_object_agg` keeps every pair. Postgres clients get `jsonb_object_agg` as `jsonb` (`{"a": 1, "b": 2}`, keys in `jsonb` order).
- **NULL:** a `NULL` value becomes JSON `null`. A `NULL` key is an error (SQLSTATE `22023`), as in PostgreSQL. No rows give `NULL`.
- **Notes:** `ORDER BY` inside the call sets the order of the keys: `jsonb_object_agg(k, v ORDER BY k)`. PostgreSQL writes `json_object_agg` as `{ "a" : 1 }`; KDB keeps JSON compact: `{"a":1}`.
**Example: Prices by symbol in one object (a symbol seen twice keeps its last row)** (sample data: sample)
```sql
SELECT jsonb_object_agg(symbol, ltp ORDER BY symbol) AS prices
FROM live_data;
```
Result:
| prices (JSON) |
|---|
| {"CITY":527.1,"COAST":401.9,"TELCO":897.7,"VOLT":184} |
(1 row)