Featured image of post DuckDB 嵌套 JSON 一键展平 & 列表级运算实战

DuckDB 嵌套 JSON 一键展平 & 列表级运算实战

DuckDB 的 json_tree() 函数和 list_transform/list_reduce 让你用纯 SQL 处理嵌套 JSON 和列表数据,告别 Python 循环。本文覆盖 API 数据展平、列表级运算、电商订单处理等 6 大实战场景。

一、问题场景:嵌套 JSON 与列表数据怎么处理?

在真实的数据分析工作中,我们经常会碰到两类"长得不太规整"的数据:

  1. 嵌套 JSON 数据:API 返回的 JSON 层层嵌套,有的字段在深层对象里,有的是数组;
  2. 列表/数组字段:电商订单的商品列表、用户的标签数组、时间序列的多点数据。

传统做法是什么?用 Python 的 json.loads() 一层层 dict.get(),或者写循环遍历数组——代码冗长、难以调试、性能差。

DuckDB 提供了两个强大的武器来解决这些问题:

  • json_tree():一键将任意嵌套 JSON 展平为关系表
  • list_transform / list_reduce / list_filter:在 SQL 中对列表做逐元素变换和聚合

下面我们通过 6 个实战场景,看看它们如何让你的数据处理效率翻倍。


二、场景一:json_tree() 一键展平嵌套 JSON

json_tree() 是 DuckDB 处理嵌套 JSON 的杀手锏。它能把任意深度的 JSON 树展开成一个包含 keyvaluetypefullkey(JSONPath)、path 等列的关系表。

2.1 基本用法

SELECT * FROM json_tree('[
  {"user": "Alice", "orders": [
    {"id": 1, "amount": 100, "items": ["book", "pen"]},
    {"id": 2, "amount": 250}
  ]},
  {"user": "Bob", "orders": [
    {"id": 3, "amount": 50}
  ]}
]');

返回的每一行代表 JSON 树中的一个节点,fullkey 列给出了该节点的完整 JSONPath,方便你精确定位。

2.2 从文件中读取 NDJSON 并展平

-- 读取 NDJSON 文件并展平
SELECT fullkey, value::VARCHAR AS name
FROM json_tree(read_ndjson_auto('users.json'))
WHERE key = 'name';

配合 read_ndjson_auto(),你可以直接处理 JSON Lines 文件,无需先将数据加载到 Python 中。

2.3 提取特定字段

-- 只提取每个订单的 amount
SELECT 
  fullkey,
  value::BIGINT AS amount
FROM json_tree('[
  {"user": "Alice", "orders": [
    {"id": 1, "amount": 100},
    {"id": 2, "amount": 250}
  ]}
]')
WHERE key = 'amount';

2.4 从 API 响应中提取数据

这是最常见的实战场景——从第三方 API 返回的嵌套 JSON 中提取你需要的字段:

-- 模拟 API 返回的嵌套响应
WITH api_response AS (
  SELECT '{
    "status": "success",
    "data": {
      "store": "Shop_A",
      "products": [
        {"sku": "A001", "price": 29.99, "stock": 150},
        {"sku": "A002", "price": 49.99, "stock": 0},
        {"sku": "A003", "price": 19.99, "stock": 80}
      ],
      "meta": {"total_items": 3, "currency": "USD"}
    }
  }' AS json_data
)
SELECT 
  key,
  value,
  type,
  fullkey
FROM api_response,
     json_tree(json_data::JSON)
WHERE fullkey LIKE '%.sku%' OR fullkey LIKE '%.price%';

结果:

keyvaluetypefullkey
skuA001STRING$.data.products[0].sku
price29.99DOUBLE$.data.products[0].price
skuA002STRING$.data.products[1].sku
price49.99DOUBLE$.data.products[1].price
skuA003STRING$.data.products[2].sku
price19.99DOUBLE$.data.products[2].price

三、场景二:list_transform — 列表逐元素变换

如果你用过 Pandas 的 apply,那 list_transform 就是你的新宠——在 SQL 里直接对列表的每个元素做变换。

3.1 电商折扣计算

WITH orders AS (
  SELECT 1 AS order_id, list_value(100.0, 250.0, 80.0) AS amounts
  UNION ALL SELECT 2, list_value(50.0, 120.0)
  UNION ALL SELECT 3, list_value(200.0, 300.0, 150.0, 90.0)
)
SELECT 
  order_id,
  list_transform(amounts, (x) -> x * 0.8) AS discounted_amounts,
  list_transform(amounts, (x) -> round(x * 0.8, 2)) AS rounded_discounted
FROM orders;

结果:

