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
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
| -- 大小写转换
SELECT lower('HELLO WORLD'); -- hello world
SELECT upper('hello world'); -- HELLO WORLD
-- 长度计算(注意:按字符计数,不是字节)
SELECT length('Hello 世界'); -- 8(不是11)
-- 子字符串
SELECT substring('Hello World', 1, 5); -- Hello
-- 去空格
SELECT trim(' hello '); -- hello
SELECT ltrim(' hello '); -- hello
SELECT rtrim(' hello '); -- hello
```sql
拼接函数也很实用:
```sql
-- 普通拼接
SELECT concat('Hello', ' ', 'World'); -- Hello World
-- 带分隔符拼接(这个好用)
SELECT concat_ws('-', '2026', '09', '30'); -- 2026-09-30
```text
查找和判断类函数:
```sql
-- 查找位置
SELECT strpos('Hello World', 'World'); -- 7
-- 判断开头/结尾/包含
SELECT starts_with('Hello World', 'Hello'); -- true
SELECT ends_with('Hello World', 'World'); -- true
SELECT contains('Hello World', 'lo Wo'); -- true
```sql
填充和重复:
```sql
-- 左填充(补零场景很实用)
SELECT lpad('42', 5, '0'); -- 00042
-- 右填充
SELECT rpad('hi', 5, '!'); -- hi!!!
-- 重复
SELECT repeat('ha', 3); -- hahaha
```text
还有个有意思的 `split()` 函数:
```sql
SELECT split('a,b,c', ','); -- ['a', 'b', 'c']
```sql
返回的是数组,可以直接用数组索引访问:
```sql
SELECT split('2026-09-30', '-')[1]; -- 2026
```text
## 正则表达式:9个函数实战
正则表达式是文本处理的大杀器。DuckDB提供了9个正则函数,我挑几个最常用的说说。
### 匹配判断
```sql
-- 判断是否包含数字
SELECT regexp_matches('hello123', '[0-9]+'); -- true
-- 判断邮箱格式
SELECT regexp_matches('alice@example.com',
'^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$'); -- true
```sql
### 正则替换
```sql
-- 替换所有数字为NUM
SELECT regexp_replace('hello 123 world 456', '[0-9]+', 'NUM', 'g');
-- 结果: hello NUM world NUM
-- 去除所有非数字字符(清洗手机号)
SELECT regexp_replace('138-1234-5678', '[^0-9]', '', 'g');
-- 结果: 13812345678
```text
### 正则提取(这个最常用)
```sql
-- 提取邮箱
SELECT regexp_extract('Contact: alice@example.com',
'([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})', 1);
-- 结果: alice@example.com
-- 提取URL
SELECT regexp_extract('Visit https://duckdb.org today',
'(https?://[^\s]+)', 1);
-- 结果: https://duckdb.org
-- 提取手机号
SELECT regexp_extract('Call 13812345678 now',
'(1[3-9][0-9]{9})', 1);
-- 结果: 13812345678
-- 提取IP地址
SELECT regexp_extract('Server: 192.168.1.1 is down',
'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})', 1);
-- 结果: 192.168.1.1
-- 提取价格
SELECT regexp_extract('price: $123.45', '\$(\d+\.\d+)', 1);
-- 结果: 123.45
```sql
第三个参数 `1` 表示提取第一个捕获组。如果要提取整个匹配,可以省略这个参数。
### 提取所有匹配
```sql
-- 提取所有字母
SELECT regexp_extract_all('a1b2c3', '[a-z]+');
-- 结果: ['a', 'b', 'c']
-- 提取日志中的所有IP
SELECT regexp_extract_all('From 192.168.1.1 to 10.0.0.5',
'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}');
-- 结果: ['192.168.1.1', '10.0.0.5']
```text
### 正则分割
```sql
-- 按数字分割
SELECT string_split_regex('a1b22c', '[0-9]+');
-- 结果: ['a', 'b', 'c']
```sql
## 模式匹配:LIKE vs SIMILAR TO vs GLOB
除了正则,DuckDB还支持几种模式匹配方式:
```sql
-- LIKE:标准SQL通配符
SELECT 'Hello World' LIKE 'Hello%'; -- true
SELECT 'Hello' LIKE 'H_llo'; -- true(_匹配单个字符)
-- ILIKE:不区分大小写
SELECT 'Hello' ILIKE 'HELLO'; -- true
-- SIMILAR TO:支持正则语法
SELECT 'Hello123' SIMILAR TO '^[A-Za-z]+[0-9]+$'; -- true
-- GLOB:shell风格通配符
SELECT 'Hello' GLOB 'H*llo'; -- true
```sql
实际使用中,简单匹配用 `LIKE` 或 `contains()`,复杂匹配用正则。
## 字符串相似度:模糊匹配利器
做数据清洗时经常遇到模糊匹配需求。DuckDB提供了4个相似度函数:
```sql
-- 编辑距离
SELECT levenshtein('hello', 'hullo'); -- 1(改一个字符)
-- Damerau-Levenshtein距离(支持字符转置)
SELECT damerau_levenshtein('hello', 'hlelo'); -- 1
-- Jaro相似度(0-1之间,越大越相似)
SELECT jaro_similarity('hello', 'hullo'); -- 0.867
-- Jaro-Winkler相似度(对前缀匹配更友好)
SELECT jaro_winkler_similarity('hello', 'hullo'); -- 0.88
```sql
编辑距离返回的是整数(需要多少次操作),相似度返回的是0-1之间的浮点数。
## 哈希与编码
```sql
-- MD5哈希
SELECT md5('hello');
-- 结果: 5d41402abc4b2a76b9719d911017c592
-- SHA256哈希
SELECT sha256('hello');
-- 结果: 2cf24dba5fb0a30e26e83b2ac5b9e29e...
-- Base64编码/解码
SELECT to_base64('hello'); -- aGVsbG8=
SELECT from_base64('aGVsbG8='); -- hello
-- URL编码/解码
SELECT url_encode('hello world'); -- hello%20world
SELECT url_decode('hello%20world'); -- hello world
```sql
## 性能测试:100万行数据
光说功能不够,得看性能。我准备了100万行数据,测试各种字符串函数的表现。
### 基础函数性能
| 函数 | 耗时 (ms) | 说明 |
|------|----------|------|
| `length()` | 4.02 | ⚡ 最快 |
| `substring()` | 5.77 | 很快 |
| `upper()` | 5.83 | 很快 |
| `reverse()` | 5.99 | 很快 |
| `concat()` | 8.70 | 快 |
| `lower()` | 16.49 | 中等 |
| `replace()` | 16.96 | 中等 |
| `trim()` | 19.05 | 较慢 |
`length()` 最快(4ms),因为只需要计数。`trim()` 最慢(19ms),因为要扫描两端。但所有基础函数都在20ms内完成,性能相当优秀。
### 正则表达式性能
| 函数 | 耗时 (ms) | 说明 |
|------|----------|------|
| `regexp_matches()` | 14.34 | 快(只返回bool) |
| `regexp_replace()` | 54.69 | 中等 |
| `regexp_extract()` | 67.15 | 较慢 |
正则比简单字符串操作慢3-5倍,这是预期内的。`regexp_matches()` 最快,因为只返回布尔值;`regexp_extract()` 最慢,因为要处理捕获组。
### 模式匹配对比
| 方法 | 耗时 (ms) | 说明 |
|------|----------|------|
| `LIKE '%World%'` | 4.86 | ⚡ 最快 |
| `contains()` | 4.82 | ⚡ 最快 |
| `regexp_matches()` | 4.63 | ⚡ 最快 |
简单包含匹配时,这三种方式性能几乎相同。但 `contains()` 代码更清晰,推荐优先使用。
### 字符串分割性能
| 函数 | 耗时 (ms) | 说明 |
|------|----------|------|
| `string_split()` | 8.78 | ⚡ 快 |
| `string_split_regex()` | 46.47 | 慢5.3倍 |
固定分隔符用 `string_split()`,性能更好。只有在需要正则分割时才用 `string_split_regex()`。
### 哈希函数性能
| 函数 | 耗时 (ms) | 说明 |
|------|----------|------|
| `md5()` | 22.96 | ⚡ 快 |
| `sha256()` | 93.35 | 慢4倍 |
MD5比SHA256快4倍。如果不需要密码学安全(比如只是生成唯一标识),用MD5性能更好。
### 相似度函数性能
| 函数 | 耗时 (ms) | 说明 |
|------|----------|------|
| `jaro_similarity()` | 16.19 | ⚡ 快 |
| `levenshtein()` | 197.96 | 慢12倍 |
Levenshtein距离计算非常慢(198ms)。大批量相似度匹配建议用Jaro相似度,性能快12倍。
## 实战:数据清洗
回到开头那个任务,用DuckDB清洗脏数据:
```sql
-- 创建测试数据
CREATE TABLE dirty_data AS SELECT * FROM (VALUES
(' Alice ', 'alice@EXAMPLE.com', '138-1234-5678', '2026/09/30'),
('BOB ', 'bob@test', '13812345678', '2026-9-30'),
(' charlie ', 'CHARLIE@demo.com', '138 1234 5678', '2026.09.30'),
('david ', 'invalid-email', '12345', '30/09/2026')
) AS t(name, email, phone, date);
-- 清洗数据
SELECT
-- 姓名清洗:去空格 + 首字母大写
concat(upper(left(trim(name), 1)), lower(substring(trim(name), 2))) as clean_name,
-- 邮箱清洗:转小写 + 验证
lower(trim(email)) as clean_email,
regexp_matches(lower(trim(email)), '^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$') as email_valid,
-- 手机号清洗:去分隔符 + 验证
regexp_replace(phone, '[^0-9]', '', 'g') as clean_phone,
regexp_matches(regexp_replace(phone, '[^0-9]', '', 'g'), '^1[3-9][0-9]{9}$') as phone_valid
FROM dirty_data;
```text
清洗结果:
```text
Alice | alice@example.com | 13812345678 | ✓邮箱 ✓手机
Bob | bob@test | 13812345678 | ✗邮箱 ✓手机
Charlie | charlie@demo.com | 13812345678 | ✓邮箱 ✓手机
David | invalid-email | 12345 | ✗邮箱 ✗手机
```sql
纯SQL搞定,不用写Python脚本。
## 实战:日志分析
另一个常见场景是日志解析:
```sql
-- 测试数据
CREATE TABLE logs AS SELECT * FROM (VALUES
('2026-09-30 10:15:23 ERROR: Connection timeout to 192.168.1.100'),
('2026-09-30 10:16:45 INFO: User alice@example.com logged in from 10.0.0.5'),
('2026-09-30 10:17:12 WARN: Disk usage at 85% on /dev/sda1')
) AS t(log_line);
-- 解析日志
SELECT
regexp_extract(log_line, '(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})', 1) as timestamp,
regexp_extract(log_line, '(ERROR|WARN|INFO)', 1) as level,
regexp_extract_all(log_line, '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}') as ip_addresses,
regexp_extract(log_line, '([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})') as email
FROM logs;
```text
结果:
```text
时间: 2026-09-30 10:15:23 | 级别: ERROR | IPs: ['192.168.1.100']
时间: 2026-09-30 10:16:45 | 级别: INFO | IPs: ['10.0.0.5'] | 邮箱: alice@example.com
时间: 2026-09-30 10:17:12 | 级别: WARN | IPs: []
```text
`regexp_extract_all()` 可以提取所有匹配的IP地址,这个功能很实用。
## 中文处理
DuckDB对中文字符的支持相当好:
```sql
-- 中文字符串长度(按字符计数,不是字节)
SELECT length('你好世界Hello'); -- 9
-- 中文子串
SELECT substring('你好世界Hello', 1, 4); -- 你好世界
-- 中文反转
SELECT reverse('你好世界'); -- 界世好你
-- 中文填充
SELECT lpad('张三', 5, '*'); -- ***张三
-- 中文正则提取
SELECT regexp_extract('订单号:DD20260930001', 'DD([0-9]+)', 1);
-- 结果: 20260930001
```sql
所有字符串函数都支持中文,`length()` 按字符计数(与Python一致),正则表达式也完美支持中文。
## 踩坑记录
测试过程中踩了几个坑,记录下来免得再踩。
### 坑1:`initcap()` 不存在
PostgreSQL有 `initcap()` 函数(首字母大写),DuckDB没有。需要手动实现:
```sql
-- 错误方式(不工作)
SELECT initcap('hello world');
-- 正确方式
SELECT concat(upper(left('hello world', 1)), lower(substring('hello world', 2)));
```text
### 坑2:`regexp_split()` 不存在
文档可能提到 `regexp_split()`,但实际不存在。应该用 `string_split_regex()`:
```sql
-- 错误方式(不工作)
SELECT regexp_split('a1b22c', '[0-9]+');
-- 正确方式
SELECT string_split_regex('a1b22c', '[0-9]+'); -- ['a', 'b', 'c']
```sql
### 坑3:正则转义问题
在SQL字符串中,反斜杠需要双重转义:
```sql
-- 错误方式(不工作)
SELECT regexp_extract('price: $123', '$(\d+)', 1);
-- 正确方式
SELECT regexp_extract('price: $123', '\\$(\\d+)', 1);
```text
### 坑4:正则不支持大小写转换
`regexp_replace()` 不支持 `\U`、`\L` 等大小写转换修饰符:
```sql
-- 错误方式(不工作)
SELECT regexp_replace('hello world', '(\w+)', '\u\1', 'g');
-- 正确方式:用字符串函数组合
SELECT string_agg(upper(left(word,1)) || substring(word,2), ' ')
FROM (SELECT unnest(string_split('hello world', ' ')) as word);
```sql
### 坑5:`hamming_distance()` 和 `soundex()` 不存在
这些函数在其他数据库存在,DuckDB不支持。可以用 `levenshtein()` 或 `jaro_similarity()` 替代。
### 坑6:`encode()` 函数不存在
PostgreSQL的 `encode(text, 'base64')` 在DuckDB不存在。应该用 `to_base64()` 和 `from_base64()`。
## 性能优化建议
根据测试结果,总结几条优化建议:
### 1. 优先使用简单字符串函数
```sql
-- ❌ 慢:正则匹配
WHERE regexp_matches(email, '@example\.com$')
-- ✅ 快:ends_with
WHERE ends_with(email, '@example.com')
```text
### 2. 固定分隔符用 `string_split()`
```sql
-- ❌ 慢:正则分割(46ms)
string_split_regex('a,b,c', ',')
-- ✅ 快:固定分割(8ms)
string_split('a,b,c', ',')
```sql
### 3. 简单包含用 `contains()`
```sql
-- 性能相同,但 contains() 代码更清晰
WHERE contains(text, 'keyword')
```text
### 4. 避免大批量使用 `levenshtein()`
```sql
-- ❌ 慢:198ms/10000行
SELECT avg(levenshtein(text, 'target')) FROM large_table
-- ✅ 快:16ms/10000行
SELECT avg(jaro_similarity(text, 'target')) FROM large_table
```text
### 5. 非密码学场景用 `md5()`
```sql
-- ❌ 慢:93ms
sha256(text)
-- ✅ 快:23ms(4倍快)
md5(text)
```text
## 总结
DuckDB的字符串和正则表达式功能相当强大,26个基础函数 + 9个正则函数,基本覆盖了所有文本处理需求。
性能方面,基础字符串函数都在20ms内完成(100万行),正则表达式慢3-5倍但仍在可接受范围。中文支持完美,所有函数都能正确处理中文字符。
实际使用中,数据清洗和日志分析是最常见的场景。组合使用 `trim()` + `lower()` + `regexp_matches()` + `regexp_extract()` 可以完成80%的清洗工作。
踩坑方面,主要是一些函数不存在(`initcap()`、`regexp_split()`)和转义问题。记录下来免得再踩。
最后,如果需要在SQL中做复杂的文本处理,DuckDB完全可以胜任,不用切换到Python。性能够快,功能够全,用起来也简单。
## 参考资源
- [DuckDB 官方文档 - String Functions](https://duckdb.org/docs/sql/functions/string)
- [DuckDB 官方文档 - Regular Expressions](https://duckdb.org/docs/sql/functions/regular_expressions)
- [DuckDB 官方文档 - Pattern Matching](https://duckdb.org/docs/sql/functions/pattern_matching)
- [PostgreSQL String Functions](https://www.postgresql.org/docs/current/functions-string.html)(对比参考)
## 总结
DuckDB的字符串和正则表达式功能相当强大,26个基础函数 + 9个正则函数,基本覆盖了所有文本处理需求。
性能方面,基础字符串函数都在20ms内完成(100万行),正则表达式慢3-5倍但仍在可接受范围。中文支持完美,所有函数都能正确处理中文字符。
实际使用中,数据清洗和日志分析是最常见的场景。组合使用 `trim()` + `lower()` + `regexp_matches()` + `regexp_extract()` 可以完成80%的清洗工作。
踩坑方面,主要是一些函数不存在(`initcap()`、`regexp_split()`)和转义问题。记录下来免得再踩。
最后,如果需要在SQL中做复杂的文本处理,DuckDB完全可以胜任,不用切换到Python。性能够快,功能够全,用起来也简单。
## 参考资源
- [DuckDB 官方文档 - String Functions](https://duckdb.org/docs/sql/functions/string)
- [DuckDB 官方文档 - Regular Expressions](https://duckdb.org/docs/sql/functions/regular_expressions)
- [DuckDB 官方文档 - Pattern Matching](https://duckdb.org/docs/sql/functions/pattern_matching)
- [PostgreSQL String Functions](https://www.postgresql.org/docs/current/functions-string.html)(对比参考)
|