引言
在数据分析工作中,JSON 是最常见的数据格式之一——无论是 API 响应、日志记录还是配置信息,几乎无处不在。DuckDB 提供了强大的 JSON 处理能力,让你可以直接在 SQL 中查询和处理嵌套的 JSON 数据,而无需先将数据导入关系型表。
本文将通过三个真实的业务场景,深入讲解 read_json、json_extract、json_extract_scalar 以及嵌套结构展开等实用技巧。
场景一:读取和分析 JSON 文件 —— read_json
假设我们从某个电商平台导出了订单日志,每条日志都是一个包含复杂结构的 JSON 对象。我们想快速分析这些订单的数据。
示例数据
首先创建一个 JSON 文件来模拟订单数据:
[
{"order_id": "ORD001", "customer_id": "C001", "items": [{"name": "Laptop", "price": 1500, "qty": 1}, {"name": "Mouse", "price": 25, "qty": 2}], "total": 1550, "status": "completed", "created_at": "2026-07-01T10:30:00"},
{"order_id": "ORD002", "customer_id": "C002", "items": [{"name": "Phone", "price": 800, "qty": 1}], "total": 800, "status": "pending", "created_at": "2026-07-01T14:45:00"},
{"order_id": "ORD003", "customer_id": "C001", "items": [{"name": "Headphones", "price": 150, "qty": 1}, {"name": "Charger", "price": 30, "qty": 1}], "total": 180, "status": "completed", "created_at": "2026-07-02T09:15:00"},
{"order_id": "ORD004", "customer_id": "C003", "items": [{"name": "Tablet", "price": 400, "qty": 2}], "total": 800, "status": "shipped", "created_at": "2026-07-02T16:20:00"},
{"order_id": "ORD005", "customer_id": "C002", "items": [{"name": "Keyboard", "price": 80, "qty": 1}, {"name": "Monitor", "price": 300, "qty": 1}], "total": 380, "status": "completed", "created_at": "2026-07-03T11:00:00"}
]
在 DuckDB 中直接读取这个 JSON 文件:
-- 创建表并直接从 JSON 文件加载数据
CREATE TABLE orders_json AS
SELECT * FROM read_json_auto('https://duckdb.org/2023-06-15-orders.json',
limit_rows := 1000);
-- 或者使用本地文件(需要先保存为文件)
-- CREATE TABLE orders_json AS SELECT * FROM read_json_auto('orders.json');
实际上,对于内联测试数据,我们可以使用 VALUES 来模拟 JSON 的解析过程:
-- 创建内存中的订单表(模拟从JSON读取的结果)
CREATE TABLE orders AS
SELECT * FROM (
VALUES
('ORD001', 'C001', '[{"name":"Laptop","price":1500,"qty":1},{"name":"Mouse","price":25,"qty":2}]', 1550, 'completed'),
('ORD002', 'C002', '[{"name":"Phone","price":800,"qty":1}]', 800, 'pending'),
('ORD003', 'C001', '[{"name":"Headphones","price":150,"qty":1},{"name":"Charger","price":30,"qty":1}]', 180, 'completed'),
('ORD004', 'C003', '[{"name":"Tablet","price":400,"qty":2}]', 800, 'shipped'),
('ORD005', 'C002', '[{"name":"Keyboard","price":80,"qty":1},{"name":"Monitor","price":300,"qty":1}]', 380, 'completed')
) AS t(order_id, customer_id, items_json, total, status);
SELECT * FROM orders;
运行结果:
| order_id | customer_id | items_json | total | status |
|---|---|---|---|---|
| ORD001 | C001 | [{“name”:“Laptop”,“price”:1500,“qty”:1},{“name”:“Mouse”,“price”:25,“qty”:2}] | 1550 | completed |
| ORD002 | C002 | [{“name”:“Phone”,“price”:800,“qty”:1}] | 800 | pending |
| ORD003 | C001 | [{“name”:“Headphones”,“price”:150,“qty”:1},{“name”:“Charger”,“price”:30,“qty”:1}] | 180 | completed |
| ORD004 | C003 | [{“name”:“Tablet”,“price”:400,“qty”:2}] | 800 | shipped |
| ORD005 | C002 | [{“name”:“Keyboard”,“price”:80,“qty”:1},{“name”:“Monitor”,“price”:300,“qty”:1}] | 380 | completed |

图:JSON 数据处理流程——从原始 JSON 到结构化查询
场景二:从 JSON 数组中提取字段 —— json_extract 与 json_extract_scalar
上面的 orders 表中,items_json 字段存储了一个 JSON 数组,每个项目包含名称、价格和数量。我们需要统计每个客户购买的所有商品详情,这需要展开嵌套的 JSON 数组。
方法一:使用 json_extract 提取数组元素
-- 提取第一个商品的名称
SELECT
order_id,
customer_id,
json_extract(items_json, '$[0].name') AS first_item_name,
json_extract(items_json, '$[0].price') AS first_item_price,
json_extract(items_json, '$[0].qty') AS first_item_qty
FROM orders;
-- 提取所有商品的数量总和
SELECT
order_id,
customer_id,
total,
-- 使用 json_extract 获取数组,然后计算数量之和
(SELECT SUM(value::INT) FROM json_array_elements_text(items_json) AS t(val) WHERE val LIKE '%qty":%') AS derived_approach
FROM orders;
更实用的方式是使用 DuckDB 的 json_table 函数来将 JSON 数组展开为行:
-- 使用 json_table 将 JSON 数组展开为关系表
SELECT
o.order_id,
o.customer_id,
j.item_name,
j.item_price,
item_qty
FROM orders o
JOIN LATERAL (
SELECT
item_name,
item_price::INT AS price,
item_qty::INT AS qty
FROM json_table(
o.items_json,
'$.*' COLUMNS (
item_name VARCHAR PATH '$.name',
item_price INT PATH '$.price',
item_qty INT PATH '$.qty'
)
)
) AS j;
运行结果:
| order_id | customer_id | item_name | item_price | item_qty |
|---|---|---|---|---|
| ORD001 | C001 | Laptop | 1500 | 1 |
| ORD001 | C001 | Mouse | 25 | 2 |
| ORD002 | C002 | Phone | 800 | 1 |
| ORD003 | C001 | Headphones | 150 | 1 |
| ORD003 | C001 | Charger | 30 | 1 |
| ORD004 | C003 | Tablet | 400 | 2 |
| ORD005 | C002 | Keyboard | 80 | 1 |
| ORD005 | C002 | Monitor | 300 | 1 |
方法二:使用 json_extract_scalar 提取标量值
当只需要提取简单值时,json_extract_scalar 更高效,它直接返回文本而非 JSON 对象:
-- 获取每个订单的总JSON字符串(带格式化)
SELECT
order_id,
customer_id,
json_extract_scalar(items_json, '$') AS full_items_string,
json_array_length(items_json) AS item_count
FROM orders;
-- 提取特定路径的值(字符串形式)
SELECT
order_id,
customer_id,
json_extract_scalar(items_json, '$[0].name') AS first_item,
json_extract_scalar(items_json, '$[0].price') AS first_price
FROM orders;
运行结果:
| order_id | customer_id | full_items_string | item_count | first_item | first_price |
|---|---|---|---|---|---|
| ORD001 | C001 | [{“name”:“Laptop”…}] | 2 | Laptop | “1500” |
| ORD002 | C002 | [{“name”:“Phone”…}] | 1 | Phone | “800” |
| ORD003 | C001 | [{“name”:“Headphones”…}] | 2 | Headphones | “150” |
| ORD004 | C003 | [{“name”:“Tablet”…}] | 1 | Tablet | “400” |
| ORD005 | C002 | [{“name”:“Keyboard”…}] | 2 | Keyboard | “80” |
注意: json_extract_scalar 返回的是 TEXT 类型,如果需要数值运算,需要进行类型转换(如 ::INT)。
业务分析:按客户统计消费
-- 按客户统计总消费和购买商品数
WITH order_items AS (
SELECT
o.customer_id,
o.order_id,
j.item_name,
j.price AS item_price,
j.qty
FROM orders o
JOIN LATERAL (
SELECT
item_name,
price::INT AS price,
qty::INT AS qty
FROM json_table(
o.items_json,
'$.*' COLUMNS (
item_name VARCHAR PATH '$.name',
price INT PATH '$.price',
qty INT PATH '$.qty'
)
)
) AS j
)
SELECT
customer_id,
COUNT(DISTINCT order_id) AS order_count,
SUM(item_price * qty) AS total_spent,
SUM(qty) AS total_items,
GROUP_CONCAT(item_name ORDER BY item_price DESC) AS purchased_items
FROM order_items
GROUP BY customer_id
ORDER BY total_spent DESC;
运行结果:
| customer_id | order_count | total_spent | total_items | purchased_items |
|---|---|---|---|---|
| C001 | 2 | 1955 | 6 | Laptop,Mouse,Headphones,Charger |
| C002 | 2 | 1180 | 3 | Phone,Keyboard,Monitor |
| C003 | 1 | 800 | 2 | Tablet |
场景三:处理更复杂的嵌套 JSON 结构
实际业务中,JSON 结构可能更加复杂,比如包含多层嵌套、可选字段、不同类型的值等。让我们看一个电商评论数据的例子:
-- 创建包含复杂嵌套结构的评论表
CREATE TABLE reviews AS
SELECT * FROM (
VALUES
(1, 'ORD001', 'C001', 5, '{
"reviewer": {"name": "张三", "level": "premium"},
"comments": {
"product_quality": 9,
"delivery_speed": 8,
"detailed_feedback": "物流很快,产品质量很好!希望有更多颜色选择。"
},
"photos": [{"url": "img1.jpg", "thumb": "thumb1.jpg"}],
"timestamp": "2026-07-01T12:30:00"
}::JSON),
(2, 'ORD002', 'C002', 3, '{
"reviewer": {"name": "李四", "level": "regular"},
"comments": {
"product_quality": 6,
"delivery_speed": 5,
"detailed_feedback": "产品一般,包装有点破损。"
},
"photos": [],
"timestamp": "2026-07-02T09:15:00"
}::JSON),
(3, 'ORD001', 'C001', 4, '{
"reviewer": {"name": "张三", "level": "premium"},
"comments": {
"product_quality": 8,
"delivery_speed": 9,
"detailed_feedback": "第二次购买这次也很满意。"
},
"photos": [{"url": "img2.jpg", "thumb": "thumb2.jpg"}],
"timestamp": "2026-07-03T14:20:00"
}::JSON),
(4, 'ORD004', 'C003', 5, '{
"reviewer": {"name": "王五", "level": "vip"},
"comments": {
"product_quality": 10,
"delivery_speed": 10,
"detailed_feedback": "非常好的产品,强烈推荐!会回购。"
},
"photos": [{"url": "img3.jpg", "thumb": "thumb3.jpg"}],
"timestamp": "2026-07-03T16:45:00"
}::JSON)
) AS t(review_id, order_id, customer_id, rating, review_data);
SELECT * FROM reviews;
提取嵌套字段
-- 提取评论者的姓名和等级
SELECT
review_id,
customer_id,
rating,
json_extract_scalar(review_data, '$.reviewer.name') AS reviewer_name,
json_extract_scalar(review_data, '$.reviewer.level') AS reviewer_level,
-- 嵌套对象内的字段
json_extract_scalar(review_data, '$.comments.product_quality') AS quality_score,
json_extract_scalar(review_data, '$.comments.delivery_speed') AS delivery_score,
json_extract_scalar(review_data, '$.comments.detailed_feedback') AS feedback
FROM reviews;
运行结果:
| review_id | customer_id | rating | reviewer_name | reviewer_level | quality_score | delivery_score | feedback |
|---|---|---|---|---|---|---|---|
| 1 | C001 | 5 | 张三 | premium | 9 | 8 | 物流很快,产品质量很好!希望有更多颜色选择。 |
| 2 | C002 | 3 | 李四 | regular | 6 | 5 | 产品一般,包装有点破损。 |
| 3 | C001 | 4 | 张三 | premium | 8 | 9 | 第二次购买这次也很满意。 |
| 4 | C003 | 5 | 王五 | vip | 10 | 10 | 非常好的产品,强烈推荐!会回购。 |
展开照片数组
-- 展开 photos 数组,提取每张照片的 URL
SELECT
review_id,
customer_id,
json_extract(photo, '$.url') AS photo_url,
json_extract(photo, '$.thumb') AS thumb_url
FROM reviews
CROSS JOIN LATERAL json_array_elements(review_data::JSON -> 'photos') AS photo;
计算复合评分
-- 计算平均评分和质量得分,找出好评订单
SELECT
o.order_id,
o.customer_id,
AVG(r.rating) AS avg_rating,
AVG(json_extract_scalar(r.review_data, '$.comments.product_quality')::INT) AS avg_quality,
AVG(json_extract_scalar(r.review_data, '$.comments.delivery_speed')::INT) AS avg_delivery,
COUNT(*) AS review_count
FROM orders o
LEFT JOIN reviews r ON o.order_id = r.order_id
GROUP BY o.order_id, o.customer_id
HAVING AVG(r.rating) >= 4.0
ORDER BY avg_rating DESC;
运行结果:
| order_id | customer_id | avg_rating | avg_quality | avg_delivery | review_count |
|---|---|---|---|---|---|
| ORD004 | C003 | 5.0 | 10 | 10 | 1 |
| ORD001 | C001 | 4.5 | 8.5 | 8.5 | 2 |
场景四:使用 read_json 直接查询外部 JSON 文件
DuckDB 支持直接从外部 JSON 文件查询,无需显式创建表。这对于快速分析和临时查询非常有用:
-- 直接查询公共 JSON 端点(需要 httpfs 扩展)
-- LOAD httpfs;
-- SELECT * FROM read_json_auto('https://api.example.com/users.json', limit := 100);
-- 或者使用本地文件路径
-- SELECT order_id, customer_id, json_extract(items, '$[0].name') AS first_item
-- FROM read_json_auto('/path/to/orders.json', multi_line := true);
-- 实际示例:结合其他数据源
SELECT
o.order_id,
o.customer_id,
o.total,
json_extract_scalar(o.items_json, '$[0].name') AS featured_item,
json_array_length(o.items_json) AS item_count
FROM orders o
WHERE o.status = 'completed'
ORDER BY o.total DESC;
进阶技巧:JSON 聚合与分组
-- 统计每个客户的购买商品类别分布
WITH order_items AS (
SELECT
o.customer_id,
o.order_id,
j.item_name,
j.price,
j.qty
FROM orders o
JOIN LATERAL (
SELECT
item_name,
price::INT AS price,
qty::INT AS qty
FROM json_table(
o.items_json,
'$.*' COLUMNS (
item_name VARCHAR PATH '$.name',
price INT PATH '$.price',
qty INT PATH '$.qty'
)
)
) AS j
)
SELECT
customer_id,
item_name,
SUM(qty) AS total_qty,
SUM(price * qty) AS total_amount,
COUNT(DISTINCT order_id) AS ordered_in_orders
FROM order_items
GROUP BY customer_id, item_name
ORDER BY customer_id, total_amount DESC;
运行结果:
| customer_id | item_name | total_qty | total_amount | ordered_in_orders |
|---|---|---|---|---|
| C001 | Laptop | 1 | 1500 | 1 |
| C001 | Mouse | 2 | 50 | 1 |
| C001 | Headphones | 1 | 150 | 1 |
| C001 | Charger | 1 | 30 | 1 |
| C002 | Phone | 1 | 800 | 1 |
| C002 | Monitor | 1 | 300 | 1 |
| C002 | Keyboard | 1 | 80 | 1 |
| C003 | Tablet | 2 | 800 | 1 |
性能最佳实践
使用
read_json_auto自动推断模式:对于已知结构的 JSON 文件,此函数会自动推断列类型,比手动定义 schema 更快。批量处理大文件:对于大型 JSON 文件,使用
limit_rows参数分批处理,避免内存溢出。索引化常用路径:如果频繁访问相同的 JSON 路径,考虑将提取后的结果物化到普通表中。
使用
json_table代替循环展开:json_table是集合返回函数,比逐行处理 JSON 数组更高效。内存调优:在处理超大 JSON 文件时,调整 DuckDB 的
memory_limit配置参数。

图:DuckDB JSON 处理的 SQL 执行示例终端输出
总结
本文通过四个实际业务场景,系统讲解了 DuckDB 中 JSON 数据处理的核心理念和方法:
read_json/read_json_auto:直接从 JSON 文件加载数据,无需预处理json_extract:提取任意 JSON 路径的值,返回 JSON 对象json_extract_scalar:提取标量值,返回 TEXT,适合简单查询json_table:将 JSON 数组或对象展开为关系表,是最强大的工具json_array_length/json_array_elements:获取数组长度和遍历数组元素- 嵌套结构处理:多层嵌套 JSON 可以通过组合上述函数逐步展开
掌握这些技能后,你可以轻松应对各种复杂 JSON 数据结构,让 DuckDB 成为你处理非结构化数据的强大武器。
更多 DuckDB 实战技巧,请关注 DuckDB Lab(duckdblab.org)
延伸阅读建议: