Skip to main content

Nested Types: LIST, STRUCT, MAP

Learning Focus

Use this lesson to work with nested data structures in DuckDB — a major advantage over traditional RDBMS systems.

Why Nested Types?

DuckDB natively supports nested types, making it easy to work with:

  • JSON documents
  • Arrays of values
  • Key-value pairs
  • Nested structs

This avoids the need to flatten all data into separate tables.

LIST Type

A LIST holds an ordered collection of values of the same type.

-- Create a table with a LIST column
CREATE TABLE student_scores (
student_id INTEGER,
name VARCHAR,
test_scores INTEGER[]
);

INSERT INTO student_scores VALUES
(1, 'Alice', [95, 87, 91, 88]),
(2, 'Bob', [72, 68, 75, 80]),
(3, 'Carol', [99, 98, 97, 100]);

-- Query list elements (1-indexed)
SELECT name, test_scores[1] AS first_test FROM student_scores;

-- Get list length
SELECT name, len(test_scores) AS num_tests FROM student_scores;

-- Compute average over list
SELECT name, list_avg(test_scores) AS avg_score FROM student_scores;

-- Filter rows where any element satisfies condition
SELECT * FROM student_scores
WHERE list_contains(test_scores, 100);

STRUCT Type

A STRUCT holds named fields of potentially different types — like an inline record.

-- Create table with STRUCT column
CREATE TABLE employees (
id INTEGER,
name VARCHAR,
address STRUCT(street VARCHAR, city VARCHAR, zip VARCHAR)
);

INSERT INTO employees VALUES
(1, 'Alice', {'street': '123 Main St', 'city': 'Austin', 'zip': '78701'}),
(2, 'Bob', {'street': '456 Oak Ave', 'city': 'Denver', 'zip': '80201'});

-- Access struct fields
SELECT name, address.city AS city FROM employees;

-- Build a struct inline
SELECT {'name': 'Alice', 'age': 30} AS person;

MAP Type

A MAP holds dynamic key-value pairs where keys are of one type and values of another.

CREATE TABLE product_attrs (
product_id INTEGER,
attributes MAP(VARCHAR, VARCHAR)
);

INSERT INTO product_attrs VALUES
(1, MAP {'color': 'red', 'size': 'large', 'material': 'cotton'}),
(2, MAP {'color': 'blue', 'weight': '500g'});

-- Access map value by key
SELECT product_id, attributes['color'] AS color FROM product_attrs;

-- Get all keys
SELECT product_id, map_keys(attributes) AS keys FROM product_attrs;

-- Get all values
SELECT product_id, map_values(attributes) AS vals FROM product_attrs;

Working with JSON as Nested Types

-- Read JSON file with nested arrays
SELECT * FROM read_json_auto('orders.json');

-- Unnest a list into rows
SELECT name, unnest(test_scores) AS score
FROM student_scores;

-- Flatten struct fields
SELECT id, name, address.city, address.zip FROM employees;

List Functions Reference

SELECT list_avg([1,2,3,4,5]);             -- 3.0
SELECT list_sum([1,2,3,4,5]); -- 15
SELECT list_min([1,2,3,4,5]); -- 1
SELECT list_max([1,2,3,4,5]); -- 5
SELECT list_contains([1,2,3], 2); -- true
SELECT list_distinct([1,1,2,3,3]); -- [1, 2, 3]
SELECT list_sort([3,1,2]); -- [1, 2, 3]
SELECT list_slice([1,2,3,4,5], 2, 4); -- [2, 3, 4]
SELECT flatten([[1,2],[3,4]]); -- [1, 2, 3, 4]

Common Pitfalls

PitfallConsequencePrevention
Using 0-based indexing on listsWrong element returnedDuckDB lists are 1-indexed
Querying MAP with missing keysReturns NULLUse coalesce(map['key'], 'default')
Joining on LIST columnsUnexpected behaviorUse unnest() first to normalize

Quick Reference

-- List
SELECT [1,2,3][1]; -- 1
SELECT list_avg([10,20,30]); -- 20.0
SELECT unnest([1,2,3]); -- 3 rows

-- Struct
SELECT {'a': 1, 'b': 2}.a; -- 1

-- Map
SELECT MAP {'k': 'v'}['k']; -- v
SELECT map_keys(MAP {'x': 1, 'y': 2}); -- [x, y]

What's Next