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)
|