Chapter 09

性能调优

EXPLAIN ANALYZE 解读执行计划,并行设置,内存管理,Zone Maps 剪枝,与 Spark 性能基准对比

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;

执行计划关键指标解读

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 大幅胜
DuckDB
1GB 5s
pandas
1GB 45s
Spark(1节点)
1GB 35s
polars
1GB 8s
🎯

选择建议 数据量 < 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 更快,无启动开销,运营成本为零。