PostgreSQL ROW_NUMBER() 窗口函数完全解析

一、什么是"窗口函数"?(大白话版)

"窗口"不是窗户,而是"视野范围"!

  • 普通聚合函数 (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          -- 必填:排序规则
)

三个关键点:

  1. OVER():声明这是窗口函数
  2. PARTITION BY :可选,类似 GROUP BY,但不压缩行数
  3. 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 不能直接调,外层过滤才正确

九、总结

核心要点

  1. 窗口函数 = 在不压缩行的前提下,附加计算结果
  2. ROW_NUMBER() = 给每组数据编连续序号(1、2、3...)
  3. 最常用场景 = 去重、分页、取前 N 名
  4. 必须配合 = OVER() + ORDER BY
  5. 使用时机 = 需要"组内排名"或"唯一标识"时

快速参考

sql 复制代码
-- 基本模板
SELECT * FROM (
    SELECT 
        字段列表,
        ROW_NUMBER() OVER (
            PARTITION BY 分组字段     -- 可选
            ORDER BY 排序字段 DESC    -- 必填
        ) AS rn
    FROM 表名
) t
WHERE rn = 1;  -- 或其他条件