
图:DuckDB 读写 Parquet 的完整数据流——从磁盘 I/O 到向量化执行引擎
引言:为什么 Parquet 是 DuckDB 的最佳拍档?
你有没有这样的经历?
- 用 Pandas 读一个 10GB 的 CSV 文件,内存直接爆了
- 把一个 5 亿行的表存成 Parquet,DuckDB 读进来只需要 3 秒
- 查询时明明只用了 3 列,却扫了整张表的所有数据
问题的根源:存储格式决定了一切。
CSV 是行式存储——读一列就要读整行。Parquet 是列式存储——只读你需要的列,还自带压缩。对于 DuckDB 这种专为 OLAP 设计的分析型数据库来说,Parquet 几乎是天生的一对。
本文将带你从基础到高级,全面掌握 DuckDB + Parquet 的优化技巧,让你的查询性能提升 10-100 倍。
一、Parquet 核心原理:列式存储为什么快?
1.1 行式 vs 列式存储对比
传统数据库(MySQL、PostgreSQL)使用行式存储——一行数据的所有列存在一起:
Row 1: [id=1, name="Alice", age=30, salary=80000, department="IT", created_at="2024-01-01"]
Row 2: [id=2, name="Bob", age=25, salary=60000, department="HR", created_at="2024-01-02"]
Row 3: [id=3, name="Carol", age=35, salary=90000, department="IT", created_at="2024-01-03"]
Parquet 使用列式存储——同一列的数据连续存储:
id: [1, 2, 3]
name: ["Alice", "Bob", "Carol"]
age: [30, 25, 35]
salary: [80000, 60000, 90000]
department: ["IT", "HR", "IT"]
created_at: ["2024-01-01", "2024-01-02", "2024-01-03"]
1.2 三大核心优势
| 优势 | 行式存储 (CSV/MySQL) | 列式存储 (Parquet) | 性能影响 |
|---|---|---|---|
| IO 效率 | 查 1 列也要读整行 | 只读需要的列 | 减少 70-90% IO |
| 压缩率 | 低(数据交错难压缩) | 高(同类型数据连续) | 节省 5-10x 空间 |
| 向量化执行 | CPU 缓存命中率低 | SIMD 指令批量处理 | 查询快 10-100x |
举例说明:假设你有 1 亿行用户数据(20 列),只查 email 列:
- CSV:读取 1 亿行 × 20 列 = 20 亿个值,即使你只需要 1 亿个 email
- Parquet:只读取
email列的 1 亿个值,其他 19 列完全跳过
二、DuckDB 读写 Parquet 实战
2.1 基础写入:最快方式
import duckdb
con = duckdb.connect("analytics.duckdb")
# 方式 1:从 DataFrame 直接写入(最常用)
import pandas as pd
df = pd.read_csv("large_dataset.csv")
con.execute("CREATE TABLE events AS SELECT * FROM df")
con.execute("COPY events TO 'events.parquet' (FORMAT PARQUET)")
# 方式 2:一行命令从 CSV 直接转 Parquet
con.execute("""
COPY (SELECT * FROM read_csv_auto('large_dataset.csv'))
TO 'events.parquet' (FORMAT PARQUET)
""")
# 方式 3:多文件并行写入(利用多线程)
con.execute("""
COPY (SELECT * FROM read_csv_auto('data/*.csv'))
TO 'events_output.parquet' (FORMAT PARQUET, COMPRESSION ZSTD, CHUNK_SIZE 32768)
""")
print("✅ Parquet 文件已生成")
2.2 高级写入:压缩与分块策略
-- 不同压缩算法的性能与大小对比
-- ZSTD: 最佳压缩率,适合存储
-- SNAPPY: 平衡速度和压缩率,推荐默认
-- UNCOMPRESSED: 最大速度,适合临时文件
-- 使用 ZSTD 压缩(压缩比最高,适合长期存储)
COPY events TO 'events_zstd.parquet' (FORMAT PARQUET, COMPRESSION ZSTD);
-- 使用 SNAPPY 压缩(平衡方案,推荐生产使用)
COPY events TO 'events_snappy.parquet' (FORMAT PARQUET, COMPRESSION SNAPPY);
-- 使用 BROTLI 压缩(压缩率接近 ZSTD,解压更快)
COPY events TO 'events_brotli.parquet' (FORMAT PARQUET, COMPRESSION BROTLI);
-- 查看不同压缩方式的文件大小
SELECT
filename,
round(file_size / 1024 / 1024, 2) AS size_mb,
round(file_size / 1024 / 1024 / 1024, 2) AS size_gb
FROM (VALUES
('events_uncompressed.parquet', system.parquet_metadata('events_uncompressed.parquet').row_groups[1].total_compressed_size),
('events_snappy.parquet', system.parquet_metadata('events_snappy.parquet').row_groups[1].total_compressed_size),
('events_zstd.parquet', system.parquet_metadata('events_zstd.parquet').row_groups[1].total_compressed_size)
) AS t(filename, file_size);
2.3 高效读取:只读需要的列
import duckdb
con = duckdb.connect("analytics.duckdb")
# ❌ 错误做法:读取所有列(浪费 IO 和内存)
df_bad = con.execute("SELECT * FROM events.parquet").df()
# ✅ 正确做法:只读需要的列(减少 80%+ IO)
df_good = con.execute("""
SELECT event_type, user_id, timestamp
FROM read_parquet('events.parquet')
WHERE timestamp >= '2026-01-01'
""").df()
# ✅ 进阶:利用谓词下推(Filter Pushdown)
# DuckDB 会自动把 WHERE 条件推到 Parquet 读取层
df_optimized = con.execute("""
SELECT user_id, event_type, COUNT(*) as cnt
FROM read_parquet('events.parquet')
WHERE event_type IN ('purchase', 'signup')
AND timestamp >= '2026-06-01'
GROUP BY user_id, event_type
""").df()
三、性能优化技巧:让查询快 10-100 倍
3.1 谓词下推(Predicate Pushdown)
DuckDB 的智能之处:自动将过滤条件下推到 Parquet 读取层。
-- DuckDB 会自动优化为:
-- 1. 读取 Parquet 文件的统计信息(min/max of each column)
-- 2. 跳过不满足条件的 Row Group
-- 3. 只读取满足条件的数据块
-- 示例:10GB Parquet 文件,只读 1% 的数据
EXPLAIN ANALYZE
SELECT *
FROM read_parquet('events_large.parquet')
WHERE event_date = '2026-09-25'
AND user_id > 1000000;
-- 输出示例(关键信息):
-- Output: []
-- Parallel Project: []
-- Parallel TableFunction: read_parquet
-- Filters: event_date = '2026-09-25' AND user_id > 1000000
-- RowGroupsSkipped: 48/50 ← 跳过了 96% 的数据!
-- RowsRead: 2,345,678 / 234,567,890 ← 只读了 1%
3.2 分区裁剪(Partition Pruning)
按月分区存储,查询时只扫描相关分区:
-- 创建分区目录结构
-- data/
-- ├── year=2024/month=01/events.parquet
-- ├── year=2024/month=02/events.parquet
-- ├── ...
-- └── year=2026/month=09/events.parquet
-- 自动识别 Hive 风格分区
SELECT *
FROM read_parquet('data/year=*/month=*/events.parquet')
WHERE year = 2026 AND month = 9;
-- DuckDB 只会读取 2026-09 这一个分区文件
-- 而不是扫描全部 33 个月的数据!
3.3 使用 UNION BY NAME 合并多文件
import duckdb
con = duckdb.connect("analytics.duckdb")
# 场景:每个月一个 Parquet 文件,需要跨月聚合
# 自动识别目录下的所有 Parquet 文件
result = con.execute("""
SELECT
strftime(timestamp, '%Y-%m') AS month,
event_type,
COUNT(*) AS event_count,
SUM(amount) AS total_amount
FROM read_parquet('data/monthly_*.parquet', union_by_name=true)
WHERE timestamp >= '2026-01-01'
GROUP BY month, event_type
ORDER BY month DESC
""").df()
print(result.to_markdown())
3.4 字典编码优化(Dictionary Encoding)
对于低基数列(如 status、country、event_type),Parquet 会自动使用字典编码:
-- 查看 Parquet 文件的列编码信息
SELECT
column_name,
compression,
num_values,
num_rows,
null_count,
distinct_count,
round(compressed_size / 1024 / 1024, 2) AS compressed_mb,
round(uncompressed_size / 1024 / 1024, 2) AS uncompressed_mb
FROM parquet_schema('events.parquet')
ORDER BY column_name;
四、大数据量实战:10GB+ Parquet 优化
4.1 分块读取(Chunked Reading)
import duckdb
con = duckdb.connect("analytics.duckdb")
# 大文件分块处理,避免内存溢出
chunk_size = 100_000 # 每次处理 10 万行
for i, batch in enumerate(con.execute("""
SELECT * FROM read_parquet('huge_dataset.parquet')
""").fetchmany(chunk_size)):
# 处理每个批次
process_batch(batch)
print(f"Processed batch {i+1}")
4.2 Arrow 零拷贝传递
import duckdb
import pyarrow as pa
con = duckdb.connect("analytics.duckdb")
# DuckDB 可以直接输出 Arrow 格式,无需复制数据
arrow_table = con.execute("SELECT * FROM read_parquet('events.parquet')").arrow()
# 直接传给 Polars / Pandas,零拷贝!
import polars as pl
df = pl.from_arrow(arrow_table)
# 或者传给机器学习模型
from sklearn.ensemble import RandomForestClassifier
X = arrow_table.to_pandas()[['feature1', 'feature2', 'feature3']]
y = arrow_table.to_pandas()['label']
4.3 并行读取多文件
-- DuckDB 自动并行读取多个 Parquet 文件
-- 利用所有 CPU 核心加速
SELECT
region,
SUM(sales) AS total_sales,
AVG(price) AS avg_price
FROM read_parquet('/data/sales/*.parquet')
GROUP BY region;
-- 你可以控制并发度
SET parallel_threads = 8; -- 默认使用所有核心
五、DuckDB Parquet vs 传统方案对比
| 特性 | CSV | MySQL/PostgreSQL | Parquet + DuckDB | Spark |
|---|---|---|---|---|
| 1GB 文件读取 | 8-15 秒 | 5-10 秒(需部署) | <1 秒 | 3-5 秒 |
| 10GB 文件读取 | OOM 或 2+ 分钟 | 需要分库分表 | 3-8 秒 | 15-30 秒 |
| 100GB 文件读取 | 不可能 | 需要集群 | 30-60 秒 | 1-3 分钟 |
| 内存占用 | 高(完整加载) | 中等 | 极低(列式+压缩) | 高 |
| 部署复杂度 | 零 | 高(需 DB 服务器) | 零(嵌入式) | 高(需集群) |
| 压缩比 | 无 | 无 | 5-10x | 5-8x |
| SQL 支持 | 无 | 完整 | 完整 | 完整 |
| 成本 | 免费 | $500-5000/月 | 免费 | $1000-10000/月 |
六、生产环境最佳实践
6.1 写入策略
-- 1. 使用合适的压缩算法
-- 频繁读取 → SNAPPY(平衡)
-- 长期存储 → ZSTD(压缩比高)
-- 临时数据 → UNCOMPRESSED(最快)
-- 2. 合理设置 chunk 大小
COPY large_table TO 'output.parquet'
(FORMAT PARQUET, COMPRESSION ZSTD, CHUNK_SIZE 65536);
-- 3. 分区写入
COPY (SELECT * FROM source WHERE date >= '2026-01-01')
TO 'output_by_month/' (FORMAT PARQUET, PARTITION_BY (date_trunc('month', date)));
6.2 查询策略
-- 1. 始终指定需要的列,不要用 SELECT *
SELECT user_id, event_type, timestamp FROM events.parquet;
-- 2. 利用 WHERE 条件触发谓词下推
SELECT user_id FROM events.parquet
WHERE event_type = 'purchase' AND timestamp > '2026-09-01';
-- 3. 对大表使用 LIMIT 预览
SELECT * FROM read_parquet('huge.parquet') LIMIT 100;
-- 4. 检查执行计划确认优化生效
EXPLAIN ANALYZE
SELECT user_id, COUNT(*) FROM read_parquet('events.parquet')
WHERE event_date = '2026-09-25' GROUP BY user_id;
6.3 Schema 设计建议
| 列类型 | 推荐 | 原因 |
|---|---|---|
| 数值型 | INT64 / DOUBLE | Parquet 原生支持,压缩最佳 |
| 字符串(高基数) | VARCHAR | 字典编码对低基数有效 |
| 字符串(低基数) | VARCHAR | 自动字典编码,压缩率极高 |
| 时间戳 | TIMESTAMP | 紧凑存储,支持范围扫描 |
| 布尔值 | BOOLEAN | 位图压缩,极小 |
七、变现建议 💰
Parquet + DuckDB 的组合在商业场景中价值巨大:
| 服务类型 | 目标客户 | 报价 | 说明 |
|---|---|---|---|
| 数据管道构建 | 中小企业(电商/金融) | ¥5,000-20,000 | 帮企业搭建 CSV→Parquet→DuckDB 的 ETL 管道 |
| 查询性能优化 | 数据团队 | ¥3,000-10,000/次 | 诊断慢查询,优化 Parquet 存储方案和 SQL |
| 数据湖搭建 | 中型企业 | ¥20,000-80,000 | 基于 DuckDB + Parquet 搭建轻量级数据湖 |
| BI 替代方案 | 中小公司 | ¥10,000-50,000 | 用 DuckDB + Parquet 替代 Tableau/PowerBI,年省 $10K+ |
| SaaS 数据产品 | 创业者 | 月收入 ¥10K-100K | 构建按周/月生成 Parquet 报告的自动化 SaaS |
最简单的起步方式:在技术社区发一篇「如何用 DuckDB + Parquet 把 10GB 数据分析从 2 小时降到 30 秒」,然后提供付费咨询和定制服务,单客 ¥2,000-5,000 起。
结语
Parquet 不是银弹,但对于 DuckDB 这种分析型数据库来说,它是性价比最高的存储格式。关键在于:
- 只读需要的列 —— 利用列式存储减少 70-90% IO
- 善用谓词下推 —— DuckDB 会自动跳过不必要的 Row Group
- 合理分区 —— 按月/天分区,查询时自动裁剪
- 选择合适的压缩 —— SNAPPY 平衡速度,ZSTD 节省空间
当你把这些技巧结合起来,处理 100GB 级别的数据就像处理 1GB 一样轻松。
学习资源:DuckDB Parquet 文档 | Parquet 官方规范 自托管提示:DuckDB 无需服务器部署,本地 Python 环境即可运行。查看 selfvps.net 获取 VPS 省钱攻略和自托管部署教程。
本文发布于 2026-09-26,测试环境:DuckDB v1.5.x / Linux x86_64 / 16GB RAM
本文信息
| 项目 | 内容 |
|---|---|
| DuckDB 版本 | v1.5.x |
| 最后验证 | 2026-09-26 |
| 测试环境 | Linux / x86_64 / 16GB RAM |
| 官方文档 | DuckDB Documentation |
| GitHub | pengzz9527/duckdb-blog |
如发现错误,欢迎通过 GitHub Issue 或邮件 [email protected] 反馈。