KDB knowledge

GROUP BY and HAVING

Make one row per group (per symbol, per broker, per account) with totals like count, sum and max, and filter the groups with HAVING.

GROUP BY puts the rows that have the same value (for example the same symbol) into one group, and gives one output row per group. In the select list you use aggregates like count(*), sum(x), avg(x), min(x) and max(x): they make one value from all the rows of a group. The full list is in aggregate functions.

HAVING keeps only some groups, like WHERE keeps only some rows. For groups by time (candles per minute, day, week, month), use SAMPLE BY.

Example: September 2026 summary per symbol (sample data: sample)

SELECT symbol,
       count(*)             AS days,
       min(low)             AS lowest,
       max(high)            AS highest,
       round(avg(close), 2) AS avg_close,
       sum(volume)          AS total_volume
FROM ochl_day
WHERE time IN '2026-09'
GROUP BY symbol
ORDER BY total_volume DESC;

Result:

symbol (SYMBOL)days (LONG)lowest (DOUBLE)highest (DOUBLE)avg_close (DOUBLE)total_volume (DOUBLE)
COAST8364.1398.8374.73910270
CITY8494.1512.4500.18876210
VOLT8167.3192.1179.3713310

(3 rows)

GROUP BY

Syntax:

SELECT key, ..., aggregate(...), ...
FROM ...
[WHERE condition]
GROUP BY expression, ...
[HAVING condition]
[ORDER BY ...]

Rows with equal values in all the GROUP BY expressions are one group. A key can be a column or an expression (date_trunc('month', time), upper(symbol)).

PartMeaning
expression, ...the group keys: one output row for each different combination
aggregate(...)a value computed from all the rows of the group

Example: Broker turnover as buyer on 2026-09-10 (sample data: sample)

SELECT f.buyer, b.name, count(*) AS trades, sum(f.amount) AS bought
FROM trade_log f
JOIN brokers b ON b.id = f.buyer
WHERE f.date IN '2026-09-10'
GROUP BY f.buyer, b.name
ORDER BY bought DESC;

Result:

buyer (INT)name (STRING)trades (LONG)bought (DOUBLE)
49Online Securities5960370
45Imperial Securities5695950
58Northgate Securities4260190
1Keystone Securities7239420
34Vision Securities3205450

(5 rows)

Example: Two keys: shares per broker per symbol (sample data: sample)

SELECT buyer, symbol, sum(quantity) AS shares
FROM trade_log
WHERE date IN '2026-09-10' AND buyer IN (1, 58)
GROUP BY buyer, symbol
ORDER BY buyer, symbol;

Result:

buyer (INT)symbol (SYMBOL)shares (LONG)
1CITY170
1COAST200
1TELCO50
1VOLT150
58COAST560
58VOLT200

(6 rows)

Example: An expression as the key: average close per month (sample data: sample)

SELECT date_trunc('month', time) AS month, round(avg(close), 2) AS avg_close, count(*) AS days
FROM ochl_day
WHERE symbol = 'CITY'
GROUP BY date_trunc('month', time)
ORDER BY month;

Result:

month (TIMESTAMP)avg_close (DOUBLE)days (LONG)
2025-12-01T00:00:00.000Z540.94
2026-08-01T00:00:00.000Z508.497
2026-09-01T00:00:00.000Z500.188

(3 rows)

Example: Accounting: debit, credit and balance per account (sample data: query-app)

SELECT a.code, a.name,
       coalesce(sum(l.debit), 0)  AS debit,
       coalesce(sum(l.credit), 0) AS credit,
       coalesce(sum(l.debit), 0) - coalesce(sum(l.credit), 0) AS balance
FROM ledger l
JOIN accounts a ON a.code = l.account
GROUP BY a.code, a.name
ORDER BY a.code;

Result:

code (INT)name (STRING)debit (DECIMAL(2))credit (DECIMAL(2))balance (DECIMAL(2))
1000Cash30000.0025000.005000.00
1100Bank - City518850.00288530.00230320.00
1200Shares at cost257500.0010300.00247200.00
2000Broker payable257500.00257500.000.00
3000Owner capital0500000.00-500000.00
4000Dividend income06250.00-6250.00
4100Gain on sale02300.00-2300.00
5000Broker commission1030.0001030.00
5100Office rent25000.00025000.00

