一、什么是"窗口函数"?(大白话版)
"窗口"不是窗户,而是"视野范围"!
- 普通聚合函数 (SUM、COUNT):把多行压缩成一行,你看不到原始数据了
- 窗口函数 (ROW_NUMBER):在每行旁边附加计算结果,原始数据还在
比喻:
普通聚合:把全班成绩汇总成平均分 → 你看不出每个人的分数
窗口函数:在每个人旁边标注"班级第几名" → 既看到分数,又看到排名
为什么叫"窗口"?
因为你可以定义一个"滑动窗口"(比如"当前行 + 前2行"),在这个范围内计算。
二、ROW_NUMBER() 一句话解释
给每组数据编个号:1、2、3、4...,从 1 开始连续递增。
三、9 个最实用场景
场景 1:去重(保留最新/最早的一条)
需求: 用户可能有多条订单,只保留每个用户的最新订单
sql
SELECT * FROM (
SELECT
user_id,
order_no,
created_at,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders
) t
WHERE rn = 1; -- 只取每个用户的第一条(最新的)
原理:
PARTITION BY user_id:按用户分组ORDER BY created_at DESC:每组内按时间倒序ROW_NUMBER():编号 1、2、3...WHERE rn = 1:只要第一条
场景 2:分页查询(高效分页)
需求: 查询第 11-20 条记录
sql
SELECT * FROM (
SELECT
id,
name,
created_at,
ROW_NUMBER() OVER (ORDER BY created_at DESC) AS rn
FROM users
) t
WHERE rn BETWEEN 11 AND 20;
优势: 比 LIMIT/OFFSET 在大数据量时更快(尤其是深分页)
场景 3:找出每组的前 N 名
需求: 每个部门工资最高的 3 个人
sql
SELECT * FROM (
SELECT
dept_name,
emp_name,
salary,
ROW_NUMBER() OVER (PARTITION BY dept_name ORDER BY salary DESC) AS rn
FROM employees
) t
WHERE rn <= 3; -- 每个部门前 3 名
场景 4:删除重复数据
需求: 清理重复的用户记录,只保留 ID 最小的
sql
DELETE FROM users
WHERE id IN (
SELECT id FROM (
SELECT
id,
email,
ROW_NUMBER() OVER (PARTITION BY email ORDER BY id ASC) AS rn
FROM users
) t
WHERE rn > 1 -- 保留 rn=1 的,删除其他的
);
场景 5:对比当前行和上一行
需求: 计算每日销售额环比增长
sql
SELECT
sale_date,
daily_amount,
LAG(daily_amount) OVER (ORDER BY sale_date) AS prev_day_amount,
ROUND(
(daily_amount - LAG(daily_amount) OVER (ORDER BY sale_date))
/ LAG(daily_amount) OVER (ORDER BY sale_date) * 100,
2
) AS growth_rate
FROM daily_sales;
注意: 这里用 LAG() 更适合,但 ROW_NUMBER() 也可以实现类似效果。
场景 6:标记首次/最后一次行为
需求: 标记用户的首次登录和最后登录
sql
SELECT
user_id,
login_time,
CASE
WHEN ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_time ASC) = 1
THEN '首次登录'
WHEN ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_time DESC) = 1
THEN '最后登录'
ELSE '普通登录'
END AS login_type
FROM user_logins;
场景 7:分组后取中间值
需求: 去掉最高分和最低分,取中间的平均分
sql
SELECT
student_id,
AVG(score) AS avg_score
FROM (
SELECT
student_id,
score,
ROW_NUMBER() OVER (PARTITION BY student_id ORDER BY score ASC) AS rn_asc,
ROW_NUMBER() OVER (PARTITION BY student_id ORDER BY score DESC) AS rn_desc,
COUNT(*) OVER (PARTITION BY student_id) AS total_count
FROM exam_scores
) t
WHERE rn_asc > 1 AND rn_desc > 1; -- 去掉最低和最高
场景 8:检测数据连续性
需求: 找出用户连续登录的天数
sql
SELECT
user_id,
login_date,
login_date - (ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) || ' days')::INTERVAL AS grp
FROM user_logins
GROUP BY user_id, login_date;
-- 相同的 grp 表示连续登录
SELECT
user_id,
MIN(login_date) AS start_date,
MAX(login_date) AS end_date,
COUNT(*) AS consecutive_days
FROM (
-- 上面的子查询
) t
GROUP BY user_id, grp
HAVING COUNT(*) >= 3; -- 连续登录 3 天以上
场景 9:排行榜(带并列处理)
需求: 生成销售排行榜,相同业绩排名相同
sql
-- ROW_NUMBER():即使分数相同,排名也不同(1、2、3、4)
SELECT
emp_name,
sales_amount,
ROW_NUMBER() OVER (ORDER BY sales_amount DESC) AS rank
FROM sales_performance;
-- 如果需要并列排名,用 RANK() 或 DENSE_RANK()
-- RANK(): 1, 2, 2, 4 (跳过 3)
-- DENSE_RANK(): 1, 2, 2, 3 (不跳过)
四、核心语法拆解
sql
ROW_NUMBER() OVER (
PARTITION BY column1, column2 -- 可选:分组依据
ORDER BY column3 DESC -- 必填:排序规则
)
三个关键点:
OVER():声明这是窗口函数PARTITION BY:可选,类似GROUP BY,但不压缩行数ORDER BY:必填,决定编号顺序
五、ROW_NUMBER vs RANK vs DENSE_RANK
| 函数 | 相同值处理 | 示例 | 适用场景 |
|---|---|---|---|
ROW_NUMBER() |
强制不同 | 1, 2, 3, 4 | 去重、分页 |
RANK() |
并列,跳号 | 1, 2, 2, 4 | 排行榜(允许空缺) |
DENSE_RANK() |
并列,不跳号 | 1, 2, 2, 3 | 排行榜(紧凑排名) |
示例对比:
sql
SELECT
name,
score,
ROW_NUMBER() OVER (ORDER BY score DESC) AS row_num,
RANK() OVER (ORDER BY score DESC) AS rank,
DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rank
FROM students;
-- 结果:
-- name | score | row_num | rank | dense_rank
-- ------+-------+---------+------+-----------
-- 张三 | 100 | 1 | 1 | 1
-- 李四 | 100 | 2 | 1 | 1 ← 并列第一
-- 王五 | 95 | 3 | 3 | 2 ← RANK 跳过 2,DENSE 不跳
-- 赵六 | 90 | 4 | 4 | 3
六、性能优化建议
1. 避免全表扫描
sql
-- ❌ 慢:全表编号后再过滤
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (ORDER BY created_at) AS rn
FROM orders
) t WHERE rn <= 10;
-- ✅ 快:先过滤再编号
SELECT *, ROW_NUMBER() OVER (ORDER BY created_at) AS rn
FROM orders
WHERE created_at >= '2026-01-01'
ORDER BY created_at
LIMIT 10;
2. 合理使用索引
sql
-- 为 PARTITION BY 和 ORDER BY 字段创建索引
CREATE INDEX idx_orders_user_created ON orders (user_id, created_at DESC);
-- 这样查询会很快
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders
) t WHERE rn = 1;
3. 避免不必要的 PARTITION BY
sql
-- ❌ 如果不需要分组,不要加 PARTITION BY
ROW_NUMBER() OVER (PARTITION BY 1 ORDER BY id) -- 多余!
-- ✅ 直接全局编号
ROW_NUMBER() OVER (ORDER BY id)
七、常见错误
错误 1:忘记 ORDER BY
sql
-- ❌ 错误:窗口函数必须包含 ORDER BY
ROW_NUMBER() OVER (PARTITION BY user_id)
-- ✅ 正确
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at)
错误 2:在 WHERE 中直接使用
sql
-- ❌ 错误:窗口函数不能在 WHERE 中使用
SELECT * FROM orders
WHERE ROW_NUMBER() OVER (ORDER BY id) = 1;
-- ✅ 正确:用子查询
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (ORDER BY id) AS rn
FROM orders
) t WHERE rn = 1;
错误 3:误解 PARTITION BY
sql
-- ❌ 错误理解:以为 PARTITION BY 会分组返回
SELECT user_id, COUNT(*)
FROM orders
GROUP BY user_id; -- 这才是分组
-- ✅ 正确理解:PARTITION BY 不减少行数
SELECT user_id,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY id) AS rn
FROM orders; -- 行数不变,只是加了编号
八、记忆口诀
ROW_NUMBER 编序号,分组排序不能少
去重分页最常用,子查询里套一层
PARTITION 是分堆,ORDER 决定谁在前
WHERE 不能直接调,外层过滤才正确
九、总结
核心要点
- 窗口函数 = 在不压缩行的前提下,附加计算结果
- ROW_NUMBER() = 给每组数据编连续序号(1、2、3...)
- 最常用场景 = 去重、分页、取前 N 名
- 必须配合 =
OVER()+ORDER BY - 使用时机 = 需要"组内排名"或"唯一标识"时
快速参考
sql
-- 基本模板
SELECT * FROM (
SELECT
字段列表,
ROW_NUMBER() OVER (
PARTITION BY 分组字段 -- 可选
ORDER BY 排序字段 DESC -- 必填
) AS rn
FROM 表名
) t
WHERE rn = 1; -- 或其他条件