# 5. Queries - Category: Tutorial ## What you will learn - `SELECT` with `WHERE`, `ORDER BY`, `GROUP BY` and the special time filters. - Candles with `SAMPLE BY` (1m, 15m, 1h in Nepal time, 1d, Sunday weeks, months), `LATEST ON` and joins. - Indicators (`sma`, `ema`, `rsi`, `atr`, ...), window functions, views, and a cookbook of market-data queries. The examples use the database `market` with the **fresh** sample data (load it again if you ran chapter 4). Each example is a file in `examples/05-queries/`. ## The order KarelaDB works in A query is written in this order: ```sql SELECT ... FROM ... [JOIN ...] WHERE ... [LATEST ON ...] [SAMPLE BY ...] [GROUP BY ...] [HAVING ...] [UNION ALL SELECT ...] [ORDER BY ...] [LIMIT n [OFFSET m]] ``` KarelaDB runs it in this order: | Step | Part | What happens | |---|---|---| | 1 | `FROM`, `JOIN` | Read the tables | | 2 | `WHERE` | Keep only matching rows. A time condition skips whole partitions. | | 3 | `LATEST ON` | Keep the newest row per group | | 4 | `SAMPLE BY` / `GROUP BY` | Make groups, compute `sum`, `max`, ... | | 5 | `HAVING` | Keep only matching groups | | 6 | window functions | `sma`, `lag`, `row_number`, ... | | 7 | `SELECT` list, `DISTINCT` | Compute the output columns | | 8 | `UNION ALL` | Add rows of the next query | | 9 | `ORDER BY`, `LIMIT`, `OFFSET` | Sort and cut | ## SELECT basics ```sql SELECT * FROM brokers; SELECT symbol, close, round(high - low, 2) AS day_range, round((close - open) / open * 100, 2) AS change_pct FROM ochl_day WHERE time IN '2026-09-10'; ``` ```text id | name ---+-------------------- 1 | Keystone Securities 34 | Vision Securities 45 | Imperial Securities 49 | Online Securities 58 | Northgate Securities (5 rows, 0.3 ms) symbol | close | day_range | change_pct -------+-------+-----------+----------- CITY | 504.9 | 6.3 | 0.06 COAST | 376.1 | 8.8 | -1.05 VOLT | 191.2 | 7.9 | 3.02 (3 rows, 0.2 ms) ``` - `AS name` gives a column a name. `AS` is optional: `close c` also works. So a **missing comma** (`symbol close`) is not an error: it names the column `close`. - `/` always gives a decimal result: `7 / 2` is `3.5`. - Without `round()`, `high - low` can show `6.30000000000001`. That is normal for DOUBLE. ## WHERE ```sql SELECT time, symbol, close FROM ochl_day WHERE symbol IN ('CITY', 'VOLT') AND close > 190 AND time IN '2026-09'; SELECT symbol, name FROM securities WHERE name LIKE '%Bank%'; SELECT time, symbol, close FROM ochl_day WHERE symbol = 'COAST' AND time BETWEEN '2026-09-07' AND '2026-09-09'; ``` ```text time | symbol | close -------------------------+--------+------ 2026-09-01T00:00:00.000Z | CITY | 502.4 2026-09-02T00:00:00.000Z | CITY | 496.4 2026-09-03T00:00:00.000Z | CITY | 497.9 2026-09-06T00:00:00.000Z | CITY | 499 2026-09-07T00:00:00.000Z | CITY | 498.7 2026-09-08T00:00:00.000Z | CITY | 497.5 2026-09-09T00:00:00.000Z | CITY | 504.6 2026-09-10T00:00:00.000Z | CITY | 504.9 2026-09-10T00:00:00.000Z | VOLT | 191.2 (9 rows, 0.2 ms) symbol | name -------+---------------------- CITY | City Bank Limited COAST | Coastal Bank Limited (2 rows, 0.1 ms) time | symbol | close -------------------------+--------+------ 2026-09-07T00:00:00.000Z | COAST | 379.2 2026-09-08T00:00:00.000Z | COAST | 377.2 2026-09-09T00:00:00.000Z | COAST | 380.1 (3 rows, 0.0 ms) ``` | Condition | Meaning | |---|---| | `=`, `!=` or `<>`, `<`, `<=`, `>`, `>=` | compare | | `a AND b`, `a OR b`, `NOT a` | combine. Use `( )` when you mix AND and OR. | | `x IN (1, 2, 3)` / `x NOT IN (...)` | one of a list | | `x BETWEEN a AND b` | `a <= x <= b` (both ends included) | | `x LIKE 'CI%'` | text pattern: `%` = any text, `_` = one character | | `x IS NULL` / `x IS NOT NULL` | no value / has a value | | `time IN '2026'` | the whole year 2026 (UTC) | | `time IN '2026-09'` | the whole month (UTC) | | `time IN '2026-09-10'` | the whole day (UTC) | | `time IN '2026-09-10T11'` | that hour (UTC) | - A text like `'2026-09-07'` compared with a time column is read as a time. - `BETWEEN '2026-09-07' AND '2026-09-09'` ends at `2026-09-09T00:00:00`. For whole days, use `time >= '2026-09-07' AND time < '2026-09-10'`. - For Nepal times, add `+05:45` (chapter 4). - `NULL` never matches `=`. Use `IS NULL`. ## ORDER BY, LIMIT, OFFSET ```sql SELECT time, symbol, volume FROM ochl_day ORDER BY volume DESC LIMIT 3; SELECT time, symbol, volume FROM ochl_day ORDER BY volume DESC LIMIT 3 OFFSET 3; ``` ```text time | symbol | volume -------------------------+--------+------- 2026-09-09T00:00:00.000Z | COAST | 186330 2026-09-10T00:00:00.000Z | COAST | 186200 2026-09-10T00:00:00.000Z | CITY | 183040 (3 rows, 0.3 ms) time | symbol | volume -------------------------+--------+------- 2026-09-09T00:00:00.000Z | CITY | 182390 2026-08-31T00:00:00.000Z | CITY | 159240 2026-09-03T00:00:00.000Z | VOLT | 145930 (3 rows, 0.1 ms) ``` - `ASC` (small first) is the default. `DESC` = big first. - `ORDER BY 2` sorts by the second output column. - `NULLS FIRST` / `NULLS LAST` choose where NULL goes. - Without ORDER BY, time-series rows come sorted by partition, then by `SORT BY`. Do not depend on it: add ORDER BY when order matters. ## GROUP BY and aggregates ```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; ``` ```text symbol | days | lowest | highest | avg_close | total_volume -------+------+--------+---------+-----------+------------- 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, 0.3 ms) ``` Aggregates: `count(*)`, `count(x)`, `count(DISTINCT x)`, `sum`, `avg`, `min`, `max`, `first`, `last`, `median`, `percentile(x, 0.9)`, `stddev`, `variance`, `vwap(price, volume)` and more (chapter 11). `first(x)` and `last(x)` use the **time order** of the rows. `first(open)` is the open of the earliest row. A `GROUP BY` with very many groups still works. When the groups need more than half of the memory for results, KarelaDB writes some of them to temporary files and adds them up at the end. The result is the same, only slower. `kareladb serve` stops a query with more than 5,000,000 groups (`--max-groups`, chapter 21). ### The implicit GROUP BY rule Like QuestDB, KarelaDB does not need GROUP BY. **Every column that is not inside an aggregate becomes a group key.** ```sql -- No GROUP BY: symbol is not inside an aggregate, so KarelaDB groups by symbol. SELECT symbol, max(high) AS highest FROM ochl_day WHERE time IN '2026-09'; ``` ```text symbol | highest -------+-------- CITY | 512.4 COAST | 398.8 VOLT | 192.1 (3 rows, 0.3 ms) ``` In normal SQL this would be an error. Writing `GROUP BY symbol` gives the same result and is clearer for readers. ### HAVING `HAVING` filters groups (WHERE filters rows). ```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; ``` ```text symbol | shares | trades -------+--------+------- COAST | 2850 | 9 CITY | 910 | 7 (2 rows, 0.4 ms) ``` ## SAMPLE BY: candles `SAMPLE BY interval` puts rows into time buckets. It works on time-series tables. The time column in the output is the **start** of each bucket. Other plain columns (like `symbol`) become group keys, as above. Intervals: `1s`, `1m` (minute), `5m`, `15m`, `1h`, `1d`, `1w` (week), `1M` (month, capital M), `1y`. ### 15-minute candles ```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; ``` ```text time | open | high | low | close | volume -------------------------+-------+-------+-------+-------+------- 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, 1.0 ms) ``` 05:15 UTC is 11:00 Nepal time. For 1m, 5m and 15m, UTC buckets and Nepal buckets are the same. ### 1-hour candles: use TIME ZONE Without a time zone, hours start at the full **UTC** hour. The sample market opens at 05:15 UTC, so you get a small 45-minute candle, three full ones, and a 15-minute one: ```sql SELECT time, first(open) AS open, last(close) AS close, count(*) AS minutes FROM ochl_minutes WHERE symbol = 'CITY' AND time IN '2026-09-10' SAMPLE BY 1h; ``` ```text time | open | close | minutes -------------------------+-------+-------+-------- 2026-09-10T05:00:00.000Z | 504.6 | 506.4 | 45 2026-09-10T06:00:00.000Z | 506.4 | 501.2 | 60 2026-09-10T07:00:00.000Z | 501.2 | 502.6 | 60 2026-09-10T08:00:00.000Z | 502.6 | 505 | 60 2026-09-10T09:00:00.000Z | 505 | 504.9 | 15 (5 rows, 0.8 ms) ``` With `TIME ZONE 'Asia/Kathmandu'`, buckets follow the Nepal clock: four clean candles, 11:00, 12:00, 13:00 and 14:00 NPT. ```sql SELECT time, 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 symbol = 'CITY' AND time IN '2026-09-10' SAMPLE BY 1h TIME ZONE 'Asia/Kathmandu'; ``` ```text time | open | high | low | close | volume | minutes -------------------------+-------+-------+-------+-------+--------+-------- 2026-09-10T05:15:00.000Z | 504.6 | 507 | 503.8 | 505.3 | 47020 | 60 2026-09-10T06:15:00.000Z | 505.3 | 506.6 | 500.7 | 502.7 | 41230 | 60 2026-09-10T07:15:00.000Z | 502.7 | 504.1 | 501.5 | 502.6 | 46180 | 60 2026-09-10T08:15:00.000Z | 502.6 | 507 | 502.2 | 504.9 | 48610 | 60 (4 rows, 0.7 ms) ``` The bucket times are still shown in UTC (`05:15Z` = 11:00 NPT). Options of SAMPLE BY (any order, after the interval): | Option | Meaning | |---|---| | `TIME ZONE 'Asia/Kathmandu'` | Buckets follow that clock. Also `'+05:45'`, `'UTC'`, `'Asia/Kolkata'` and a few more fixed zones. Zones with daylight saving (like `'Europe/London'`) are refused. | | `WITH OFFSET '+00:15'` | Moves bucket starts by that amount. The sign (`+` or `-`) is needed. | | `ALIGN TO CALENDAR` | Buckets on the clock (the default). | | `ALIGN TO FIRST OBSERVATION` | Buckets start at the first row's time. Not with months or years. | | `WEEK START SUNDAY` / `MONDAY` | For `1w`: which day starts the week. Default: Monday. | | `FILL(NONE / NULL / PREV / LINEAR / value)` | What to do with empty buckets (see below). | ### Daily candles from minute candles ```sql -- Daily candles made from minute candles. SELECT time, symbol, first(open) AS open, max(high) AS high, min(low) AS low, last(close) AS close, sum(volume) AS volume FROM ochl_minutes SAMPLE BY 1d ORDER BY symbol, time; ``` ```text time | symbol | open | high | low | close | volume -------------------------+--------+-------+-------+-------+-------+------- 2026-09-09T00:00:00.000Z | CITY | 512 | 512.4 | 504 | 504.6 | 182390 2026-09-10T00:00:00.000Z | CITY | 504.6 | 507 | 500.7 | 504.9 | 183040 2026-09-09T00:00:00.000Z | COAST | 398 | 398.8 | 379.2 | 380.1 | 186330 2026-09-10T00:00:00.000Z | COAST | 380.1 | 382 | 373.2 | 376.1 | 186200 (4 rows, 1.0 ms) ``` Without TIME ZONE the day bucket is stamped `00:00Z`, like the rows in `ochl_day`. With `TIME ZONE 'Asia/Kathmandu'` the bucket starts at Nepal midnight, which is shown as `18:15Z` of the day before. The trading hours are inside the same day either way. ### Weekly candles: Sunday to Thursday The sample market has a Sunday-to-Thursday trading week (as on some exchanges). Use `WEEK START SUNDAY`: ```sql SELECT time, symbol, first(open) AS open, max(high) AS high, min(low) AS low, last(close) AS close, sum(volume) AS volume FROM ochl_day WHERE symbol = 'CITY' AND time IN '2026' SAMPLE BY 1w WEEK START SUNDAY; ``` ```text time | symbol | open | high | low | close | volume -------------------------+--------+-------+-------+-------+-------+------- 2026-08-23T00:00:00.000Z | CITY | 505 | 517 | 502 | 505.9 | 601770 2026-08-30T00:00:00.000Z | CITY | 507.3 | 511.2 | 494.1 | 497.9 | 538130 2026-09-06T00:00:00.000Z | CITY | 497.4 | 512.4 | 495 | 504.9 | 591860 (3 rows, 0.3 ms) ``` 2026-08-23, 08-30 and 09-06 are Sundays. ### Monthly candles `1M` is a calendar month (capital `M`; small `m` is minutes). ```sql SELECT time, symbol, first(open) AS open, max(high) AS high, min(low) AS low, last(close) AS close FROM ochl_day WHERE symbol = 'VOLT' SAMPLE BY 1M; ``` ```text time | symbol | open | high | low | close -------------------------+--------+-------+-------+-------+------ 2025-12-01T00:00:00.000Z | VOLT | 187.7 | 199.7 | 187.5 | 194.3 2026-08-01T00:00:00.000Z | VOLT | 182.4 | 183.6 | 170.6 | 172.6 2026-09-01T00:00:00.000Z | VOLT | 171.3 | 192.1 | 167.3 | 191.2 (3 rows, 0.6 ms) ``` Months with no rows (January to July 2026 here) are not shown. Use FILL if you need them. ### FILL: empty buckets COAST had no trades from 12:31 to 12:34 Nepal time. Without FILL, those minutes are missing: ```sql -- COAST had no trades from 12:31 to 12:34 Nepal time. SELECT time, last(close) AS close, sum(volume) AS volume FROM ochl_minutes WHERE symbol = 'COAST' AND time >= '2026-09-10T12:30:00+05:45' AND time < '2026-09-10T12:36:00+05:45' SAMPLE BY 1m; SELECT time, last(close) AS close, sum(volume) AS volume FROM ochl_minutes WHERE symbol = 'COAST' AND time >= '2026-09-10T12:30:00+05:45' AND time < '2026-09-10T12:36:00+05:45' SAMPLE BY 1m FILL(PREV); ``` ```text time | close | volume -------------------------+-------+------- 2026-09-10T06:45:00.000Z | 380.2 | 620 2026-09-10T06:50:00.000Z | 379.8 | 800 (2 rows, 0.7 ms) time | close | volume -------------------------+-------+------- 2026-09-10T06:45:00.000Z | 380.2 | 620 2026-09-10T06:46:00.000Z | 380.2 | 620 2026-09-10T06:47:00.000Z | 380.2 | 620 2026-09-10T06:48:00.000Z | 380.2 | 620 2026-09-10T06:49:00.000Z | 380.2 | 620 2026-09-10T06:50:00.000Z | 379.8 | 800 (6 rows, 0.3 ms) ``` | FILL | Empty bucket gets | Example value for close | |---|---|---| | (none) or `FILL(NONE)` | no row | - | | `FILL(NULL)` | NULL | `NULL` | | `FILL(PREV)` | the values of the bucket before | `380.2` | | `FILL(LINEAR)` | a value on the straight line between the neighbors | `380.12`, `380.04`, ... | | `FILL(0)` | that constant | `0` | FILL uses one rule for **every** aggregate column. `FILL(PREV)` also copies the volume (620 above), which is wrong for volume. If you need "close = previous, volume = 0", get the rows with `FILL(NULL)` and handle them in your program, or run two queries. The outputs of FILL(NULL), LINEAR and 0 are in `examples/05-queries/14-fill-other.out`. FILL only fills between the first and the last bucket that have data. ## LATEST ON: the newest row per symbol ```sql SELECT symbol, ltp, pct_change, timestamp FROM live_data LATEST ON timestamp PARTITION BY symbol ORDER BY pct_change DESC; ``` ```text symbol | ltp | pct_change | timestamp -------+-------+------------+------------------------- CITY | 527.1 | 2.71 | 2026-09-10T09:14:00.000Z TELCO | 897.7 | 1.9 | 2026-09-10T09:14:00.000Z COAST | 401.9 | 0.7 | 2026-09-10T09:14:00.000Z VOLT | 184 | -0.86 | 2026-09-10T09:14:00.000Z (4 rows, 0.2 ms) ``` - `LATEST ON