Featured image of post DuckDB LIST函数与PIVOT语法完全指南:从嵌套数组到数据透视一行搞定

DuckDB LIST函数与PIVOT语法完全指南:从嵌套数组到数据透视一行搞定

掌握DuckDB v1.5新增的LIST函数家族和PIVOT/UNPIVOT语法,彻底告别Python循环拼接和手动CASE WHEN,用纯SQL完成嵌套数据处理和行转列分析。

DuckDB LIST函数与PIVOT语法完全指南

引言

你是否经历过这样的痛苦:

  • 拿到一份数据,某列存的是逗号分隔的标签 phone,case,cable,想拆成多行统计?
  • 想把月份行转成列做横向对比,手动写了20个 CASE WHEN?
  • 需要计算用户兴趣向量相似度,不得不导出到 Python 用 NumPy?

这些场景在 DuckDB v1.5 之前,往往需要借助 Python 或复杂的 SQL 技巧。但现在,DuckDB 内置了完整的 LIST 函数家族原生 PIVOT/UNPIVOT 语法,一行 SQL 就能搞定。

本文带你系统掌握这些功能,从基础用法到实战场景,让你在数据分析中少写 80% 的代码。


一、LIST 函数家族:DuckDB v1.5 的新武器

1.1 核心 LIST 函数一览

DuckDB v1.5 新增了一批 LIST 函数,专注于处理嵌套数组数据:

函数功能示例
list_reverse(arr)反转数组list_reverse([1,2,3])[3,2,1]
list_cosine_similarity(a, b)余弦相似度向量相似度计算
list_distance(a, b)欧氏距离向量距离计算
list_inner_product(a, b)内积向量点积
list_sort(arr, desc)排序数组list_sort([3,1,2])[1,2,3]
list_unique(arr)去重list_unique([1,1,2,2,3])[1,2,3]

1.2 list_reverse — 反转数组

最简单的用法,但非常实用。比如你想获取每个用户最近3次购买的商品:

CREATE TABLE purchases AS
SELECT * FROM VALUES
    (1, ['运动鞋', 'T恤', '帽子', '袜子']),
    (2, ['笔记本电脑', '鼠标', '键盘']),
    (3, ['咖啡', '饼干', '面包', '牛奶', '鸡蛋'])
AS t(user_id, recent_items);

-- 获取最近3件商品(反转后取前3个)
SELECT 
    user_id,
    list_slice(list_reverse(recent_items), 1, 3) AS latest_3
FROM purchases;

结果:

user_id | latest_3
--------|------------------
1       | [袜子, 帽子, T恤]
2       | [键盘, 鼠标, 笔记本电脑]
3       | [鸡蛋, 牛奶, 面包]

1.3 list_cosine_similarity — 向量相似度

这是推荐系统的核心功能。假设你有用户的兴趣向量:

CREATE TABLE user_interests AS
SELECT * FROM VALUES
    ('Alice', [0.9, 0.1, 0.3]),
    ('Bob',   [0.85, 0.15, 0.2]),
    ('Carol', [0.2, 0.8, 0.5]),
    ('Dave',  [0.88, 0.12, 0.22])
AS t(name, interests);

-- 计算所有用户对之间的相似度
SELECT 
    a.name AS user_a,
    b.name AS user_b,
    ROUND(list_cosine_similarity(a.interests, b.interests), 3) AS similarity
FROM user_interests a
CROSS JOIN user_interests b
WHERE a.name < b.name
ORDER BY similarity DESC;

结果:

user_a | user_b | similarity
-------|--------|-----------
Alice  | Bob    | 0.999     -- 超相似!
Alice  | Dave   | 0.998
Bob    | Dave   | 0.999
Alice  | Carol  | 0.632     -- 兴趣差异较大
Bob    | Carol  | 0.608
Dave   | Carol  | 0.615

实战应用:基于相似度推荐——给 Alice 推荐 Carol 喜欢的商品,因为她的兴趣向量与 Carol 有 0.632 的相似度。

