Featured image of post DuckDB 数组操作全攻略:从入门到实战

DuckDB 数组操作全攻略:从入门到实战

DuckDB 的数组(List)功能比想象中强大得多。50+ 个数组函数、Lambda 表达式、高性能聚合,这篇带你一次性搞懂。

数组操作在数据分析里太常见了——用户购买历史、文章标签、时间序列数据,到处都是列表的影子。之前用 Python 处理这些数据得写一堆 for 循环,现在发现 DuckDB 的数组功能比想象中强大得多。

这篇文章把 DuckDB 的数组操作从头到尾捋一遍,从基础创建到 Lambda 表达式,再到性能测试,希望能帮你少踩点坑。

数组创建:4 种方式

字面量创建

最直观的方式,直接用方括号:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
-- 整数数组
SELECT [1, 2, 3, 4, 5] AS int_array;

-- 字符串数组
SELECT ['apple', 'banana', 'cherry'] AS str_array;

-- 浮点数数组
SELECT [1.1, 2.2, 3.3] AS float_array;
```sql

### LIST_VALUE 函数

和字面量效果一样,只是用函数形式:

```sql
SELECT LIST_VALUE(10, 20, 30) AS list_value_array;
-- 结果: [10, 20, 30]
```sql

### generate_series 生成序列

需要大数组时特别有用:

```sql
SELECT generate_series(1, 100000) AS large_array;
```sql

实测创建 10 万元素的数组只要 **0.07 秒**,这个速度可以接受。

### LIST 聚合函数

从表数据生成数组,这个最实用:

```sql
CREATE TABLE test_products AS
SELECT * FROM (VALUES
    ('A', 'apple', 10),
    ('A', 'banana', 20),
    ('A', 'cherry', 30),
    ('B', 'date', 15),
    ('B', 'elderberry', 25)
) AS t(category, product, price);

SELECT 
    category,
    LIST(product) AS products,
    LIST(price) AS prices
FROM test_products
GROUP BY category;
```text

结果:
```text
Category A: products=['apple', 'banana', 'cherry'], prices=[10, 20, 30]
Category B: products=['date', 'elderberry'], prices=[15, 25]
```sql

## 索引访问:注意是从 1 开始

这里要特别强调——**DuckDB 数组索引从 1 开始**,不是 0。习惯了 Python/JavaScript 的人第一次用绝对会踩坑。

```sql
SELECT 
    [10, 20, 30, 40, 50][1] AS first_elem,    -- 10
    [10, 20, 30, 40, 50][3] AS third_elem,    -- 30
    [10, 20, 30, 40, 50][-1] AS last_elem;    -- 50(支持负数索引)
```sql

负数索引 `-1` 表示最后一个元素,这个设计挺人性化。

### 数组切片

```sql
SELECT 
    [10, 20, 30, 40, 50][1:3] AS slice_1_3,   -- [10, 20, 30]
    [10, 20, 30, 40, 50][2:] AS slice_from_2, -- [20, 30, 40, 50]
    [10, 20, 30, 40, 50][:3] AS slice_to_3;   -- [10, 20, 30]
```sql

切片语法 `[begin:end]`,可以省略 begin 或 end。但**不支持 step 参数**,`[::2]` 这种写法会报错。需要步长切片的话,得用 Lambda 表达式(后面会讲)。

## 数组操作函数

### 基本操作

```sql
SELECT 
    list_append([1, 2, 3], 4) AS append_result,     -- [1, 2, 3, 4]
    list_prepend(0, [1, 2, 3]) AS prepend_result,   -- [0, 1, 2, 3]
    list_reverse([1, 2, 3, 4, 5]) AS reverse_result, -- [5, 4, 3, 2, 1]
    list_sort([3, 1, 4, 1, 5, 9, 2]) AS sort_result; -- [1, 1, 2, 3, 4, 5, 9]
