9.1 EXPLAIN ANALYZE:解读执行计划
理解查询执行计划是性能调优的第一步。DuckDB 提供了比 PostgreSQL 更易读的执行计划输出。
SQL-- EXPLAIN:显示执行计划(不实际执行)
EXPLAIN
SELECT region, SUM(amount)
FROM 'orders.parquet'
WHERE order_date >= '2024-01-01'
GROUP BY region;
-- EXPLAIN ANALYZE:实际执行并附带真实统计
EXPLAIN ANALYZE
SELECT region, SUM(amount)
FROM 'orders.parquet'
WHERE order_date >= '2024-01-01'
GROUP BY region;
执行计划关键指标解读
- PARQUET_SCAN 扫描 Parquet 文件。查看 Filters 行,确认谓词是否下推;查看 Row Groups 被跳过了多少(Zone Maps 效果)。
- HASH_GROUP_BY 基于哈希的 GROUP BY。如果数据量大、基数高,这里会消耗大量内存,可能触发溢写。
- HASH_JOIN 哈希 JOIN,DuckDB 会把小表放到内存构建哈希表,大表流式扫描。如果两个表都很大,注意内存压力。
- Estimated Cardinality 估计的行数。如果与实际行数差距大,说明统计信息不准,可能导致次优的执行计划。
9.2 并行查询:充分利用多核
SQL-- 查看当前线程数
SELECT current_setting('threads');
-- 设置并行线程数(默认等于 CPU 核心数)
SET threads = 8;
-- 禁用并行(用于调试)
SET threads = 1;
-- Python 中设置
# con = duckdb.connect(config={'threads': 8})
# 或
# con.execute("SET threads = 8")
DuckDB 并行的工作方式 DuckDB 使用摩尔登并行(Morsel-Driven Parallelism)。查询被分成小块(morsel,约 122880 行),多个工作线程动态领取任务块执行。这种方式比静态分区更灵活,负载均衡更好。Parquet 文件的不同行组可以并行扫描,DML 操作也可以并行。
9.3 内存管理
SQL-- 设置最大内存限制
SET memory_limit = '4GB';
SET memory_limit = '80%'; -- 系统内存的 80%(推荐)
-- 设置临时文件目录(超内存时溢写到此)
SET temp_directory = '/fast-nvme/duckdb_tmp';
-- 查看当前内存使用
SELECT * FROM duckdb_memory();
-- 查看当前配置
SELECT * FROM duckdb_settings() WHERE name IN ('memory_limit', 'threads', 'temp_directory');
超内存处理(Spilling to Disk)
当查询数据量超过内存限制时,DuckDB 会自动将中间结果溢写到磁盘临时文件,查询仍然可以完成,只是速度会下降。这是 DuckDB 处理"内存放不下的数据"的核心机制。
SQL-- 处理 300GB 数据,机器只有 32GB 内存
SET memory_limit = '28GB';
SET temp_directory = '/tmp/duckdb';
SET threads = 16;
-- DuckDB 会自动决定何时溢写,用户无需干预
SELECT
product_category,
date_trunc('month', order_date) AS month,
SUM(amount) AS revenue
FROM 's3://huge-bucket/orders/**/*.parquet'
GROUP BY ALL
ORDER BY month, revenue DESC;
9.4 查询优化技巧
技巧1:列裁剪 — 只选需要的列
SQL-- ❌ 避免:SELECT * 读取所有列
SELECT * FROM 'wide_table.parquet' WHERE id = 1;
-- ✅ 推荐:只选需要的列(列存优势最大化)
SELECT id, name, amount FROM 'wide_table.parquet' WHERE id = 1;
技巧2:谓词下推 — 让过滤尽早发生
SQL-- ❌ 避免:在 JOIN 之后过滤(大量中间数据)
SELECT * FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.amount > 10000 AND u.country = 'CN';
-- ✅ DuckDB 会自动做谓词下推,但显式写出更安全
WITH big_orders AS (
SELECT * FROM orders WHERE amount > 10000
),
cn_users AS (
SELECT * FROM users WHERE country = 'CN'
)
SELECT * FROM big_orders o JOIN cn_users u ON o.user_id = u.id;
技巧3:预先排序写入 Parquet(Zone Maps 最大化)
SQL-- 如果常按 order_date 过滤,就按 order_date 排序写入
COPY (
SELECT * FROM orders ORDER BY order_date -- 关键:排序!
) TO 'orders_sorted.parquet' (FORMAT PARQUET, ROW_GROUP_SIZE 122880);
技巧4:缓冲池与文件缓存
SQL-- 启用查询结果缓存(同一查询重复执行时直接返回缓存)
SET enable_object_cache = true;
-- 设置 HTTP 文件缓存(对 S3 远程文件有效)
SET enable_http_file_cache = true;
SET http_file_cache_dir = '/tmp/duckdb_cache';
-- 分析内存中每类对象的占用
SELECT tag, sum(size) / 1024 / 1024 AS mb
FROM duckdb_memory()
GROUP BY tag ORDER BY mb DESC;
9.5 DuckDB vs Spark 性能对比
| 场景 | DuckDB(单机) | Spark(集群) | 结论 |
|---|---|---|---|
| 1GB CSV 聚合查询 | 2-5 秒 | 30-60 秒(启动开销) | DuckDB 胜 |
| 10GB Parquet 分析 | 10-30 秒 | 20-60 秒 | DuckDB 胜或持平 |
| 100GB 复杂聚合 | 2-5 分钟 | 1-3 分钟(10节点) | Spark 小幅胜 |
| 1TB 数据 | 难以处理(内存/磁盘限制) | 可处理 | Spark 胜 |
| 启动时间 | <1 秒 | 30-120 秒 | DuckDB 大幅胜 |
| 运营成本 | 0(本地) | 高(集群费用) | DuckDB 大幅胜 |
选择建议 数据量 < 100GB:优先考虑 DuckDB,速度快、成本低、部署简单。100GB ~ 1TB:评估是否可以增加 DuckDB 的机器规格(内存、磁盘)来处理,MotherDuck(DuckDB 云版)也是选项。> 1TB 或需要多用户并发:考虑 Spark、Trino 或云数仓。
本章小结 性能调优三板斧:1. EXPLAIN ANALYZE 定位瓶颈;2. SET threads=N 充分利用多核;3. SET memory_limit 合理设置内存上限,启用 temp_directory 允许溢写。写入 Parquet 时按过滤列排序可最大化 Zone Maps 剪枝效果。DuckDB 在 100GB 以下的数据上通常比 Spark 更快,无启动开销,运营成本为零。