(9 rows)

Example: App users per plan, with NULL cities counted apart (sample data: query-app)

SELECT plan, count(*) AS users, count(city) AS with_city, min(created_at) AS first_signup
FROM app_users
GROUP BY plan
ORDER BY users DESC;

Result:

plan (STRING)users (LONG)with_city (LONG)first_signup (TIMESTAMP)
free422026-02-11T03:45:00.000Z
pro332026-01-05T04:15:00.000Z
admin112026-01-01T03:15:00.000Z

(3 rows)

Example: Posts per forum thread (sample data: content-forum)

SELECT t.title, count(*) AS posts, max(p.created_at) AS last_post
FROM posts p
JOIN threads t ON t.id = p.thread_id
GROUP BY t.title
ORDER BY posts DESC;

Result:

title (STRING)posts (LONG)last_post (TIMESTAMP)
Is CITY a good buy?62026-09-01T06:15:00.000Z
Hydropower IPOs this month22026-09-02T03:25:00.000Z

(2 rows)

Example: Sort by a calculation of the output columns: average trade size (sample data: sample)

SELECT symbol, count(*) AS n, sum(quantity) AS q
FROM trade_log
GROUP BY symbol
ORDER BY q / n DESC;

Result:

symbol (SYMBOL)n (LONG)q (LONG)
TELCO62770
COAST113070
CITY112420
VOLT4400

(4 rows)

Example: No rows: GROUP BY gives nothing, a plain aggregate gives one row (sample data: sample)

SELECT symbol, count(*) AS trades FROM trade_log WHERE symbol = 'ACME' GROUP BY symbol;
SELECT count(*) AS trades, sum(quantity) AS shares FROM trade_log WHERE symbol = 'ACME';

Result:

SELECT symbol, count(*) AS trades FROM trade_log WHERE symbol = 'ACME' GROUP BY symbol

symbol (SYMBOL)trades (LONG)

(0 rows)

SELECT count(*) AS trades, sum(quantity) AS shares FROM trade_log WHERE symbol = 'ACME'

trades (LONG)shares (STRING)
0NULL

(1 row)

Example: An alias as the GROUP BY key (sample data: sample)

SELECT date_trunc('month', time) AS month, count(*) AS candles FROM ochl_day GROUP BY month ORDER BY month;

Result:

month (TIMESTAMP)candles (LONG)
2025-12-01T00:00:00.000Z12
2026-08-01T00:00:00.000Z21
2026-09-01T00:00:00.000Z24

(3 rows)

Example: GROUP BY 1: the first column of the select list (sample data: sample)

SELECT symbol, sum(quantity) AS shares FROM trade_log GROUP BY 1;

Result:

symbol (SYMBOL)shares (LONG)
CITY2420
COAST3070
TELCO2770
VOLT400

(4 rows)

Example: A window function over the groups: each symbol's share of all trades (sample data: sample)

SELECT symbol, count(*) AS trades,
       round(count(*) * 100.0 / sum(count(*)) OVER (), 1) AS pct
FROM trade_log
GROUP BY symbol
ORDER BY trades DESC;

Result:

symbol (SYMBOL)trades (LONG)pct (DOUBLE)
CITY1134.4
COAST1134.4
TELCO618.8
VOLT412.5

(4 rows)

Example: Rank the groups: a window function over sum() (sample data: sample)

SELECT symbol, sum(quantity) AS shares,
       rank() OVER (ORDER BY sum(quantity) DESC) AS place
FROM trade_log
GROUP BY symbol
ORDER BY place;

Result:

symbol (SYMBOL)shares (LONG)place (LONG)
COAST30701
TELCO27702
CITY24203
VOLT4004

(4 rows)

Example: Month after month: lag() of the monthly average (sample data: sample)

