Featured image of post DuckDB QUALIFY 子句:一行代码搞定窗口函数过滤,告别子查询嵌套

DuckDB QUALIFY 子句:一行代码搞定窗口函数过滤,告别子查询嵌套

DuckDB 的 QUALIFY 子句是 SQL 标准扩展,让你直接在窗口函数计算后进行过滤,无需子查询嵌套。本文详解 QUALIFY 的用法、执行计划、实战场景,以及与 PostgreSQL、Snowflake 的对比。

痛点:窗口函数过滤为什么要套两层?

写过 SQL 的人都有这种经历——想要按部门取薪资 Top 3,你的代码长这样:

SELECT dept, name, salary
FROM (
  SELECT *,
    RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS rnk
  FROM employees
) sub
WHERE rnk <= 3;

两层嵌套,一不留神括号就写错了。这还不算最糟的——如果你需要在窗口函数外部加 HAVING、GROUP BY 或者复杂 JOIN,代码会迅速膨胀成一团乱麻。

DuckDB 的答案:QUALIFY 子句。

一句话解释:QUALIFY 是 SQL 标准的一个扩展子句(SQL:1999 引入,但大多数数据库没实现),它让你直接在窗口函数计算之后、结果返回之前进行过滤,完全省掉外层子查询。

SELECT dept, name, salary
FROM employees
QUALIFY RANK() OVER (PARTITION BY dept ORDER BY salary DESC) <= 3;

区别?QUALIFY 是干净的 3 行,子查询是 8 行。可读性差距至少 2 倍。

下面这张图展示了 QUALIFY 在 SQL 执行流程中的位置:

QUALIFY 在 SQL 执行流程中的位置


QUALIFY 到底是什么?

QUALIFY 本质上是一个语法糖,它被放在 WHEREGROUP BY 之后、ORDER BYLIMIT 之前。SQL 的标准执行顺序是:

  1. FROM + JOIN
  2. WHERE
  3. GROUP BY + 聚合函数
  4. HAVING
  5. 窗口函数计算 ← 这里
  6. QUALIFY ← DuckDB 在这里过滤
  7. SELECT(投影)
  8. DISTINCT
  9. UNION / INTERSECT / EXCEPT
  10. ORDER BY
  11. LIMIT / OFFSET

注意:QUALIFY在窗口函数计算之后、SELECT 投影之前执行的。这意味着你可以在 QUALIFY 中引用窗口函数的结果,但不能引用 SELECT 中的别名。

核心规则

  • QUALIFY 只能引用窗口函数表达式(RANK、ROW_NUMBER、SUM OVER 等)
  • 它不能引用普通列(但窗口函数里可以包含普通列)
  • 它和 WHERE互补的——WHERE 在聚合前过滤行,QUALIFY 在窗口计算后过滤
  • 性能上 QUALIFY 和子查询等价(优化器生成相同的执行计划),但代码可读性天差地别

实战场景一:按部门 Top N 排名

这是最经典的 QUALIFY 用例。假设你有一张销售表:

-- 创建销售数据
CREATE TABLE sales AS
SELECT * FROM (VALUES
  ('华东', '张三', '2026-01', 85000),
  ('华东', '李四', '2026-01', 92000),
  ('华东', '王五', '2026-01', 78000),
  ('华东', '赵六', '2026-01', 105000),
  ('华东', '钱七', '2026-02', 88000),
  ('华南', '孙八', '2026-01', 95000),
  ('华南', '周九', '2026-01', 72000),
  ('华南', '吴十', '2026-01', 110000),
  ('华南', '郑十一', '2026-02', 87000),
  ('华北', '冯十二', '2026-01', 65000),
  ('华北', '陈十三', '2026-01', 89000),
  ('华北', '褚十四', '2026-01', 92000)
) AS t(region, salesperson, month, amount);

-- 取每个区域销售额 Top 2
SELECT region, salesperson, month, amount
FROM sales
QUALIFY RANK() OVER (
  PARTITION BY region 
  ORDER BY amount DESC
) <= 2
ORDER BY region, amount DESC;

