Featured image of post DuckDB 直接查询 CSV:零导入,秒级分析

DuckDB 直接查询 CSV:零导入,秒级分析

不用导入,直接对 CSV 文件写 SQL。DuckDB 的 read_csv_auto() 能做到 100 万行数据 2 秒内查完,自动识别列类型,支持多文件 glob 查询。实测数据 + 完整代码 + 踩坑记录。

拿到一个 CSV 文件,第一反应是什么?pd.read_csv() 加载进 Pandas,然后开始写 .groupby() 链式调用。

但如果这个 CSV 有 100 万行呢?加载就要等好几秒,内存直接飙上去。更烦的是,只是想快速看一眼数据分布,还得先等它加载完。

DuckDB 给了一个很直接的答案:不用导入,直接查。

  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
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
SELECT status, COUNT(*), AVG(amount)
FROM read_csv_auto('orders.csv')
GROUP BY status;
```sql

就这么一条 SQL,CSV 文件直接当表用。没有 `read_csv()`,没有 `CREATE TABLE`,连类型都不用手动指定。

---

## read_csv_auto() 到底做了什么

第一次看到这个函数的时候,我觉得不就是个语法糖嘛。实际用了之后才发现,它背后做的事情比想象中多。

**自动类型检测(CSV Sniffer)** 是核心。DuckDB 会先采样文件的前 N 行,自动推断每列的数据类型:整数、浮点数、日期、字符串,都能识别。

拿实际数据测一下:

```python
import duckdb

conn = duckdb.connect(':memory:')

# 查看自动检测的 Schema
schema = conn.execute("""
    DESCRIBE SELECT * FROM read_csv_auto('orders_100000.csv')
""").fetchall()

for col in schema:
    print(f"  {col[0]:<20} {col[1]}")
```sql

输出:

```text
order_id             BIGINT
customer_id          VARCHAR
product_category     VARCHAR
amount               DOUBLE
status               VARCHAR
order_date           DATE
```text

注意最后一行:`order_date` 被自动识别成了 `DATE` 类型。这意味着可以直接用日期函数:

```sql
SELECT 
    EXTRACT(YEAR FROM order_date) as year,
    EXTRACT(MONTH FROM order_date) as month,
    COUNT(*) as orders
FROM read_csv_auto('orders.csv')
GROUP BY year, month
ORDER BY year, month;
```sql

不用 `pd.to_datetime()`,不用手动转换,直接 `EXTRACT(MONTH FROM ...)` 就能用。

---

## 性能到底怎么样

说再多不如跑一遍。用不同规模的数据测了一下,从 1000 行到 100 万行:

```python
import duckdb
import time

conn = duckdb.connect(':memory:')

sizes = [1000, 10000, 100000, 1000000]

for size in sizes:
    filename = f'data/orders_{size}.csv'
    
    start = time.perf_counter()
    result = conn.execute(f"""
        SELECT * FROM read_csv_auto('{filename}')
    """).fetchall()
    elapsed = time.perf_counter() - start
    
    throughput = size / elapsed / 1000
    print(f"{size:>10,} 行  |  {elapsed*1000:>8.2f} ms  |  {throughput:>6.0f} K rows/s")
```text

实测结果(macOS,DuckDB 1.5.5):

| 数据量 | 文件大小 | 查询时间 | 吞吐量 |
|--------|---------|---------|--------|
| 1,000 行 | 46.9 KB | 79.57 ms | 13K rows/s |
| 10,000 行 | 478.1 KB | 70.60 ms | 142K rows/s |
| 100,000 行 | 4,879 KB | 265.74 ms | 376K rows/s |
| 1,000,000 行 | 49,767 KB | 2,199.74 ms | 455K rows/s |

100 万行数据,2.2 秒查完。吞吐量随着数据量增加反而在提升——这是向量化执行引擎的特点,数据越多,CPU 缓存利用率越高。

不过 1000 行的查询时间(79ms)比 10000 行(70ms)还长,这个让我有点意外。原因是小文件的 IO 开销占比更大,DuckDB 的启动和采样开销在小数据集上反而明显。

---

## 直接查询 vs 先导入再查询

一个很自然的问题:既然直接查询这么快,还有必要导入到表里吗?

拿 10 万行数据对比了一下:

```python
import duckdb
import time

