DuckDB has an unnest()
function that we can use to unnest lists and structs. Well, it can also be applied to NULL
, but that’ll return an empty result.
By “unnest” I mean it takes the list or struct, and it returns its contents as rows in a table. You might say that it converts lists and structs into tables, where each item in the list or struct becomes a row in the table.
Below are examples of using DuckDB’s unnest()
function to unnest lists and structs.
Lists
Let’s start with examples to demonstrate how unnest()
works with lists.
Unnest a Simple List
Lists can be unnested like this:
SELECT unnest(['Wag', 'Bark', 'Pant']) AS Result;
Result:
+--------+
| Result |
+--------+
| Wag |
| Bark |
| Pant |
+--------+
We can see that the list was converted to a table, with each list item ending up as a row in that table.
Any NULL
list items return NULL
for its row:
SELECT unnest(['Scratch', NULL, 'Purr']) AS Result;
Result:
+---------+
| Result |
+---------+
| Scratch |
| |
| Purr |
+---------+
I ran that in the DuckDB CLI, and by default, NULL values return an empty string, which is what we got here. We can change that with the .nullvalue
dot-command.
Adding a Column
We can use techniques like the following to add a column to the result:
SELECT
unnest(['Wag', 'Bark', 'Pant']) AS Name,
'Dog' AS Type;
Result:
+------+------+
| Name | Type |
+------+------+
| Wag | Dog |
| Bark | Dog |
| Pant | Dog |
+------+------+
We only needed to provide Dog
once for it to be listed on all rows.
Unnesting Two Lists Side-By Side
If we want the second column to have different values on each row, then we can use another list, with its own unnest()
:
SELECT
unnest(['Wag', 'Tweet', 'Scratch']) AS Name,
unnest(['Dog', 'Bird', 'Cat']) AS Type;
Result:
+---------+------+
| Name | Type |
+---------+------+
| Wag | Dog |
| Tweet | Bird |
| Scratch | Cat |
+---------+------+
If the lists are of different sizes, then we’ll end up with NULL
in one or more columns:
.nullvalue 'null'
SELECT
unnest(['Wag', 'Tweet', 'Scratch', 'Homer']) AS Name,
unnest(['Dog', 'Bird', 'Cat']) AS Type;
Result:
+---------+------+
| Name | Type |
+---------+------+
| Wag | Dog |
| Tweet | Bird |
| Scratch | Cat |
| Homer | null |
+---------+------+
I started this example with .nullvalue 'null'
in order to show any NULL values. This is a dot-command that we can use in the DuckDB CLI to show NULL values. As mentioned, the default value for NULL values is an empty string, so setting it to 'null'
like we did here makes it easier to tell when a NULL value is returned.
Unnest from a Subquery
We can use unnest()
to unnest a list column from a subquery. We can also use other expressions to modify the result, such as concatenating with a string.
Example:
SELECT unnest(dogs) || ' (Dog)'
FROM (VALUES (['Wag', 'Bark', 'Pant'])) tbl(dogs);
Result:
+----------------------------+
| (unnest(dogs) || ' (Dog)') |
+----------------------------+
| Wag (Dog) |
| Bark (Dog) |
| Pant (Dog) |
+----------------------------+
Unnest an Empty List
Unnesting an empty list returns an empty result. Here’s the output from my terminal when I pass an empty list:
D SELECT unnest([]);
D
Nothing is returned.
Unnest NULL
Unnesting NULL
also returns an empty result. Here’s the output from my terminal when I pass NULL
:
D SELECT unnest(null);
D
Same effect as passing an empty list.
Structs
We can also use the unnest()
function to unnest structs:
SELECT unnest({'name': 'Hesham', 'age': 35});
Result:
+--------+-----+
| name | age |
+--------+-----+
| Hesham | 35 |
+--------+-----+
Recursive Unnest
We can use recursive := true
to recursively unnest any nested lists (lists inside lists):
SELECT
unnest([
['Wag', 'Tweet', 'Scratch'],
['Bark', 'Purr']
], recursive := true ) AS recursive;
Result:
+-----------+
| recursive |
+-----------+
| Wag |
| Tweet |
| Scratch |
| Bark |
| Purr |
+-----------+
Here’s what happens if we omit recursive := true
or use recursive := false
:
SELECT
unnest([
['Wag', 'Tweet', 'Scratch'],
['Bark', 'Purr']
], recursive := false ) AS recursive_false,
unnest([
['Wag', 'Tweet', 'Scratch'],
['Bark', 'Purr']
] ) AS recursive_omitted;
Result:
+-----------------------+-----------------------+
| recursive_false | recursive_omitted |
+-----------------------+-----------------------+
| [Wag, Tweet, Scratch] | [Wag, Tweet, Scratch] |
| [Bark, Purr] | [Bark, Purr] |
+-----------------------+-----------------------+
Only the outer list has been unnested.
We can use the recursive := true
option to unnest a list of structs:
SELECT
unnest(
[
{'name': 'Barnesh', 'age': 71},
{'name': 'Alesha', 'age': 17}
],
recursive := true
);
Result:
+---------+-----+
| name | age |
+---------+-----+
| Barnesh | 71 |
| Alesha | 17 |
+---------+-----+
However, if a struct contains any lists, those lists aren’t unnested.
Here’s an example of what I mean:
SELECT
unnest(
[
{'name': 'Barnesh', 'scores': [12,34,17]},
{'name': 'Alesha', 'scores': [43,76,89]}
],
recursive := true
);
Result:
+---------+--------------+
| name | scores |
+---------+--------------+
| Barnesh | [12, 34, 17] |
| Alesha | [43, 76, 89] |
+---------+--------------+
Here’s an example of a nested struct:
SELECT
unnest(
{ 'person' : {'name': 'Barnesh', 'age': 71} },
recursive := true
);
Result:
+---------+-----+
| name | age |
+---------+-----+
| Barnesh | 71 |
+---------+-----+
Changing recursive
to false
or removing it altogether results in this:
SELECT
unnest(
{ 'person' : {'name': 'Barnesh', 'age': 71} },
recursive := false
) AS recursive_false,
unnest(
{ 'person' : {'name': 'Barnesh', 'age': 71} }
) AS recursive_omitted;
Result:
+------------------------------+------------------------------+
| person | person |
+------------------------------+------------------------------+
| {'name': Barnesh, 'age': 71} | {'name': Barnesh, 'age': 71} |
+------------------------------+------------------------------+
Specify the Depth for Unnesting
DuckDB also provides us with an alternative to the recursive
parameter. We can provide the depth to which we want unnesting to occur. More specifically, we provide the maximum depth.
Here’s an example of setting a maximum depth of 1
(which is the default):
SELECT unnest(
[[['Red', 'Blue'], ['Yellow','Green']], [['Dog','Cat'],['Tree','Flower','Seed']]],
max_depth := 1
) AS Result;
Result:
+------------------------------------+
| Result |
+------------------------------------+
| [[Red, Blue], [Yellow, Green]] |
| [[Dog, Cat], [Tree, Flower, Seed]] |
+------------------------------------+
That’s the same result that we’d get if we hadn’t specified a max_depth
.
Let’s increase it to 2
:
SELECT unnest(
[[['Red', 'Blue'], ['Yellow','Green']], [['Dog','Cat'],['Tree','Flower','Seed']]],
max_depth := 2
) AS Result;
Result:
+----------------------+
| Result |
+----------------------+
| [Red, Blue] |
| [Yellow, Green] |
| [Dog, Cat] |
| [Tree, Flower, Seed] |
+----------------------+
And now 3
:
SELECT unnest(
[[['Red', 'Blue'], ['Yellow','Green']], [['Dog','Cat'],['Tree','Flower','Seed']]],
max_depth := 3
) AS Result;
Result:
+--------+
| Result |
+--------+
| Red |
| Blue |
| Yellow |
| Green |
| Dog |
| Cat |
| Tree |
| Flower |
| Seed |
+--------+