结果:

regionsalespersonmonthamount
华东赵六2026-01105000
华东李四2026-0192000
华南吴十2026-01110000
华南孙八2026-0195000
华北褚十四2026-0192000
华北陈十三2026-0189000

如果用子查询写法,代码量是两倍,而且明显更难读:你需要先在心里把子查询的结果「展开」,再看外层 WHERE。


实战场景二:取每个用户最后一条记录(去重)

数据湖中经常遇到这种情况——增量数据里同一个用户有多条记录,你只想要最新的一条:

-- 用户事件数据
CREATE TABLE user_events AS
SELECT * FROM (VALUES
  ('user_001', 'login', '2026-05-29 10:30:00'),
  ('user_001', 'purchase', '2026-05-29 10:35:00'),
  ('user_001', 'logout', '2026-05-29 11:00:00'),
  ('user_002', 'login', '2026-05-29 09:00:00'),
  ('user_002', 'view_item', '2026-05-29 09:15:00'),
  ('user_002', 'purchase', '2026-05-29 09:20:00'),
  ('user_003', 'login', '2026-05-28 22:00:00')
) AS t(user_id, event, event_time);

-- 取每个用户的最后一条事件
SELECT user_id, event, event_time
FROM user_events
QUALIFY ROW_NUMBER() OVER (
  PARTITION BY user_id 
  ORDER BY event_time DESC
) = 1;

结果:

user_ideventevent_time
user_001logout2026-05-29 11:00:00
user_002purchase2026-05-29 09:20:00
user_003login2026-05-28 22:00:00

这个模式等价于传统 SQL 中的 DISTINCT ON(PostgreSQL 特有语法),但 QUALIFY 是 SQL 标准,可移植性更好。

面试题级应用:取每个品类的销量冠军、每个月的用户增长冠军——任何时候你需要"每组取 Top N",QUALIFY 就是答案。


实战场景三:异常检测 + 窗口聚合过滤

QUALIFY 不只能和 RANK/ROW_NUMBER 搭配,它支持所有窗口函数。比如用 LAG 做环比检测:

-- 每日营收数据
CREATE TABLE daily_revenue AS
SELECT * FROM (VALUES
  ('2026-05-20', 12000),
  ('2026-05-21', 13500),
  ('2026-05-22', 11000),
  ('2026-05-23', 8500),
  ('2026-05-24', 9000),
  ('2026-05-25', 14000),
  ('2026-05-26', 16000),
  ('2026-05-27', 15500),
  ('2026-05-28', 17000),
  ('2026-05-29', 10000)
) AS t(dt, revenue);

-- 发现营收环比下降超过 20% 的日期
SELECT dt, revenue,
  ROUND((revenue - LAG(revenue) OVER (ORDER BY dt)) 
    / NULLIF(LAG(revenue) OVER (ORDER BY dt), 0) * 100, 1) AS pct_change
FROM daily_revenue
QUALIFY (revenue - LAG(revenue) OVER (ORDER BY dt)) 
    / NULLIF(LAG(revenue) OVER (ORDER BY dt), 0) < -0.15;

结果:

dtrevenuepct_change
2026-05-2211000-18.5
2026-05-238500-22.7
2026-05-2910000-41.2

这是电商数据分析中最常见的需求之一——自动发现异常波动。使用 QUALIFY,你不需要创建视图、CTE 或子查询,一条 SQL 搞定。


执行计划:QUALIFY 真的更快吗?

让我们用 EXPLAIN 看一下:

EXPLAIN
SELECT dept, name, salary
FROM employees
QUALIFY RANK() OVER (PARTITION BY dept ORDER BY salary DESC) <= 3;

DuckDB 的优化器会将 QUALIFY 转换为和子查询等价的物理计划。换句话说,性能没有区别。那么为什么还要用 QUALIFY?