SELECT date_trunc('month', time) AS month,
       round(avg(close), 2) AS avg_close,
       round(lag(avg(close)) OVER (ORDER BY date_trunc('month', time)), 2) AS previous_month
FROM ochl_day
WHERE symbol = 'CITY'
GROUP BY date_trunc('month', time)
ORDER BY month;

Result:

month (TIMESTAMP)avg_close (DOUBLE)previous_month (DOUBLE)
2025-12-01T00:00:00.000Z540.9NULL
2026-08-01T00:00:00.000Z508.49540.9
2026-09-01T00:00:00.000Z500.18508.49

(3 rows)

ROLLUP, CUBE and GROUPING SETS

Syntax:

GROUP BY ROLLUP (a, b)
GROUP BY CUBE (a, b)
GROUP BY GROUPING SETS ((a, b), (a), ())
GROUP BY key, ROLLUP (a, b)
GROUPING(a [, b ...])

Subtotals and a grand total in one query, as in PostgreSQL. The query groups once for each grouping set and gives the rows of all of them together.

WrittenThe grouping sets
ROLLUP (a, b)(a, b), (a), (): the groups, a subtotal per a, the grand total
CUBE (a, b)(a, b), (a), (b), (): every combination
GROUPING SETS ((a, b), (a), ())exactly the sets listed; () is the grand total
key, ROLLUP (a, b)key is in every set: (key, a, b), (key, a), (key)
ROLLUP (a, (b, c))(b, c) counts as one item: (a, b, c), (a), ()

Example: Shares per symbol and buyer, a subtotal per symbol, and the grand total (sample data: sample)

SELECT symbol, buyer, sum(quantity) AS shares, GROUPING(symbol, buyer) AS level
FROM trade_log
WHERE date IN '2026-09-10' AND buyer IN (1, 58)
GROUP BY ROLLUP (symbol, buyer)
ORDER BY level, symbol, buyer;

Result:

symbol (SYMBOL)buyer (INT)shares (LONG)level (LONG)
CITY11700
COAST12000
COAST585600
TELCO1500
VOLT11500
VOLT582000
CITYNULL1701
COASTNULL7601
TELCONULL501
VOLTNULL3501
NULLNULL13303

(11 rows)

Example: CUBE: totals per symbol, per buyer, and both (sample data: sample)

SELECT symbol, buyer, count(*) AS trades
FROM trade_log
WHERE date IN '2026-09-10' AND buyer IN (1, 58) AND symbol IN ('CITY', 'VOLT')
GROUP BY CUBE (symbol, buyer)
ORDER BY GROUPING(symbol, buyer), symbol, buyer;

Result:

symbol (SYMBOL)buyer (INT)trades (LONG)
CITY13
VOLT12
VOLT581
CITYNULL3
VOLTNULL3
NULL15
NULL581
NULLNULL6

(8 rows)

Example: GROUPING SETS: a row per month, a row per symbol, and the grand total (sample data: sample)

SELECT date_trunc('month', time) AS month, symbol, round(avg(close), 2) AS avg_close
FROM ochl_day
WHERE symbol IN ('CITY', 'VOLT')
GROUP BY GROUPING SETS ((month), (symbol), ())
ORDER BY month, symbol;

Result:

month (TIMESTAMP)symbol (SYMBOL)avg_close (DOUBLE)
2025-12-01T00:00:00.000ZNULL367.29
2026-08-01T00:00:00.000ZNULL342.06
2026-09-01T00:00:00.000ZNULL339.74
NULLCITY511.81
NULLVOLT180.97
NULLNULL346.39

(6 rows)

Example: GROUPING() names the total rows (sample data: sample)

SELECT CASE WHEN GROUPING(symbol) = 1 THEN 'all symbols' ELSE symbol END AS symbol, sum(quantity) AS shares
FROM trade_log
WHERE date IN '2026-09-10'
GROUP BY ROLLUP (symbol)
ORDER BY GROUPING(symbol), shares DESC;

Result:

symbol (SYMBOL)shares (LONG)
COAST2850
CITY910
TELCO770
VOLT400
all symbols4930

(5 rows)

