# UNION and subqueries > Put the rows of two queries together, and use one query inside another. - Category: Querying - Keywords: UNION, UNION ALL, ALL, subquery, sub-query, nested query, inner query, EXISTS, NOT EXISTS, IN (SELECT), NOT IN (SELECT), scalar subquery, derived table, correlated subquery, INTERSECT, EXCEPT, UNION DISTINCT, combine results - See also: [SELECT (from)](select.md#from), [WHERE (in-list)](where.md#in-list), [Joins (join)](joins.md#join), [Joins (left-join)](joins.md#left-join), [GROUP BY and HAVING (group-by)](group-by.md#group-by), [ORDER BY, LIMIT, OFFSET (order-by)](order-by-limit.md#order-by), [Views](views.md) A **subquery** is a `SELECT` inside another statement, always in `( )`. `UNION ALL` puts the rows of two or more `SELECT`s one after the other. - A subquery that uses no column of the outer query runs **once**, before the outer query. Then KarelaDB uses the result like a fixed value, a fixed list or a small table. - A subquery **may use columns of the outer query** (a "correlated" subquery): see [Correlated subqueries](union-subqueries.md#correlated). - Subqueries work in `SELECT`, `INSERT`, `UPDATE` and `DELETE` (also inside `CREATE VIEW` and `EXPLAIN`). - Subqueries and views can be nested at most 16 levels deep. **Example: Brokers who bought or sold TELCO: a subquery with UNION ALL inside** (sample data: sample) ```sql SELECT id, name FROM brokers WHERE id IN (SELECT buyer FROM trade_log WHERE symbol = 'TELCO' UNION ALL SELECT seller FROM trade_log WHERE symbol = 'TELCO') ORDER BY name; ``` Result: | id (INT) | name (STRING) | |---|---| | 45 | Imperial Securities | | 1 | Keystone Securities | | 58 | Northgate Securities | | 49 | Online Securities | | 34 | Vision Securities | (5 rows) ## UNION ALL Syntax: ```sql SELECT ... [FROM ... WHERE ... GROUP BY ...] UNION ALL SELECT ... [FROM ... WHERE ... GROUP BY ...] [UNION ALL SELECT ...] ... [ORDER BY result_column | position [ASC | DESC], ...] [LIMIT n [OFFSET m]] ``` Runs each `SELECT` and puts all their rows into one result. Rows that are the same are **kept** (that is the meaning of `ALL`). | Part | Meaning | |---|---| | each `SELECT` | a normal query: it can have `FROM`, joins, `WHERE`, `GROUP BY`, `SAMPLE BY`, `HAVING` | | `ORDER BY`, `LIMIT` at the end | work on **all** rows together | - **Returns:** the rows of the first part, then the rows of the second part, and so on. The column **names** come from the first part. - **NULL:** NULL values are kept like any other value. - **Notes:** - Every part must have the same number of columns. - KarelaDB does not check that the column types match. Put the same kind of value in the same position, or the column mixes numbers and text. - `ORDER BY` after `UNION ALL` can only use a result column name, an alias of the first part, or a position (`ORDER BY 2`). An expression like `ORDER BY amount * 2` is an error. - One part cannot have its own `ORDER BY` or `LIMIT`. Wrap that part in `FROM (SELECT ... LIMIT n) t` (example below). - `UNION` (without ALL) removes rows that are the same. `INTERSECT` keeps rows found in both parts, `EXCEPT` rows of the first part that are not in the second. With `ALL` they count copies. `INTERSECT` goes before `UNION` and `EXCEPT`. **Example: Broker 58: what it bought and sold, per symbol** (sample data: sample) ```sql SELECT symbol, 'buy' AS side, sum(quantity) AS shares FROM trade_log WHERE buyer = 58 GROUP BY symbol UNION ALL SELECT symbol, 'sell' AS side, sum(quantity) AS shares FROM trade_log WHERE seller = 58 GROUP BY symbol ORDER BY symbol, side; ``` Result: | symbol (SYMBOL) | side (STRING) | shares (LONG) | |---|---|---| | CITY | sell | 20 | | COAST | buy | 760 | | COAST | sell | 1200 | | TELCO | buy | 1000 | | VOLT | buy | 200 | | VOLT | sell | 50 | (6 rows) **Example: Turnover of each broker on one day (bought + sold)** (sample data: sample) ```sql SELECT broker, sum(amount) AS turnover, count(*) AS trades FROM ( SELECT buyer AS broker, amount FROM trade_log WHERE date IN '2026-09-10' UNION ALL SELECT seller AS broker, amount FROM trade_log WHERE date IN '2026-09-10' ) t GROUP BY broker ORDER BY turnover DESC; ``` Result: | broker (INT) | turnover (DOUBLE) | trades (LONG) | |---|---|---| | 45 | 1288160 | 11 | | 49 | 1173580 | 9 | | 34 | 773260 | 9 | | 58 | 761690 | 8 | | 1 | 726070 | 11 | (5 rows) A `GROUP BY` over a `UNION ALL` like the one above is grouped part by part: each part sums its own rows in the column engine and the outer query adds up one row per broker and part, so it costs about as much as the two parts on their own. `EXPLAIN` shows it as `aggregates pushed into the 2 UNION ALL parts`. See [GROUP BY with many groups](performance-tips.md#many-groups) for what it covers. Only `sum`, `count`, `min` and `max` can be added up part by part. With `avg`, `first`, `last` or `count(DISTINCT ...)` the parts are not grouped first, but their rows still go straight into the groups without being kept: `EXPLAIN` says `read part by part into the grouping (not kept)`. **Example: EXPLAIN: grouped part by part, and read part by part** (sample data: sample) ```sql EXPLAIN SELECT broker, sum(amount) AS turnover, count(*) AS trades FROM (SELECT buyer AS broker, amount FROM trade_log WHERE date IN '2026-09-10' UNION ALL SELECT seller AS broker, amount FROM trade_log WHERE date IN '2026-09-10') t GROUP BY broker; EXPLAIN SELECT broker, avg(amount) AS avg_amount FROM (SELECT buyer AS broker, amount FROM trade_log WHERE date IN '2026-09-10' UNION ALL SELECT seller AS broker, amount FROM trade_log WHERE date IN '2026-09-10') t GROUP BY broker; ``` Result: `EXPLAIN SELECT broker, sum(amount) AS turnover, count(*) AS trades FROM (SELECT buyer AS broker, amo` | plan (STRING) | |---| | aggregates pushed into the 2 UNION ALL parts: each part groups its own table first | | scan result of t (0 rows) | | group by 1 key(s), 2 aggregate(s) | | engine: row (reads a subquery or view result) | (4 rows) `EXPLAIN SELECT broker, avg(amount) AS avg_amount FROM (SELECT buyer AS broker, amount FROM trade_log` | plan (STRING) | |---| | scan rows of t read part by part into the grouping (not kept) | | group by 1 key(s), 1 aggregate(s) | | engine: row (reads a subquery or view result) | (3 rows) **Example: One activity list from news and forum posts, newest first** Setup: ```sql CREATE TABLE news (id LONG PRIMARY KEY, title STRING, published_at TIMESTAMP); CREATE TABLE forum_posts (id LONG PRIMARY KEY, title STRING, created_at TIMESTAMP); INSERT INTO news VALUES (1, 'Market index gains 12 points', '2026-09-10T15:10:00+05:45'), (2, 'CITY announces bonus shares', '2026-09-09T16:00:00+05:45'); INSERT INTO forum_posts VALUES (10, 'Is VOLT a good buy now?', '2026-09-10T13:20:00+05:45'), (11, 'Hydro stocks after the monsoon', '2026-09-08T09:00:00+05:45'); ``` ```sql SELECT 'news' AS kind, id, title, published_at AS at FROM news UNION ALL SELECT 'forum', id, title, created_at FROM forum_posts ORDER BY at DESC LIMIT 3; ``` Result: | kind (STRING) | id (LONG) | title (STRING) | at (TIMESTAMP) | |---|---|---|---| | news | 1 | Market index gains 12 points | 2026-09-10T09:25:00.000Z | | forum | 10 | Is VOLT a good buy now? | 2026-09-10T07:35:00.000Z | | news | 2 | CITY announces bonus shares | 2026-09-09T10:15:00.000Z | (3 rows) **Example: Different values in the same column: KarelaDB does not stop you** ```sql SELECT 1 AS n, 'a' AS t UNION ALL SELECT 'x', 2; ``` Result: | n (LONG) | t (STRING) | |---|---| | 1 | a | | x | 2 | (2 rows) **Example: LIMIT for one part: wrap it in FROM ( )** (sample data: sample) ```sql SELECT * FROM (SELECT transaction_id, symbol, amount FROM trade_log WHERE date IN '2026-09-09' ORDER BY amount DESC LIMIT 1) a UNION ALL SELECT * FROM (SELECT transaction_id, symbol, amount FROM trade_log WHERE date IN '2026-09-10' ORDER BY amount DESC LIMIT 1) b; ``` Result: | transaction_id (LONG) | symbol (SYMBOL) | amount (DOUBLE) | |---|---|---| | 100001 | TELCO | 881000 | | 100017 | TELCO | 440500 | (2 rows) **Example: Symbols that traded and also have a daily candle (no copies)** (sample data: sample) ```sql SELECT symbol FROM trade_log INTERSECT SELECT symbol FROM ochl_day ORDER BY 1; ``` Result: | symbol (SYMBOL) | |---| | CITY | | COAST | | VOLT | (3 rows) **Example: Every part needs the same number of columns** (sample data: sample; shows an error) ```sql SELECT symbol FROM trade_log UNION ALL SELECT symbol, close FROM ochl_day; ``` Result: Error: `UNION ALL part 2 has 2 columns, the first part has 1` **Example: ORDER BY after UNION ALL needs a result column** (sample data: sample; shows an error) ```sql SELECT symbol, quantity FROM trade_log UNION ALL SELECT symbol, volume FROM ochl_day ORDER BY quantity * 2; ``` Result: Error: `line 4, column 19: ORDER BY after UNION ALL must name a result column or position` **Example: A part cannot have its own LIMIT** (sample data: sample; shows an error) ```sql SELECT symbol FROM securities LIMIT 1 UNION ALL SELECT 'X'; ``` Result: Error: `line 2, column 1: expected OFFSET, ';' or the end of the input but found 'UNION'` ## (SELECT ...) as a value Syntax: ```sql (SELECT expression FROM ... [WHERE ...] [LIMIT 1]) ``` A subquery in `( )` where a value is expected: in the `SELECT` list, in `WHERE`, in `HAVING` or in a join's `ON`. It must return **0 or 1 row**. - **Returns:** the value of the first column of that row. - **NULL:** when the subquery returns no row, the value is `NULL`. - **Notes:** - More than one row is an error: `subquery returned 3 rows; a value needs exactly one (add LIMIT 1?)`. - With more than one column, KarelaDB uses the **first** column and does not complain. Select only one column. - The subquery runs once, so every row of the outer query gets the same value. **Example: Each close next to the best close of the day** (sample data: sample) ```sql SELECT symbol, close, (SELECT max(close) FROM ochl_day WHERE time IN '2026-09-10') AS best_close, round(close / (SELECT max(close) FROM ochl_day WHERE time IN '2026-09-10') * 100, 1) AS pct_of_best FROM ochl_day WHERE time IN '2026-09-10'; ``` Result: | symbol (SYMBOL) | close (DOUBLE) | best_close (DOUBLE) | pct_of_best (DOUBLE) | |---|---|---|---| | CITY | 504.9 | 504.9 | 100 | | COAST | 376.1 | 504.9 | 74.5 | | VOLT | 191.2 | 504.9 | 37.9 | (3 rows) **Example: The trade at the highest CITY rate** (sample data: sample) ```sql SELECT date, transaction_id, buyer, seller, quantity, rate FROM trade_log WHERE symbol = 'CITY' AND rate = (SELECT max(rate) FROM trade_log WHERE symbol = 'CITY'); ``` Result: | date (TIMESTAMP) | transaction_id (LONG) | buyer (INT) | seller (INT) | quantity (LONG) | rate (DOUBLE) | |---|---|---|---|---|---| | 2026-09-10T00:00:00.000Z | 100029 | 49 | 45 | 200 | 518 | (1 row) **Example: The last trading day in the table, without typing the date** (sample data: sample) ```sql SELECT time, symbol, close FROM ochl_day WHERE time = (SELECT max(time) FROM ochl_day) ORDER BY close DESC; ``` Result: | time (TIMESTAMP) | symbol (SYMBOL) | close (DOUBLE) | |---|---|---| | 2026-09-10T00:00:00.000Z | CITY | 504.9 | | 2026-09-10T00:00:00.000Z | COAST | 376.1 | | 2026-09-10T00:00:00.000Z | VOLT | 191.2 | (3 rows) **Example: Symbols with more trades than the average symbol** (sample data: sample) ```sql SELECT symbol, count(*) AS trades FROM trade_log GROUP BY symbol HAVING count(*) > (SELECT avg(n) FROM (SELECT symbol, count(*) AS n FROM trade_log GROUP BY symbol) t); ``` Result: | symbol (SYMBOL) | trades (LONG) | |---|---| | CITY | 11 | | COAST | 11 | (2 rows) **Example: No row gives NULL** (sample data: sample) ```sql SELECT (SELECT max(close) FROM ochl_day WHERE symbol = 'TELCO') AS telco_best_close; ``` Result: | telco_best_close (STRING) | |---| | NULL | (1 row) **Example: More than one row is an error** (sample data: sample; shows an error) ```sql SELECT symbol, (SELECT close FROM ochl_day WHERE time IN '2026-09-10') AS close FROM securities; ``` Result: Error: `line 1, column 16: subquery returned 3 rows; a value needs exactly one (add LIMIT 1?)` ## IN (SELECT ...), NOT IN (SELECT ...) Syntax: ```sql expression IN (SELECT column FROM ... [WHERE ...]) expression NOT IN (SELECT column FROM ... [WHERE ...]) ``` True when the value is (or is not) one of the values the subquery returns. It works like `IN ('a', 'b', ...)` with the list made by the subquery (see [IN](where.md#in-list)). - **Returns:** `TRUE` or `FALSE` (or `NULL`, see below). - **NULL:** a `NULL` on the left gives `NULL`. When the value is not found and the list has a `NULL`, the result is `NULL` too, so `NOT IN` keeps **no** rows. Add `WHERE column IS NOT NULL` inside the subquery. - **Notes:** - An empty subquery gives an empty list: `IN` is false for every row, `NOT IN` is true. - With more than one column, KarelaDB uses the **first** column. To match two columns at once, write `(a, b) IN (SELECT x, y FROM ...)` (see [row IN](where.md#row-in)). - The subquery can use `UNION ALL`, `GROUP BY`, `ORDER BY` and `LIMIT`. - For a symbol column, `IN (SELECT ...)` still lets KarelaDB read only the rows of those symbols (see [EXPLAIN](explain.md#explain)). - In `UPDATE` and `DELETE` too: in `SET` and in the `WHERE`. **Example: Securities with a trade of 1000 shares or more** (sample data: sample) ```sql SELECT symbol, name FROM securities WHERE symbol IN (SELECT symbol FROM trade_log WHERE quantity >= 1000); ``` Result: | symbol (SYMBOL) | name (STRING) | |---|---| | COAST | Coastal Bank Limited | | TELCO | Northwind Telecom Limited | (2 rows) **Example: Securities with no daily candles yet** (sample data: sample) ```sql SELECT symbol, name, sector FROM securities WHERE symbol NOT IN (SELECT symbol FROM ochl_day); ``` Result: | symbol (SYMBOL) | name (STRING) | sector (STRING) | |---|---|---| | ACME | Acme Investment Company | Investment | | TELCO | Northwind Telecom Limited | Telecom | (2 rows) **Example: Candles of the 3 symbols with the biggest turnover (TELCO has no candles)** (sample data: sample) ```sql SELECT time, symbol, close, volume FROM ochl_day WHERE time IN '2026-09-10' AND symbol IN (SELECT symbol FROM (SELECT symbol, sum(amount) AS turnover FROM trade_log WHERE date IN '2026-09-10' GROUP BY symbol ORDER BY turnover DESC LIMIT 3) top3); ``` Result: | time (TIMESTAMP) | symbol (SYMBOL) | close (DOUBLE) | volume (DOUBLE) | |---|---|---|---| | 2026-09-10T00:00:00.000Z | CITY | 504.9 | 183040 | | 2026-09-10T00:00:00.000Z | COAST | 376.1 | 186200 | (2 rows) **Example: Users who wrote a post, for an app** Setup: ```sql CREATE TABLE app_users (id LONG PRIMARY KEY, name STRING, banned BOOL); CREATE TABLE posts (id LONG PRIMARY KEY, user_id LONG, title STRING); INSERT INTO app_users VALUES (1, 'sita', FALSE), (2, 'ram', FALSE), (3, 'hari', TRUE); INSERT INTO posts VALUES (100, 1, 'COAST results'), (101, 3, 'Buy now!!!'), (102, 1, 'Hydro outlook'); ``` ```sql SELECT id, name FROM app_users WHERE id IN (SELECT user_id FROM posts) ORDER BY id; SELECT id, name FROM app_users WHERE id NOT IN (SELECT user_id FROM posts); ``` Result: `SELECT id, name FROM app_users WHERE id IN (SELECT user_id FROM posts) ORDER BY id` | id (LONG) | name (STRING) | |---|---| | 1 | sita | | 3 | hari | (2 rows) `SELECT id, name FROM app_users WHERE id NOT IN (SELECT user_id FROM posts)` | id (LONG) | name (STRING) | |---|---| | 2 | ram | (1 row) **Example: A NULL in the list: NOT IN keeps nothing** Setup: ```sql CREATE TABLE app_users (id LONG PRIMARY KEY, name STRING); CREATE TABLE posts (id LONG PRIMARY KEY, user_id LONG); INSERT INTO app_users VALUES (1, 'sita'), (2, 'ram'); INSERT INTO posts VALUES (100, 1), (101, NULL); ``` ```sql SELECT name FROM app_users WHERE id NOT IN (SELECT user_id FROM posts); SELECT name FROM app_users WHERE id NOT IN (SELECT user_id FROM posts WHERE user_id IS NOT NULL); ``` Result: `SELECT name FROM app_users WHERE id NOT IN (SELECT user_id FROM posts)` | name (STRING) | |---| (0 rows) `SELECT name FROM app_users WHERE id NOT IN (SELECT user_id FROM posts WHERE user_id IS NOT NULL)` | name (STRING) | |---| | ram | (1 row) **Example: Clean up posts of users who are gone** Setup: ```sql CREATE TABLE app_users (id LONG PRIMARY KEY, name STRING); CREATE TABLE posts (id LONG PRIMARY KEY, user_id LONG); INSERT INTO app_users VALUES (1, 'sita'); INSERT INTO posts VALUES (100, 1), (101, 9); ``` ```sql DELETE FROM posts WHERE user_id NOT IN (SELECT id FROM app_users); SELECT * FROM posts; ``` Result: `DELETE FROM posts WHERE user_id NOT IN (SELECT id FROM app_users)` ```text 1 row(s) deleted ``` `SELECT * FROM posts` | id (LONG) | user_id (LONG) | |---|---| | 100 | 1 | (1 row) ## EXISTS, NOT EXISTS Syntax: ```sql EXISTS (SELECT ... FROM ... [WHERE ...]) NOT EXISTS (SELECT ... FROM ... [WHERE ...]) ``` `EXISTS` is true when the subquery returns at least one row. What the subquery selects does not matter (`SELECT 1` is usual). - **Returns:** `TRUE` or `FALSE`, never `NULL`. - **NULL:** a row full of NULL values still counts as a row. - **Notes:** because the subquery cannot use outer columns, `EXISTS` is the **same for every row**: all rows are kept or none. It is useful as an on/off switch ("only if there is any big trade today"). To test each row ("securities that have trades"), use [IN (SELECT ...)](union-subqueries.md#in-subquery) or a join. **Example: Show today's candles only if today's trade log is loaded** (sample data: sample) ```sql SELECT symbol, close FROM ochl_day WHERE time IN '2026-09-10' AND EXISTS (SELECT 1 FROM trade_log WHERE date IN '2026-09-10'); ``` Result: | symbol (SYMBOL) | close (DOUBLE) | |---|---| | CITY | 504.9 | | COAST | 376.1 | | VOLT | 191.2 | (3 rows) **Example: As a column: is there any trade of 1000 shares or more?** (sample data: sample) ```sql SELECT EXISTS (SELECT 1 FROM trade_log WHERE quantity >= 1000) AS big_trade, NOT EXISTS (SELECT 1 FROM trade_log WHERE quantity > 5000) AS no_huge_trade; ``` Result: | big_trade (BOOL) | no_huge_trade (BOOL) | |---|---| | true | true | (1 row) **Example: Accounting check: warn when a journal entry does not balance** Setup: ```sql CREATE TABLE journal (entry_id LONG, line INT, account STRING, debit DECIMAL(2), credit DECIMAL(2)); INSERT INTO journal VALUES (1, 1, 'Cash', 5000.00, 0), (1, 2, 'Sales', 0, 5000.00), (2, 1, 'Rent', 1200.00, 0), (2, 2, 'Cash', 0, 1000.00); ``` ```sql SELECT EXISTS (SELECT entry_id FROM journal GROUP BY entry_id HAVING sum(debit) != sum(credit)) AS has_unbalanced_entry; SELECT entry_id, sum(debit) AS debit, sum(credit) AS credit FROM journal GROUP BY entry_id HAVING sum(debit) != sum(credit); ``` Result: `SELECT EXISTS (SELECT entry_id FROM journal GROUP BY entry_id HAVING sum(debit) != sum(credit)) AS h` | has_unbalanced_entry (BOOL) | |---| | true | (1 row) `SELECT entry_id, sum(debit) AS debit, sum(credit) AS credit FROM journal GROUP BY entry_id HAVING su` | entry_id (LONG) | debit (DECIMAL(2)) | credit (DECIMAL(2)) | |---|---|---| | 2 | 1200.00 | 1000.00 | (1 row) ## FROM (SELECT ...) alias Syntax: ```sql SELECT ... FROM (SELECT ...) [AS] alias [JOIN ...] [WHERE ...] ... SELECT ... FROM table t JOIN (SELECT ...) [AS] alias ON condition ``` Uses the result of a query as a table. This is also called a **derived table**. The subquery **needs a name** (the alias). | Part | Meaning | |---|---| | `(SELECT ...)` | any query, also with `GROUP BY`, `SAMPLE BY`, window functions, `UNION ALL`, `ORDER BY` and `LIMIT` | | `alias` | the name of the result; write `alias.column` in the outer query | - **Returns:** the columns of the subquery, with their names or aliases. Give every computed column an alias, so the outer query can name it. - **Notes:** - Use it to work in steps: make candles inside, compute an indicator outside; group inside, filter or join outside. - When the subquery has a `TIMESTAMP` column, the outer query can use `SAMPLE BY` on it. - A subquery in `FROM` or `JOIN` keeps all its rows in memory. Filter by time **inside** it for big tables. - One exception: when the outer query is a `GROUP BY` and the subquery is its only source, and the subquery (or each `UNION ALL` part of it) is a plain `SELECT` of one table with no `GROUP BY`, `DISTINCT`, `ORDER BY` or `LIMIT` of its own, its rows go **straight into the groups**, one after another. Only the groups are kept, so millions of rows fit (and very many groups go to disk, see [big GROUP BY](group-by.md#big-group-by)). **Example: Group inside, filter outside** (sample data: sample) ```sql SELECT t.symbol, t.trades, t.shares FROM (SELECT symbol, count(*) AS trades, sum(quantity) AS shares FROM trade_log GROUP BY symbol) t WHERE t.trades > 5 ORDER BY t.shares DESC; ``` Result: | symbol (SYMBOL) | trades (LONG) | shares (LONG) | |---|---|---| | COAST | 11 | 3070 | | TELCO | 6 | 2770 | | CITY | 11 | 2420 | (3 rows) **Example: Weekly candles inside, 2-week moving average outside** (sample data: sample) ```sql SELECT time, close, sma(close, 2) AS sma2 FROM ( SELECT time, last(close) AS close FROM ochl_day WHERE symbol = 'CITY' AND time IN '2026' SAMPLE BY 1w WEEK START SUNDAY ) w; ``` Result: | time (TIMESTAMP) | close (DOUBLE) | sma2 (DOUBLE) | |---|---|---| | 2026-08-23T00:00:00.000Z | 505.9 | NULL | | 2026-08-30T00:00:00.000Z | 497.9 | 501.9 | | 2026-09-06T00:00:00.000Z | 504.9 | 501.4 | (3 rows) **Example: Join each trade to the symbol's total for the day** (sample data: sample) ```sql SELECT f.transaction_id, f.symbol, f.quantity, d.total, round(f.quantity * 100.0 / d.total, 1) AS pct_of_day FROM trade_log f JOIN (SELECT symbol, sum(quantity) AS total FROM trade_log WHERE date IN '2026-09-10' GROUP BY symbol) AS d ON d.symbol = f.symbol WHERE f.date IN '2026-09-10' AND f.symbol = 'COAST' ORDER BY pct_of_day DESC LIMIT 4; ``` Result: | transaction_id (LONG) | symbol (SYMBOL) | quantity (LONG) | total (LONG) | pct_of_day (DOUBLE) | |---|---|---|---|---| | 100015 | COAST | 1000 | 2850 | 35.1 | | 100031 | COAST | 1000 | 2850 | 35.1 | | 100023 | COAST | 500 | 2850 | 17.5 | | 100026 | COAST | 200 | 2850 | 7 | (4 rows) **Example: A cash book from two tables, with a running balance** Setup: ```sql CREATE TABLE sales (id LONG PRIMARY KEY, day DATE, amount DECIMAL(2)); CREATE TABLE expenses (id LONG PRIMARY KEY, day DATE, amount DECIMAL(2)); INSERT INTO sales VALUES (1, '2026-09-01', 25000.00), (2, '2026-09-03', 18000.00); INSERT INTO expenses VALUES (1, '2026-09-02', 12000.00), (2, '2026-09-03', 2500.50); ``` ```sql SELECT day, kind, amount, cumsum(amount) OVER (ORDER BY day, kind) AS balance FROM ( SELECT day, 'sale' AS kind, amount FROM sales UNION ALL SELECT day, 'expense', -amount FROM expenses ) book ORDER BY day, kind; ``` Result: | day (DATE) | kind (STRING) | amount (DECIMAL(2)) | balance (DOUBLE) | |---|---|---|---| | 2026-09-01 | sale | 25000.00 | 25000 | | 2026-09-02 | expense | -12000.00 | 13000 | | 2026-09-03 | expense | -2500.50 | 10499.5 | | 2026-09-03 | sale | 18000.00 | 28499.5 | (4 rows) **Example: The subquery needs no name** (sample data: sample) ```sql SELECT count(*) FROM (SELECT symbol FROM securities); ``` Result: | count(*) (LONG) | |---| | 5 | (1 row) ## Correlated subqueries Syntax: ```sql SELECT ..., (SELECT ... FROM t2 WHERE t2.col = t1.col) FROM t1 SELECT ... FROM t1 WHERE [NOT] EXISTS (SELECT 1 FROM t2 WHERE t2.col = t1.col) SELECT ... FROM t1 LEFT JOIN LATERAL (SELECT ... WHERE t2.col = t1.col) x ON TRUE ``` A **correlated** subquery uses a column of the outer query (`t1.col` above). - **Speed:** KarelaDB does not run it again for every outer row. When the subquery joins the outer row with `=` (`t2.col = t1.col`), it runs once per **different** outer value first, with that value as a constant, so an index on `t2.col` answers it. Once the outer query has asked for more values than that pays for (one value per 256 rows of `t2` when `col` has an index or is the primary key, at least 8; 8 without an index), KarelaDB reads `t2` once, builds a hash table by `col` and looks every further outer row up in it. So a page about one user costs a few index lookups (0.4 ms where the old plan took 930 ms on 300,000 logins), and a report over every user costs one pass over `t2`. Other subqueries run once per different outer value. - **Returns:** a value subquery gives NULL when no row matches (`count(*)` gives 0). It is an error when it finds more than one row; add `LIMIT 1` or an aggregate. - **Notes:** a join with `GROUP BY` or a window function (`lag(x) OVER (PARTITION BY ...)`) gives the same answers and is often easier to read. **Example: The best close of each symbol, as a subquery per row** (sample data: sample) ```sql SELECT s.symbol, (SELECT max(close) FROM ochl_day d WHERE d.symbol = s.symbol) AS best_close FROM securities s ORDER BY s.symbol; ``` Result: | symbol (SYMBOL) | best_close (DOUBLE) | |---|---| | ACME | NULL | | CITY | 542.4 | | COAST | 426.5 | | TELCO | NULL | | VOLT | 196.4 | (5 rows) **Example: The same question as a LEFT JOIN with GROUP BY** (sample data: sample) ```sql SELECT s.symbol, max(d.close) AS best_close FROM securities s LEFT JOIN ochl_day d ON d.symbol = s.symbol GROUP BY s.symbol ORDER BY s.symbol; ``` Result: | symbol (SYMBOL) | best_close (DOUBLE) | |---|---| | ACME | NULL | | CITY | 542.4 | | COAST | 426.5 | | TELCO | NULL | | VOLT | 196.4 | (5 rows) **Example: Symbols with at least one trade: EXISTS with an outer column** (sample data: sample) ```sql SELECT symbol FROM securities s WHERE EXISTS (SELECT 1 FROM trade_log f WHERE f.symbol = s.symbol) ORDER BY symbol; ``` Result: | symbol (SYMBOL) | |---| | CITY | | COAST | | TELCO | | VOLT | (4 rows) **Example: The same test with IN (SELECT ...)** (sample data: sample) ```sql SELECT symbol FROM securities WHERE symbol IN (SELECT symbol FROM trade_log) ORDER BY symbol; ``` Result: | symbol (SYMBOL) | |---| | CITY | | COAST | | TELCO | | VOLT | (4 rows) **Example: Latest close per symbol compared with the day before: a window function instead** (sample data: sample) ```sql SELECT symbol, prev_close, close, round((close - prev_close) / prev_close * 100, 2) AS change_pct FROM ( SELECT time, symbol, close, lag(close) OVER (PARTITION BY symbol ORDER BY time) AS prev_close FROM ochl_day ) t WHERE time IN '2026-09-10' ORDER BY change_pct DESC; ``` Result: | symbol (SYMBOL) | prev_close (DOUBLE) | close (DOUBLE) | change_pct (DOUBLE) | |---|---|---|---| | VOLT | 187.3 | 191.2 | 2.08 | | CITY | 504.6 | 504.9 | 0.06 | | COAST | 380.1 | 376.1 | -1.05 | (3 rows)