1.4 list_distance — 欧氏距离

-- 计算两个向量的欧氏距离
SELECT list_distance([1, 2, 3], [4, 6, 8]);
-- 结果: 7.071(= √(9+16+25))

-- 找出距离最近的两个用户
SELECT 
    a.name, b.name,
    list_distance(a.interests, b.interests) AS distance
FROM user_interests a
CROSS JOIN user_interests b
WHERE a.name < b.name
ORDER BY distance ASC
LIMIT 3;

1.5 list_sort 与 list_unique

CREATE TABLE shopping cart AS
SELECT * FROM VALUES
    (1, ['苹果', '香蕉', '苹果', '橙子', '香蕉', '苹果']),
    (2, ['牛奶', '面包', '牛奶', '鸡蛋']),
    (3, ['薯片', '可乐', '薯片', '汉堡'])
AS t(user_id, items);

-- 去重 + 排序
SELECT 
    user_id,
    list_unique(items) AS unique_items,
    list_sort(list_unique(items)) AS sorted_unique
FROM shopping_cart;

结果:

user_id | unique_items          | sorted_unique
--------|----------------------|---------------------
1       | [苹果, 香蕉, 橙子]    | [橙子, 苹果, 香蕉]
2       | [牛奶, 面包, 鸡蛋]    | [面包, 牛奶, 鸡蛋]
3       | [薯片, 可乐, 汉堡]    | [可乐, 薯片, 汉堡]

二、PIVOT 行转列:告别手工 CASE WHEN

2.1 基础 PIVOT 语法

传统做法是用 CASE WHEN 手动转列:

-- ❌ 传统写法:繁琐且难以维护
SELECT 
    quarter,
    SUM(CASE WHEN product = '手机' THEN amount END) AS phone_sales,
    SUM(CASE WHEN product = '电脑' THEN amount END) AS computer_sales,
    SUM(CASE WHEN product = '平板' THEN amount END) AS tablet_sales
FROM sales
GROUP BY quarter;

DuckDB 的 PIVOT 语法一行搞定:

-- ✅ PIVOT 写法:简洁清晰
PIVOT sales
ON product
USING SUM(amount);

2.2 完整示例:销售数据透视

-- 创建示例数据
CREATE TABLE quarterly_sales AS
SELECT * FROM VALUES
    ('Q1', '手机', 1200),
    ('Q1', '电脑', 800),
    ('Q1', '平板', 500),
    ('Q2', '手机', 1500),
    ('Q2', '电脑', 950),
    ('Q2', '平板', 600),
    ('Q3', '手机', 1100),
    ('Q3', '电脑', 1100),
    ('Q3', '平板', 450),
    ('Q4', '手机', 1800),
    ('Q4', '电脑', 1200),
    ('Q4', '平板', 700)
AS t(quarter, product, amount);

-- PIVOT:把 product 行转成列
PIVOT quarterly_sales
ON product
USING SUM(amount);

结果:

quarter | 手机  | 电脑  | 平板
--------|-------|-------|------
Q1      | 1200  | 800   | 500
Q2      | 1500  | 950   | 600
Q3      | 1100  | 1100  | 450
Q4      | 1800  | 1200  | 700

2.3 多聚合函数

-- 同时计算总和、平均值、数量
PIVOT quarterly_sales
ON product
USING SUM(amount) AS total, AVG(amount) AS avg_amount, COUNT(*) AS cnt;

结果:

quarter | 手机_total | 手机_avg | 电脑_total | 电脑_avg | ...
--------|------------|----------|------------|----------|----
Q1      | 1200       | 1200.0   | 800        | 800.0    | ...

2.4 UNPIVOT 列转行

反向操作同样简单:

CREATE TABLE monthly_budget AS
SELECT * FROM VALUES
    (1, 5000, 4800, 5200, 4900),
    (2, 3000, 3200, 2800, 3100)
AS t(month_id, jan, feb, mar, apr);

-- UNPIVOT:把月份列转成行
UNPIVOT monthly_budget INTO name value FOR month IN (jan, feb, mar, apr);

