场景引入
又到月底了,小明作为数据分析师收到老板的需求:"把每个销售员的销售额排个名,还要看每个部门内部的排名情况,最后把销售员分成 4 个等级。"
小明心想:"用 GROUP BY 只能汇总,没法保留明细行又同时排名。用子查询自关联太麻烦,性能还差。"
这时候,窗口函数(Window Function) 就是最佳答案。
什么是窗口函数?
窗口函数在不合并行 的前提下,对表的"窗口"(一组行)执行计算。普通的 GROUP BY 会把多行聚合成一行,窗口函数则保留每一行,同时输出聚合/排名结果。
sql
-- 基本语法
<窗口函数>() OVER (
[PARTITION BY 列1, 列2, ...] -- 分区(类似 GROUP BY 但不合并行)
[ORDER BY 列1 [ASC|DESC]] -- 窗口内排序
)
两个核心概念:
| 关键字 | 作用 |
|---|---|
PARTITION BY |
将数据分成独立窗口(可选,不写则全部数据为一个窗口) |
ORDER BY |
窗口内的排序规则,影响排名函数的计算结果 |
准备测试数据
sql
-- 建表:销售记录
CREATE TABLE sales_records (
id SERIAL PRIMARY KEY,
emp_name VARCHAR(50),
department VARCHAR(50),
sale_amount NUMERIC(10,2),
sale_date DATE
);
-- 插入测试数据
INSERT INTO sales_records (emp_name, department, sale_amount, sale_date) VALUES
('张三', '手机部门', 12000, '2026-07-01'),
('李四', '手机部门', 15000, '2026-07-02'),
('王五', '手机部门', 12000, '2026-07-03'),
('赵六', '电脑部门', 20000, '2026-07-01'),
('钱七', '电脑部门', 18000, '2026-07-02'),
('孙八', '电脑部门', 25000, '2026-07-03'),
('周九', '配件部门', 8000, '2026-07-01'),
('吴十', '配件部门', 9500, '2026-07-02'),
('郑一', '配件部门', 9500, '2026-07-03');
四大排名函数详解
1. ROW_NUMBER() --- 严格行号
每一行分配一个唯一且连续的编号,即使值相同也不重复。
sql
SELECT
emp_name,
department,
sale_amount,
ROW_NUMBER() OVER (ORDER BY sale_amount DESC) AS rn
FROM sales_records;
yaml
emp_name | department | sale_amount | rn
----------+------------+-------------+----
孙八 | 电脑部门 | 25000.00 | 1
赵六 | 电脑部门 | 20000.00 | 2
钱七 | 电脑部门 | 18000.00 | 3
李四 | 手机部门 | 15000.00 | 4
张三 | 手机部门 | 12000.00 | 5
王五 | 手机部门 | 12000.00 | 6
吴十 | 配件部门 | 9500.00 | 7
郑一 | 配件部门 | 9500.00 | 8
周九 | 配件部门 | 8000.00 | 9
注意:张三和王五都是 12000,但
ROW_NUMBER()给了不同编号(5 和 6)。
2. RANK() --- 排名(跳跃)
值相同时排名并列,但下一个排名会跳跃(跳过并列数量)。
sql
SELECT
emp_name,
department,
sale_amount,
RANK() OVER (ORDER BY sale_amount DESC) AS rk
FROM sales_records;
yaml
emp_name | department | sale_amount | rk
----------+------------+-------------+----
孙八 | 电脑部门 | 25000.00 | 1
赵六 | 电脑部门 | 20000.00 | 2
钱七 | 电脑部门 | 18000.00 | 3
李四 | 手机部门 | 15000.00 | 4
张三 | 手机部门 | 12000.00 | 5
王五 | 手机部门 | 12000.00 | 5 ← 并列第5
吴十 | 配件部门 | 9500.00 | 7 ← 跳过第6,直接第7
郑一 | 配件部门 | 9500.00 | 7 ← 并列第7
周九 | 配件部门 | 8000.00 | 9
3. DENSE_RANK() --- 排名(不跳跃)
值相同时排名并列,但下一个排名连续(不跳过)。
sql
SELECT
emp_name,
department,
sale_amount,
DENSE_RANK() OVER (ORDER BY sale_amount DESC) AS dr
FROM sales_records;
yaml
emp_name | department | sale_amount | dr
----------+------------+-------------+----
孙八 | 电脑部门 | 25000.00 | 1
赵六 | 电脑部门 | 20000.00 | 2
钱七 | 电脑部门 | 18000.00 | 3
李四 | 手机部门 | 15000.00 | 4
张三 | 手机部门 | 12000.00 | 5
王五 | 手机部门 | 12000.00 | 5
吴十 | 配件部门 | 9500.00 | 6 ← 不跳跃
郑一 | 配件部门 | 9500.00 | 6
周九 | 配件部门 | 8000.00 | 7
三者对比(重点):
| 场景 | ROW_NUMBER | RANK | DENSE_RANK |
|---|---|---|---|
| 值并列时是否相同 | ❌ 不同 | ✅ 相同 | ✅ 相同 |
| 并列后是否跳过 | --- | ✅ 跳过 | ❌ 不跳过 |
| 典型用途 | 分页/去重 | 竞赛排名(第1、第1、第3) | 等级制(第1、第1、第2) |
4. NTILE(N) --- 分桶
将窗口内的行尽量均匀地分成 N 个桶(bucket),返回桶编号(1 到 N)。
sql
SELECT
emp_name,
sale_amount,
NTILE(4) OVER (ORDER BY sale_amount DESC) AS quartile
FROM sales_records;
yaml
emp_name | sale_amount | quartile
----------+-------------+---------
孙八 | 25000.00 | 1
赵六 | 20000.00 | 1
钱七 | 18000.00 | 1 ← 第1组(前3行)
李四 | 15000.00 | 2
张三 | 12000.00 | 2
王五 | 12000.00 | 2 ← 第2组
吴十 | 9500.00 | 3
郑一 | 9500.00 | 3 ← 第3组
周九 | 8000.00 | 4 ← 第4组
9 行 ÷ 4 桶 = 前 3 桶各 3 行 ×,最后一桶 0 行?不对,NTILE 尽量均匀分配:9 ÷ 4 = 2 余 1,所以前 1 桶多一行,结果是 3, 2, 2, 2。实际输出如上。
口诀: NTILE 将行分桶,不是按值分。
PARTITION BY --- 分组内部排名
PARTITION BY 让排名在每个分组内部重新计算。这就是老板要的"部门内排名"。
sql
SELECT
emp_name,
department,
sale_amount,
ROW_NUMBER() OVER (
PARTITION BY department
ORDER BY sale_amount DESC
) AS dept_rn,
RANK() OVER (
PARTITION BY department
ORDER BY sale_amount DESC
) AS dept_rank
FROM sales_records;
yaml
emp_name | department | sale_amount | dept_rn | dept_rank
----------+------------+-------------+---------+-----------
孙八 | 电脑部门 | 25000.00 | 1 | 1
赵六 | 电脑部门 | 20000.00 | 2 | 2
钱七 | 电脑部门 | 18000.00 | 3 | 3
李四 | 手机部门 | 15000.00 | 1 | 1
张三 | 手机部门 | 12000.00 | 2 | 2
王五 | 手机部门 | 12000.00 | 3 | 2 ← RANK 并列
吴十 | 配件部门 | 9500.00 | 1 | 1
郑一 | 配件部门 | 9500.00 | 2 | 1 ← RANK 并列
周九 | 配件部门 | 8000.00 | 3 | 3
每个部门的排名都从 1 重新开始,完美满足部门内排名需求。
进阶示例:分组分桶
把每个部门的人按销售额分成 2 个等级(A/B):
sql
SELECT
emp_name,
department,
sale_amount,
NTILE(2) OVER (
PARTITION BY department
ORDER BY sale_amount DESC
) AS dept_grade
FROM sales_records;
yaml
emp_name | department | sale_amount | dept_grade
----------+------------+-------------+------------
孙八 | 电脑部门 | 25000.00 | 1
赵六 | 电脑部门 | 20000.00 | 1
钱七 | 电脑部门 | 18000.00 | 2 ← 电脑部门前2名A级,最后1名B级
李四 | 手机部门 | 15000.00 | 1
张三 | 手机部门 | 12000.00 | 1
王五 | 手机部门 | 12000.00 | 2
吴十 | 配件部门 | 9500.00 | 1
郑一 | 配件部门 | 9500.00 | 1
周九 | 配件部门 | 8000.00 | 2
注意事项
| # | 注意点 | 说明 |
|---|---|---|
| 1 | 窗口函数只能在 SELECT 或 ORDER BY 中 | 不能出现在 WHERE、GROUP BY、HAVING 中 |
| 2 | 性能影响 | ORDER BY 列建立索引可显著提升窗口函数性能 |
| 3 | NULL 排序 | PostgreSQL 默认 NULLS LAST(升序时)或 NULLS FIRST(降序时),可用 ORDER BY col NULLS FIRST/LAST 控制 |
| 4 | DISTINCT 搭配 | 如果去重窗口函数结果,外部包一层子查询或 CTE |
| 5 | PARTITION BY 列需要选择性好 | 分区过多且分区内行数很少时,窗口函数优势变小 |
| 6 | NTILE 行数不均匀 | 余数从第 1 桶开始分配,靠前桶会多一行 |
| 7 | PostgreSQL 支持性 | PostgreSQL 8.4+ 开始支持窗口函数,当前所有主流版本完全兼容 |
今日总结
- 四大排名函数:
ROW_NUMBER()严格行号、RANK()跳跃排名、DENSE_RANK()连续排名、NTILE(N)分桶 - PARTITION BY: 分组内部独立计算,相当于"按组排名"
- ORDER BY: 决定排名顺序和分桶方向
- 窗口函数不合并行,保留细节的同时完成计算,比子查询优雅得多
有了窗口函数,小明一行 SQL 就搞定了老板的三个需求------排名、部门内排名、分等级。这就是窗口函数的威力 💪