Featured image of post DuckDB 嵌套数据三剑客:LIST、STRUCT、MAP 一行解法

DuckDB 嵌套数据三剑客:LIST、STRUCT、MAP 一行解法

DuckDB 原生支持 LIST、STRUCT、MAP 三种嵌套数据类型,一条 SQL 就能完成以前需要几十行 Python 代码才能搞定的嵌套数据处理。本文详解三种类型的语法、组合使用和实战案例。

DuckDB 嵌套数据三剑客:LIST、STRUCT、MAP 一行解法

你是否遇到过这种场景:一条 API 返回的 JSON 数据里,订单包含多个商品,每个商品有标签数组。你只想统计「每个标签出现的次数」,用 pandas 写一堆循环,代码冗长、性能还差,最后还容易出 bug。

根本原因不是你的代码写得不好,而是你用了行式思维处理嵌套数据。

DuckDB 原生支持 LIST(数组)STRUCT(结构体)MAP(键值对) 三种嵌套数据类型。一条 SQL 就能完成以前需要写几十行 Python 代码才能搞定的事。

DuckDB 嵌套数据三剑客架构

图:DuckDB 三种嵌套数据类型的处理流程


1. LIST 类型:数组的一万次调用

LIST 是 DuckDB 处理嵌套数据的基石。它的语法简洁而强大:

1.1 基础语法

-- 创建带 LIST 列的表
CREATE TABLE orders (
    order_id INTEGER,
    tags LIST(VARCHAR),
    amounts LIST(DOUBLE)
);

-- 插入数据
INSERT INTO orders VALUES
    (1, ['电子产品', '热销', '手机'], [7999.00, 99.00]),
    (2, ['服装', '夏季'], [199.00]),
    (3, ['电子产品', '电脑', '高端'], [12999.00, 299.00, 59.00]);

1.2 UNNEST 展开数组

UNNEST 是最核心的操作——把数组展开成行:

-- 每个标签单独一行
SELECT 
    order_id,
    tag
FROM orders,
     UNNEST(tags) AS tag;

输出:

┌──────────┬───────────┐
│ order_id │   tag     │
├──────────┼───────────┤
│    1     │ 电子产品  │
│    1     │   热销    │
│    1     │   手机    │
│    2     │   服装    │
│    2     │   夏季    │
│    3     │ 电子产品  │
│    3     │   电脑    │
│    3     │   高端    │
└──────────┴───────────┘

1.3 实战:标签统计

-- 统计每个标签的使用次数
SELECT 
    tag,
    COUNT(*) AS usage_count,
    COUNT(DISTINCT order_id) AS order_count
FROM orders,
     UNNEST(tags) AS tag
GROUP BY tag
ORDER BY usage_count DESC;

输出:

┌──────────┬───────────────┬───────────────┐
│   tag    │ usage_count   │ order_count   │
├──────────┼───────────────┼───────────────┤
│ 电子产品  │      2        │      2        │
│   热销    │      1        │      1        │
│   手机    │      1        │      1        │
│   服装    │      1        │      1        │
│   夏季    │      2        │      2        │
│   电脑    │      1        │      1        │
│   高端    │      1        │      1        │
└──────────┴───────────────┴───────────────┘

1.4 LIST 常用函数

函数作用示例
array_length(arr)获取数组长度array_length(['a','b','c']) → 3
array_concat(a, b)合并两个数组array_concat([1,2], [3,4]) → [1,2,3,4]
list_contains(arr, val)判断是否包含某值list_contains(['a','b'], 'a') → true
list_sort(arr)排序数组list_sort([3,1,2]) → [1,2,3]
list_distinct(arr)去重list_distinct([1,1,2,2,3]) → [1,2,3]
list_range(start, end)生成数字数组list_range(1, 5) → [1,2,3,4,5]

2. STRUCT 类型:结构化嵌套字段

STRUCT 让你像访问对象属性一样访问嵌套字段,语法优雅:

2.1 基础语法

-- 创建带 STRUCT 的表
CREATE TABLE employees (
    id INTEGER,
    info STRUCT(
        name VARCHAR,
        age INTEGER,
        skills LIST(VARCHAR),
        address STRUCT(
            city VARCHAR,
            district VARCHAR
        )
    )
);

