📊 SQL 入门 Day 12: 窗口函数基础

场景引入

又到月底了,小明作为数据分析师收到老板的需求:"把每个销售员的销售额排个名,还要看每个部门内部的排名情况,最后把销售员分成 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 就搞定了老板的三个需求------排名、部门内排名、分等级。这就是窗口函数的威力 💪

相关推荐
玖石书2 小时前
ASP.NET Core 迁移至 Spring系列:编译生态篇
后端·spring·asp.net
Java技术小馆2 小时前
AI 如何理解对话
后端
小满zs2 小时前
Go语言第五章(数组和切片)
后端·go
AskHarries2 小时前
RSS 有必要做吗
后端
颜进强2 小时前
前端看后端 14:什么是 CORS?
前端·后端
用户69371750013843 小时前
了解一下 Agent Harness
android·前端·后端
swipe4 小时前
16|(前端转全栈)前端人排查后端问题:curl、traceId、日志、MySQL、Redis 怎么用?
前端·后端·面试
亦暖筑序4 小时前
重新认识 AgentScope-Java 2.0:ReActAgent 负责推理,HarnessAgent 负责运行
java·后端·agent
小林ixn4 小时前
Redis 实战避坑指南:从缓存击穿到高可用,一文全搞定
redis·后端