Featured image of post DuckDB Parquet 列式存储优化:从入门到生产级性能调优

DuckDB Parquet 列式存储优化:从入门到生产级性能调优

掌握 DuckDB 读写 Parquet 文件的全部优化技巧:压缩算法选择、谓词下推、分区裁剪、向量化执行。让你的分析查询从分钟级降到秒级。附完整 SQL 示例和变现建议。

DuckDB Parquet 列式存储架构图

图: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 传统方案对比

特性CSVMySQL/PostgreSQLParquet + DuckDBSpark
1GB 文件读取8-15 秒5-10 秒(需部署)<1 秒3-5 秒
10GB 文件读取OOM 或 2+ 分钟需要分库分表3-8 秒15-30 秒
100GB 文件读取不可能需要集群30-60 秒1-3 分钟
内存占用高(完整加载)中等极低(列式+压缩)高
部署复杂度零高(需 DB 服务器)零(嵌入式)高(需集群)
压缩比无无5-10x5-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 / DOUBLEParquet 原生支持,压缩最佳
字符串(高基数)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 这种分析型数据库来说,它是性价比最高的存储格式。关键在于:

  1. 只读需要的列 —— 利用列式存储减少 70-90% IO
  2. 善用谓词下推 —— DuckDB 会自动跳过不必要的 Row Group
  3. 合理分区 —— 按月/天分区,查询时自动裁剪
  4. 选择合适的压缩 —— 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
GitHubpengzz9527/duckdb-blog

如发现错误,欢迎通过 GitHub Issue 或邮件 [email protected] 反馈。

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials

使用 Hugo 构建
主题 Stack 由 Jimmy 设计