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 monetization 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)

6. Monetization Ideas

Mastering these 5 JSON functions opens up several revenue streams:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials

Built with Hugo
Theme Stack designed by Jimmy

⚠️ This site is an independent community project, not affiliated with, endorsed by, or sponsored by the DuckDB Foundation or official DuckDB project.

"DuckDB" is a registered trademark of the DuckDB Foundation. This site uses the name solely for factual description purposes.

All content is for educational and community promotion purposes only and does not constitute any commercial service.