order_iddiscounted_amountsrounded_discounted
1[80.0, 200.0, 64.0][80.0, 200.0, 64.0]
2[40.0, 96.0][40.0, 96.0]
3[160.0, 240.0, 120.0, 72.0][160.0, 240.0, 120.0, 72.0]

3.2 字符串列表处理

-- 将所有标签转为大写
SELECT 
  user_id,
  list_transform(tags, (t) -> upper(t)) AS uppercase_tags
FROM (
  SELECT 1 AS user_id, list_value('vip_gold', 'regular_user', 'new_member') AS tags
);
-- 结果: ['VIP_GOLD', 'REGULAR_USER', 'NEW_MEMBER']

3.3 条件变换:只修改符合条件的元素

-- 将库存为 0 的商品标记为 "out_of_stock"
SELECT 
  product_id,
  list_transform(stock_levels, (s) -> CASE WHEN s = 0 THEN 'out_of_stock' ELSE s END) AS status_flags
FROM (
  SELECT 1 AS product_id, list_value(150, 0, 80, 0, 25) AS stock_levels
);

四、场景三:list_reduce — 列表聚合

list_reduce 类似 SQL 中的 SUM,但它在列表级别工作——对列表中的每个元素做累积计算。

4.1 计算列表总和

SELECT 
  order_id,
  list_reduce(amounts, (acc, x) -> acc + x) AS total
FROM (
  SELECT 1 AS order_id, list_value(100, 250, 80) AS amounts
  UNION ALL SELECT 2, list_value(50, 120)
  UNION ALL SELECT 3, list_value(200, 300, 150, 90)
);

4.2 计算列表平均值

SELECT 
  user_id,
  list_reduce(scores, (acc, x) -> acc + x) / list_length(scores)::DOUBLE AS avg_score
FROM (
  SELECT 1 AS user_id, list_value(85, 92, 78, 95) AS scores
  UNION ALL SELECT 2, list_value(60, 75, 80)
);

4.3 更复杂的 reduce:累积求和

-- 计算累积和
SELECT 
  day_id,
  list_reduce(daily_sales, (acc, x) -> acc || (list_last(acc) + x)) AS cumulative_sales
FROM (
  SELECT 1 AS day_id, list_value(100, 150, 200, 180, 250) AS daily_sales
);
-- 结果: [100, 250, 450, 630, 880]

五、场景四:list_filter — 条件筛选列表元素

-- 从用户标签中筛选 VIP 标签
SELECT 
  user_id,
  list_filter(tags, t -> t LIKE 'VIP%') AS vip_tags
FROM (
  SELECT 1 AS user_id, list_value('VIP_gold', 'regular', 'VIP_platinum', 'trial') AS tags
  UNION ALL SELECT 2, list_value('regular', 'trial')
  UNION ALL SELECT 3, list_value('VIP_silver', 'premium', 'VIP_diamond')
);

结果:

user_idvip_tags
1[‘VIP_gold’, ‘VIP_platinum’]
2[]
3[‘VIP_silver’, ‘VIP_diamond’]

5.1 数字列表的条件筛选

-- 筛选出销售额超过 100 的记录
SELECT 
  region,
  list_filter(sales, s -> s > 100) AS high_sales
FROM (
  SELECT 'North' AS region, list_value(80, 150, 200, 50) AS sales
  UNION ALL SELECT 'South', list_value(120, 90, 180, 60)
);

六、场景五:组合拳 — json_tree + list_transform + list_reduce

真正的威力在于将这些函数组合使用。下面是一个完整的电商订单分析示例:

6.1 从 NDJSON 读取订单数据并分析

-- 假设有一个 orders.ndjson 文件,每行是一个订单:
-- {"order_id": 1, "items": [{"name": "Book", "price": 29.99, "qty": 2}, ...]}
-- {"order_id": 2, "items": [{"name": "Pen", "price": 5.99, "qty": 5}, ...]}

-- 第一步:展平订单结构
WITH flattened AS (
  SELECT 
    o.order_id,
    item.value::VARCHAR AS item_name,
    (item.value::JSON)->>'price' AS price,
    (item.value::JSON)->>'qty' AS qty
  FROM read_ndjson_auto('orders.ndjson') o,
       LATERAL UNNEST(o.items) AS t(item)
),
-- 第二步:按订单汇总
order_totals AS (
  SELECT 
    order_id,
    item_name,
    price::DOUBLE * qty::DOUBLE AS line_total
  FROM flattened
)
SELECT 
  order_id,
  list_transform(list_value(100, 200, 150), (x) -> round(x * 0.9, 2)) AS discounted,
  list_reduce(list_value(100, 200, 150), (acc, x) -> acc + x) AS total
FROM order_totals;

6.2 从 API 获取实时数据并分析

-- 模拟从电商 API 获取的实时库存数据
WITH api_data AS (
  SELECT '[
    {"warehouse": "WH_A", "products": [
      {"sku": "A001", "stock": 150, "min_stock": 50},
      {"sku": "A002", "stock": 30, "min_stock": 50},
      {"sku": "A003", "stock": 200, "min_stock": 50}
    ]},
    {"warehouse": "WH_B", "products": [
      {"sku": "B001", "stock": 5, "min_stock": 20},
      {"sku": "B002", "stock": 100, "min_stock": 20}
    ]}
  ]' AS json_data
)
SELECT 
  warehouse,
  sku,
  stock::INT AS stock_level,
  min_stock::INT AS threshold,
  stock::INT - min_stock::INT AS buffer,
  CASE WHEN stock::INT < min_stock::INT THEN 'REORDER' ELSE 'OK' END AS alert_status
FROM api_data,
     json_tree(json_data::JSON)
WHERE fullkey LIKE '%.warehouse' 
   OR fullkey LIKE '%.sku' 
   OR fullkey LIKE '%.stock' 
   OR fullkey LIKE '%.min_stock';

七、与传统工具对比

维度Python (json + loop)jqDuckDB json_tree + list_*
安装依赖Python 环境~2MB~50MB 单文件
嵌套 JSON 展平递归函数,易出错需多层管道一行 json_tree()
列表逐元素变换map() / 列表推导式不支持list_transform()
列表聚合sum() / reduce()不支持list_reduce()
列表条件筛选filter()不支持list_filter()
并行处理需 multiprocessing单线程自动并行
SQL 集成需 pandas/sqlalchemy原生 SQL
大数据量受限于内存受限于内存列式存储,流式处理

结论:对于小到中等规模的 JSON 数据处理(<10GB),DuckDB 的 json_tree() + 列表函数组合提供了最佳的性价比——零依赖、单文件、SQL 驱动、自动并行。


八、性能提示

8.1 使用 read_ndjson_auto 而非 read_json_auto

对于 JSON Lines 格式的文件(每行一个 JSON 对象),read_ndjson_auto()read_json_auto() 更快,因为它不需要将整个文件加载到内存中:

-- 快!流式读取 NDJSON
SELECT * FROM read_ndjson_auto('large_orders.ndjson');

-- 慢!需要预扫描整个文件来推断 schema
SELECT * FROM read_json_auto('large_orders.json');

8.2 配合 WHERE 过滤减少数据量

在展平之前先过滤,可以减少 json_tree() 的处理量:

-- 先过滤再展平(更高效)
SELECT fullkey, value
FROM read_ndjson_auto('orders.ndjson')
WHERE (data->>'status') = 'completed'
  AND (data->>'total')::DOUBLE > 100
LIMIT 1000;

8.3 利用 EXPLAIN ANALYZE 优化查询

EXPLAIN ANALYZE
SELECT 
  order_id,
  list_reduce(items, (acc, x) -> acc + (x->>'price')::DOUBLE * (x->>'qty')::DOUBLE) AS total
FROM read_ndjson_auto('orders.ndjson');

九、变现建议

掌握这些 DuckDB 高级 JSON 和列表处理能力,可以帮你打开多种变现渠道:

  1. 构建数据产品:用 json_tree() + list_transform() 搭建 API 数据清洗管道,为跨境电商卖家提供竞品价格监控 SaaS。每月收费 $29-$99,边际成本几乎为零。

  2. 自动化报表服务:为中小企业搭建基于 DuckDB 的自动化报表系统。每周从多个 API 拉取数据,用 list_reduce 做聚合,自动生成 PDF 报告。按项目收费 ¥5000-¥20000。

  3. 数据清洗即服务:很多公司收到客户传来的各种格式的 JSON 数据,需要清洗后入库。用 DuckDB 的 json_tree() 可以快速处理这些脏数据,提供按次计费的数据清洗服务。

  4. 技术培训课程:制作"DuckDB 高级数据处理"系列课程,覆盖 json_tree、list_transform、list_reduce 等主题。在 Udemy、慕课网或知识星球上线,定价 ¥99-¥299。

  5. 咨询与外包:很多团队在处理嵌套 JSON 时还在用 Python 写复杂的递归解析器。你能用一行 json_tree() 替代,这就是你的差异化优势。为企业提供 DuckDB 迁移咨询服务。


架构示意图

DuckDB json_tree() 与列表函数的数据处理流程


本文使用的是 DuckDB 1.5.x 版本。DuckDB 持续快速迭代中,建议定期关注官方 Release Notes

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials

使用 Hugo 构建
主题 StackJimmy 设计

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

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

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