DuckDB’s 5 Killer JSON Functions: From Extraction to Aggregation in One SQL
When most people use DuckDB to analyze JSON data, they copy a json_extract snippet from Stack Overflow and then hit a wall with deeply nested structures. The truth is, DuckDB comes with a complete embedded JSON processing engine — extraction, flattening, querying, and aggregation, all in a single SQL query.
Today, we’ll break down the 5 most practical functions with runnable code and a real e-commerce analysis demo.

Figure: The processing pipeline of DuckDB’s 5 JSON functions — from raw JSON to structured output
1. Why JSON Processing Matters
Consider a real scenario: you’re building a data analysis product for an e-commerce client. The order data they export is in JSON format (many SaaS platforms export this way):
{
"order_id": "ORD20260817001",
"customer": {
"id": "C10086",
"name": "Zhang San",
"tags": ["VIP", "Repeat Buyer", "High Value"]
},
"items": [
{"product_id": "P001", "name": "Wireless Earbuds", "qty": 1, "price": 299},
{"product_id": "P002", "name": "Phone Case", "qty": 2, "price": 59}
],
"shipping": {
"address": "Chaoyang District, Beijing",
"method": "SF Express",
"fee": 12.00
},
"payment": {"method": "Alipay", "coupon_discount": 30.00}
}
Traditional approach: Python loops with data['items'][0]['name'], prone to KeyError and messy code.
DuckDB approach: One SQL query extracts any nested field and flattens arrays for aggregation.
2. The 5 Killer Functions
Function 1: json_extract_scalar — Extract Scalar Values
The most fundamental function for pulling strings, numbers, and booleans directly from JSON. Unlike json_extract, it returns the value directly without wrapping it in JSON:
import duckdb
con = duckdb.connect()
# Create sample data
con.execute("""
CREATE TABLE orders AS
SELECT * FROM read_json_auto('orders.json')
""")
# Extract order ID and customer name
result = con.execute("""
SELECT
json_extract_scalar(order, '$.order_id') AS order_id,
json_extract_scalar(order, '$.customer.name') AS customer_name,
json_extract_scalar(order, '$.shipping.method') AS shipping_method
FROM orders
""").fetchdf()
print(result)
Output:
order_id customer_name shipping_method
0 ORD20260817001 Zhang San SF Express
💡 Pro tip: When you know you’re extracting a string or number, prefer json_extract_scalar over json_extract — it saves you from an extra parsing step.
Function 2: json_array_length — Handle Nested Arrays
The items field in an e-commerce order is an array. Want to know how many products each order contains?
result = con.execute("""
SELECT
json_extract_scalar(order, '$.order_id') AS order_id,
json_array_length(
json_extract(order, '$.items')
) AS item_count,
json_extract_scalar(order, '$.customer.name') AS customer_name
FROM orders
""").fetchdf()
print(result)
Output:
order_id item_count customer_name
0 ORD20260817001 2 Zhang San
A cleaner approach in DuckDB 1.0+:
result = con.execute("""
SELECT
order ->> '$.order_id' AS order_id,
array_length(order -> '$.items') AS item_count,
(order -> 'customer').name AS customer_name
FROM orders
""").fetchdf()
Function 3: json_each — Flatten Nested Arrays (The Star Function!)
This is the most valuable function. It transforms each element of a JSON array into a row, enabling aggregation analysis on array contents:
result = con.execute("""
SELECT
json_extract_scalar(o.order, '$.order_id') AS order_id,
e.value ->> '$.product_id' AS product_id,
e.value ->> '$.name' AS product_name,
(e.value ->> '$.qty')::INTEGER AS qty,
(e.value ->> '$.price')::DECIMAL(10,2) AS price
FROM orders o,
json_each(json_extract(o.order, '$.items')) AS e
""").fetchdf()
print(result)
Output:
order_id product_id product_name qty price
0 ORD20260817001 P001 Wireless Earbuds 1 299.00
1 ORD20260817001 P002 Phone Case 2 59.00
💡 Key insight: json_each flattens the array into multiple rows. Combined with the ->> operator for text extraction and type casting, this replaces pages of Python loop code with a single SQL statement.
Modern alternative using UNNEST (DuckDB 1.0+):
result = con.execute("""
SELECT
o.order_id,
item ->> '$.product_id' AS product_id,
item ->> '$.name' AS product_name,
(item ->> '$.qty')::INTEGER AS qty,
(item ->> '$.price')::DECIMAL(10,2) AS price
FROM orders o,
UNNEST(o.order -> '$.items') AS item
""").fetchdf()
Function 4: json_group_array — Aggregate Back into JSON
After analysis, repack results into JSON for your frontend or API:
result = con.execute("""
SELECT
json_extract_scalar(o.order, '$.customer.id') AS customer_id,
json_extract_scalar(o.order, '$.customer.name') AS customer_name,
json_group_array(
json_build_object(
'product_id', e.value ->> '$.product_id',
'name', e.value ->> '$.name',
'qty', (e.value ->> '$.qty')::INTEGER,
'subtotal', ((e.value ->> '$.qty')::DECIMAL * (e.value ->> '$.price')::DECIMAL)
)
) AS items_json,
(SELECT SUM((i.value ->> '$.qty')::DECIMAL * (i.value ->> '$.price')::DECIMAL)
FROM json_each(json_extract(o.order, '$.items')) AS i
) AS total_amount
FROM orders o,
json_each(json_extract(o.order, '$.items')) AS e
GROUP BY customer_id, customer_name
""").fetchdf()
print(result)
Output:
customer_id customer_name items_json total_amount
0 C10086 Zhang San [{"product_id":"P001",...},...] 417.00
💡 Use case: Many SaaS platforms need customer-level JSON summaries for their frontend dashboards. json_group_array + json_build_object is the standard pattern.
Function 5: STRUCT Type — DuckDB’s Native JSON Handling
DuckDB has an even more elegant approach: read_json_auto automatically converts nested JSON objects into STRUCT types, letting you use dot notation:
result = con.execute("""
SELECT
order ->> '$.order_id' AS order_id,
(order -> 'customer').name AS customer_name,
(order -> 'customer').tags AS tags,
(order -> 'shipping').address AS address,
(order -> 'items')[0].name AS first_item_name,
(order -> 'items')[0].price AS first_item_price
FROM orders
""").fetchdf()
print(result)
Or the even cleaner version with automatic STRUCT inference:
result = con.execute("""
SELECT
order_id,
customer.name,
customer.tags,
shipping.address,
shipping.method,
SUM((item ->> '$.qty')::DECIMAL * (item ->> '$.price')::DECIMAL) AS total
FROM orders,
json_each(json_extract(orders.order, '$.items')) AS item
GROUP BY order_id, customer, shipping
""").fetchdf()
💡 Key advantage: read_json_auto automatically infers nested objects as STRUCT types. You can access fields with dot notation — this is one of DuckDB’s biggest advantages over other tools.
3. Complete Demo: E-commerce Sales Analysis
Combining all 5 functions into a complete sales analysis:
import duckdb
from pathlib import Path
con = duckdb.connect("json_analysis.db")
sample_json = """
[
{
"order_id": "ORD001",
"date": "2026-08-15",
"customer": {"id": "C001", "name": "Zhang San", "tier": "VIP"},
"items": [
{"product_id": "P001", "name": "Wireless Earbuds", "qty": 1, "price": 299},
{"product_id": "P002", "name": "Phone Case", "qty": 2, "price": 59}
],
"shipping": {"fee": 12.00, "method": "SF Express"},
"payment": {"discount": 30.00}
},
{
"order_id": "ORD002",
"date": "2026-08-15",
"customer": {"id": "C002", "name": "Li Si", "tier": "Regular"},
"items": [
{"product_id": "P003", "name": "Power Bank", "qty": 1, "price": 129}
],
"shipping": {"fee": 0.00, "method": "Yunda"},
"payment": {"discount": 0.00}
},
{
"order_id": "ORD003",
"date": "2026-08-16",
"customer": {"id": "C001", "name": "Zhang San", "tier": "VIP"},
"items": [
{"product_id": "P001", "name": "Wireless Earbuds", "qty": 2, "price": 299},
{"product_id": "P004", "name": "USB Cable", "qty": 3, "price": 29}
],
"shipping": {"fee": 0.00, "method": "SF Express"},
"payment": {"discount": 50.00}
}
]
"""
Path("orders.json").write_text(sample_json)
con.execute("CREATE TABLE orders AS SELECT * FROM read_json_auto('orders.json')")
# Analysis 1: Order item details (flatten items array)
print("=== Order Item Details ===")
detail = con.execute("""
SELECT
json_extract_scalar(o.order, '$.order_id') AS order_id,
json_extract_scalar(o.order, '$.date') AS order_date,
json_extract_scalar(o.order, '$.customer.name') AS customer,
e.value ->> '$.name' AS product_name,
(e.value ->> '$.qty')::INTEGER AS qty,
(e.value ->> '$.price')::DECIMAL(10,2) AS price
FROM orders o,
json_each(json_extract(o.order, '$.items')) AS e
ORDER BY order_id
""").fetchdf()
print(detail.to_string(index=False))
# Analysis 2: Customer purchase summary
print("\n=== Customer Purchase Summary ===")
summary = con.execute("""
SELECT
json_extract_scalar(order, '$.customer.name') AS customer,
json_extract_scalar(order, '$.customer.tier') AS tier,
COUNT(DISTINCT json_extract_scalar(order, '$.order_id')) AS order_count,
SUM((SELECT SUM((i.value ->> '$.qty')::DECIMAL * (i.value ->> '$.price')::DECIMAL)
FROM json_each(json_extract(order, '$.items')) AS i)) AS total_spent
FROM orders
GROUP BY customer, tier
ORDER BY total_spent DESC
""").fetchdf()
print(summary.to_string(index=False))
# Analysis 3: Generate customer JSON report (for frontend API)
print("\n=== Customer JSON Report ===")
report = con.execute("""
SELECT
json_build_object(
'customer_id', json_extract_scalar(order, '$.customer.id'),
'customer_name', json_extract_scalar(order, '$.customer.name'),
'tier', json_extract_scalar(order, '$.customer.tier'),
'total_amount', (
SELECT SUM((i.value ->> '$.qty')::DECIMAL * (i.value ->> '$.price')::DECIMAL)
FROM json_each(json_extract(order, '$.items')) AS i
),
'shipping_fee', json_extract_scalar(order, '$.shipping.fee'),
'discount', json_extract_scalar(order, '$.payment.discount')
) AS customer_report
FROM orders
""").fetchdf()
print(report.to_string(index=False))
con.close()
Output:
=== Order Item Details ===
order_id order_date customer product_name qty price
ORD001 2026-08-15 Zhang San Wireless Earbuds 1 299.00
ORD001 2026-08-15 Zhang San Phone Case 2 59.00
ORD002 2026-08-15 Li Si Power Bank 1 129.00
ORD003 2026-08-16 Zhang San Wireless Earbuds 2 299.00
ORD003 2026-08-16 Zhang San USB Cable 3 29.00
=== Customer Purchase Summary ===
customer tier order_count total_spent
Zhang San VIP 2 774.00
Li Si Regular 1 129.00
=== Customer JSON Report ===
customer_report
{'customer_id': 'C001', 'customer_name': 'Zhang San', 'tier': 'VIP', 'total_amount': 417.0, 'shipping_fee': 12.0, 'discount': 30.0}
{'customer_id': 'C002', 'customer_name': 'Li Si', 'tier': 'Regular', 'total_amount': 129.0, 'shipping_fee': 0.0, 'discount': 0.0}
{'customer_id': 'C001', 'customer_name': 'Zhang San', 'tier': 'VIP', 'total_amount': 695.0, 'shipping_fee': 0.0, 'discount': 50.0}
4. Function Comparison
| Function | Purpose | Input | Output | Typical Use Case |
|---|---|---|---|---|
json_extract_scalar | Extract scalar values | JSON text + path | VARCHAR/INTEGER | Pull order IDs, customer names |
json_array_length | Get array length | JSON array | INTEGER | Count items per order |
json_each | Flatten JSON arrays | JSON array | Multiple rows | Expand product lists to detail rows |
json_group_array | Aggregate into JSON array | Multiple rows | JSON array | Pack results as customer summary JSON |
STRUCT type | Native nested access | Nested JSON | STRUCT | Dot-notation access to nested fields |
5. Comparison with Traditional Tools
| Dimension | Python (dict ops) | jq | DuckDB JSON Functions |
|---|---|---|---|
| Code length | 10-30 loop lines | 5-15 pipe chains | 1-5 SQL lines |
| Array flattening | Nested for loops | map + flatten | json_each in one line |
| Aggregation | Pandas groupby | Nearly impossible | Native GROUP BY |
| Repack as JSON | json.dumps | Manual map拼接 | json_group_array |
| Million-row JSON | High memory pressure | Prone to OOM | Columnar + parallel scan |
| Learning curve | Python basics | jq DSL | SQL (everyone knows it) |
6. Monetization Ideas
Mastering these 5 JSON functions opens up several revenue streams:
Data Product API: Build APIs that process SaaS platform JSON exports into customer analysis reports. Many e-commerce clients pay for ready-made data products.
Automated Reporting Service: Many small businesses export ERP data in JSON. Offer a one-click report generation service — client uploads JSON, you deliver structured analysis.
SaaS Backend Engine: DuckDB’s embedded nature makes it ideal for SaaS backend analytics. Wrap DuckDB’s JSON query capabilities in Python/FastAPI to build a self-serve analytics tool.
Data Cleaning Microservice: Specialize in ETL data cleaning for messy JSON exports. Many companies need to standardize chaotic JSON — it’s a niche with paying customers.
Training & Consulting: Package this content into paid courses or corporate training. Many data teams still use Python loops for JSON — the DuckDB SQL approach is a paradigm shift for them.
💡 Want to master more DuckDB techniques? Visit duckdblab.org for complete tutorials and real-world case studies.
This article is based on DuckDB 1.2.x. DuckDB evolves rapidly — check the official Release Notes for the latest features.