Example: GROUPING() needs a GROUP BY key (sample data: sample; shows an error)

SELECT symbol, GROUPING(buyer), count(*) FROM trade_log GROUP BY ROLLUP (symbol);

Result:

Error: line 1, column 25: GROUPING(buyer): the argument must be one of the GROUP BY expressions

Big GROUP BY: many groups and memory

A GROUP BY can make millions of groups, for example one per day, symbol and broker. KDB keeps it fast and keeps the server safe.

SituationWhat happens
Up to 5 keys, each a SYMBOL, a whole number, a DATE or a TIMESTAMP (also date_trunc or to_char of a time)The groups are added up a block of rows at a time, on all threads. 30 days of one row per symbol and broker (31,500 groups) take about 16 ms. Two keys that are each a SYMBOL or an INT (GROUP BY symbol, buyer) are kept as one number per group, which is 5-15% faster again. What else keeps a query on this fast path: GROUP BY with many groups.
A STRING or DOUBLE key, or 5 keys and moreThe same result, but the rows are grouped one by one, which is slower.
The groups need more than half of --max-result-memory (1 GB by default), or more than SET work_memRow by row: the rows of the groups that do not fit yet are written to sorted temporary files in the database folder (_tmp/) and added up after the scan, one group at a time. On the fast path (since 2026-10-02): the groups themselves, with their totals so far, go to 64 temporary files chosen by the key, and each file is finished on its own after the scan; about twice the time of the same query in memory, instead of many times. The result is the same, only slower (a sum of DOUBLE values may differ in its last digit). The files are removed when the query ends.
The temporary files would pass 16 GB for one queryThe query stops: the GROUP BY needs more than 16384 MB of temporary files (max_spill_bytes).
More than --max-groups groups (5,000,000 by default)The query stops: the query makes more than N groups (max_groups).

Implicit GROUP BY

Syntax:

SELECT key, ..., aggregate(...), ... FROM ... [WHERE ...]

Like QuestDB, KDB does not need the words GROUP BY. When the select list has an aggregate and there is no GROUP BY, every select item that is not an aggregate becomes a group key.

Select listGroups by
symbol, max(high)symbol
symbol, buyer, sum(quantity)symbol, buyer
upper(symbol) AS s, count(*)upper(symbol)
count(*), sum(volume)nothing: one row for all rows
'orders' AS t, count(*)nothing: a fixed value is not a key, so there is one row even when no row matches (as in PostgreSQL)
time, symbol, last(close) ... SAMPLE BY 1dsymbol (the time column is the bucket)

Example: No GROUP BY: symbol is not an aggregate, so it is the key (sample data: sample)

SELECT symbol, max(high) AS highest FROM ochl_day WHERE time IN '2026-09';

Result:

symbol (SYMBOL)highest (DOUBLE)
CITY512.4
COAST398.8
VOLT192.1

(3 rows)

Example: The same with GROUP BY (sample data: sample)

SELECT symbol, max(high) AS highest FROM ochl_day WHERE time IN '2026-09' GROUP BY symbol;

Result:

symbol (SYMBOL)highest (DOUBLE)
CITY512.4
COAST398.8
VOLT192.1

(3 rows)

Example: Careful: one more column is one more key (sample data: sample)

SELECT symbol, round(close / 10) AS price_band, count(*) AS days
FROM ochl_day
WHERE time IN '2026-09' AND symbol = 'COAST';

Result:

symbol (SYMBOL)price_band (DOUBLE)days (LONG)
COAST373
COAST385

(2 rows)

Example: Only aggregates: one row for the whole table (sample data: query-app)

SELECT count(*) AS lines, sum(debit) AS total_debit, sum(credit) AS total_credit FROM ledger;

Result:

lines (LONG)total_debit (DECIMAL(2))total_credit (DECIMAL(2))
171089880.001089880.00

(1 row)

HAVING

Syntax:

SELECT ... FROM ... [WHERE ...] GROUP BY ... HAVING condition

HAVING keeps only the groups where the condition is true. It runs after the groups are made, so it can use aggregates. WHERE runs before, on single rows.