-- 插入数据
INSERT INTO employees VALUES
    (1, STRUCT('张三', 30, ['Python', 'SQL'], STRUCT('北京', '朝阳区'))),
    (2, STRUCT('李四', 25, ['Java', 'Go'], STRUCT('上海', '浦东'))),
    (3, STRUCT('王五', 35, ['Python', 'R'], STRUCT('广州', '天河')));

2.2 点号访问嵌套字段

-- 访问嵌套字段
SELECT 
    info.name AS 姓名,
    info.age AS 年龄,
    info.skills[1] AS 第一技能,
    info.address.city AS 城市
FROM employees;

输出:

┌───────┬──────┬──────────┬────────┐
│ 姓名  │ 年龄 │ 第一技能 │  城市  │
├───────┼──────┼──────────┼────────┤
│ 张三  │  30  │  Python  │  北京  │
│ 李四  │  25  │   Java   │  上海  │
│ 王五  │  35  │  Python  │  广州  │
└───────┴──────┴──────────┴────────┘

2.3 STRUCT 的过滤和聚合

-- 过滤:年龄大于 28 的员工
SELECT info.name, info.skills
FROM employees
WHERE info.age > 28;

-- 聚合:每个技能出现在多少人的技能列表中
SELECT 
    UNNEST(info.skills) AS skill,
    COUNT(*) AS count
FROM employees
GROUP BY skill
ORDER BY count DESC;

3. MAP 类型:动态键值对

MAP 适合处理动态字段或不规则结构——这是 LIST 和 STRUCT 都无法替代的场景:

3.1 基础语法

-- 创建带 MAP 的表
CREATE TABLE user_configs (
    user_id INTEGER,
    settings MAP(VARCHAR, VARCHAR),
    preferences MAP(VARCHAR, INTEGER)
);

-- 插入数据
INSERT INTO user_configs VALUES
    (1, MAP({'theme': 'dark', 'lang': 'zh', 'notifications': 'true'}),
         MAP({'clicks': 1500, 'views': 8000, 'orders': 23})),
    (2, MAP({'theme': 'light', 'lang': 'en', 'notifications': 'false'}),
         MAP({'clicks': 300, 'views': 1200, 'orders': 5})),
    (3, MAP({'theme': 'dark', 'lang': 'ja', 'notifications': 'true'}),
         MAP({'clicks': 2000, 'views': 10000, 'orders': 45}));

3.2 MAP 查询

-- 查询特定键
SELECT 
    user_id,
    settings['theme'] AS 主题,
    settings['lang'] AS 语言,
    preferences['clicks'] AS 点击数
FROM user_configs;

-- 过滤:主题是 dark 的用户
SELECT * 
FROM user_configs 
WHERE settings['theme'] = 'dark';

3.3 MAP 常用函数

函数作用示例
map_keys(m)获取所有键map_keys({'a':1, 'b':2}) → [‘a’,‘b’]
map_values(m)获取所有值map_values({'a':1, 'b':2}) → [1,2]
map_entries(m)获取键值对列表map_entries({'a':1}) → [(‘a’,1)]
map_contains(m, k)判断是否包含某键map_contains({'a':1}, 'a') → true
map_concat(m1, m2)合并 MAPmap_concat({'a':1}, {'b':2}) → {‘a’:1,‘b’:2}

4. 组合实战:从 JSON API 直接提取嵌套数据

实际工作中,大多数嵌套数据来自 JSON API。DuckDB 可以直接读取并展开:

4.1 读取 JSON 并展开嵌套

-- 假设 orders.json 格式如下:
-- [{"order_id":1, "items":[{"name":"iPhone","price":7999,"tags":["电子产品","热销"]}]}, ...]

SELECT 
    order_id,
    item.name AS 商品名,
    item.price AS 价格,
    item.tags[1] AS 主标签
FROM read_json_auto('orders.json'),
     LATERAL UNNEST(items) AS item;

4.2 多层嵌套展开

-- 从订单中提取所有标签及其出现次数
SELECT 
    item.tags[i] AS tag,
    COUNT(*) AS usage_count
FROM read_json_auto('orders.json'),
     LATERAL UNNEST(items) AS item,
     LATERAL UNNEST(item.tags) WITH ORDINALITY AS tags(tag, i)
GROUP BY tag
ORDER BY usage_count DESC;

4.3 STRUCT + LIST 联合使用

