窗口函数的使用限制
窗口函数只能出现在 SELECT 列表与 ORDER BY 里,不能写进 WHERE、GROUP BY 或 HAVING——它们的执行时机都在窗口计算之前。要按窗口结果过滤,必须把查询包成派生表或 CTE,再在外层写条件。此外还有几条硬性限制:窗口函数不能互相嵌套;MySQL 8 的窗口函数不支持 DISTINCT 聚合;两个库都不允许在 UPDATE / DELETE 里直接使用窗口函数,需要先用 CTE 算出目标值再更新。
基本信息
| 允许的位置 | SELECT 列表、ORDER BY |
|---|---|
| 不允许的位置 | WHERE、GROUP BY、HAVING、UPDATE / DELETE 的直接使用 |
| 嵌套限制 | 窗口函数不能嵌套在另一个窗口函数内部 |
| MySQL 限制 | 窗口函数里不支持 DISTINCT 聚合(如 COUNT(DISTINCT x) OVER (…)) |
| 过滤窗口结果 | 包一层派生表或 CTE,再在外层写 WHERE |
典型使用场景
- 按行号筛选每组前 N 行,需要包一层派生表
- 按排名或占比结果过滤(取占比超过 10% 的项)
- 排查「窗口函数写在 WHERE 报错」这类语法问题
- 用 CTE 先算出窗口结果,再据此执行更新
- 把窗口结果与其他聚合函数分层组合使用
过滤窗口结果的标准写法
过滤窗口结果的标准写法
# 正确:窗口结果在派生表里算好,外层再过滤
SELECT id, user_id, amount
FROM (
SELECT id, user_id, amount,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC, id DESC) AS rn
FROM orders
) t
WHERE t.rn = 1;
# 错误写法(语法报错):窗口函数不能直接写在 WHERE
-- SELECT * FROM orders WHERE ROW_NUMBER() OVER (PARTITION BY user_id) = 1;PostgreSQL 里用 CTE 配合更新
PostgreSQL 里用 CTE 配合更新
-- 两个库都不允许在 UPDATE 里直接使用窗口函数:先用 CTE 算出结果
WITH ranked AS (
SELECT id,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC, id DESC) AS rn
FROM orders
)
UPDATE orders o
SET is_latest = (ranked.rn = 1)
FROM ranked
WHERE ranked.id = o.id;
-- MySQL 里改写成 UPDATE … JOIN:
-- UPDATE orders o JOIN (SELECT id, ROW_NUMBER() OVER (...) AS rn FROM orders) t
-- ON t.id = o.id SET o.is_latest = (t.rn = 1);限制清单
| 位置或用法 | 是否允许 | 替代方案 |
|---|---|---|
| SELECT 列表 | 允许 | — |
| ORDER BY | 允许 | — |
| WHERE / HAVING | 不允许 | 包派生表或 CTE 后在外层过滤 |
| GROUP BY | 不允许 | 改用语义等价的聚合写法,或分层实现 |
| UPDATE / DELETE | 两库都不允许直接使用 | 用 CTE 先算出目标值再更新 |
| 窗口函数嵌套 | 不允许 | 拆成多层派生表或 CTE |
窗口函数与聚合函数的执行时机不同:GROUP BY 会先把行折叠,窗口函数则在保留行的前提下计算;同一层里既要分组又想让窗口基于明细行,只能分层实现(先聚合出派生表,再在外层开窗)。
MySQL 8 的窗口函数不支持 DISTINCT 聚合,写 COUNT(DISTINCT x) OVER (…) 会直接报错;需要组内去重计数时,先用派生表去重,再用窗口计数。