Featured image of post DuckDB 导入 CSV 完全指南:10 种场景下的最佳实践

DuckDB 导入 CSV 完全指南:10 种场景下的最佳实践

DuckDB 导入 CSV 的 10 种实战场景:自动检测、手动指定 Schema、分隔符处理、编码问题、大文件分片、从 URL 远程加载、错误行跳过、日期解析、Parquet 转换导出。附性能对比和效率提升技巧。

引言

在日常数据分析工作中,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,字段分隔符
headertrue首行是否为列名
encodingutf-8文件编码,中文可用 gbk
quote"引号字符
escape"转义字符(CSV 内双引号转义)
nullstr''指定空值字符串(如 NULLNA
skip0跳过开头 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 的好处:

  1. 零报错:列类型完全由你控制
  2. 更高的性能:DuckDB 不需要扫描数据行来推断类型
  3. 一致性保证:即使 CSV 里有一行 amount 列的值为 "N/A",DuckDB 也会将其转为 NULL 而非报错

日期解析的特殊处理

日期格式千差万别,DuckDB 默认支持 ISO 格式(2026-01-15),但如果你遇到 01/15/20262026年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"的全场景。核心要点:

  1. 80% 场景用 read_csv_auto —— 自动检测、零配置
  2. 15% 场景用 read_csv —— 遇到问题时手动指定分隔符/编码/Schema
  3. 5% 场景需要 ignore_errors + reject_errors —— 处理脏数据
  4. 导入后立刻转 Parquet —— 后续查询提速 10 倍
  5. Schema 越早指定,性能越好 —— 跳过类型推断

最实用的一个技巧:初次遇到新 CSV 时,永远先做 LIMIT 100 预览数据类型,确定没问题后再全量导入。这能帮你避免 90% 的导入问题。

📺 Watch video tutorials → DuckDB Lab YouTube

Subscribe for more DuckDB & AI automation tutorials

使用 Hugo 构建
主题 StackJimmy 设计