痛点:窗口函数过滤为什么要套两层?
写过 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 到底是什么?
QUALIFY 本质上是一个语法糖,它被放在 WHERE 和 GROUP BY 之后、ORDER BY 和 LIMIT 之前。SQL 的标准执行顺序是:
FROM+JOINWHEREGROUP BY+ 聚合函数HAVING- 窗口函数计算 ← 这里
QUALIFY← DuckDB 在这里过滤SELECT(投影)DISTINCTUNION/INTERSECT/EXCEPTORDER BYLIMIT/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;
结果:
| region | salesperson | month | amount |
|---|---|---|---|
| 华东 | 赵六 | 2026-01 | 105000 |
| 华东 | 李四 | 2026-01 | 92000 |
| 华南 | 吴十 | 2026-01 | 110000 |
| 华南 | 孙八 | 2026-01 | 95000 |
| 华北 | 褚十四 | 2026-01 | 92000 |
| 华北 | 陈十三 | 2026-01 | 89000 |
如果用子查询写法,代码量是两倍,而且明显更难读:你需要先在心里把子查询的结果「展开」,再看外层 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_id | event | event_time |
|---|---|---|
| user_001 | logout | 2026-05-29 11:00:00 |
| user_002 | purchase | 2026-05-29 09:20:00 |
| user_003 | login | 2026-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;
结果:
| dt | revenue | pct_change |
|---|---|---|
| 2026-05-22 | 11000 | -18.5 |
| 2026-05-23 | 8500 | -22.7 |
| 2026-05-29 | 10000 | -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 其他工具/数据库的实现
| 特性 | DuckDB | PostgreSQL | Snowflake | BigQuery | MySQL | SQLite |
|---|---|---|---|---|---|---|
| QUALIFY 支持 | ✅ 原生支持 | ❌ 不直接支持(需用 DISTINCT ON 或子查询) | ✅ 原生支持 | ❌ 不支持(需子查询/CTE) | ❌ 不支持(8.0+ 窗口函数但不支持 QUALIFY) | ❌ 不支持 |
| DISTINCT ON | ❌ 不支持 | ✅ 原生支持 | ❌ 不支持 | ❌ 不支持 | ❌ 不支持 | ❌ 不支持 |
| 子查询替代 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
| CTE + 窗口过滤 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
| 执行顺序 | FROM→WHERE→GROUP BY→HAVING→窗口函数→QUALIFY→SELECT | FROM→WHERE→GROUP BY→HAVING→窗口函数→SELECT | FROM→WHERE→GROUP BY→HAVING→窗口函数→QUALIFY→SELECT | FROM→WHERE→GROUP BY→HAVING→窗口函数→SELECT | FROM→WHERE→GROUP BY→HAVING→窗口函数→SELECT | FROM→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 必须放在 WHERE 和 GROUP BY 之后、ORDER BY 和 LIMIT 之前:
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 技能变成收入
SQL 面试题集 — QUALIFY 是很多数据分析师面试中的盲区。写一个「DuckDB QUALIFY 面试 50 题」的小课程或电子书,定价 ¥29-49,在掘金、知乎、小红书上卖。针对"每组取 Top N"这个高频场景,用 QUALIFY 一行解决的方案远比传统子查询优雅,面试官看了都会加分。
企业 SQL 规范咨询 — 很多公司的 SQL 规范还在用 2000 年的写法。主动帮团队做一次 SQL 代码审查,找出可以用 QUALIFY 简化的窗口查询,打包成「SQL 现代化改造报告」——每家公司收 ¥2000-5000,解决 30-50 个查询就够了。
数据分析自动化服务 — 结合之前的 Shopify 数据监控方案,在异常检测环节用 QUALIFY + LAG 做环比分析。打包成一个「DuckDB 驱动的电商自动化监控 SaaS」,按月订阅 ¥500-2000/客户。技术上你的优势是:QUALIFY 让异常检测 SQL 精简 50%,代码少了维护成本就低。
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