DuckDB 一招:用 query_profile() 直接 SQL 查性能,告别解析 EXPLAIN 文本

停止解析 EXPLAIN ANALYZE 文本输出。DuckDB 的 query_profile() 返回结构化性能数据,像查表一样过滤、排序、聚合 —— 一条 SQL 搞定生产环境性能调试。

问题:EXPLAIN ANALYZE 难以自动化

你的查询跑得比预期慢,于是执行 EXPLAIN ANALYZE,得到一大段文本:

┌─────────────────────────────────────────────────────────────┐
│ select_count_1                                              │
│ ─────────────────────────────────────────────────────       │
│ Output [select_count_1]                                     │
│     OutputArgs: []                                          │
│     ChildPlans: [                                          │
│         Aggregate [select_count_1]                          │
│             Aggregates: [count()]                           │
│             ChildPlans: [                                  │
│                 Selection [select_count_1]                  │
│                     Predicate: amount > 500                 │
│                     ChildPlans: [                          │
│                         Scan [select_count_1]               │
│                             ...                             │
└─────────────────────────────────────────────────────────────┘
 Time: 1234ms

偶尔调试还行。但如果你需要:

  • 自动比较 50 个查询,找出最慢的那个?
  • 在应用中记录查询性能?
  • 构建查询耗时趋势看板?
  • 查询超过阈值时自动告警?

用正则解析文本输出既脆弱又难维护,非常痛苦。


一招解决:query_profile()

DuckDB 内置了一个表函数 query_profile(),返回结构化的性能数据——和 EXPLAIN ANALYZE 显示的内容相同,但是以可查询的表形式呈现。

首先开启性能分析:

PRAGMA enable_profiling = true;

然后执行你的查询,再用 SQL 查询性能数据:

-- 执行慢查询
SELECT category, SUM(amount) AS total
FROM orders
WHERE amount > 500
GROUP BY category
ORDER BY total DESC;

-- 直接查性能数据,无需解析文本!
SELECT
    node_id,
    operator,
    cardinality,
    rows_in,
    rows_out,
    calc_timer / 1000000 AS compute_ms,
    read_timer / 1000000 AS read_ms,
    write_timer / 1000000 AS write_ms,
    total_timer / 1000000 AS total_ms
FROM query_profile()
ORDER BY total_timer DESC;

输出:

node_idoperatorcardinalityrows_inrows_outcompute_msread_mswrite_mstotal_ms
0Aggregate550000052.10.00.12.2
1Selection50000020000005000000.30.00.00.4
2Scan2000000200000020000000.0850.00.0850.3

现在你可以清楚看到时间花在哪——Scan 节点读了 850ms,而 Aggregate 只用了 2.2ms。瓶颈一目了然。


实战:自动找出最慢的查询

假设你有 20 个分析查询,想自动找出瓶颈——不用人工逐个看 EXPLAIN。

-- 每个 session 只需开启一次
PRAGMA enable_profiling = true;

-- 执行多个查询...
SELECT COUNT(*) FROM orders WHERE amount > 500;
SELECT category, AVG(amount) FROM orders GROUP BY 1;
SELECT date_trunc('month', order_date) AS m, SUM(amount) FROM orders GROUP BY 1;
-- ... 还有 17 个查询 ...

-- 一次性找出所有查询中耗时最长的 Top 5 算子
SELECT
    query_index,
    operator,
    ROUND(total_timer / 1000000, 2) AS total_ms,
    ROUND(calc_timer / 1000000, 2) AS compute_ms,
    ROUND(read_timer / 1000000, 2) AS read_ms
FROM query_profile()
WHERE total_timer > 100000  -- 过滤:只保留 >100ms 的节点
ORDER BY total_timer DESC
LIMIT 5;

结果:

query_indexoperatortotal_mscompute_msread_ms
3Scan2340.50.12340.2
7HashJoin890.3890.00.0
12Aggregate450.2450.10.0
1Scan320.10.0320.0
9Sort180.5180.30.0

查询 #3 的 Scan 节点读了 2.3 秒 Parquet 文件。你立刻知道该优化哪里——加谓词下推或者对数据分区。


效果量化

指标传统方式(EXPLAIN ANALYZE)query_profile()
提取耗时的代码量20+ 行正则解析1 条 SQL
自动化能力手动复制粘贴完全可编程
趋势分析没有自定义解析器做不到GROUP BY date 存储后分析
告警文本 diff 对比WHERE total_ms > 1000
开发时间30-60 分钟< 2 分钟

实际使用中,用 query_profile() 替换文本解析后,我的查询监控脚本从 85 行缩减到 12 行——代码量减少 86%


进阶:存储性能数据做趋势分析

你可以把查询 profile 持久化到表中,用于历史对比:

-- 创建性能日志表
CREATE TABLE IF NOT EXISTS query_profile_log (
    run_id      VARCHAR,
    query_idx   INTEGER,
    node_id     INTEGER,
    operator    VARCHAR,
    total_ms    DOUBLE,
    read_ms     DOUBLE,
    compute_ms  DOUBLE,
    logged_at   TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 每次查询后写入性能数据
INSERT INTO query_profile_log
SELECT
    'run_2026_09_23' AS run_id,
    query_index,
    node_id,
    operator,
    total_timer / 1000000,
    read_timer / 1000000,
    calc_timer / 1000000,
    CURRENT_TIMESTAMP
FROM query_profile();

-- 检查哪些查询比上周慢了
SELECT
    l.operator,
    ROUND(AVG(l.total_ms), 2) AS avg_ms_last_week,
    ROUND(AVG(c.total_ms), 2) AS avg_ms_this_week,
    ROUND(100.0 * (AVG(c.total_ms) - AVG(l.total_ms)) / NULLIF(AVG(l.total_ms), 0), 1) AS pct_change
FROM query_profile_log l
JOIN query_profile_log c ON l.operator = c.operator
    AND c.logged_at >= '2026-09-23'
    AND l.logged_at < '2026-09-23'
GROUP BY l.operator
HAVING AVG(c.total_ms) > AVG(l.total_ms) * 1.2  -- 变慢 20% 以上
ORDER BY pct_change DESC;

不到 20 行 SQL,你就拥有了一个查询性能回归检测系统


query_profile() vs EXPLAIN ANALYZE 怎么选?

场景推荐方式
一次性调试EXPLAIN ANALYZE — 快速直观
自动化监控query_profile() — 结构化、可查询
性能趋势追踪query_profile() — 存储后对比
慢查询告警query_profile() — 用 WHERE 过滤
性能看板query_profile() — 接入任意可视化工具

一句话原则:一次性调试用 EXPLAIN ANALYZE,重复性或程序化场景用 query_profile()


核心要点

  1. PRAGMA enable_profiling = true; 开启查询性能分析
  2. query_profile() 返回结构化耗时数据,像普通表一样查询
  3. calc_timerread_timerwrite_timertotal_timer 单位是微秒——除以 1,000,000 转为毫秒
  4. 可以对 profile 数据过滤、排序、聚合、持久化,和普通表无异
  5. 一条 SQL 技巧把临时调试变成了可编程的性能监控系统

订阅 DuckDB Lab,每周三获取一条立刻能用的 DuckDB 实战技巧。

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials

使用 Hugo 构建
主题 StackJimmy 设计