-- 创建订单表:每个订单包含多个商品(STRUCT列表)
CREATE TABLE orders_complex AS
SELECT * FROM (
    VALUES
    (1, [
        STRUCT('iPhone', 7999.00, ['电子产品', '热销']),
        STRUCT('保护壳', 99.00, ['配件'])
    ]),
    (2, [
        STRUCT('T恤', 199.00, ['服装', '夏季']),
        STRUCT('短裤', 149.00, ['服装', '夏季'])
    ]),
    (3, [
        STRUCT('MacBook', 12999.00, ['电子产品', '电脑', '高端'])
    ])
) AS t(order_id, items);

-- 计算每个订单总价
SELECT 
    order_id,
    SUM(item.col1) AS total
FROM orders_complex,
     UNNEST(items) AS item
GROUP BY order_id;

5. 三种类型对比表

特性LISTSTRUCTMAP
数据模型有序数组固定字段对象动态键值对
访问方式arr[1], UNNESTobj.fieldmap['key']
字段顺序有序(索引从1开始)固定结构无序
适用场景标签、列表、多选记录、对象、固定结构配置、动态字段、不规则数据
嵌套深度无限无限无限
性能极快(列式存储)极快(列式存储)中等(哈希查找)
序列化[1,2,3]{'a':1}{'key':'val'}

6. 与传统方案对比

维度Python + pandasjqDuckDB 嵌套类型
安装需 Anaconda/venv~2MB 单文件~50MB 单文件
嵌套数据读取json.loads() + 循环jq '.items[]'UNNEST 一行
数组展开explode() 或循环[] + `` 管道
嵌套字段访问obj['a']['b'].a.bobj.field
动态字段查询需手动处理 KeyError复杂路径表达式map['key']
处理 1000 万行需分块,内存压力大单线程,OOM 风险并行扫描,~500MB 内存
代码行数20-50 行5-15 行管道1-5 行 SQL

7. 变现建议

掌握了 DuckDB 嵌套数据处理能力后,你可以从以下几个方向变现:

7.1 数据清洗 SaaS 服务

为电商、SaaS 企业提供 API 数据清洗服务。很多平台的 API 返回 deeply nested JSON,用 DuckDB 可以一条 SQL 完成以前需要写半天 Python 的代码。

  • 目标客户:跨境电商、DTC 品牌
  • 定价:单次项目 ¥5,000-20,000,月度维护 ¥2,000-5,000

7.2 DuckDB 嵌套数据处理培训

面向数据团队开设线上/线下培训,教授 LIST/STRUCT/MAP 高级用法。

  • 课程定价:¥299-999/人
  • 企业内训:¥5,000-20,000/天

7.3 构建数据产品后端

用 DuckDB 嵌套类型快速构建数据分析产品的后端。比如:

  • 用户行为标签分析系统(LIST 存储标签)
  • 多租户配置管理系统(MAP 存储动态配置)
  • 商品目录管理系统(STRUCT 存储分类层级)

7.4 开源工具 + 付费插件

构建围绕 DuckDB 嵌套数据的开源工具(如 duckdb-nested-tools),通过付费插件/企业版变现。

  • 开源核心:UNNEST 可视化、STRUCT 字段提取器
  • 付费功能:多层嵌套预览、性能优化工具

7.5 内容变现

在 Medium、Dev.to、掘金、知乎等平台发布 DuckDB 嵌套数据教程,通过广告、联盟营销、付费订阅变现。这类技术性内容在搜索引擎上有很长的长尾流量。


总结

DuckDB 的 LIST、STRUCT、MAP 三种嵌套数据类型,让你用一条 SQL 就能完成以前需要写几十行 Python 代码的嵌套数据处理任务。

核心心法

  • LIST → 数组展开用 UNNEST
  • STRUCT → 嵌套字段用 . 点号访问
  • MAP → 动态字段用 ['key'] 下标访问
  • 组合 → 三层嵌套(LIST of STRUCT with MAP)也能一行搞定

下次遇到「一个字段存多个值」的痛点,别再写循环了——用 DuckDB 的嵌套类型,一条 SQL 就搞定。


本文基于 DuckDB 1.2.x 编写。DuckDB 更新频繁,建议关注官方 Release Notes 获取最新版本特性。

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials

使用 Hugo 构建
主题 StackJimmy 设计

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

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

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