```sql

### 数组连接

用 `||` 操作符最方便:

```sql
SELECT [1, 2] || [3, 4] AS concat_result;
-- 结果: [1, 2, 3, 4]
```sql

有个坑:`list_concat` 函数存在,但语法要求严格,必须两个数组参数。`list_concat([1,2], 3)` 会报错,得写成 `list_concat([1,2], [3])`。不如直接用 `||` 来得简单。

### 去重

```sql
SELECT list_distinct([1, 2, 2, 3, 3, 3, 4]) AS distinct_result;
-- 结果: [4, 3, 2, 1](注意顺序可能改变)
```sql

注意 `list_distinct` **不保证原始顺序**,去重后顺序可能变。如果需要保持顺序,得用 Lambda 表达式组合实现。

## Lambda 表达式:数组的灵魂

Lambda 表达式让数组操作变得灵活,这是 DuckDB 数组功能的精华所在。

### list_transform:转换每个元素

```sql
SELECT 
    list_transform([1, 2, 3, 4], x -> x * 2) AS double_result,    -- [2, 4, 6, 8]
    list_transform([1, 2, 3, 4], x -> x * x) AS square_result,    -- [1, 4, 9, 16]
    list_transform(['hello', 'world'], x -> upper(x)) AS upper_result; -- ['HELLO', 'WORLD']
```sql

### list_filter:过滤元素

```sql
SELECT 
    list_filter([1, 2, 3, 4, 5, 6], x -> x > 3) AS filter_gt3,  -- [4, 5, 6]
    list_filter([1, 2, 3, 4, 5, 6], x -> x % 2 = 0) AS even_filter; -- [2, 4, 6]
```sql

用 Lambda 实现步长切片:

```sql
-- 每隔一个元素取一个(相当于 [::2])
SELECT list_filter([1,2,3,4,5], (x, i) -> i % 2 = 0) AS step_slice;
-- 结果: [2, 4]
```sql

### list_reduce:归约操作

```sql
SELECT 
    list_reduce([1, 2, 3, 4, 5], (x, y) -> x + y) AS sum_reduce,     -- 15
    list_reduce([1, 2, 3, 4, 5], (x, y) -> x * y) AS product_reduce; -- 120
```sql

实测 Lambda 转换 1000 个数组(每个 100 元素)只要 **0.04 秒**,性能不错。

## 数组聚合函数

DuckDB 提供了一整套数组聚合函数:

```sql
SELECT 
    list_sum([1, 2, 3, 4, 5]) AS sum_result,           -- 15
    list_avg([1.0, 2.0, 3.0, 4.0, 5.0]) AS avg_result, -- 3.0
    list_min([3, 1, 4, 1, 5]) AS min_result,           -- 1
    list_max([3, 1, 4, 1, 5]) AS max_result,           -- 5
    list_count([1, 2, 3, 4, 5]) AS count_result,       -- 5
    list_product([1, 2, 3, 4, 5]) AS product_result;   -- 120.0
```sql

这些函数配合 LIST 聚合特别好用。比如统计每个用户的购买总额:

```sql
SELECT 
    user_id,
    LIST(price) AS price_history,
    list_sum(LIST(price)) AS total_spent,
    len(LIST(product_name)) AS purchase_count
FROM purchases
GROUP BY user_id;
```sql

## UNNEST:数组展开为行

UNNEST 是把数组展开成多行的操作,和聚合反过来:

```sql
SELECT UNNEST([1, 2, 3, 4, 5]) AS unnested;
-- 结果: 5行,分别为 1, 2, 3, 4, 5
```sql

典型用法是统计标签频率:

```sql
SELECT 
    UNNEST(tags) AS tag,
    COUNT(*) AS article_count
FROM articles
GROUP BY tag
ORDER BY article_count DESC;
```sql

实测 UNNEST 展开 10000 个数组(每个 10 元素,共 10 万行)只要 **0.04 秒**,这个性能相当可以。

## 嵌套数组

DuckDB 支持嵌套数组(数组的数组):

```sql
SELECT 
    [[1, 2], [3, 4], [5, 6]] AS nested_array,
    flatten([[1, 2], [3, 4], [5, 6]]) AS flattened;