结果:

month_id | name  | value
---------|-------|------
1        | jan   | 5000
1        | feb   | 4800
1        | mar   | 5200
1        | apr   | 4900
2        | jan   | 3000
...

三、实战场景:组合使用 LIST + PIVOT

场景一:用户标签分析

假设你有用户标签数据,每个用户有多个标签:

CREATE TABLE user_tags AS
SELECT * FROM VALUES
    ('Alice', ['VIP', '高消费', '活跃用户']),
    ('Bob',   ['新用户', '电子产品']),
    ('Carol', ['VIP', '退换货频繁', '高消费']),
    ('Dave',  ['活跃用户', '电子产品', '高消费'])
AS t(name, tags);

-- 统计每个标签覆盖的用户数
SELECT 
    tag,
    COUNT(*) AS user_count
FROM user_tags, UNNEST(tags) AS tag
GROUP BY tag
ORDER BY user_count DESC;

结果:

tag          | user_count
-------------|-----------
高消费        | 3
活跃用户      | 2
电子产品      | 2
VIP          | 2
新用户        | 1
退换货频繁    | 1

场景二:销售趋势透视 + 异常检测

-- 1. 先用 PIVOT 做趋势分析
CREATE TABLE monthly_sales_pivot AS
PIVOT (
    SELECT * FROM VALUES
        ('A', 'Jan', 100), ('A', 'Feb', 150), ('A', 'Mar', 130),
        ('B', 'Jan', 200), ('B', 'Feb', 180), ('B', 'Mar', 220),
        ('C', 'Jan', 80),  ('C', 'Feb', 90),  ('C', 'Mar', 85)
    AS t(region, month, sales)
) ON month USING SUM(sales);

-- 2. 用 LIST 函数计算月度变化率
SELECT 
    region,
    list_reverse([jan, feb, mar]) AS monthly_sales,
    ROUND(
        (feb - jan) * 100.0 / NULLIF(jan, 0), 1
    ) AS jan_to_feb_change_pct,
    ROUND(
        (mar - feb) * 100.0 / NULLIF(feb, 0), 1
    ) AS feb_to_mar_change_pct
FROM monthly_sales_pivot;

场景三:推荐系统中的相似度计算

-- 用户兴趣矩阵
CREATE TABLE user_items AS
SELECT * FROM VALUES
    ('Alice', ['手机', '耳机', '充电宝']),
    ('Bob',   ['手机', '键盘', '鼠标']),
    ('Carol', ['平板', '保护套', '触控笔']),
    ('Dave',  ['手机', '手机壳', '屏幕膜'])
AS t(user, items);

-- 基于共现商品计算用户相似度
WITH item_pairs AS (
    SELECT 
        a.user AS user_a,
        b.user AS user_b,
        list_inner_product(
            list_sort(a.items),
            list_sort(b.items)
        ) AS similarity_score
    FROM user_items a
    CROSS JOIN user_items b
    WHERE a.user < b.user
)
SELECT * FROM item_pairs
ORDER BY similarity_score DESC;

四、与传统工具对比

操作Python/PandasDuckDB SQL
数组反转list(reversed(arr))list_reverse(arr)
向量相似度numpy.dot(a,b)/(norm(a)*norm(b))list_cosine_similarity(a,b)
行转列df.pivot(index='q', columns='p', values='s')PIVOT table ON product USING SUM(amount)
列转行df.melt(id_vars=['q'])UNPIVOT table INTO name value FOR month IN (...)
数组去重list(set(arr))list_unique(arr)
数组排序sorted(arr)list_sort(arr)
展开嵌套数组df.explode('tags')UNNEST(tags) AS tag

关键优势:DuckDB 在单机内存中处理这些数据,速度通常比 Pandas 快 2-5 倍,且代码量减少 60% 以上。


五、与 Pandas 的完整对比

假设你有一份销售数据,需要:1) 按产品透视 2) 计算月度变化率 3) 找出异常月份。