WHEREHAVING
runs onrowsgroups
can use aggregates (sum(x) > 10)noyes
runsbefore groupingafter grouping

Example: Symbols with 800 shares or more on 2026-09-10 (sample data: sample)

SELECT symbol, sum(quantity) AS shares, count(*) AS trades
FROM trade_log
WHERE date IN '2026-09-10'
GROUP BY symbol
HAVING sum(quantity) >= 800
ORDER BY shares DESC;

Result:

symbol (SYMBOL)shares (LONG)trades (LONG)
COAST28509
CITY9107

(2 rows)

Example: Symbols that moved more than 20 dollars in September (sample data: sample)

SELECT symbol, max(high) - min(low) AS price_range
FROM ochl_day
WHERE time IN '2026-09'
GROUP BY symbol
HAVING max(high) - min(low) > 20;

Result:

symbol (SYMBOL)price_range (DOUBLE)
COAST34.7
VOLT24.8

(2 rows)

Example: Journal entries that do not balance (none, in a correct book) (sample data: query-app)

SELECT entry_id, sum(debit) AS debit, sum(credit) AS credit
FROM ledger
GROUP BY entry_id
HAVING sum(debit) != sum(credit);

Result:

entry_id (INT)debit (STRING)credit (STRING)

(0 rows)

Example: Cities with two or more app users (sample data: query-app)

SELECT city, count(*) AS users
FROM app_users
WHERE city IS NOT NULL
GROUP BY city
HAVING count(*) >= 2;

Result:

city (STRING)users (LONG)
Kathmandu2
Pokhara2

(2 rows)

Example: An alias cannot be used in HAVING (sample data: sample; shows an error)

SELECT buyer, sum(amount) AS bought FROM trade_log GROUP BY buyer HAVING bought > 500000;

Result:

Error: line 1, column 74: unknown column 'bought' (table 'trade_log' has: date transaction_id symbol buyer seller quantity rate amount)

Example: HAVING without groups or aggregates (sample data: sample; shows an error)

SELECT symbol FROM trade_log HAVING symbol = 'TELCO';

Result:

Error: line 1, column 44: HAVING needs GROUP BY or aggregates

Columns that are not grouped

Syntax:

SELECT key, other_column, aggregate(...) FROM ... GROUP BY key

In PostgreSQL, a column in the select list that is neither a GROUP BY key nor inside an aggregate is an error. KDB gives no error. It shows the value of one row of the group (the first row it read), which is usually not the value you want.

Example: time is not grouped: KDB shows the time of one row (2025-12-28), not of the highest price (sample data: sample)

SELECT symbol, time, max(high) AS highest FROM ochl_day GROUP BY symbol;

Result:

symbol (SYMBOL)time (TIMESTAMP)highest (DOUBLE)
CITY2025-12-28T00:00:00.000Z546.2
COAST2025-12-28T00:00:00.000Z429.9
VOLT2025-12-28T00:00:00.000Z199.7

(3 rows)

Example: Say what you want: the first and the last day of each symbol (sample data: sample)

SELECT symbol, min(time) AS first_day, max(time) AS last_day, max(high) AS highest
FROM ochl_day
GROUP BY symbol;

Result:

symbol (SYMBOL)first_day (TIMESTAMP)last_day (TIMESTAMP)highest (DOUBLE)
CITY2025-12-28T00:00:00.000Z2026-09-10T00:00:00.000Z546.2
COAST2025-12-28T00:00:00.000Z2026-09-10T00:00:00.000Z429.9
VOLT2025-12-28T00:00:00.000Z2026-09-10T00:00:00.000Z199.7

(3 rows)

Example: The latest close of each symbol: last(close) uses the time order (sample data: sample)

SELECT symbol, last(close) AS latest_close, first(close) AS first_close
FROM ochl_day
GROUP BY symbol;

Result:

symbol (SYMBOL)latest_close (DOUBLE)first_close (DOUBLE)
CITY504.9542.2
COAST376.1424.8
VOLT191.2191.7

(3 rows)