# 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](functions-aggregate.md), [SAMPLE BY](sample-by.md), [SELECT (distinct)](select.md#distinct), [ORDER BY, LIMIT, OFFSET](order-by-limit.md), [Window and series functions](functions-window.md), [Joins](joins.md) `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](functions-aggregate.md). `HAVING` keeps only some groups, like `WHERE` keeps only some rows. For groups by time (candles per minute, day, week, month), use [SAMPLE BY](sample-by.md). **Example: September 2026 summary per symbol** (sample data: sample) ```sql 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: ```sql 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:** `NULL` key values make their own group. Aggregates skip `NULL` values (`count(*)` counts every row). - **Notes:** a select **alias can be used in `GROUP BY`**: `GROUP BY month` groups by the column written as `date_trunc('month', time) AS month`. Writing the expression again does the same thing. An alias still cannot be used in `HAVING`. - `GROUP BY 1` means the first column of the select list, as in PostgreSQL (`GROUP BY 1, 2`: the first two). It cannot point at an aggregate. - `ORDER BY` after `GROUP BY` can 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 to `GROUP BY`. It runs **after** the groups are made, over the grouped rows, as in PostgreSQL: its arguments and its `ORDER BY` use the group keys and aggregates, like `rank() OVER (ORDER BY sum(quantity) DESC)`. See [window functions](functions-window.md). - With no matching rows, `GROUP BY` gives **0 rows**. Without `GROUP BY`, aggregates give **one row** (`count(*)` is `0`, `sum` is `NULL`). **Example: Broker turnover as buyer on 2026-09-10** (sample data: sample) ```sql 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) ```sql 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) ```sql 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) ```sql 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) ```sql 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) ```sql 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) ```sql 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) ```sql 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) ```sql 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) ```sql 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) ```sql 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) ```sql 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) ```sql 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: ```sql 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 `NULL`** there. Aggregates still use the real values of all the rows of the group. - `GROUPING(a, b)` tells a subtotal `NULL` from a `NULL` in the data: one bit per argument, `1` when that key is not grouped in the row's set. The leftmost argument is the highest bit, so with `ROLLUP (a, b)` it is `0` for the groups, `1` for the subtotals and `3` for the grand total. Its arguments must be `GROUP BY` keys. - `ORDER BY`, `LIMIT` and `DISTINCT` are for the whole result. `ORDER BY` may use a result column, an aggregate or `GROUPING(...)`. - 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)`). `CUBE` takes 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) ```sql 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) ```sql 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) ```sql 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) ```sql 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) ```sql 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](performance-tips.md#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 BY` and a read-only database never write temporary files: they keep every group in memory, and only `--max-groups` stops them. - `ORDER BY ... LIMIT` over hundreds of thousands of groups still turns every group into a row before the sort, so it is slower than the same query without `ORDER BY`. - The limits are server settings: see [protection](protection.md) and [limits](limits.md). - To make fewer groups: add a `WHERE` on time, group by fewer columns, or use a longer [SAMPLE BY](sample-by.md) interval. ## Implicit GROUP BY Syntax: ```sql 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 BY` of those items. - **Notes:** in PostgreSQL this is an error. In KDB it is allowed, but **every extra column adds a key**: adding `close` to `SELECT symbol, count(*)` splits the groups by close price. Writing `GROUP BY` makes the query clearer for readers. **Example: No GROUP BY: symbol is not an aggregate, so it is the key** (sample data: sample) ```sql 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) ```sql 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) ```sql 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) ```sql 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: ```sql 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 `NULL` removes the group. - **Notes:** a select **alias cannot be used in `HAVING`**: write `HAVING sum(amount) > 500000`, not `HAVING bought > 500000`. - `HAVING` needs `GROUP BY` or an aggregate in the query. A key column can be used too: `HAVING symbol LIKE 'N%'` (but `WHERE` is 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) ```sql 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) ```sql 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) ```sql 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) ```sql 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) ```sql 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) ```sql 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: ```sql 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](group-by.md#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) ```sql 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) ```sql 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) ```sql 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)