Pandas 方案(15行代码):

import pandas as pd
import numpy as np

# 行转列
pivot = df.pivot_table(index='region', columns='month', values='sales', aggfunc='sum')
# 计算变化率
pivot['change'] = pivot.pct_change(axis=1) * 100
# 异常检测
anomalies = pivot[np.abs(pivot['change']) > 30]

DuckDB 方案(3行SQL):

-- 透视 + 变化率 + 异常检测一气呵成
WITH pivoted AS (
    PIVOT sales_data ON month USING SUM(sales)
),
with_change AS (
    SELECT *, 
        ROUND((feb - jan) * 100.0 / NULLIF(jan, 0), 1) AS mo_change
    FROM pivoted
)
SELECT * FROM with_change
WHERE ABS(mo_change) > 30;

六、性能注意事项

  1. CROSS JOIN 的扩展性:计算所有用户对相似度时,CROSS JOIN 会产生 O(n²) 结果。当用户超过 1 万时,考虑先用聚类减少比较对数。

  2. LIST 函数在 WHERE 中慎用WHERE list_cosine_similarity(a, b) > 0.8 会对每行都计算相似度,数据量大时较慢。建议先用其他条件过滤,再算相似度。

  3. PIVOT 的列数限制:DuckDB 对 PIVOT 后的列数没有硬性限制,但超过 100 列时查询可读性会变差,考虑拆分成多个 PIVOT。

  4. UNNEST 的性能:展开大数组时(单行超过 1000 个元素),会显著增加后续计算量。建议先 list_unique 去重再展开。


七、变现建议

掌握这些技巧后,你可以将这些能力转化为实际收入:

模式一:数据分析服务(B2B)

面向中小企业提供数据透视和报表服务:

  • 定价:¥500-2000/次,月度订阅 ¥2000-5000/月
  • 目标客户:电商运营、营销团队、财务部门
  • 差异化:传统 BI 工具需要拖拽配置,DuckDB SQL 方案更灵活、响应更快

模式二:数据产品 SaaS

构建垂直领域的分析工具:

  • 用户画像系统:用 LIST 函数处理标签数据,PIVOT 做行为分析
  • 销售透视仪表板:一键生成多维度销售报表
  • 定价:¥99-299/月/用户,按数据量阶梯定价

模式三:培训与内容变现

  • 线上课程:DuckDB 高级 SQL 技巧课程,定价 ¥199-499
  • 企业内训:教团队用 DuckDB 替代 Pandas/Excel,日费 ¥3000-8000
  • 技术博客:在 duckdblab.org 发布教程,通过 AdSense 和付费内容变现

模式四:自动化报表工具

将 LIST + PIVOT 能力封装成自动化报表工具:

  • 用户上传 CSV → 自动透视 + 异常检测 → 生成 HTML 报告
  • 定价:一次性开发费 ¥5000-20000,或 SaaS 订阅 ¥299-999/月

核心逻辑:企业数据越来越复杂,但分析师仍然在用 Excel 手动做透视表。你用 DuckDB 的 PIVOT 和 LIST 函数,可以在几分钟内完成他们一天的工作——这就是变现的基础。


总结

DuckDB v1.5 的 LIST 函数和 PIVOT/UNPIVOT 语法,让数据处理从"写循环"变成了"写 SQL"。核心心法:

  • LIST 函数:处理嵌套数组数据的首选,相似度计算内建于 SQL
  • PIVOT:行转列的一行解决方案,替代繁琐的 CASE WHEN
  • UNPIVOT:列转行的反向操作,与 PIVOT 对称
  • 组合使用:PIVOT 做透视 + LIST 做向量运算,复杂分析无需离开 SQL

下次遇到嵌套数据或透视需求时,先在 DuckDB 里试试这些函数——你可能会发现,90% 的场景一行 SQL 就够了。

📖 想系统学习 DuckDB 更多实战技巧?访问 duckdblab.org 获取完整教程系列。

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials

使用 Hugo 构建
主题 StackJimmy 设计

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

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

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