⚠️ 版本说明:本文基于 DuckDB v1.5.x 编写。DuckDB v2.0 Preview 中引入了更完整的分区表支持(
PARTITION BY语法),但当前 v1.5.x 主要通过目录组织和谓词下推实现类似效果。
DuckDB 分区表实战:理解分区裁剪与数据管理
你是否遇到过这样的场景?
业务跑得好好的,某天你往数据库里写入了几千万行日志数据。从此以后,每次做聚合分析,查询都要等上几十秒甚至几分钟。
你试过加索引、调参数、换机器,但数据量就像滚雪球一样越来越大。
其实,理解**分区裁剪(Partition Pruning)**的原理,可以帮助你更高效地组织数据。
DuckDB 的分区机制
DuckDB v1.5.x 不原生支持 PARTITION BY 建表语法(v2.0 Preview 中已引入)。但在生产环境中,我们可以通过以下方式利用分区优势:
方法 1:Hive 风格分区目录
将数据按时间/地区等维度组织为分区目录,DuckDB 读取时会自动利用目录结构:
import duckdb
# 数据目录结构:
# /data/orders/year=2023/month=01/
# /data/orders/year=2023/month=02/
# ...
conn = duckdb.connect('orders.db')
# 创建外部表指向分区目录
conn.execute("""
CREATE VIEW orders_view AS
SELECT * FROM read_parquet('/data/orders/*.parquet')
""")
# DuckDB 会自动优化路径扫描
result = conn.execute("""
SELECT month, SUM(amount) as total
FROM orders_view
WHERE month >= '2023-06'
GROUP BY month
""").fetchdf()
方法 2:使用 CREATE TABLE AS 创建分区数据
import duckdb
conn = duckdb.connect('sales.db')
# 创建基础表
conn.execute("""
CREATE TABLE orders (
order_id INTEGER,
customer_id INTEGER,
amount DECIMAL(10,2),
order_date DATE,
region VARCHAR
)
""")
# 插入数据
conn.execute("""
INSERT INTO orders
SELECT
gen,
(random() * 10000)::INTEGER,
ROUND(random() * 1000, 2),
DATE '2023-01-01' + (random() * 365)::INTEGER,
CASE random() * 4
WHEN 0 THEN 'East'
WHEN 1 THEN 'South'
WHEN 2 THEN 'North'
WHEN 3 THEN 'West'
ELSE 'Central'
END
FROM generate_series(1, 1000000) AS t(gen)
""")
# 查看数据分布
print(conn.execute("SELECT count(*) FROM orders").fetchone()[0])
print(conn.execute("SELECT region, count(*) FROM orders GROUP BY region").fetchdf())
分区裁剪原理
当查询包含过滤条件时,DuckDB 的优化器会尝试将条件推入数据源扫描阶段,只读取匹配的数据块:
-- DuckDB 会自动将日期过滤推入 Parquet 文件扫描
-- 利用 Parquet 文件的 zone map 跳过不匹配的列块
SELECT region, SUM(amount)
FROM read_parquet('/data/sales/*.parquet')
WHERE order_date >= '2023-06-01'
GROUP BY region;
验证优化器行为
使用 EXPLAIN 查看查询计划:
EXPLAIN
SELECT region, SUM(amount)
FROM read_parquet('/data/sales/*.parquet')
WHERE order_date >= '2023-06-01'
GROUP BY region;
如果看到 Filter Predicate: order_date >= 2023-06-01 出现在扫描阶段,说明谓词下推已成功。
生产环境最佳实践
1. 使用 Parquet 格式
Parquet 文件自带 zone map,DuckDB 可以利用它快速跳过不匹配的数据块:
# 导出为 Parquet(带分区)
conn.execute("""
COPY (
SELECT * FROM orders
WHERE order_date >= '2023-01-01'
) TO '/data/export/orders_2023.parquet' (FORMAT PARQUET)
""")
2. 合理的分区粒度
| 场景 | 建议分区策略 | 原因 |
|---|---|---|
| 日级数据 | 按月分区 | 平衡文件大小和扫描效率 |
| 实时数据 | 按小时分区 | 频繁追加小文件 |
| 静态数据 | 不按分区 | 避免过多小文件 |
3. 避免过多小文件
# 合并小文件提高读取效率
conn.execute("""
CREATE TABLE merged_orders AS
SELECT * FROM read_parquet('/data/small_partitions/*.parquet')
""")
v2.0 预览:原生分区表支持
DuckDB v2.0 Preview(Alpha)已引入原生 PARTITION BY 语法。未来文章更新将介绍:
CREATE TABLE ... PARTITION BY RANGE/LIST/HASH- 自动分区裁剪优化
- 分区维护命令
⚠️ v2.0 仍处于 Alpha 阶段,API 可能变化。建议在正式发布后再用于生产环境。
总结
| 特性 | v1.5.x | v2.0 Preview |
|---|---|---|
| 分区表语法 | ❌ 不支持 | ✅ PARTITION BY |
| 谓词下推 | ✅ 自动 | ✅ 增强 |
| Zone Map 利用 | ✅ | ✅ |
| 分区维护命令 | ❌ | ✅ 预览中 |
核心建议:
- 在 v1.5.x 中,通过 Parquet + 谓词下推实现类似分区的效果
- 关注 v2.0 正式发布后的原生分区表功能
- 始终使用
EXPLAIN验证优化器行为
DuckDB 官方文档:https://duckdb.org/docs/ 本文最后验证:2026-09-12,DuckDB v1.5.2
本文信息
| 项目 | 内容 |
|---|---|
| DuckDB 版本 | v1.5.x(部分功能基于 v2.0 Preview) |
| 最后验证 | 2026-09-12 |
| 测试环境 | Linux / x86_64 / 16GB RAM |
| 官方文档 | DuckDB Documentation |
| GitHub | pengzz9527/duckdb-blog |
如发现错误,欢迎通过 GitHub Issue 或邮件 contact@duckdblab.org 反馈。
