DuckDB COPY TO 导出完全指南:从查询结果到文件,生产级数据管道必备
做数据分析时,你肯定遇到过这个需求:把查询结果保存成文件,然后发给老板、导入其他系统、或者作为下一步管道的输入。
传统做法是什么?写 Python 脚本,用 to_csv()、to_parquet(),处理编码、分隔符、日期格式……代码冗长,而且容易出错。
DuckDB 的方案更优雅: 一条 SQL 就能导出任何查询结果,支持 CSV、Parquet、JSON、Excel 等多种格式,还能自动分片、压缩、远程写入。
一、基础用法:一句话导出
导出为 CSV
-- 最简单的方式
COPY (SELECT * FROM orders WHERE amount > 1000)
TO 'orders_expensive.csv';
-- 指定分隔符和编码
COPY (SELECT * FROM sales)
TO 'sales.csv' (HEADER, DELIMITER ',', ENCODING 'UTF-8');
导出为 Parquet
-- 列式存储,体积缩小 5-10 倍
COPY (SELECT * FROM transactions)
TO 'transactions.parquet' (FORMAT PARQUET);
-- 带压缩(默认 SNAPPY)
COPY (SELECT * FROM logs)
TO 'logs.parquet' (FORMAT PARQUET, COMPRESSION 'ZSTD');
导出为 JSON
-- NDJSON 格式(每行一个 JSON 对象)
COPY (SELECT * FROM users)
TO 'users.json' (FORMAT JSON);
-- 数组格式
COPY (SELECT * FROM products)
TO 'products.json' (FORMAT JSON, ARRAY true);
二、大文件处理:自动分片
当查询结果超过 1GB 时,单个文件可能不好处理。DuckDB 可以自动分片:
-- 按行数分片(每片最多 100 万行)
COPY (SELECT * FROM massive_table)
TO 'output/part_*.csv' (HEADER, ROWS_PER_GROUP 1000000);
-- 按文件大小分片(每片约 500MB)
COPY (SELECT * FROM logs)
TO 'logs_*.parquet' (FORMAT PARQUET, MAX_FILE_SIZE '500MB');
实战:导出 10 亿行日志
-- 假设你有 10 亿行应用日志
COPY (
SELECT timestamp, level, message, user_id
FROM app_logs
WHERE timestamp >= '2026-07-01'
)
TO 'logs_202607_*.parquet'
(FORMAT PARQUET, ROWS_PER_GROUP 500000);
-- 输出:logs_202607_001.parquet, logs_202607_002.parquet, ...
三、远程文件导出
DuckDB 可以直接写入远程存储,不需要先保存到本地:
-- AWS S3
COPY (SELECT * FROM metrics)
TO 's3://bucket/metrics.parquet'
(FORMAT PARQUET, ACCESS_KEY_ID 'YOUR_KEY', SECRET_ACCESS_KEY 'YOUR_SECRET');
-- Azure Blob Storage
COPY (SELECT * FROM reports)
TO 'az://container/reports.csv' (HEADER, DELIMITER ',');
-- Google Cloud Storage
COPY (SELECT * FROM analytics)
TO 'gs://bucket/analytics.json' (FORMAT JSON);
配置凭证(更安全的方式)
-- 设置 S3 凭证一次,后续查询复用
SET s3_access_key_id = 'YOUR_KEY';
SET s3_secret_access_key = 'YOUR_SECRET';
SET s3_region = 'us-east-1';
-- 现在可以直接写入
COPY (SELECT * FROM data)
TO 's3://bucket/data.parquet' (FORMAT PARQUET);
四、数据管道集成
场景 1:每日 ETL 导出
-- 每天运行,导出前一天的销售数据
COPY (
SELECT * FROM sales
WHERE sale_date = CURRENT_DATE - INTERVAL '1 day'
)
TO 'daily_sales/' || FORMAT_DATE(CURRENT_DATE - INTERVAL '1 day', 'YYYY-MM-DD') || '.parquet'
(FORMAT PARQUET);
场景 2:多表合并导出
-- 合并多个表后导出
COPY (
SELECT o.order_id, c.customer_name, p.product_name, o.amount, o.sale_date
FROM orders o
JOIN customers c ON o.customer_id = c.id
JOIN products p ON o.product_id = p.id
WHERE o.sale_date >= '2026-07-01'
)
TO 'monthly_report.parquet' (FORMAT PARQUET);
场景 3:API 数据源 → DuckDB → 导出
import duckdb
import requests
# 1. 获取 API 数据
response = requests.get('https://api.example.com/sales')
data = response.json()
# 2. 加载到 DuckDB
con = duckdb.connect()
con.execute("CREATE TABLE api_data AS SELECT * FROM ?::JSON", [data])
# 3. 清洗并导出
con.execute("""
COPY (
SELECT * FROM api_data
WHERE amount IS NOT NULL AND amount > 0
) TO 'cleaned_sales.parquet' (FORMAT PARQUET)
""")
con.close()
五、性能优化技巧
1. 使用 Parquet 而不是 CSV
| 格式 | 压缩比 | 读取速度 | 适用场景 |
|---|---|---|---|
| CSV | 1x | 慢 | 人类可读、小文件 |
| Parquet | 5-10x | 快 | 分析、大数据 |
| JSON | 2-3x | 中 | API 交换 |
-- 推荐:日常分析用 Parquet
COPY (SELECT * FROM large_table)
TO 'output.parquet' (FORMAT PARQUET);
-- 仅当需要人类可读时用 CSV
COPY (SELECT * FROM summary)
TO 'summary.csv' (HEADER, DELIMITER ',');
2. 控制并行度
-- 限制导出线程数(避免占用太多 CPU)
SET parallel_export_threads = 2;
COPY (SELECT * FROM data)
TO 'output.csv';
3. 流式写入
对于超大结果集,使用流式写入避免内存爆炸:
-- 自动分批写入,不一次性加载全部结果
COPY (
SELECT * FROM huge_table
ORDER BY id
)
TO 'stream_output.parquet'
(FORMAT PARQUET, ROWS_PER_GROUP 100000);
六、常见陷阱与解决方案
陷阱 1:CSV 编码问题
-- ❌ 中文乱码
COPY (SELECT * FROM chinese_data) TO 'output.csv';
-- ✅ 指定 UTF-8
COPY (SELECT * FROM chinese_data)
TO 'output.csv' (HEADER, ENCODING 'UTF-8');
陷阱 2:日期格式不一致
-- ❌ 默认格式可能不符合预期
COPY (SELECT * FROM events) TO 'events.csv';
-- ✅ 显式格式化日期
COPY (
SELECT event_date::DATE AS date, event_time::TIME AS time, *
FROM events
) TO 'events.csv' (HEADER);
陷阱 3:空值处理
-- ❌ NULL 在 CSV 中显示为空,难以区分
COPY (SELECT * FROM data) TO 'output.csv';
-- ✅ 用 COALESCE 替换 NULL
COPY (
SELECT COALESCE(name, 'UNKNOWN') AS name, COALESCE(amount, 0) AS amount
FROM data
) TO 'output.csv' (HEADER);
七、生产级模板
这是一个可直接复用的导出函数:
import duckdb
from pathlib import Path
class DataExporter:
def __init__(self, db_path: str):
self.con = duckdb.connect(db_path)
def export_query(self, query: str, output_path: str,
fmt: str = 'parquet', **kwargs):
"""通用导出函数"""
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
self.con.execute(f"""
COPY ({query})
TO '{output_path}'
(FORMAT {fmt.upper()}, {', '.join(f'{k}={v}' for k,v in kwargs.items())})
""")
def export_daily(self, table: str, date: str):
"""导出指定日期的数据"""
query = f"SELECT * FROM {table} WHERE date = '{date}'"
output = f"exports/{date}.parquet"
self.export_query(query, output)
# 使用示例
exporter = DataExporter('analytics.duckdb')
exporter.export_daily('sales', '2026-07-21')
exporter.export_daily('users', '2026-07-21')
八、效果量化
| 指标 | Python pandas | DuckDB COPY TO |
|---|---|---|
| 代码行数 | 10-20 行 | 1 行 SQL |
| 处理 1GB 数据 | ~30 秒 | ~5 秒 |
| 内存占用 | 全量加载 | 流式写入 |
| 并行导出 | 需手动实现 | 内置支持 |
| 远程写入 | 需额外库 | 原生支持 |
总结
DuckDB 的 COPY TO 让你能够:
- ✅ 一行 SQL 导出任意查询结果
- ✅ 支持 CSV、Parquet、JSON、Excel 等多种格式
- ✅ 自动分片大文件,避免内存爆炸
- ✅ 直接写入 S3、Azure、GCS 等远程存储
- ✅ 集成到数据管道,无需 Python 预处理
记住:导入和导出同样重要。你的数据管道不应该只进不出——用 COPY TO 让查询结果真正流动起来。
📌 下一步行动:下次需要导出数据时,试试
COPY (SELECT ...) TO 'file.parquet',看看比 Python 快多少。