```sql

结果:
- nested: `[[1, 2], [3, 4], [5, 6]]`
- flattened: `[1, 2, 3, 4, 5, 6]`

`flatten` 函数把嵌套数组展平为一维数组,处理 JSON 数据时特别有用。

## 数组查找

```sql
SELECT 
    list_contains([1, 2, 3, 4, 5], 3) AS contains_3,   -- True
    list_contains([1, 2, 3, 4, 5], 10) AS contains_10, -- False
    list_position([1, 2, 3, 4, 5], 3) AS position_3,   -- 3
    list_position([1, 2, 3, 4, 5], 10) AS position_10, -- NULL
    list_has_any([1, 2, 3], [3, 4, 5]) AS has_any,     -- True(有交集)
    list_has_all([1, 2, 3, 4, 5], [2, 4]) AS has_all;  -- True(全包含)
```text

实测在 10000 个数组(每个 100 元素)中搜索包含特定元素,只要 **0.005 秒**,速度飞快。

## 性能测试

跑了一组性能测试,数据说话:

| 测试场景 | 数据规模 | 耗时 |
|---------|---------|------|
| 创建大数组 | 100,000 元素 | 0.07s |
| 大表数组聚合 | 100,000 行 | 0.23s |
| UNNEST 展开 | 10,000 数组 × 10 元素 | 0.04s |
| Lambda 转换 | 1,000 数组 × 100 元素 | 0.04s |
| 数组包含搜索 | 10,000 数组 × 100 元素 | 0.005s |
| 嵌套数组 flatten | 3 × 100 元素 | 0.0008s |

整体来看,DuckDB 的数组操作性能优秀。10 万行数据分组聚合为数组不到 0.25 秒,UNNEST 展开 10 万行不到 0.05 秒。对于日常数据分析来说,这个性能完全够用。

## 踩坑记录

### 1. 索引从 1 开始

第一次用 `[10, 20, 30][0]` 取第一个元素,结果返回 NULL。当时愣了一下,才想起来 DuckDB 是 1-based。这个坑几乎每个从 Python/JS 过来的人都会踩。

### 2. 切片不支持 step

`[1,2,3,4,5][::2]` 会报错。需要步长切片的话,得用 Lambda:

```sql
-- 错误: [1,2,3,4,5][::2]
-- 正确: list_filter([1,2,3,4,5], (x, i) -> i % 2 = 0)
```sql

### 3. list_distinct 不保证顺序

去重后顺序可能变,这个容易忽略。如果需要保持原始顺序,得自己用 Lambda 实现。

### 4. list_concat 语法严格

`list_concat([1,2], 3)` 会报错,必须两个数组参数。不如直接用 `||` 操作符来得简单。

### 5. LIST 聚合的 ORDER BY

`LIST(column)` 不保证顺序,需要明确排序的话得用 `LIST(column ORDER BY other_column)`。

## 实战案例

### 用户购买行为分析

```sql
SELECT 
    user_id,
    LIST(product_name ORDER BY purchase_date) AS purchase_history,
    list_sum(LIST(price)) AS total_spent,
    len(LIST(product_name)) AS purchase_count
FROM purchases
GROUP BY user_id;
```sql

### 标签系统

```sql
SELECT 
    article_id,
    LIST(tag_name) AS tags,
    list_contains(LIST(tag_name), 'DuckDB') AS has_duckdb_tag
FROM article_tags
GROUP BY article_id;
```sql

### 数组展开统计

```sql
-- 将标签数组展开,统计每个标签的文章数
SELECT 
    UNNEST(tags) AS tag,
    COUNT(*) AS article_count
FROM articles
GROUP BY tag
ORDER BY article_count DESC;
```text

## 总结

DuckDB 的数组功能比想象中强大,50+ 个数组函数覆盖了创建、访问、转换、聚合、查找等各个方面。Lambda 表达式让数组操作变得灵活,性能也足够好。

几个关键点:
- 索引从 1 开始,不是 0
- 切片不支持 step,得用 Lambda
- `||` 操作符连接数组最简单
- LIST 聚合可以加 ORDER BY 保证顺序
- UNNEST 展开性能很好,10 万行不到 0.05 秒

掌握这些数组操作,处理嵌套数据会轻松很多。

## 参考资源

- [DuckDB List Functions](https://duckdb.org/docs/current/sql/functions/list.html)
- [DuckDB List Type](https://duckdb.org/docs/current/sql/data-types/list.html)
- [DuckDB Array Functions](https://duckdb.org/docs/current/sql/functions/array.html)
Built with Hugo
Theme Stack designed by Jimmy