因为人脑不是编译器。 你阅读代码的速度取决于代码的行数和嵌套深度。QUALIFY 把 3 层嵌套压平为 1 层,认知负荷至少降低 50%。

-- ❌ 子查询:阅读时需要跟踪两个 SELECT 层级
SELECT dept, name, salary FROM (
  SELECT *, RANK() OVER (...) AS rnk FROM employees
) WHERE rnk <= 3;

-- ✅ QUALIFY:线性阅读,无需跳转
SELECT dept, name, salary FROM employees
QUALIFY RANK() OVER (...) <= 3;

对比表:QUALIFY vs 其他工具/数据库的实现

特性DuckDBPostgreSQLSnowflakeBigQueryMySQLSQLite
QUALIFY 支持✅ 原生支持❌ 不直接支持(需用 DISTINCT ON 或子查询)✅ 原生支持❌ 不支持(需子查询/CTE)❌ 不支持(8.0+ 窗口函数但不支持 QUALIFY)❌ 不支持
DISTINCT ON❌ 不支持✅ 原生支持❌ 不支持❌ 不支持❌ 不支持❌ 不支持
子查询替代支持支持支持支持支持支持
CTE + 窗口过滤支持支持支持支持支持支持
执行顺序FROM→WHERE→GROUP BY→HAVING→窗口函数→QUALIFY→SELECTFROM→WHERE→GROUP BY→HAVING→窗口函数→SELECTFROM→WHERE→GROUP BY→HAVING→窗口函数→QUALIFY→SELECTFROM→WHERE→GROUP BY→HAVING→窗口函数→SELECTFROM→WHERE→GROUP BY→HAVING→窗口函数→SELECTFROM→WHERE→GROUP BY→HAVING→窗口函数→SELECT
代码简洁度⭐⭐⭐⭐⭐⭐⭐⭐(DISTINCT ON 部分简化)⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
语法标准SQL:1999 扩展PostgreSQL 扩展SQL:1999 扩展Google 方言MySQL 方言SQLite 方言

关键结论

  • DuckDB 和 Snowflake 是对 QUALIFY 支持最好的两个现代分析数据库
  • PostgreSQL 虽然功能强大,但在窗口过滤这个细分场景上,你必须多写 3-5 行代码
  • BigQuery 和 MySQL 完全不支持 QUALIFY,只能用子查询或 CTE + WHERE

QUALIFY 的局限和注意事项

1. 不能引用普通列

-- ❌ 错误:QUALIFY 中不能引用非窗口函数表达式
SELECT dept, name, salary
FROM employees
QUALIFY salary > 10000 AND RANK() OVER (...) <= 3;

-- ✅ 正确:用 WHERE 过滤列,用 QUALIFY 过滤窗口函数
SELECT dept, name, salary
FROM employees
WHERE salary > 10000
QUALIFY RANK() OVER (...) <= 3;

2. QUALIFY 的顺序

QUALIFY 必须放在 WHEREGROUP BY 之后、ORDER BYLIMIT 之前:

SELECT ...
FROM ...
WHERE ...              -- 先过滤原始行
GROUP BY ...           -- 再聚合
HAVING ...             -- 再过滤聚合结果
QUALIFY ...            -- 再过滤窗口函数结果
ORDER BY ...           -- 最后排序
LIMIT ...;

3. 不能在 QUALIFY 中使用 SELECT 别名

-- ❌ 错误:SELECT 别名在 QUALIFY 之后才可用
SELECT 
  RANK() OVER (ORDER BY salary DESC) AS salary_rank
FROM employees
QUALIFY salary_rank <= 10;

-- ✅ 正确:直接写窗口函数表达式
SELECT 
  RANK() OVER (ORDER BY salary DESC) AS salary_rank
FROM employees
QUALIFY RANK() OVER (ORDER BY salary DESC) <= 10;

4. 性能和子查询等价

如前所述,QUALIFY 是语法糖,不是性能优化。但代码可读性的提升直接转化为维护成本的降低调试时间的减少


