From b8352a3f1859397daebc0db0ed0e9d98283309ce Mon Sep 17 00:00:00 2001 From: javier Date: Fri, 27 Feb 2026 20:16:07 +0100 Subject: [PATCH 1/4] Add UNNEST documentation page --- documentation/query/sql/unnest.md | 337 ++++++++++++++++++++++++++++++ documentation/sidebars.js | 1 + 2 files changed, 338 insertions(+) create mode 100644 documentation/query/sql/unnest.md diff --git a/documentation/query/sql/unnest.md b/documentation/query/sql/unnest.md new file mode 100644 index 000000000..3255efbee --- /dev/null +++ b/documentation/query/sql/unnest.md @@ -0,0 +1,337 @@ +--- +title: UNNEST keyword +sidebar_label: UNNEST +description: + Reference documentation for UNNEST, which expands arrays and JSON arrays + into rows in QuestDB. +--- + +UNNEST expands arrays or JSON arrays into rows - one row per element. It +supports two modes: + +- **Array UNNEST**: Expands native `DOUBLE[]` columns (or literal arrays) into + rows of `DOUBLE` values. +- **JSON UNNEST**: Expands a JSON array stored as `VARCHAR` into rows with + explicitly typed columns. + +UNNEST appears in the `FROM` clause and behaves like a table - you can join it +with other tables, filter its output with `WHERE`, and use it in CTEs and +subqueries. + +## Array UNNEST + +### Syntax + +```questdb-sql +SELECT ... +FROM table_name, UNNEST(array_expr [, array_expr2 ...]) + [WITH ORDINALITY] + [[AS] alias] + [(col_alias1 [, col_alias2 ...])] +``` + +### Basic usage + +Expand an array column into individual rows: + +```questdb-sql title="Expand an array literal into rows" +SELECT value FROM UNNEST(ARRAY[1.0, 2.0, 3.0]); +``` + +| value | +| :---- | +| 1.0 | +| 2.0 | +| 3.0 | + +### With a table + +```questdb-sql title="Expand array column from a table" +SELECT t.symbol, u.size +FROM market_data t, UNNEST(t.bid_sizes) u(size); +``` + +You can also use `CROSS JOIN` - the behavior is identical to the comma syntax: + +```questdb-sql title="Equivalent CROSS JOIN syntax" +SELECT t.symbol, u.size +FROM market_data t +CROSS JOIN UNNEST(t.bid_sizes) u(size); +``` + +### WITH ORDINALITY + +Add a 1-based index column that resets for each input row. The ordinality column +is always the last output column. + +Since `ordinality` is a reserved keyword, either alias it or quote it as +`"ordinality"`: + +```questdb-sql title="Array with position index" +SELECT u.val, u.pos +FROM UNNEST(ARRAY[10.0, 20.0, 30.0]) WITH ORDINALITY u(val, pos); +``` + +| val | pos | +| :--- | :-- | +| 10.0 | 1 | +| 20.0 | 2 | +| 30.0 | 3 | + +### Multiple arrays + +Pass multiple arrays to a single UNNEST. Shorter arrays are padded with `NULL`: + +```questdb-sql title="Two arrays side by side" +SELECT u.a, u.b +FROM UNNEST(ARRAY[1.0, 2.0, 3.0], ARRAY[10.0, 20.0]) u(a, b); +``` + +| a | b | +| :-- | :--- | +| 1.0 | 10.0 | +| 2.0 | 20.0 | +| 3.0 | NULL | + +### Multidimensional arrays + +Unnesting reduces dimensionality by one level. A `DOUBLE[][]` produces +`DOUBLE[]` elements: + +```questdb-sql title="Unnest a 2D array into 1D rows" +SELECT value +FROM UNNEST(ARRAY[ARRAY[1.0, 2.0], ARRAY[3.0, 4.0]]); +``` + +| value | +| :-------- | +| [1.0,2.0] | +| [3.0,4.0] | + +### Column aliases + +Default column names are `value` for a single source or `value1`, `value2`, ... +for multiple sources. Override them with parenthesized aliases: + +```questdb-sql title="Custom column name" +SELECT u.price FROM UNNEST(ARRAY[1.5, 2.5]) u(price); +``` + +### NULL and empty array handling + +- `NULL` array: produces 0 rows +- Empty array: produces 0 rows +- `NULL` elements within an array: preserved as `NULL` in the output + +## JSON UNNEST + +JSON UNNEST expands a JSON array (stored as `VARCHAR`) into rows with explicitly +typed columns. The `COLUMNS(...)` clause distinguishes JSON UNNEST from array +UNNEST. + +### Syntax + +```questdb-sql +SELECT ... +FROM table_name, UNNEST( + varchar_expr COLUMNS(col_name TYPE [, col_name TYPE ...]) +) [WITH ORDINALITY] [[AS] alias] [(col_alias1, ...)] +``` + +### Supported column types + +`BOOLEAN`, `SHORT`, `INT`, `LONG`, `DOUBLE`, `VARCHAR`, `TIMESTAMP` + +### Object arrays + +Extract typed fields from an array of JSON objects. Column names in `COLUMNS()` +are used as JSON field names for extraction: + +```questdb-sql title="Extract fields from JSON objects" +SELECT u.name, u.age +FROM UNNEST( + '[{"name":"Alice","age":30},{"name":"Bob","age":25}]'::VARCHAR + COLUMNS(name VARCHAR, age INT) +) u; +``` + +| name | age | +| :---- | :-- | +| Alice | 30 | +| Bob | 25 | + +### Scalar arrays + +When `COLUMNS()` declares a single column and the JSON array contains scalars +(not objects), each element is extracted directly: + +```questdb-sql title="Scalar JSON array" +SELECT u.val +FROM UNNEST('[1.5, 2.5, 3.5]'::VARCHAR COLUMNS(val DOUBLE)) u; +``` + +| val | +| :-- | +| 1.5 | +| 2.5 | +| 3.5 | + +### WITH ORDINALITY + +Works the same as array UNNEST - alias the ordinality column as the last entry: + +```questdb-sql title="JSON UNNEST with position index" +SELECT u.val, u.pos +FROM UNNEST( + '[10, 20, 30]'::VARCHAR COLUMNS(val LONG) +) WITH ORDINALITY u(val, pos); +``` + +| val | pos | +| :-- | :-- | +| 10 | 1 | +| 20 | 2 | +| 30 | 3 | + +### Timestamps + +JSON string values are parsed using QuestDB's standard timestamp formats. +Numeric values are treated as microseconds since epoch: + +```questdb-sql title="Timestamps from JSON strings" +SELECT u.ts, u.val +FROM UNNEST( + '[{"ts":"2024-01-15T10:30:00.000000Z","val":1.5}, + {"ts":"2024-06-20T14:00:00.000000Z","val":2.5}]'::VARCHAR + COLUMNS(ts TIMESTAMP, val DOUBLE) +) u; +``` + +| ts | val | +| :-------------------------- | :-- | +| 2024-01-15T10:30:00.000000Z | 1.5 | +| 2024-06-20T14:00:00.000000Z | 2.5 | + +### Column aliasing + +The names in `COLUMNS()` serve as both JSON field names and default output column +names. Override the output names with aliases after the table alias: + +```questdb-sql title="JSON field 'price' output as 'cost'" +SELECT u.cost +FROM UNNEST( + '[{"price":1.5},{"price":2.5}]'::VARCHAR + COLUMNS(price DOUBLE) +) u(cost); +``` + +### With json_extract() + +Use `json_extract()` to reach nested JSON paths before unnesting: + +```questdb-sql title="Unnest a nested JSON array" +SELECT u.price +FROM events e, UNNEST( + json_extract(e.payload, '$.items')::VARCHAR + COLUMNS(price DOUBLE) +) u; +``` + +### NULL and invalid input handling + +| Input | Result | +| :---- | :----- | +| `NULL` VARCHAR | 0 rows | +| Empty string | 0 rows | +| Invalid JSON | 0 rows (no error) | +| Empty array `[]` | 0 rows | +| `null` element | `NULL` for all columns (except `BOOLEAN` which returns `false`) | +| Missing field in object | `NULL` for that column (except `BOOLEAN` which returns `false`) | + +### Type coercion + +When a JSON value does not match the declared column type, the result is `NULL` +(except `BOOLEAN`, which defaults to `false`): + +```questdb-sql title="Missing fields produce NULL" +SELECT u.a, u.b +FROM UNNEST( + '[{"a":1},{"a":2,"b":99},{"a":null}]'::VARCHAR + COLUMNS(a INT, b INT) +) u; +``` + +| a | b | +| :--- | :--- | +| 1 | NULL | +| 2 | 99 | +| NULL | NULL | + +## Common patterns + +### Filter unnested rows + +```questdb-sql title="Filter by unnested value" +SELECT t.symbol, u.size +FROM market_data t, UNNEST(t.bid_sizes) u(size) +WHERE u.size > 100.0 +ORDER BY t.timestamp; +``` + +### Aggregate unnested values + +```questdb-sql title="Total bid size per symbol" +SELECT t.symbol, sum(u.size) AS total_bid_size +FROM market_data t, UNNEST(t.bid_sizes) u(size) +GROUP BY t.symbol; +``` + +### Aggregate JSON array fields + +```questdb-sql title="Sum quantities from JSON per event" +SELECT e.id, sum(u.qty) AS total_qty +FROM events e, UNNEST( + e.payload COLUMNS(qty INT) +) u +GROUP BY e.id; +``` + +### CTE with UNNEST + +```questdb-sql title="Wrap UNNEST in a CTE for further processing" +WITH expanded AS ( + SELECT m.symbol, m.timestamp, u.size, u.level + FROM market_data m, UNNEST(m.bid_sizes) WITH ORDINALITY u(size, level) +) +SELECT symbol, level, avg(size) AS avg_size +FROM expanded +GROUP BY symbol, level +ORDER BY symbol, level; +``` + +### DISTINCT on unnested values + +```questdb-sql title="Unique values from an array column" +SELECT DISTINCT u.val +FROM t, UNNEST(t.arr) u(val) +ORDER BY u.val; +``` + +## Limitations + +- **FROM clause only**: UNNEST cannot appear in the `SELECT` list. Use + `SELECT * FROM UNNEST(...)` instead. +- **Array types**: Only `DOUBLE[]` is currently supported as a native array + column type. Array literals like `ARRAY[1.0, 2.0]` produce `DOUBLE[]`. +- **COLUMNS requires VARCHAR**: The `COLUMNS(...)` clause is for JSON (VARCHAR) + sources only. Using it with a typed array produces an error. +- **No nested dot paths in COLUMNS**: Column names like `foo.bar` are not + supported in `COLUMNS()`. Use `json_extract()` to reach nested paths first. +- **VARCHAR field size limit**: Individual `VARCHAR` and `TIMESTAMP` field values + extracted from JSON are limited to 4096 bytes per field. + +:::info Related documentation +- [Array functions](/docs/query/functions/array/) +- [JSON functions](/docs/query/functions/json/) +- [SELECT](/docs/query/sql/select/) +::: diff --git a/documentation/sidebars.js b/documentation/sidebars.js index 645e259ef..7aaa2f5b6 100644 --- a/documentation/sidebars.js +++ b/documentation/sidebars.js @@ -425,6 +425,7 @@ module.exports = { "query/sql/order-by", "query/sql/pivot", "query/sql/sample-by", + "query/sql/unnest", "query/sql/where", "query/sql/window-join", "query/sql/with", From 449e9b7e45a23a26affa2cd73624401fce19d26f Mon Sep 17 00:00:00 2001 From: javier Date: Mon, 2 Mar 2026 11:03:25 +0100 Subject: [PATCH 2/4] Add cairo.json.unnest.max.value.size config property --- .../configuration/configuration-utils/_cairo.config.json | 4 ++++ documentation/query/sql/unnest.md | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/documentation/configuration/configuration-utils/_cairo.config.json b/documentation/configuration/configuration-utils/_cairo.config.json index 20af3db59..85791a00a 100644 --- a/documentation/configuration/configuration-utils/_cairo.config.json +++ b/documentation/configuration/configuration-utils/_cairo.config.json @@ -315,6 +315,10 @@ "default": "8", "description": "Sets max memory pages for storing IR for JIT compilation." }, + "cairo.json.unnest.max.value.size": { + "default": "4096", + "description": "Maximum byte size of a single VARCHAR or TIMESTAMP field value extracted during JSON [UNNEST](/docs/query/sql/unnest/). Numeric types (DOUBLE, LONG, INT, SHORT, BOOLEAN) are unaffected. Each VARCHAR/TIMESTAMP column allocates `2 * maxValueSize` bytes of native memory per active UNNEST cursor, so increase with care." + }, "cairo.sql.page.frame.min.rows": { "default": "1000", "description": "Sets the minimum number of rows in page frames used in SQL queries." diff --git a/documentation/query/sql/unnest.md b/documentation/query/sql/unnest.md index 3255efbee..3ff2340fe 100644 --- a/documentation/query/sql/unnest.md +++ b/documentation/query/sql/unnest.md @@ -328,7 +328,13 @@ ORDER BY u.val; - **No nested dot paths in COLUMNS**: Column names like `foo.bar` are not supported in `COLUMNS()`. Use `json_extract()` to reach nested paths first. - **VARCHAR field size limit**: Individual `VARCHAR` and `TIMESTAMP` field values - extracted from JSON are limited to 4096 bytes per field. + extracted from JSON are limited to 4096 bytes per field by default. If your + JSON contains large string fields (log messages, descriptions, etc.), increase + the + [`cairo.json.unnest.max.value.size`](/docs/configuration/overview/#cairo-engine) + server property. Each VARCHAR/TIMESTAMP column allocates + `2 x maxValueSize` bytes of native memory per active UNNEST cursor, so + increase with care. :::info Related documentation - [Array functions](/docs/query/functions/array/) From 2054ec8d59e1a59bfa75ffbc48a4e3e2ead6428f Mon Sep 17 00:00:00 2001 From: javier Date: Fri, 6 Mar 2026 18:19:26 +0100 Subject: [PATCH 3/4] Improve UNNEST page: demo queries, backtick keywords, intro text - Add demo tags to all queries that can run on the demo instance - Update market_data queries to use real columns (asks[2]) with time filter - Backtick all SQL keywords in prose text - Expand intro to mention filtering, aggregations, and window functions - Note CROSS JOIN clarity benefit when joining other tables --- documentation/query/sql/unnest.md | 110 ++++++++++++++---------------- 1 file changed, 52 insertions(+), 58 deletions(-) diff --git a/documentation/query/sql/unnest.md b/documentation/query/sql/unnest.md index 3ff2340fe..37c92aa7b 100644 --- a/documentation/query/sql/unnest.md +++ b/documentation/query/sql/unnest.md @@ -6,15 +6,17 @@ description: into rows in QuestDB. --- -UNNEST expands arrays or JSON arrays into rows - one row per element. It +UNNEST expands arrays or JSON arrays into rows - one row per element. This +allows you to filter by individual element values, run aggregations or window +functions over array contents, or join array elements with other tables. It supports two modes: -- **Array UNNEST**: Expands native `DOUBLE[]` columns (or literal arrays) into +- **Array `UNNEST`**: Expands native `DOUBLE[]` columns (or literal arrays) into rows of `DOUBLE` values. -- **JSON UNNEST**: Expands a JSON array stored as `VARCHAR` into rows with +- **JSON `UNNEST`**: Expands a JSON array stored as `VARCHAR` into rows with explicitly typed columns. -UNNEST appears in the `FROM` clause and behaves like a table - you can join it +`UNNEST` appears in the `FROM` clause and behaves like a table - you can join it with other tables, filter its output with `WHERE`, and use it in CTEs and subqueries. @@ -34,7 +36,7 @@ FROM table_name, UNNEST(array_expr [, array_expr2 ...]) Expand an array column into individual rows: -```questdb-sql title="Expand an array literal into rows" +```questdb-sql title="Expand an array literal into rows" demo SELECT value FROM UNNEST(ARRAY[1.0, 2.0, 3.0]); ``` @@ -46,17 +48,22 @@ SELECT value FROM UNNEST(ARRAY[1.0, 2.0, 3.0]); ### With a table -```questdb-sql title="Expand array column from a table" -SELECT t.symbol, u.size -FROM market_data t, UNNEST(t.bid_sizes) u(size); +```questdb-sql title="Expand array column from a table" demo +SELECT t.symbol, u.vol +FROM market_data t, UNNEST(t.asks[2]) u(vol) +WHERE t.timestamp IN '$now-1m..$now' + AND t.symbol = 'EURUSD'; ``` -You can also use `CROSS JOIN` - the behavior is identical to the comma syntax: +You can also use `CROSS JOIN` - the behavior is identical to the comma syntax, +but can be clearer when the query also joins other tables: -```questdb-sql title="Equivalent CROSS JOIN syntax" -SELECT t.symbol, u.size +```questdb-sql title="Equivalent CROSS JOIN syntax" demo +SELECT t.symbol, u.vol FROM market_data t -CROSS JOIN UNNEST(t.bid_sizes) u(size); +CROSS JOIN UNNEST(t.asks[2]) u(vol) +WHERE t.timestamp IN '$now-1m..$now' + AND t.symbol = 'EURUSD'; ``` ### WITH ORDINALITY @@ -67,7 +74,7 @@ is always the last output column. Since `ordinality` is a reserved keyword, either alias it or quote it as `"ordinality"`: -```questdb-sql title="Array with position index" +```questdb-sql title="Array with position index" demo SELECT u.val, u.pos FROM UNNEST(ARRAY[10.0, 20.0, 30.0]) WITH ORDINALITY u(val, pos); ``` @@ -80,9 +87,9 @@ FROM UNNEST(ARRAY[10.0, 20.0, 30.0]) WITH ORDINALITY u(val, pos); ### Multiple arrays -Pass multiple arrays to a single UNNEST. Shorter arrays are padded with `NULL`: +Pass multiple arrays to a single `UNNEST`. Shorter arrays are padded with `NULL`: -```questdb-sql title="Two arrays side by side" +```questdb-sql title="Two arrays side by side" demo SELECT u.a, u.b FROM UNNEST(ARRAY[1.0, 2.0, 3.0], ARRAY[10.0, 20.0]) u(a, b); ``` @@ -95,10 +102,10 @@ FROM UNNEST(ARRAY[1.0, 2.0, 3.0], ARRAY[10.0, 20.0]) u(a, b); ### Multidimensional arrays -Unnesting reduces dimensionality by one level. A `DOUBLE[][]` produces +`UNNEST` reduces dimensionality by one level. A `DOUBLE[][]` produces `DOUBLE[]` elements: -```questdb-sql title="Unnest a 2D array into 1D rows" +```questdb-sql title="Unnest a 2D array into 1D rows" demo SELECT value FROM UNNEST(ARRAY[ARRAY[1.0, 2.0], ARRAY[3.0, 4.0]]); ``` @@ -113,7 +120,7 @@ FROM UNNEST(ARRAY[ARRAY[1.0, 2.0], ARRAY[3.0, 4.0]]); Default column names are `value` for a single source or `value1`, `value2`, ... for multiple sources. Override them with parenthesized aliases: -```questdb-sql title="Custom column name" +```questdb-sql title="Custom column name" demo SELECT u.price FROM UNNEST(ARRAY[1.5, 2.5]) u(price); ``` @@ -125,9 +132,9 @@ SELECT u.price FROM UNNEST(ARRAY[1.5, 2.5]) u(price); ## JSON UNNEST -JSON UNNEST expands a JSON array (stored as `VARCHAR`) into rows with explicitly -typed columns. The `COLUMNS(...)` clause distinguishes JSON UNNEST from array -UNNEST. +JSON `UNNEST` expands a JSON array (stored as `VARCHAR`) into rows with +explicitly typed columns. The `COLUMNS(...)` clause distinguishes JSON `UNNEST` +from array `UNNEST`. ### Syntax @@ -147,7 +154,7 @@ FROM table_name, UNNEST( Extract typed fields from an array of JSON objects. Column names in `COLUMNS()` are used as JSON field names for extraction: -```questdb-sql title="Extract fields from JSON objects" +```questdb-sql title="Extract fields from JSON objects" demo SELECT u.name, u.age FROM UNNEST( '[{"name":"Alice","age":30},{"name":"Bob","age":25}]'::VARCHAR @@ -165,7 +172,7 @@ FROM UNNEST( When `COLUMNS()` declares a single column and the JSON array contains scalars (not objects), each element is extracted directly: -```questdb-sql title="Scalar JSON array" +```questdb-sql title="Scalar JSON array" demo SELECT u.val FROM UNNEST('[1.5, 2.5, 3.5]'::VARCHAR COLUMNS(val DOUBLE)) u; ``` @@ -178,9 +185,9 @@ FROM UNNEST('[1.5, 2.5, 3.5]'::VARCHAR COLUMNS(val DOUBLE)) u; ### WITH ORDINALITY -Works the same as array UNNEST - alias the ordinality column as the last entry: +Works the same as array `UNNEST` - alias the ordinality column as the last entry: -```questdb-sql title="JSON UNNEST with position index" +```questdb-sql title="JSON UNNEST with position index" demo SELECT u.val, u.pos FROM UNNEST( '[10, 20, 30]'::VARCHAR COLUMNS(val LONG) @@ -198,7 +205,7 @@ FROM UNNEST( JSON string values are parsed using QuestDB's standard timestamp formats. Numeric values are treated as microseconds since epoch: -```questdb-sql title="Timestamps from JSON strings" +```questdb-sql title="Timestamps from JSON strings" demo SELECT u.ts, u.val FROM UNNEST( '[{"ts":"2024-01-15T10:30:00.000000Z","val":1.5}, @@ -217,7 +224,7 @@ FROM UNNEST( The names in `COLUMNS()` serve as both JSON field names and default output column names. Override the output names with aliases after the table alias: -```questdb-sql title="JSON field 'price' output as 'cost'" +```questdb-sql title="JSON field 'price' output as 'cost'" demo SELECT u.cost FROM UNNEST( '[{"price":1.5},{"price":2.5}]'::VARCHAR @@ -253,7 +260,7 @@ FROM events e, UNNEST( When a JSON value does not match the declared column type, the result is `NULL` (except `BOOLEAN`, which defaults to `false`): -```questdb-sql title="Missing fields produce NULL" +```questdb-sql title="Missing fields produce NULL" demo SELECT u.a, u.b FROM UNNEST( '[{"a":1},{"a":2,"b":99},{"a":null}]'::VARCHAR @@ -271,55 +278,42 @@ FROM UNNEST( ### Filter unnested rows -```questdb-sql title="Filter by unnested value" -SELECT t.symbol, u.size -FROM market_data t, UNNEST(t.bid_sizes) u(size) -WHERE u.size > 100.0 +```questdb-sql title="Filter by unnested value" demo +SELECT t.symbol, u.vol +FROM market_data t, UNNEST(t.asks[2]) u(vol) +WHERE t.timestamp IN '$now-1m..$now' + AND t.symbol = 'EURUSD' + AND u.vol > 100.0 ORDER BY t.timestamp; ``` ### Aggregate unnested values -```questdb-sql title="Total bid size per symbol" -SELECT t.symbol, sum(u.size) AS total_bid_size -FROM market_data t, UNNEST(t.bid_sizes) u(size) +```questdb-sql title="Total ask volume per symbol" demo +SELECT t.symbol, sum(u.vol) AS total_ask_vol +FROM market_data t, UNNEST(t.asks[2]) u(vol) +WHERE t.timestamp IN '$now-1m..$now' GROUP BY t.symbol; ``` -### Aggregate JSON array fields - -```questdb-sql title="Sum quantities from JSON per event" -SELECT e.id, sum(u.qty) AS total_qty -FROM events e, UNNEST( - e.payload COLUMNS(qty INT) -) u -GROUP BY e.id; -``` - ### CTE with UNNEST -```questdb-sql title="Wrap UNNEST in a CTE for further processing" +```questdb-sql title="Wrap UNNEST in a CTE for further processing" demo WITH expanded AS ( - SELECT m.symbol, m.timestamp, u.size, u.level - FROM market_data m, UNNEST(m.bid_sizes) WITH ORDINALITY u(size, level) + SELECT m.symbol, m.timestamp, u.vol, u.level + FROM market_data m, UNNEST(m.asks[2]) WITH ORDINALITY u(vol, level) + WHERE m.timestamp IN '$now-1m..$now' + AND m.symbol = 'EURUSD' ) -SELECT symbol, level, avg(size) AS avg_size +SELECT symbol, level, avg(vol) AS avg_vol FROM expanded GROUP BY symbol, level ORDER BY symbol, level; ``` -### DISTINCT on unnested values - -```questdb-sql title="Unique values from an array column" -SELECT DISTINCT u.val -FROM t, UNNEST(t.arr) u(val) -ORDER BY u.val; -``` - ## Limitations -- **FROM clause only**: UNNEST cannot appear in the `SELECT` list. Use +- **`FROM` clause only**: `UNNEST` cannot appear in the `SELECT` list. Use `SELECT * FROM UNNEST(...)` instead. - **Array types**: Only `DOUBLE[]` is currently supported as a native array column type. Array literals like `ARRAY[1.0, 2.0]` produce `DOUBLE[]`. From db5b9d38a8d7cf7d8002c58e6fe41f12cdcdfcd4 Mon Sep 17 00:00:00 2001 From: javier Date: Wed, 11 Mar 2026 11:06:07 +0100 Subject: [PATCH 4/4] Add chained UNNEST section and rename JSON nested arrays heading --- documentation/query/sql/unnest.md | 32 +++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/documentation/query/sql/unnest.md b/documentation/query/sql/unnest.md index 37c92aa7b..a6f6d3501 100644 --- a/documentation/query/sql/unnest.md +++ b/documentation/query/sql/unnest.md @@ -115,6 +115,34 @@ FROM UNNEST(ARRAY[ARRAY[1.0, 2.0], ARRAY[3.0, 4.0]]); | [1.0,2.0] | | [3.0,4.0] | +### Chained UNNEST + +To fully flatten a multidimensional array into individual scalars, chain +multiple `UNNEST` calls in the `FROM` clause. Each one reduces dimensionality +by one level: + +```questdb-sql title="Fully flatten a 2D array" demo +SELECT u.val +FROM UNNEST(ARRAY[ARRAY[1.0, 2.0], ARRAY[3.0, 4.0]]) t(arr), + UNNEST(t.arr) u(val); +``` + +| val | +| :-- | +| 1.0 | +| 2.0 | +| 3.0 | +| 4.0 | + +:::note + +`UNNEST` cannot be nested as an expression. Writing +`UNNEST(UNNEST(...))` produces the error +*UNNEST cannot be used as an expression; use it in the FROM clause*. Use the +chained `FROM` clause syntax shown above instead. + +::: + ### Column aliases Default column names are `value` for a single source or `value1`, `value2`, ... @@ -232,9 +260,9 @@ FROM UNNEST( ) u(cost); ``` -### With json_extract() +### Nested JSON arrays -Use `json_extract()` to reach nested JSON paths before unnesting: +Use `json_extract()` to reach a nested JSON array before unnesting: ```questdb-sql title="Unnest a nested JSON array" SELECT u.price