conn = duckdb.connect(':memory:')
filename = 'data/orders_100000.csv'

# 方法 1:直接查询
start = time.perf_counter()
result = conn.execute(f"""
    SELECT status, COUNT(*), AVG(amount)
    FROM read_csv_auto('{filename}')
    GROUP BY status
""").fetchall()
direct_time = time.perf_counter() - start

# 方法 2:先导入,再查询
conn.execute(f"""
    CREATE TABLE orders AS 
    SELECT * FROM read_csv_auto('{filename}')
""")

start = time.perf_counter()
result = conn.execute("""
    SELECT status, COUNT(*), AVG(amount)
    FROM orders
    GROUP BY status
""").fetchall()
import_time = time.perf_counter() - start

print(f"直接查询: {direct_time*1000:.2f} ms")
print(f"导入后查询: {import_time*1000:.2f} ms")
print(f"速度差异: {direct_time/import_time:.1f}x")
```sql

结果:

| 方法 | 耗时 | 说明 |
|------|------|------|
| 直接查询 | 114.82 ms | 零导入,即查即用 |
| 导入后查询 | 4.51 ms | 表已存在,直接查 |
| **速度差异** | **25.5x** | 导入后快 25 倍 |

结论很清晰:

- **单次查询**:直接查,省去了导入的开销
- **多次查询**:先导入,后续查询快 25 倍

这和数据库的索引机制类似。第一次查 CSV 需要解析文件、推断类型、读取数据;导入到表之后,数据已经结构化存储,后续查询直接走优化路径。

实际项目中的做法:先用 `read_csv_auto()` 快速探索数据,确认需要反复查询后,再 `CREATE TABLE` 导入。

---

## 复杂查询性能

聚合、过滤、多函数组合,这些日常操作的表现如何?

同样用 10 万行数据测试:

```python
queries = {
    "SELECT * LIMIT 10": """
        SELECT * FROM read_csv_auto('orders_100000.csv') LIMIT 10
    """,
    "COUNT(*)": """
        SELECT COUNT(*) FROM read_csv_auto('orders_100000.csv')
    """,
    "GROUP BY": """
        SELECT status, COUNT(*) 
        FROM read_csv_auto('orders_100000.csv') 
        GROUP BY status
    """,
    "WHERE filter": """
        SELECT * FROM read_csv_auto('orders_100000.csv') 
        WHERE amount > 500
    """,
    "Multiple aggregations": """
        SELECT status, COUNT(*), AVG(amount), SUM(amount), MAX(amount)
        FROM read_csv_auto('orders_100000.csv')
        GROUP BY status
    """,
}

for name, query in queries.items():
    start = time.perf_counter()
    result = conn.execute(query).fetchall()
    elapsed = time.perf_counter() - start
    print(f"{name:<25} {elapsed*1000:>8.2f} ms  |  {len(result)} rows")
```sql

实测结果:

| 查询类型 | 耗时 | 结果行数 |
|---------|------|---------|
| SELECT * LIMIT 10 | 80.54 ms | 10 |
| COUNT(*) | 107.65 ms | 1 |
| GROUP BY | 110.13 ms | 4 |
| WHERE 过滤 | 216.90 ms | 66,963 |
| 多聚合函数 | 114.10 ms | 4 |

几个观察:

1. **LIMIT 10 只要 80ms**。DuckDB 不会傻到把整个文件读完再取前 10 行,它会在满足条件后提前终止。
2. **COUNT(*) 比 GROUP BY 快**。全表计数只需要一个累加器,分组需要维护哈希表。
3. **WHERE 过滤反而最慢**。因为匹配了 66,963 行(约 67% 的数据),需要返回大量结果集。如果改成 `WHERE amount > 900`(只匹配少量行),会快很多。

---

## 多文件批量查询

这个功能用起来真的很爽。

假设有一个 `data/` 目录,里面按月存了多个 CSV 文件:

```sql
data/
├── sales_2024_01.csv
├── sales_2024_02.csv
├── sales_2024_03.csv
└── ...
```text

用 glob 模式一次性查所有文件:

```python
import duckdb