进阶技巧:QUALIFY + CTE 组合

QUALIFY 和 CTE(WITH 子句)配合使用,可以构建非常清晰的 ETL 管道:

-- 1. 先清洗数据
WITH cleaned_events AS (
  SELECT user_id, event, event_time
  FROM raw_events
  WHERE event IS NOT NULL
),
-- 2. 取每个用户的最新事件
latest_events AS (
  SELECT user_id, event, event_time
  FROM cleaned_events
  QUALIFY ROW_NUMBER() OVER (
    PARTITION BY user_id 
    ORDER BY event_time DESC
  ) = 1
),
-- 3. 做聚合分析
user_stats AS (
  SELECT 
    DATE_TRUNC('day', event_time) AS active_date,
    COUNT(*) AS active_users
  FROM latest_events
  GROUP BY active_date
)
-- 4. 最终输出
SELECT * FROM user_stats
ORDER BY active_date DESC;

这条管道把四个步骤清晰分离,每一步只做一件事。如果不使用 QUALIFY,你需要在第 2 步多嵌套一层子查询,管道就会多出一个不必要的层次。


变现建议:如何把你的 QUALIFY 技能变成收入

  1. SQL 面试题集 — QUALIFY 是很多数据分析师面试中的盲区。写一个「DuckDB QUALIFY 面试 50 题」的小课程或电子书,定价 ¥29-49,在掘金、知乎、小红书上卖。针对"每组取 Top N"这个高频场景,用 QUALIFY 一行解决的方案远比传统子查询优雅,面试官看了都会加分。

  2. 企业 SQL 规范咨询 — 很多公司的 SQL 规范还在用 2000 年的写法。主动帮团队做一次 SQL 代码审查,找出可以用 QUALIFY 简化的窗口查询,打包成「SQL 现代化改造报告」——每家公司收 ¥2000-5000,解决 30-50 个查询就够了。

  3. 数据分析自动化服务 — 结合之前的 Shopify 数据监控方案,在异常检测环节用 QUALIFY + LAG 做环比分析。打包成一个「DuckDB 驱动的电商自动化监控 SaaS」,按月订阅 ¥500-2000/客户。技术上你的优势是:QUALIFY 让异常检测 SQL 精简 50%,代码少了维护成本就低。

  4. YouTube 教程变现 — 把 QUALIFY 配合其他 DuckDB 特性的系列教程做成视频,在 youtube.com/@duckdblab 发布,通过 YouTube 广告 + 频道会员 + 赞助实现被动收入。单条 QUALIFY 教程视频如果做好 SEO,每月可以稳定带来 5000+ 精准数据工程师流量。


总结

要点说明
QUALIFY 是什么SQL 标准扩展子句,在窗口函数计算后直接过滤
执行顺序FROM → WHERE → GROUP BY → HAVING → 窗口函数 → QUALIFY → SELECT → ORDER BY → LIMIT
适用窗口函数RANK、ROW_NUMBER、DENSE_RANK、NTILE、LAG/LEAD、SUM/AVG OVER 等全部窗口函数
核心优势省去子查询嵌套,代码可读性提升 2x-5x
性能和子查询等价(语法糖)
支持的工具DuckDB ✅ Snowflake ✅ PostgreSQL ❌(DISTINCT ON) BigQuery ❌ MySQL ❌

一句话记住 QUALIFY: 当你写 WHERE 但条件来自窗口函数时,就该用 QUALIFY 了。

下次在 DuckDB 里写 RANK() OVER 的时候,试着加一行 QUALIFY 替代子查询——你的代码会更干净,你的同事会更感谢你。

📺 更多 DuckDB 实战教程,订阅 YouTube 频道 → youtube.com/@duckdblab

📺 Watch video tutorials → DuckDB Lab YouTube

Subscribe for more DuckDB & AI automation tutorials

使用 Hugo 构建
主题 StackJimmy 设计