DuckDB 嵌套数据三剑客:LIST、STRUCT、MAP 一行解法
你是否遇到过这种场景:一条 API 返回的 JSON 数据里,订单包含多个商品,每个商品有标签数组。你只想统计「每个标签出现的次数」,用 pandas 写一堆循环,代码冗长、性能还差,最后还容易出 bug。
根本原因不是你的代码写得不好,而是你用了行式思维处理嵌套数据。
DuckDB 原生支持 LIST(数组)、STRUCT(结构体)、MAP(键值对) 三种嵌套数据类型。一条 SQL 就能完成以前需要写几十行 Python 代码才能搞定的事。

图: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) | 合并 MAP | map_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. 三种类型对比表
| 特性 | LIST | STRUCT | MAP |
|---|---|---|---|
| 数据模型 | 有序数组 | 固定字段对象 | 动态键值对 |
| 访问方式 | arr[1], UNNEST | obj.field | map['key'] |
| 字段顺序 | 有序(索引从1开始) | 固定结构 | 无序 |
| 适用场景 | 标签、列表、多选 | 记录、对象、固定结构 | 配置、动态字段、不规则数据 |
| 嵌套深度 | 无限 | 无限 | 无限 |
| 性能 | 极快(列式存储) | 极快(列式存储) | 中等(哈希查找) |
| 序列化 | [1,2,3] | {'a':1} | {'key':'val'} |
6. 与传统方案对比
| 维度 | Python + pandas | jq | DuckDB 嵌套类型 |
|---|---|---|---|
| 安装 | 需 Anaconda/venv | ~2MB 单文件 | ~50MB 单文件 |
| 嵌套数据读取 | json.loads() + 循环 | jq '.items[]' | UNNEST 一行 |
| 数组展开 | explode() 或循环 | [] + ` | ` 管道 |
| 嵌套字段访问 | obj['a']['b'] | .a.b | obj.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 获取最新版本特性。