Featured image of post DuckDB 快速数据分析指南

DuckDB 快速数据分析指南

DuckDB 内置的性能诊断利器 EXPLAIN ANALYZE 完全指南。通过真实案例学习如何定位 JOIN 瓶颈、识别重复扫描、优化嵌套查询,让你的查询速度提升 10 倍以上。

DuckDB EXPLAIN ANALYZE 完全指南:从慢查询到毫秒级响应的实战

你是不是也经历过这样的场景:

  • 一个查询在 10 万行数据上秒出,到了 1000 万行就卡住
  • 加了 WHERE 条件反而变慢了
  • 不确定是 JOIN 慢还是聚合慢还是扫描慢

很多数据分析师面对慢查询时,第一反应是"加索引"或者"换数据库"。但在 DuckDB 的世界里,有一个内置的神器可以帮你精准定位问题根源——EXPLAIN ANALYZE。

今天这篇文章,我会通过 5 个真实场景,手把手教你用 EXPLAIN ANALYZE 找出慢查询的真正原因,并给出可执行的优化方案。

DuckDB EXPLAIN ANALYZE 架构图

一、EXPLAIN 还是 EXPLAIN ANALYZE?别搞混了

在深入之前,先搞清楚这两个命令的本质区别:

-- 只看计划(不执行查询)
EXPLAIN SELECT * FROM sales WHERE amount > 1000;

-- 执行查询 + 输出真实耗时和行数(推荐!)
EXPLAIN ANALYZE SELECT * FROM sales WHERE amount > 1000;
特性EXPLAINEXPLAIN ANALYZE
是否执行查询❌ 否✅ 是
输出预估行数✅✅
输出实际耗时❌✅
输出实际扫描行数❌✅
适用场景语法调试性能诊断

核心结论: 找性能问题,永远用 EXPLAIN ANALYZE。EXPLAIN 只能告诉你"打算怎么做",而 EXPLAIN ANALYZE 会告诉你"实际花了多久、读了多少行"。

二、实战场景一:找出 JOIN 中的性能黑洞

假设你有两张表:sales(100 万行销售记录)和 products(1 万行商品信息)。

SELECT p.category, SUM(s.amount) AS total
FROM sales s
JOIN products p ON s.product_id = p.id
WHERE s.sale_date >= '2026-01-01'
GROUP BY p.category;

不加分析前,你可能觉得这个 JOIN 没什么问题。但加上 EXPLAIN ANALYZE 看看真相:

EXPLAIN ANALYZE
SELECT p.category, SUM(s.amount) AS total
FROM sales s
JOIN products p ON s.product_id = p.id
WHERE s.sale_date >= '2026-01-01'
GROUP BY p.category;

典型的输出(简化版):

Project [category, sum(amount)]
  Output: category, sum(amount)
  Estimated Rows: 10
    Hash Join (inner)
      Output: p.category, s.amount, s.product_id
      Hash Inner: s.product_id
      Probe Side:
        Output: p.id, p.category
        Estimated Rows: 10000
          TableScan products
            Total Runtime: 1.2ms
            Total rows: 10,000
      Build Side:
        Output: s.product_id, s.amount, s.sale_date
        Estimated Rows: 500000
          SelectionScan sales
            Filter: sale_date >= '2026-01-01'
            Total Runtime: 345.8ms
            Total rows: 1,200,000
            Rows filtered: 700,000
            Rows kept: 500,000
      Total Runtime: 412.5ms

解读关键信息:

  1. TableScan products — 1.2ms,1 万行,非常快 ✅
  2. SelectionScan sales — 345.8ms,扫描了 120 万行,过滤掉 70 万行 ❗
  3. 整个查询花费 412.5ms,其中 84% 的时间花在 sales 表的扫描和过滤上

问题发现: 大部分时间花在了 sales 表的扫描和过滤上。如果 sale_date 做了 ORDER BY 排序,这一步会快很多。

优化方案:

-- 方法 1:使用 ORDER BY 对数据进行物理排序
CREATE TABLE sales_clustered AS
SELECT * FROM sales ORDER BY sale_date;

-- 方法 2:使用投影索引(DuckDB 1.1+)
CREATE PROJECTION sales_date_idx ON sales (sale_date);

-- 然后再跑 EXPLAIN ANALYZE,你会发现 SelectionScan 变成了 IndexScan,耗时从 345ms 降到几毫秒

三、实战场景二:嵌套查询的性能陷阱

有时候写得"看起来很合理"的 SQL,性能却很差。EXPLAIN ANALYZE 能帮你看到中间结果集的大小:

-- 写法 A:先过滤再 JOIN(推荐)
EXPLAIN ANALYZE
SELECT p.category, SUM(s.amount) AS total
FROM sales s
JOIN products p ON s.product_id = p.id
WHERE s.sale_date >= '2026-01-01'
GROUP BY p.category;

-- 写法 B:先 JOIN 再过滤(可能很慢)
EXPLAIN ANALYZE
SELECT category, total
FROM (
    SELECT p.category, SUM(s.amount) AS total
    FROM sales s
    JOIN products p ON s.product_id = p.id
    GROUP BY p.category
) sub
WHERE sub.category = '电子产品';

对比两个输出的 Estimated Rows 和实际行数:

  • 写法 A: WHERE 先过滤 sales,JOIN 的数据量大幅减少,中间结果集小
  • 写法 B: 先 JOIN 全量数据再过滤 category,中间结果集可能非常大,消耗更多内存

关键洞察: EXPLAIN ANALYZE 的 Estimated Rows 和实际行数差距越大,说明优化器的统计信息越不准,越需要关注。

四、实战场景三:识别不必要的计算

EXPLAIN ANALYZE
SELECT 
    product_id,
    amount * 1.13 AS amount_with_tax,
    sale_date,
    UPPER(customer_name) AS customer
FROM sales;

输出中你会看到类似这样的信息:

Projection [product_id, amount * 1.13, sale_date, UPPER(customer_name)]
  Total Runtime: 125.3ms
  Tables scanned: 1
  Total rows: 5,000,000

发现问题: UPPER(customer_name) 对 500 万行都做了一次大小写转换,但你可能根本不需要这个字段!

优化方案: 去掉不需要的计算,或者用 LIMIT 限制返回行数:

-- 只取需要的字段
EXPLAIN ANALYZE
SELECT 
    product_id,
    amount * 1.13 AS amount_with_tax,
    sale_date
FROM sales
LIMIT 1000;

加上 LIMIT 后,Runtime 可能从 125ms 降到 0.5ms。

五、实战场景四:发现重复扫描

这是最容易被忽视的性能杀手。看下面这个查询:

EXPLAIN ANALYZE
SELECT 
    (SELECT AVG(amount) FROM sales WHERE sale_date >= '2026-06-01') AS june_avg,
    (SELECT AVG(amount) FROM sales WHERE sale_date >= '2026-05-01') AS may_avg,
    (SELECT AVG(amount) FROM sales WHERE sale_date >= '2026-04-01') AS april_avg;

在 EXPLAIN ANALYZE 输出中,你会看到 sales 表被扫描了 3 次!每次都要从头到尾读一遍 500 万行。

优化方案: 用一次扫描 + conditional aggregation:

EXPLAIN ANALYZE
SELECT 
    AVG(CASE WHEN sale_date >= '2026-06-01' THEN amount END) AS june_avg,
    AVG(CASE WHEN sale_date >= '2026-05-01' THEN amount END) AS may_avg,
    AVG(CASE WHEN sale_date >= '2026-04-01' THEN amount END) AS april_avg
FROM sales;

现在 sales 表只扫描 1 次,性能提升约 3 倍。

六、实战场景五:Python 中怎么用?

在 Python 中使用 EXPLAIN ANALYZE 有两种方式:

import duckdb
import time

con = duckdb.ATTACH (":memory:")

# 创建测试数据
con.execute("""
---

## 本文信息

| 项目 | 内容 |
|------|------|
| DuckDB 版本 | v1.5.x(部分功能基于 v2.0 Preview) |
| 最后验证 | 2026-09-12 |
| 测试环境 | Linux / x86_64 / 16GB RAM |
| 官方文档 | [DuckDB Documentation](https://duckdb.org/docs/) |
| GitHub | [pengzz9527/duckdb-blog](https://github.com/pengzz9527/duckdb-blog) |

如发现错误,欢迎通过 [GitHub Issue](https://github.com/pengzz9527/duckdb-blog/issues) 或邮件 contact@duckdblab.org 反馈。

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials