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.
- Category: Querying
- Keywords: group, groups, spill, ROLLUP, CUBE, GROUPING SETS, GROUPING, grand total, rollup, cube, grouping sets, subtotals, temporary files, many groups, max_groups, window with GROUP BY, GROUP BY, HAVING, aggregate, totals, subtotal, summary, report, implicit GROUP BY, group key, not grouped, count per, sum per, group by, having, implicit
- See also: Aggregate functions, SAMPLE BY, SELECT (distinct), ORDER BY, LIMIT, OFFSET, Window and series functions, Joins
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) |
|---|---|---|---|---|---|
| COAST | 8 | 364.1 | 398.8 | 374.73 | 910270 |
| CITY | 8 | 494.1 | 512.4 | 500.18 | 876210 |
| VOLT | 8 | 167.3 | 192.1 | 179.3 | 713310 |
(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)).
| Part | Meaning |
|---|---|
expression, ... | the group keys: one output row for each different combination |
aggregate(...) | a value computed from all the rows of the group |
- Returns: one row per group. The order of the groups is not fixed: add
ORDER BY. - NULL:
NULLkey values make their own group. Aggregates skipNULLvalues (count(*)counts every row). - Notes: a select alias can be used in
GROUP BY:GROUP BY monthgroups by the column written asdate_trunc('month', time) AS month. Writing the expression again does the same thing. An alias still cannot be used inHAVING. GROUP BY 1means the first column of the select list, as in PostgreSQL (GROUP BY 1, 2: the first two). It cannot point at an aggregate.ORDER BYafterGROUP BYcan use the output columns (names, aliases, positions) and expressions of them (ORDER BY q / n), but not other table columns and not new aggregates.- A window function (
rank() OVER,lag(...) OVER,sum(...) OVER ()) can sit next toGROUP BY. It runs after the groups are made, over the grouped rows, as in PostgreSQL: its arguments and itsORDER BYuse the group keys and aggregates, likerank() OVER (ORDER BY sum(quantity) DESC). See window functions. - With no matching rows,
GROUP BYgives 0 rows. WithoutGROUP BY, aggregates give one row (count(*)is0,sumisNULL).
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) |
|---|---|---|---|
| 49 | Online Securities | 5 | 960370 |
| 45 | Imperial Securities | 5 | 695950 |
| 58 | Northgate Securities | 4 | 260190 |
| 1 | Keystone Securities | 7 | 239420 |
| 34 | Vision Securities | 3 | 205450 |
(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) |
|---|---|---|
| 1 | CITY | 170 |
| 1 | COAST | 200 |
| 1 | TELCO | 50 |
| 1 | VOLT | 150 |
| 58 | COAST | 560 |
| 58 | VOLT | 200 |
(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.000Z | 540.9 | 4 |
| 2026-08-01T00:00:00.000Z | 508.49 | 7 |
| 2026-09-01T00:00:00.000Z | 500.18 | 8 |
(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)) |
|---|---|---|---|---|
| 1000 | Cash | 30000.00 | 25000.00 | 5000.00 |
| 1100 | Bank - City | 518850.00 | 288530.00 | 230320.00 |
| 1200 | Shares at cost | 257500.00 | 10300.00 | 247200.00 |
| 2000 | Broker payable | 257500.00 | 257500.00 | 0.00 |
| 3000 | Owner capital | 0 | 500000.00 | -500000.00 |
| 4000 | Dividend income | 0 | 6250.00 | -6250.00 |
| 4100 | Gain on sale | 0 | 2300.00 | -2300.00 |
| 5000 | Broker commission | 1030.00 | 0 | 1030.00 |
| 5100 | Office rent | 25000.00 | 0 | 25000.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) |
|---|---|---|---|
| free | 4 | 2 | 2026-02-11T03:45:00.000Z |
| pro | 3 | 3 | 2026-01-05T04:15:00.000Z |
| admin | 1 | 1 | 2026-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? | 6 | 2026-09-01T06:15:00.000Z |
| Hydropower IPOs this month | 2 | 2026-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) |
|---|---|---|
| TELCO | 6 | 2770 |
| COAST | 11 | 3070 |
| CITY | 11 | 2420 |
| VOLT | 4 | 400 |
(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) |
|---|---|
| 0 | NULL |
(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.000Z | 12 |
| 2026-08-01T00:00:00.000Z | 21 |
| 2026-09-01T00:00:00.000Z | 24 |
(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) |
|---|---|
| CITY | 2420 |
| COAST | 3070 |
| TELCO | 2770 |
| VOLT | 400 |
(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) |
|---|---|---|
| CITY | 11 | 34.4 |
| COAST | 11 | 34.4 |
| TELCO | 6 | 18.8 |
| VOLT | 4 | 12.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) |
|---|---|---|
| COAST | 3070 | 1 |
| TELCO | 2770 | 2 |
| CITY | 2420 | 3 |
| VOLT | 400 | 4 |
(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.000Z | 540.9 | NULL |
| 2026-08-01T00:00:00.000Z | 508.49 | 540.9 |
| 2026-09-01T00:00:00.000Z | 500.18 | 508.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.
| Written | The 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), () |
- Returns: the rows of each set, one set after the other. A key that is not in the row's set is
NULLthere. Aggregates still use the real values of all the rows of the group. GROUPING(a, b)tells a subtotalNULLfrom aNULLin the data: one bit per argument,1when that key is not grouped in the row's set. The leftmost argument is the highest bit, so withROLLUP (a, b)it is0for the groups,1for the subtotals and3for the grand total. Its arguments must beGROUP BYkeys.ORDER BY,LIMITandDISTINCTare for the whole result.ORDER BYmay use a result column, an aggregate orGROUPING(...).- The grand total
()always gives one row, even when no row matches. - Notes: a key may be a column, an expression, an alias or a position (
ROLLUP (1)).CUBEtakes at most 12 items; a query may have at most 4096 sets. - Not with
SAMPLE BY,LATEST ON,DISTINCT ON,SELECT *or window functions (group in a subquery, and put the window outside).
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) |
|---|---|---|---|
| CITY | 1 | 170 | 0 |
| COAST | 1 | 200 | 0 |
| COAST | 58 | 560 | 0 |
| TELCO | 1 | 50 | 0 |
| VOLT | 1 | 150 | 0 |
| VOLT | 58 | 200 | 0 |
| CITY | NULL | 170 | 1 |
| COAST | NULL | 760 | 1 |
| TELCO | NULL | 50 | 1 |
| VOLT | NULL | 350 | 1 |
| NULL | NULL | 1330 | 3 |
(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) |
|---|---|---|
| CITY | 1 | 3 |
| VOLT | 1 | 2 |
| VOLT | 58 | 1 |
| CITY | NULL | 3 |
| VOLT | NULL | 3 |
| NULL | 1 | 5 |
| NULL | 58 | 1 |
| NULL | NULL | 6 |
(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.000Z | NULL | 367.29 |
| 2026-08-01T00:00:00.000Z | NULL | 342.06 |
| 2026-09-01T00:00:00.000Z | NULL | 339.74 |
| NULL | CITY | 511.81 |
| NULL | VOLT | 180.97 |
| NULL | NULL | 346.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) |
|---|---|
| COAST | 2850 |
| CITY | 910 |
| TELCO | 770 |
| VOLT | 400 |
| all symbols | 4930 |
(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.
| Situation | What 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 more | The 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_mem | Row 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 query | The 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). |
- Notes:
SAMPLE BYand a read-only database never write temporary files: they keep every group in memory, and only--max-groupsstops them. ORDER BY ... LIMITover hundreds of thousands of groups still turns every group into a row before the sort, so it is slower than the same query withoutORDER BY.- The limits are server settings: see protection and limits.
- To make fewer groups: add a
WHEREon time, group by fewer columns, or use a longer SAMPLE BY interval.
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 list | Groups 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 1d | symbol (the time column is the bucket) |
- Returns: the same as the query with
GROUP BYof those items. - Notes: in PostgreSQL this is an error. In KDB it is allowed, but every extra column adds a key: adding
closetoSELECT symbol, count(*)splits the groups by close price. WritingGROUP BYmakes the query clearer for readers.
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) |
|---|---|
| CITY | 512.4 |
| COAST | 398.8 |
| VOLT | 192.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) |
|---|---|
| CITY | 512.4 |
| COAST | 398.8 |
| VOLT | 192.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) |
|---|---|---|
| COAST | 37 | 3 |
| COAST | 38 | 5 |
(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)) |
|---|---|---|
| 17 | 1089880.00 | 1089880.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.
WHERE | HAVING | |
|---|---|---|
| runs on | rows | groups |
can use aggregates (sum(x) > 10) | no | yes |
| runs | before grouping | after grouping |
- Returns: only the groups where the condition is
true. - NULL: a condition that is
NULLremoves the group. - Notes: a select alias cannot be used in
HAVING: writeHAVING sum(amount) > 500000, notHAVING bought > 500000. HAVINGneedsGROUP BYor an aggregate in the query. A key column can be used too:HAVING symbol LIKE 'N%'(butWHEREis faster for that).- Put conditions on single rows in
WHERE: fewer rows are grouped, and time conditions skip partitions.
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) |
|---|---|---|
| COAST | 2850 | 9 |
| CITY | 910 | 7 |
(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) |
|---|---|
| COAST | 34.7 |
| VOLT | 24.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) |
|---|---|
| Kathmandu | 2 |
| Pokhara | 2 |
(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.
- Notes: do not use this. Put the column in
GROUP BY, or use an aggregate that says which value you want:first(x),last(x)(by time),min(x),max(x). - Without
GROUP BY, the same column becomes a key instead (see implicit GROUP BY).
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) |
|---|---|---|
| CITY | 2025-12-28T00:00:00.000Z | 546.2 |
| COAST | 2025-12-28T00:00:00.000Z | 429.9 |
| VOLT | 2025-12-28T00:00:00.000Z | 199.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) |
|---|---|---|---|
| CITY | 2025-12-28T00:00:00.000Z | 2026-09-10T00:00:00.000Z | 546.2 |
| COAST | 2025-12-28T00:00:00.000Z | 2026-09-10T00:00:00.000Z | 429.9 |
| VOLT | 2025-12-28T00:00:00.000Z | 2026-09-10T00:00:00.000Z | 199.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) |
|---|---|---|
| CITY | 504.9 | 542.2 |
| COAST | 376.1 | 424.8 |
| VOLT | 191.2 | 191.7 |
(3 rows)