引言
在日常数据分析工作中,CSV 是遇到频率最高的数据格式。它简单、通用、几乎所有系统都能导出,但把 CSV 正确、高效地导入数据库这件事,从来没有看起来那么简单。
你大概率遇到过这些问题:
- DuckDB 自动检测数据类型,但日期列被识别成了 VARCHAR
- CSV 里有几行坏数据,整个导入失败
- 百万行的大文件,导入慢到怀疑人生
- 中文字段名或 GBK 编码直接乱码
- 分隔符不标准——不是逗号,是制表符、分号甚至竖线
这些坑我都踩过。本文整理了 DuckDB 导入 CSV 最常见的 10 种场景,每一种都附带可直接运行的 SQL 代码和最佳实践建议。
一、read_csv_auto:绝大多数场景的首选
DuckDB 最方便的 CSV 读取函数是 read_csv_auto。它会自动检测分隔符、头行、数据类型和编码:
-- 一句话读取 CSV
SELECT * FROM read_csv_auto('data/sales.csv');
-- 也可以直接作为表创建
CREATE TABLE sales AS
SELECT * FROM read_csv_auto('data/sales.csv');
read_csv_auto 实际上执行了以下自动检测:
| 检测项 | 说明 | 成功率 |
|---|---|---|
| 分隔符 | 逗号、制表符、竖线等 | 极高 |
| 是否含表头 | 首行字段数 vs 数据行 | 高 |
| 数据类型 | 最多扫描 20,000 行推断 | 中等 |
| 编码 | UTF-8/ASCII | 低(中文场景需注意) |
| 引号符 | 双引号/单引号 | 高 |
经验:
read_csv_auto在 80% 的场景下工作完美。当它失败时,不要硬扛,换成下面介绍的read_csv手动指定参数。
二、read_csv:当你需要精确控制时
当自动检测失效,用 read_csv 手动指定格式参数:
-- 指定分隔符、编码、表头
SELECT * FROM read_csv(
'data/sales.csv',
delim='|',
header=true,
encoding='utf-8',
quote='"',
escape='"'
);
关键参数一览:
| 参数 | 默认值 | 说明 |
|---|---|---|
delim | , | 字段分隔符 |
header | true | 首行是否为列名 |
encoding | utf-8 | 文件编码,中文可用 gbk |
quote | " | 引号字符 |
escape | " | 转义字符(CSV 内双引号转义) |
nullstr | '' | 指定空值字符串(如 NULL、NA) |
skip | 0 | 跳过开头 N 行 |
sample_size | -1 | 类型推断采样行数,-1 表示全部 |
三、处理坏数据:不想丢失任何一行
现实世界的数据从不干净。一行数据多了一个逗号、某个字段包含了换行符、金额列里混入了文字——这些问题都会导致 read_csv_auto 报错中断导入。
DuckDB 提供了 ignore_errors=true 参数:
-- 跳过错误行,继续读取后续数据
SELECT * FROM read_csv(
'data/messy_sales.csv',
header=true,
ignore_errors=true
);
但简单地跳过错误行不是好习惯——你可能会不知不觉丢失 10% 的数据。更好的做法是分别查看错误行:
-- 方案A:用 reject_errors 单独导出错误行(DuckDB v1.5+)
CREATE TABLE clean_sales AS
SELECT * FROM read_csv(
'data/messy_sales.csv',
header=true,
reject_errors=true, -- 错误行不会影响主结果
file_errors='errors.csv' -- 错误行写入单独文件
);
-- 方案B:手动检查错误行
CREATE TABLE errors AS
SELECT * FROM read_csv(
'data/messy_sales.csv',
header=true,
ignore_errors=true,
columns={'line': 'VARCHAR'} -- 全部当作文本读取,再手动清洗
);
实战建议:第一次总是先用
ignore_errors=true配合reject_errors=true跑一遍,检查错误日志后再决定怎么处理。
四、指定数据类型:告别 VARCHAR 猜错
这是最常见的坑:DuckDB 把数字读成了 VARCHAR,或者日期列变成了字符串。原因通常是前几千行数据格式一致,但后面出现了异常值。
解决方案是手动声明 Schema:
SELECT * FROM read_csv(
'data/sales.csv',
header=true,
columns={
'order_id': 'INTEGER',
'product': 'VARCHAR',
'amount': 'DECIMAL(10,2)',
'quantity': 'INTEGER',
'order_date': 'DATE',
'city': 'VARCHAR'
}
);
手动指定 Schema 的好处:
- 零报错:列类型完全由你控制
- 更高的性能:DuckDB 不需要扫描数据行来推断类型
- 一致性保证:即使 CSV 里有一行
amount列的值为"N/A",DuckDB 也会将其转为 NULL 而非报错
日期解析的特殊处理
日期格式千差万别,DuckDB 默认支持 ISO 格式(2026-01-15),但如果你遇到 01/15/2026 或 2026年1月15日 这样的格式:
-- 先用 VARCHAR 读进来,再手动转换
SELECT *,
strptime(order_date, '%m/%d/%Y') AS parsed_date
FROM read_csv(
'data/sales.csv',
columns={'order_id': 'INTEGER', 'order_date': 'VARCHAR', 'amount': 'DECIMAL(10,2)'}
);
-- 常见日期格式对照
-- %Y-%m-%d → 2026-01-15(ISO)
-- %m/%d/%Y → 01/15/2026(美式)
-- %Y/%m/%d → 2026/01/15
-- %d/%m/%Y → 15/01/2026(英式)
-- %Y年%m月%d日 → 2026年01月15日
五、处理 CSV 常见陷阱
5.1 开头有注释或版权声明
有些生成的 CSV 前几行是 # Export generated at 2026-01-01 之类的注释。
-- 跳过前3行
SELECT * FROM read_csv('data/sales.csv', skip=3);
5.2 空行和空值
-- 将 'NULL'、'NA'、'-' 都视为空值
SELECT * FROM read_csv(
'data/sales.csv',
nullstr=['NULL', 'NA', '-', '']
);
5.3 字段内部包含换行符
如果一个字段包含多行文本(用双引号包裹),DuckDB 默认不支持跨行解析。你需要告诉它引号规则:
-- 确保 quote 参数正确,且字段内的换行被双引号包裹
SELECT * FROM read_csv(
'data/reviews.csv',
quote='"',
escape='"',
-- 如果数据量极大,可以关闭自动类型推断提升速度
auto_detect=false,
columns={'id': 'INTEGER', 'review_text': 'VARCHAR'}
);
5.4 无表头的 CSV
-- 无表头时,DuckDB 自动命名为 column0, column1, ...
SELECT * FROM read_csv('data/no_header.csv', header=false);
-- 手动命名
SELECT * FROM read_csv(
'data/no_header.csv',
header=false,
columns={'name': 'VARCHAR', 'age': 'INTEGER', 'salary': 'DECIMAL(10,2)'}
);
六、TSV 和特殊分隔符
不是所有 CSV 都用逗号分隔——TSV(制表符分隔)、PSV(竖线分隔)、分号分隔(欧洲地区常见)都是变体:
-- TSV:制表符分隔
SELECT * FROM read_csv('data/export.tsv', delim='\t');
-- 竖线分隔
SELECT * FROM read_csv('data/export.psv', delim='|');
-- 分号分隔(常见于欧洲 Excel 导出)
SELECT * FROM read_csv('data/export.csv', delim=';');
-- 正则分隔:连续空白
SELECT * FROM read_csv('data/ragged.txt', delim='\s+');
DuckDB 的
read_csv_auto会自动检测常见的分隔符(\t,|,;),但对于自定义分隔符还是要用read_csv + delim。
七、从 URL 直接加载 CSV
DuckDB 最强大的能力之一:直接读取远程 URL 上的 CSV:
-- 从 GitHub 加载公开 CSV
SELECT * FROM read_csv_auto('https://raw.githubusercontent.com/.../data.csv');
-- 从 S3 加载(需先加载 httpfs 扩展)
INSTALL httpfs;
LOAD httpfs;
SELECT * FROM read_csv_auto('s3://my-bucket/data/sales.csv');
-- 从 API 实时返回的 CSV
SELECT * FROM read_csv_auto('https://api.example.com/export?format=csv');
这让你可以构建完全免维护的数据管道——每天定时从公开数据源拉取 CSV,不需要下载、不需要中转存储:
-- 每天定时拉取最新数据(用 cron 调度)
CREATE TABLE daily_stock_prices AS
SELECT * FROM read_csv_auto(
'https://api.example.com/stocks/daily.csv?date=' || current_date::VARCHAR
);
八、百万级大文件:性能优化技巧
大 CSV 导入的性能主要取决于两个因素:类型推断行数和是否需要中间转换。
8.1 关闭自动类型推断
自动推断需要扫描数据行(默认 20,000 行),对于百万级大文件,这会额外增加数十秒:
-- ❌ 慢:先扫描 20,000 行推断类型,再读全部
SELECT * FROM read_csv_auto('data/large_file.csv');
-- ✅ 快:指定类型,零推断
SELECT * FROM read_csv(
'data/large_file.csv',
columns={'id': 'INTEGER', 'name': 'VARCHAR', 'amount': 'DECIMAL(10,2)'},
auto_detect=false
);
实测对比(100万行、50MB CSV):
| 方式 | 耗时 |
|---|---|
read_csv_auto 默认 | 3.2s |
read_csv 指定类型 | 1.1s |
| 先导入 DuckDB 再查询 | 0.6s(后续查询) |
8.2 直接导入表,不做中间查询
-- ✅ 最佳:直接创建表,不存在中间临时结果
CREATE TABLE large_data AS
SELECT * FROM read_csv(
'data/large_file.csv',
columns={'id': 'INTEGER', 'name': 'VARCHAR', 'amount': 'DECIMAL(10,2)'}
);
8.3 分片并行读取(DuckDB 会自动优化)
-- DuckDB 自动并行处理,你可以拆分成多个文件
CREATE TABLE all_data AS
SELECT * FROM read_csv_auto('data/part_*.csv');
part_*.csv 通配符会触发 DuckDB 的多文件并行读取机制,每个文件一个线程。
九、万物皆可 CSV:从非文件来源导入
9.1 从标准输入
-- 直接在 SQL 中嵌入 CSV 文本(适合测试和小数据)
SELECT * FROM read_csv_auto('data.csv', data=
'name,age,city
张三,28,北京
李四,32,上海
王五,25,广州');
9.2 从 Python 或 R 传递的数据框
import duckdb
# Python 数据框直接注册为 DuckDB "CSV" 表
df = pd.DataFrame({'name': ['张三', '李四'], 'age': [28, 32]})
duckdb.sql("SELECT * FROM df")
9.3 合并多个 CSV 文件夹
-- 递归扫描多级目录
SELECT * FROM read_csv_auto('data/**/*.csv');
-- 带文件名标记(追溯来源)
SELECT *, filename FROM read_csv_auto('data/**/*.csv', filename=true);
十、导入后立即导出为 Parquet
CSV 是传输格式,不是分析格式。一旦导入成功,立刻导出为 Parquet——体积缩小 5-10 倍,查询速度提升 10 倍:
-- 1. 导入 CSV
CREATE TABLE raw_data AS
SELECT * FROM read_csv('data/large.csv', columns={...}, auto_detect=false);
-- 2. 导出为 Parquet
COPY raw_data TO 'data/large.parquet' (FORMAT PARQUET);
-- 3. 下次直接读 Parquet
SELECT * FROM read_parquet('data/large.parquet');
经验法则:CSV 是"一次性消耗品",Parquet 是"长效分析格式"。保留原始 CSV 用于审计,日常查询全部用 Parquet。
常见问题
Q1:CSV 文件太大,导入到一半内存不够?
DuckDB 默认使用内存映射和磁盘溢出,理论上可以处理大于内存的数据。但如果确实遇到内存问题:
-- 限制内存使用
SET memory_limit = '2GB';
SET temp_directory = '/tmp/duckdb_tmp';
-- 分批导入
CREATE TABLE data AS SELECT * FROM read_csv('data/part_1.csv');
INSERT INTO data SELECT * FROM read_csv('data/part_2.csv');
-- ... 逐批追加
Q2:GBK 编码的中文 CSV 怎么读?
SELECT * FROM read_csv_auto('data/cn_sales.csv', encoding='gbk');
如果 gbk 不支持,先用 Python 的 iconv 转换:
# 命令行转码
iconv -f GBK -t UTF-8 input.csv > output_utf8.csv
Q3:如何只导入部分列?
-- 方式1:读取后选择列
SELECT order_id, amount FROM read_csv_auto('data/sales.csv');
-- 方式2:只读取指定列(更高效,DuckDB 会自动下推)
SELECT * FROM read_csv(
'data/sales.csv',
columns={'order_id': 'INTEGER', 'amount': 'DECIMAL(10,2)'},
auto_detect=false
);
Q4:列数不固定或有变动?
-- 全部当作 VARCHAR 读进来,再处理
SELECT * FROM read_csv(
'data/flexible.csv',
header=true,
columns={'col': 'VARCHAR'}
);
Q5:怎样只导入前 N 行做预览?
-- 只读前 100 行快速预览
SELECT * FROM read_csv_auto('data/sales.csv') LIMIT 100;
总结
DuckDB 的 CSV 导入能力覆盖了从"快速看一眼"到"生产级 ETL"的全场景。核心要点:
- 80% 场景用
read_csv_auto—— 自动检测、零配置 - 15% 场景用
read_csv—— 遇到问题时手动指定分隔符/编码/Schema - 5% 场景需要
ignore_errors+reject_errors—— 处理脏数据 - 导入后立刻转 Parquet —— 后续查询提速 10 倍
- Schema 越早指定,性能越好 —— 跳过类型推断
最实用的一个技巧:初次遇到新 CSV 时,永远先做 LIMIT 100 预览数据类型,确定没问题后再全量导入。这能帮你避免 90% 的导入问题。
