Featured image of post DuckDB's 5 Killer JSON Functions: From Extraction to Aggregation in One SQL

DuckDB's 5 Killer JSON Functions: From Extraction to Aggregation in One SQL

DuckDB's JSON capabilities go far beyond json_extract — master json_extract_scalar, json_array_length, json_each, json_group_array, and STRUCT types to handle any nested JSON in pure SQL. Includes e-commerce analysis demo and tips

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.

DuckDB JSON Functions Processing Pipeline

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

FunctionPurposeInputOutputTypical Use Case
json_extract_scalarExtract scalar valuesJSON text + pathVARCHAR/INTEGERPull order IDs, customer names
json_array_lengthGet array lengthJSON arrayINTEGERCount items per order
json_eachFlatten JSON arraysJSON arrayMultiple rowsExpand product lists to detail rows
json_group_arrayAggregate into JSON arrayMultiple rowsJSON arrayPack results as customer summary JSON
STRUCT typeNative nested accessNested JSONSTRUCTDot-notation access to nested fields

5. Comparison with Traditional Tools

DimensionPython (dict ops)jqDuckDB JSON Functions
Code length10-30 loop lines5-15 pipe chains1-5 SQL lines
Array flatteningNested for loopsmap + flattenjson_each in one line
AggregationPandas groupbyNearly impossibleNative GROUP BY
Repack as JSONjson.dumpsManual map拼接json_group_array
Million-row JSONHigh memory pressureProne to OOMColumnar + parallel scan
Learning curvePython basicsjq DSLSQL (everyone knows it)

本文信息

项目内容
DuckDB 版本v1.5.x(部分功能基于 v2.0 Preview)
最后验证2026-09-12
测试环境Linux / x86_64 / 16GB RAM
官方文档DuckDB Documentation
GitHubpengzz9527/duckdb-blog

如发现错误,欢迎通过 GitHub Issue 或邮件 [email protected] 反馈。

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials