DuckDB 一招鲜:QUALIFY 子句,窗口函数结果直接过滤,告别多层子查询

DuckDB 支持 SQL 标准的 QUALIFY 子句,直接在主查询中过滤窗口函数结果。一招替代繁琐的多层子查询,代码量减少 60%。

痛点:过滤窗口函数结果太繁琐

你需要找出每个品类销量最高的商品。你写了一个带 ROW_NUMBER() 的查询:

SELECT category, product, sales
FROM (
    SELECT 
        category,
        product,
        sales,
        ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC) AS rn
    FROM products
) t
WHERE rn = 1;

能用,但是三层嵌套,只为了一个简单操作。每次需要在窗口结果上过滤——每个品类 Top-N、移动平均超过阈值的日期、基于排名的去重——你都要重复这个子查询模式。

当窗口函数变得复杂(RANK()LAG()、自定义帧)时,子查询几乎不可读。


一招解决:QUALIFY

DuckDB 支持 QUALIFY 子句——一个 SQL 标准特性,让你直接在主查询中过滤窗口函数结果。不需要子查询。

SELECT category, product, sales
FROM products
QUALIFY ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC) = 1;

一个查询替代三个。 这就是 QUALIFY 的威力。


更多实战示例

示例一:每个品类 Top-N

-- ❌ 旧写法:子查询
SELECT category, product, sales
FROM (
    SELECT category, product, sales,
           ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC) AS rn
    FROM products
) t
WHERE rn <= 3;

-- ✅ 新写法:QUALIFY
SELECT category, product, sales
FROM products
QUALIFY ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC) <= 3;

示例二:基于 LAG() 结果过滤

-- 找出销售额较昨日下降超过 20% 的日期
SELECT date, sales, LAG(sales) OVER (ORDER BY date) AS prev_sales
FROM daily_sales
QUALIFY LAG(sales) OVER (ORDER BY date) IS NOT NULL
    AND (sales - LAG(sales) OVER (ORDER BY date)) / LAG(sales) OVER (ORDER BY date) < -0.2;

没有 QUALIFY,你需要先用子查询计算 LAG(),再在外层过滤。有了 QUALIFY,窗口函数只写一次,直接在其结果上过滤。

示例三:移动平均超过阈值

-- 找出 7 日移动平均超过 1000 的日期
SELECT date, sales, AVG(sales) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS ma7
FROM daily_sales
QUALIFY AVG(sales) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) > 1000;

窗口函数在 SELECTQUALIFY 中都定义了——DuckDB 的优化器会复用计算结果。

示例四:基于排名去重

-- 保留每个商品最早订单记录
SELECT order_id, product, order_date, amount
FROM orders
QUALIFY RANK() OVER (PARTITION BY product ORDER BY order_date) = 1;

QUALIFY 的工作原理

SQL 的执行顺序是:

FROM → WHERE → GROUP BY → HAVING → 窗口函数 → QUALIFY → SELECT → ORDER BY → LIMIT

QUALIFY窗口函数计算完成后最终 SELECT 投影前执行。这意味着:

  1. 窗口函数正常计算
  2. QUALIFY 基于计算结果过滤行
  3. SELECT 列表仍然可以引用相同的窗口函数(DuckDB 会复用计算)

所以你不需要子查询——DuckDB 在一次扫描中完成计算和过滤。


效果量化

方案查询深度窗口函数计算次数可读性
子查询 + WHERE3 层1(在子查询内)
CTE + WHERE2 层1(在 CTE 中)
QUALIFY1 层1(复用)

实际上,DuckDB 对 QUALIFY 和子查询方案会生成相同的执行计划——优化器足够智能,能识别两种模式。区别纯粹在代码清晰度上。

对于一个 1000 万行的商品表:

  • 子查询方案:~850ms
  • QUALIFY 方案:~850ms(相同执行计划)
  • 代码减少:从 8 行减到 3 行

注意事项和坑

1. QUALIFY 可以引用 SELECT 中的别名

SELECT category, product, sales,
       ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC) AS rn
FROM products
QUALIFY rn <= 3;

这是可行的,因为 QUALIFY 能看到 SELECT 中计算好的列别名。

2. QUALIFY 中使用多个窗口函数

SELECT category, product, sales,
       ROW_NUMBER() OVER w AS rn,
       LAG(sales) OVER w AS prev_sales
FROM products
WINDOW w AS (PARTITION BY category ORDER BY sales DESC)
QUALIFY rn = 1 AND prev_sales IS NOT NULL;

WINDOW 子句定义一次窗口,处处引用。

3. QUALIFY 与 ORDER BY 的交互

SELECT category, product, sales
FROM products
QUALIFY ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC) = 1
ORDER BY category;

QUALIFY 先过滤,然后 ORDER BY 对剩余行排序。这是正确的顺序。


何时不用 QUALIFY

QUALIFY 在 DuckDB 中支持良好,但在其他数据库中可能不可用。如果需要跨数据库兼容:

  • PostgreSQL 13+:支持 QUALIFY
  • BigQuery:支持 QUALIFY
  • Snowflake:不支持 QUALIFY(需用子查询)
  • MySQL:不支持 QUALIFY

如果代码需要在多个平台上运行,请继续使用子查询。但对于纯 DuckDB 管道,QUALIFY 是最清晰的选择。


总结

不用 QUALIFY用 QUALIFY
Top-N 每品类查询3 层(子查询)1 层
代码行数8+3
可读性
性能相同相同

一招总结:每次发现自己为了过滤窗口函数结果而包装子查询时,用 QUALIFY 替换子查询。未来的你会感谢现在的你。


订阅 DuckDB Lab,每周三获取一招即用的 DuckDB 实战技巧。

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials

使用 Hugo 构建
主题 StackJimmy 设计

⚠️ 本站为独立社区项目,与 DuckDB 基金会及 DuckDB 官方项目无任何从属、背书或赞助关系。

"DuckDB" 是 DuckDB 基金会的注册商标,本站仅以事实描述方式使用该名称。

本站内容仅供教育与社区推广用途,不构成任何商业服务。