DuckDB实战:JSON 数据处理实战:读取、解析与嵌套结构展开

通过真实业务场景深入讲解DuckDB的JSON处理能力,包括read_json读取JSON文件、json_extract提取嵌套字段、展开复杂嵌套结构,附带完整SQL示例和运行结果。

引言

在数据分析工作中,JSON 是最常见的数据格式之一——无论是 API 响应、日志记录还是配置信息,几乎无处不在。DuckDB 提供了强大的 JSON 处理能力,让你可以直接在 SQL 中查询和处理嵌套的 JSON 数据,而无需先将数据导入关系型表。

本文将通过三个真实的业务场景,深入讲解 read_jsonjson_extractjson_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_idcustomer_iditems_jsontotalstatus
ORD001C001[{“name”:“Laptop”,“price”:1500,“qty”:1},{“name”:“Mouse”,“price”:25,“qty”:2}]1550completed
ORD002C002[{“name”:“Phone”,“price”:800,“qty”:1}]800pending
ORD003C001[{“name”:“Headphones”,“price”:150,“qty”:1},{“name”:“Charger”,“price”:30,“qty”:1}]180completed
ORD004C003[{“name”:“Tablet”,“price”:400,“qty”:2}]800shipped
ORD005C002[{“name”:“Keyboard”,“price”:80,“qty”:1},{“name”:“Monitor”,“price”:300,“qty”:1}]380completed

JSON 架构图

图:JSON 数据处理流程——从原始 JSON 到结构化查询

场景二:从 JSON 数组中提取字段 —— json_extractjson_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_idcustomer_iditem_nameitem_priceitem_qty
ORD001C001Laptop15001
ORD001C001Mouse252
ORD002C002Phone8001
ORD003C001Headphones1501
ORD003C001Charger301
ORD004C003Tablet4002
ORD005C002Keyboard801
ORD005C002Monitor3001

方法二:使用 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_idcustomer_idfull_items_stringitem_countfirst_itemfirst_price
ORD001C001[{“name”:“Laptop”…}]2Laptop“1500”
ORD002C002[{“name”:“Phone”…}]1Phone“800”
ORD003C001[{“name”:“Headphones”…}]2Headphones“150”
ORD004C003[{“name”:“Tablet”…}]1Tablet“400”
ORD005C002[{“name”:“Keyboard”…}]2Keyboard“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_idorder_counttotal_spenttotal_itemspurchased_items
C001219556Laptop,Mouse,Headphones,Charger
C002211803Phone,Keyboard,Monitor
C00318002Tablet

场景三:处理更复杂的嵌套 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_idcustomer_idratingreviewer_namereviewer_levelquality_scoredelivery_scorefeedback
1C0015张三premium98物流很快,产品质量很好!希望有更多颜色选择。
2C0023李四regular65产品一般,包装有点破损。
3C0014张三premium89第二次购买这次也很满意。
4C0035王五vip1010非常好的产品,强烈推荐!会回购。

展开照片数组

-- 展开 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_idcustomer_idavg_ratingavg_qualityavg_deliveryreview_count
ORD004C0035.010101
ORD001C0014.58.58.52

场景四:使用 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_iditem_nametotal_qtytotal_amountordered_in_orders
C001Laptop115001
C001Mouse2501
C001Headphones11501
C001Charger1301
C002Phone18001
C002Monitor13001
C002Keyboard1801
C003Tablet28001

性能最佳实践

  1. 使用 read_json_auto 自动推断模式:对于已知结构的 JSON 文件,此函数会自动推断列类型,比手动定义 schema 更快。

  2. 批量处理大文件:对于大型 JSON 文件,使用 limit_rows 参数分批处理,避免内存溢出。

  3. 索引化常用路径:如果频繁访问相同的 JSON 路径,考虑将提取后的结果物化到普通表中。

  4. 使用 json_table 代替循环展开json_table 是集合返回函数,比逐行处理 JSON 数组更高效。

  5. 内存调优:在处理超大 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)


延伸阅读建议:

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials

使用 Hugo 构建
主题 StackJimmy 设计

⚠️ 本站为独立社区项目,与 DuckDB 基金会及 DuckDB 官方项目无任何从属、背书或赞助关系。

"DuckDB" 是 DuckDB 基金会的注册商标,本站仅以事实描述方式使用该名称。

本站内容仅供教育与社区推广用途,不构成任何商业服务。