conn = duckdb.connect(':memory:')

result = conn.execute("""
    SELECT 
        EXTRACT(YEAR FROM order_date) as year,
        EXTRACT(MONTH FROM order_date) as month,
        COUNT(*) as orders,
        SUM(amount) as revenue
    FROM read_csv_auto('data/sales_2024_*.csv')
    GROUP BY year, month
    ORDER BY year, month
""").fetchdf()

print(result)
```text

DuckDB 会自动把所有匹配的文件合并成一个虚拟表,然后执行查询。不需要手动 `pd.concat()`,不需要循环读取。

实测用 3 个文件(每个 100 行)测试,查询时间 19.92ms,结果正确合并。

---

## NULL 值和特殊字符处理

CSV 文件里经常有空字段、引号嵌套、换行符这些脏数据。DuckDB 的处理方式:

**NULL 值:**

```csv
id,name,value
1,Alice,100
2,,200
3,Charlie,
4,David,400
```sql

```python
result = conn.execute("""
    SELECT * FROM read_csv_auto('null_test.csv')
""").fetchall()

for row in result:
    print(row)
```sql

输出:

```python
(1, 'Alice', 100)
(2, None, 200)      # 空字段 -> NULL
(3, 'Charlie', None) # 空字段 -> NULL
(4, 'David', 400)
```text

空字段自动识别为 `None`(SQL 的 NULL),不需要额外配置。

**引号和换行符:**

```csv
id,name,description
1,"Alice","Has ""quotes"" inside"
2,"Bob","Line1
Line2"
3,"Charlie","Normal"
```text

DuckDB 能正确处理双引号转义(`""` -> `"`)和字段内换行符。这些都是 CSV 解析的常见坑,DuckDB 默认就能处理。

---

## 自定义 CSV 选项

不是所有 CSV 都那么规范。分隔符可能是 `|` 或 `\t`,编码可能是 GBK,日期格式可能不标准。

`read_csv_auto()` 支持一堆参数:

```python
result = conn.execute("""
    SELECT *
    FROM read_csv_auto(
        'data.csv',
        delim='|',           -- 自定义分隔符
        header=true,         -- 有表头
        quote='"',           -- 引号字符
        escape='\\',         -- 转义字符
        null_string='NA',    -- NULL 表示
        skip=1,              -- 跳过前 1 行
        sample_size=1000     -- 采样 1000 行进行类型检测
    )
""").fetchdf()
```sql

**类型覆盖:** 如果自动检测的类型不对,可以手动指定:

```python
result = conn.execute("""
    SELECT 
        id,
        CAST(amount AS DECIMAL(10,2)) as amount_decimal,
        TRY_CAST(date_str AS DATE) as parsed_date
    FROM read_csv_auto('data.csv')
""").fetchdf()
```sql

`TRY_CAST` 比 `CAST` 更安全——转换失败时返回 NULL 而不是报错。处理脏数据的时候特别有用。

---

## 踩坑记录

### 坑 1:GBK 编码的中文 CSV

DuckDB 默认 UTF-8 编码。如果 CSV 是 GBK 编码(Windows 中文环境常见),读出来会乱码。

解决办法:先用 Python 转码,或者用 Pandas 读取后传给 DuckDB:

```python
import pandas as pd
import duckdb

# 方法 1:Pandas 读取 GBK,再传给 DuckDB
df = pd.read_csv('data.csv', encoding='gbk')

conn = duckdb.connect()
result = conn.execute("""
    SELECT * FROM df WHERE amount > 100
""").fetchdf()
```sql

DuckDB 可以直接查询 Pandas DataFrame,不需要先写入 CSV 再读取。

### 坑 2:大文件内存占用

直接查询 1GB 以上的 CSV 时,内存占用会比预期高。DuckDB 虽然是流式处理,但聚合操作需要维护中间状态。

实测 100 万行(约 50MB)的 CSV,查询时内存占用稳定在 50MB 以内。但如果做 `GROUP BY` 且分组很多,内存会涨。

解决办法:减少分组数量,或者先导入到表里再查询。

### 坑 3:日期格式不标准

`read_csv_auto()` 能识别 ISO 格式的日期(`2024-01-15`),但非标准格式(`15/01/2024`、`Jan 15, 2024`)可能识别失败。

解决办法:用 `TRY_CAST` 手动指定格式:

```python
result = conn.execute("""
    SELECT 
        TRY_CAST(date_str AS DATE) as parsed_date
    FROM read_csv_auto('data.csv')
""").fetchdf()
```sql

如果 `TRY_CAST` 返回 NULL,说明格式不匹配,需要用 `strptime()` 手动解析。

---

## 什么时候用直接查询

**适合场景:**

- 一次性数据分析,快速看分布、统计
- 数据探索,不确定需要哪些列
- ETL 中间步骤,临时处理
- 多文件批量查询

**不适合场景:**

- 需要反复查询同一文件(先导入更快)
- 需要复杂索引(DuckDB 的 CSV 查询不支持索引)
- 需要并发写入(CSV 是只读的)

实际项目中的典型工作流:

1. 拿到 CSV,先用 `read_csv_auto()` 快速探索
2. 用 `DESCRIBE` 查看自动检测的类型,确认是否正确
3. 如果需要反复查询,`CREATE TABLE AS SELECT * FROM read_csv_auto(...)` 导入
4. 后续查询走表,性能提升 25 倍

---

## 和其他工具对比

拿 10 万行 CSV 查询场景对比:

| 工具 | 查询方式 | 查询时间 | 内存占用 | 易用性 |
|------|---------|---------|---------|--------|
| **DuckDB** | 直接查询 | 114 ms | 低 | ⭐⭐⭐⭐⭐ |
| Pandas | 先加载再处理 | ~150 ms | 高 | ⭐⭐⭐⭐ |
| SQLite | 需要先导入 | 需要先导入 | 低 | ⭐⭐⭐ |
| PostgreSQL | 需要先导入 | 需要先导入 | 中 | ⭐⭐ |

DuckDB 的优势在于**零导入**。Pandas 虽然也能快速加载,但内存占用更高,且复杂聚合查询不如 SQL 直观。SQLite 和 PostgreSQL 都需要先建表导入,多了一步操作。

如果是纯分析场景(查询为主,写入很少),DuckDB 的 `read_csv_auto()` 是目前最省心的方案。

---

## 总结

DuckDB 的 `read_csv_auto()` 把 CSV 查询的门槛降到了最低:不用导入,不用指定类型,写一条 SQL 就能查。

性能方面,100 万行数据 2.2 秒查完,吞吐量随数据量增加而提升。单次查询比导入后查询慢 25 倍,但省去了导入步骤,适合快速探索。

自动类型检测、多文件 glob 查询、NULL 值处理、引号转义,这些日常痛点都默认处理好了。遇到 GBK 编码或非标准日期格式,用 `TRY_CAST` 或 Pandas 转码兜底。

一句话:拿到 CSV 想快速分析,先试 `read_csv_auto()`,大概率能直接用。

---

## 参考资源

- [DuckDB CSV 导入文档](https://duckdb.org/docs/data/csv/overview)
- [DuckDB CSV Sniffer 原理](https://duckdb.org/2023/10/27/csv-sniffer.html)
- [DuckDB GitHub](https://github.com/duckdb/duckdb)

---

## 延伸阅读

- [DuckDB vs Pandas 性能对比:10 万行数据实测](/p/duckdb-vs-pandas/) - 6 个场景对比,DuckDB 平均快 2.3 倍
- [DuckDB 安装完全指南:从入门到实战](/p/duckdb-install-guide/) - Python/CLI/Node.js 三种安装方式
- [DuckDB vs PostgreSQL:嵌入式分析与服务端数据库的正面对决](/p/duckdb-vs-postgresql/) - OLAP vs OLTP 选型指南


如果 `TRY_CAST` 返回 NULL,说明格式不匹配,需要用 `strptime()` 手动解析。

---

## 什么时候用直接查询

**适合场景:**

- 一次性数据分析,快速看分布、统计
- 数据探索,不确定需要哪些列
- ETL 中间步骤,临时处理
- 多文件批量查询

**不适合场景:**

- 需要反复查询同一文件(先导入更快)
- 需要复杂索引(DuckDB 的 CSV 查询不支持索引)
- 需要并发写入(CSV 是只读的)

实际项目中的典型工作流:

1. 拿到 CSV,先用 `read_csv_auto()` 快速探索
2. 用 `DESCRIBE` 查看自动检测的类型,确认是否正确
3. 如果需要反复查询,`CREATE TABLE AS SELECT * FROM read_csv_auto(...)` 导入
4. 后续查询走表,性能提升 25 倍

---

## 和其他工具对比

拿 10 万行 CSV 查询场景对比:

| 工具 | 查询方式 | 查询时间 | 内存占用 | 易用性 |
|------|---------|---------|---------|--------|
| **DuckDB** | 直接查询 | 114 ms | 低 | ⭐⭐⭐⭐⭐ |
| Pandas | 先加载再处理 | ~150 ms | 高 | ⭐⭐⭐⭐ |
| SQLite | 需要先导入 | 需要先导入 | 低 | ⭐⭐⭐ |
| PostgreSQL | 需要先导入 | 需要先导入 | 中 | ⭐⭐ |

DuckDB 的优势在于**零导入**。Pandas 虽然也能快速加载,但内存占用更高,且复杂聚合查询不如 SQL 直观。SQLite 和 PostgreSQL 都需要先建表导入,多了一步操作。

如果是纯分析场景(查询为主,写入很少),DuckDB 的 `read_csv_auto()` 是目前最省心的方案。

---

## 总结

DuckDB 的 `read_csv_auto()` 把 CSV 查询的门槛降到了最低:不用导入,不用指定类型,写一条 SQL 就能查。

性能方面,100 万行数据 2.2 秒查完,吞吐量随数据量增加而提升。单次查询比导入后查询慢 25 倍,但省去了导入步骤,适合快速探索。

自动类型检测、多文件 glob 查询、NULL 值处理、引号转义,这些日常痛点都默认处理好了。遇到 GBK 编码或非标准日期格式,用 `TRY_CAST` 或 Pandas 转码兜底。

一句话:拿到 CSV 想快速分析,先试 `read_csv_auto()`,大概率能直接用。

---

## 参考资源

- [DuckDB CSV 导入文档](https://duckdb.org/docs/data/csv/overview)
- [DuckDB CSV Sniffer 原理](https://duckdb.org/2023/10/27/csv-sniffer.html)
- [DuckDB GitHub](https://github.com/duckdb/duckdb)

---

## 延伸阅读

- [DuckDB vs Pandas 性能对比:10 万行数据实测](/p/duckdb-vs-pandas/) - 6 个场景对比,DuckDB 平均快 2.3 倍
- [DuckDB 安装完全指南:从入门到实战](/p/duckdb-install-guide/) - Python/CLI/Node.js 三种安装方式
- [DuckDB vs PostgreSQL:嵌入式分析与服务端数据库的正面对决](/p/duckdb-vs-postgresql/) - OLAP vs OLTP 选型指南
Built with Hugo
Theme Stack designed by Jimmy