一、核心基础
1.1 定义与核心特性
窗口函数(Window Function)又称开窗函数,是MySQL 8.0版本新增的高级查询特性,核心作用是对**数据集的指定窗口(即数据子集)**进行精准计算。
======= 🌟 青柠来相伴,代码更简单。🌟 =======
📚 本文所有内容,我都整理在了 青柠合集 里。👇
🎯 搜索关注【青柠代码录】,即可查看所有合集文章 ~
======= 🌟 ================ 🌟 =======
区别于传统GROUP BY聚合函数,其最大优势是不压缩原表行数、不丢失明细数据------每一行数据都会生成对应的计算结果,实现"单行明细数据"与"窗口汇总数据"同屏展示,完美弥补传统GROUP BY聚合后,丢失明细的缺陷,这也是开发中优先使用窗口函数的核心原因。
窗口函数的三大核心特性:
- 明细保留:原表所有行完整留存,不会像GROUP BY那样,将分组数据合并为一行,而是在每行明细后,新增窗口计算结果列,实现"一行看尽个人数据与分组汇总数据",适配报表开发中"明细+汇总"同表展示的需求。
- 分区独立:通过PARTITION BY子句划分数据分区,每个分区内的窗口计算相互隔离、互不干扰。例如按部门分区计算薪资排名,技术部的排名仅在技术部内部生效,不会与产品部、销售部的员工混淆,这是窗口函数实现"分组计算"的核心逻辑。
- 范围可控:支持通过帧边界子句(ROWS/RANGE),自定义窗口内参与计算的行范围,可实现累计计算、移动计算、区间计算等精细化需求,例如"近3天订单金额移动平均""从分区首行到当前行的累计销售额",适配复杂统计场景。
1.2 与GROUP BY深度对比
很多开发者在使用时,会混淆窗口函数与GROUP BY聚合函数,二者核心差异体现在数据处理方式、结果行数、计算范围等5个关键维度:
- 数据处理方式:GROUP BY聚合函数,会将相同分组的多行数据合并为一行,仅返回分组汇总结果,彻底丢失单行明细数据;窗口函数则不会合并行,分组后保留所有单行明细,同时在每行新增窗口计算结果,明细与汇总兼顾。
- 结果行数:GROUP BY的结果行数等于分组数量,例如3个部门分组,结果仅返回3行部门汇总数据;窗口函数的结果行数与原表完全一致,原表有9条员工数据,执行窗口函数后仍返回9条数据,仅新增计算列。
- 计算范围:GROUP BY仅能对整个分组做全局聚合,无法精细化控制计算的行范围,例如无法实现"分组内从当前行向前3行的平均薪资";窗口函数可通过ROWS/RANGE子句自定义分区内的计算行范围,支持累计、移动、首尾行等多种精细化计算。
- SQL执行顺序:GROUP BY的执行时机早于WHERE、HAVING之后,先筛选数据再分组聚合;窗口函数的执行时机晚于WHERE、GROUP BY、HAVING,早于ORDER BY、LIMIT,这也是"窗口函数不能直接写在WHERE子句中"的核心原因。
- 适用场景:GROUP BY适用于简单汇总统计、分组求和/计数,例如"统计每个部门的员工总数""计算每个部门的薪资总额";窗口函数适用于排名榜单、跨行对比、累计计算、分组TopN、复杂报表、同比环比等场景,例如"每个部门薪资排名""员工薪资与部门平均薪资对比""每日销售额累计求和"。
1.3 语法详解
MySQL窗口函数的标准语法遵循"函数+OVER子句"的结构,OVER子句是窗口函数的核心,所有分区、排序、范围控制,均通过OVER子句实现:
-- 完整语法
窗口函数名([参数]) OVER (
PARTITION BY 分区字段1, 分区字段2... -- 分区子句:可选,划分数据窗口(核心)
ORDER BY 排序字段1 [ASC/DESC], 排序字段2... -- 排序子句:可选,分区内排序(影响排名、累计结果)
[ROWS | RANGE] BETWEEN 起始边界 AND 结束边界 -- 帧边界子句:可选,定义计算行范围
) AS 计算结果别名
关键字逐句拆解(结合开发场景,说明使用注意事项):
- 窗口函数名:MySQL内置的窗口函数分为四大类(排名类、聚合类、跨行取值类、分布类),不同函数的参数要求不同,例如排名类函数无需参数,分位数函数需要指定分位值参数,后续会详细讲解。
- OVER关键字 :窗口函数的**唯一标识,必须存在!**如果省略OVER子句,该函数会变成普通聚合函数,这是区分窗口函数与普通聚合函数的关键。
- PARTITION BY子句:分区关键字,核心作用是将整个数据集按指定字段拆分为多个独立的子窗口,每个子窗口内的计算相互独立。例如"PARTITION BY dept_name"表示按部门拆分窗口,技术部、产品部、销售部分别作为独立窗口计算。省略PARTITION BY时,整张表作为一个统一窗口计算。
- ORDER BY子句:分区内排序关键字,用于指定每个子窗口内的数据排序规则,直接影响排名类、累计类、跨行取值类窗口函数的结果。注意:排名类函数(ROW_NUMBER()、RANK()等)必须搭配ORDER BY使用,否则排名结果混乱;累计类函数(SUM()、AVG()等)搭配ORDER BY时,默认按"分区首行到当前行"累计计算。
- ROWS/RANGE子句(帧边界):用于精确控制窗口内参与计算的行范围,是窗口函数精细化计算的核心,分为ROWS和RANGE两种类型,二者差异显著,开发中需按需选择:
- ROWS:基于物理行定位,精准控制行数,例如"ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING"表示"当前行向前1行 + 当前行 + 当前行向后1行",共3行数据参与计算。开发首选ROWS,性能极高,适合明确行数范围的场景(如移动平均、近N行统计)。
- RANGE:基于数值范围定位,适用于连续数值(如薪资、金额)或日期区间,例如"RANGE BETWEEN 1000 PRECEDING AND 1000 FOLLOWING"表示"当前行数值±1000范围内的所有行"。大数据量下RANGE性能较差,慎用,仅在需要按数值范围计算时使用。
帧边界参数
帧边界的参数决定了窗口内参与计算的行范围,MySQL提供5种常用帧边界参数。
常用帧边界参数:
- UNBOUNDED PRECEDING:表示"分区的第一行",无边界起点,常用于"从分区首行到当前行""全分区范围"的计算,例如累计求和、全分区极值获取。
- CURRENT ROW:表示"当前行",是帧边界的核心参数,几乎所有精细化计算都需要用到,例如"当前行向前1行+当前行""当前行+向后1行"。
- N PRECEDING:表示"当前行向前N行",N为正整数,例如"1 PRECEDING"是当前行前1行,"2 PRECEDING"是当前行前2行,适用于移动平均、近N行统计。
- N FOLLOWING:表示"当前行向后N行",N为正整数,例如"1 FOLLOWING"是当前行后1行,"3 FOLLOWING"是当前行后3行,适用于跨多行数据对比。
- UNBOUNDED FOLLOWING:表示"分区的最后一行",无边界终点,常用于"全分区范围"计算,尤其是LAST_VALUE()函数(后续会详细讲解)。
帧边界默认规则:
- 情况1:仅写OVER(),无PARTITION BY、无ORDER BY、无帧边界:整个数据集作为一个窗口,所有行参与计算,例如"SUM(salary) OVER()"表示计算全表薪资总和,每行返回相同的总和值。
- 情况2:OVER(PARTITION BY 字段),无ORDER BY、无帧边界:每个分区内的所有行参与计算,例如"SUM(salary) OVER(PARTITION BY dept_name)"表示计算每个部门的薪资总和,同部门每行返回相同的部门总和。
- 情况3:OVER(ORDER BY 字段),无PARTITION BY、无帧边界:默认帧边界为"RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW",即从数据集首行到当前行参与计算,常用于累计求和、累计计数。
- 情况4:OVER(PARTITION BY 字段 ORDER BY 字段),无帧边界:默认帧边界为"RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW",即从每个分区的首行到当前行参与计算,这是累计类窗口函数的常用写法。
补充说明:开发中,若需要自定义帧边界,建议明确指定ROWS/RANGE,避免依赖默认规则导致结果错误;尤其是使用LAST_VALUE()函数时,必须指定全分区帧边界(UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING),否则会出现结果异常。
二、统一测试数据表
本文采用3张业务表,覆盖薪资统计、电商交易、业绩分析三大高频场景,所有案例均基于这3张表编写。
2.1 员工表(emp):人事薪资场景(排名、聚合、跨行对比案例专用)
用于演示排名类、聚合类、跨行取值类窗口函数,包含员工姓名、部门、岗位、薪资、奖金、入职日期等核心字段,贴合人事统计场景。
-- 创建员工表(表结构,含注释)
CREATE TABLE emp (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '员工主键ID,自增',
emp_name VARCHAR(30) NOT NULL COMMENT '员工姓名',
dept_name VARCHAR(30) NOT NULL COMMENT '所属部门',
job VARCHAR(30) NOT NULL COMMENT '岗位名称',
salary DECIMAL(10,2) NOT NULL COMMENT '月度薪资',
bonus DECIMAL(10,2) DEFAULT 0.00 COMMENT '月度奖金,默认0',
hire_date DATE NOT NULL COMMENT '入职日期'
) COMMENT '员工信息表';
-- 插入测试数据(9条数据,覆盖4个部门、不同薪资层级)
INSERT INTO emp (emp_name, dept_name, job, salary, bonus, hire_date)
VALUES
('张三', '技术部', 'Java开发工程师', 15000.00, 2000.00, '2020-01-10'),
('李四', '技术部', '架构师', 28000.00, 5000.00, '2018-05-20'),
('王五', '技术部', 'Java开发工程师', 15000.00, 1800.00, '2021-03-15'),
('赵六', '产品部', '产品经理', 22000.00, 4000.00, '2019-11-01'),
('钱七', '产品部', '产品专员', 14000.00, 1500.00, '2020-07-08'),
('孙八', '运营部', '运营主管', 12000.00, 2500.00, '2021-01-20'),
('周九', '运营部', '运营专员', 9000.00, 1000.00, '2022-09-30'),
('吴十', '销售部', '销售经理', 18000.00, 8000.00, '2019-03-12'),
('郑十一', '销售部', '销售代表', 10000.00, 6000.00, '2022-05-18');
2.2 订单明细表(order_detail):电商交易场景(累计、移动计算案例专用)
用于演示聚合类窗口函数的累计计算、移动计算,包含订单ID、用户ID、商品品类、订单金额、下单时间等核心字段,贴合电商平台订单统计场景。
-- 创建订单明细表(表结构,含注释)
CREATE TABLE order_detail (
order_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '订单ID,自增',
user_id INT NOT NULL COMMENT '用户ID,关联用户表',
goods_category VARCHAR(30) NOT NULL COMMENT '商品品类',
order_amount DECIMAL(10,2) NOT NULL COMMENT '订单金额',
create_time DATE NOT NULL COMMENT '下单时间'
) COMMENT '电商订单明细表';
-- 插入测试数据(9条数据,覆盖4个用户、2个品类、4天时间)
INSERT INTO order_detail (user_id, goods_category, order_amount, create_time)
VALUES
(101, '电子产品', 200.00, '2025-01-01'),
(101, '电子产品', 300.00, '2025-01-02'),
(101, '家居用品', 150.00, '2025-01-03'),
(102, '电子产品', 500.00, '2025-01-01'),
(102, '家居用品', 400.00, '2025-01-02'),
(103, '电子产品', 800.00, '2025-01-01'),
(103, '家居用品', 260.00, '2025-01-03'),
(104, '电子产品', 350.00, '2025-01-02'),
(104, '家居用品', 180.00, '2025-01-04');
2.3 销售统计表(sales):业绩分析场景(同比环比、分布统计案例专用)
用于演示跨行取值类、分布类窗口函数,包含销售记录ID、销售员、销售日期、销售金额等核心字段,贴合销售业绩统计场景。
-- 创建销售统计表(表结构,含注释)
CREATE TABLE sales (
sale_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '销售记录ID,自增',
sale_name VARCHAR(30) NOT NULL COMMENT '销售员姓名',
sale_date DATE NOT NULL COMMENT '销售日期',
sale_amount DECIMAL(10,2) NOT NULL COMMENT '销售金额'
) COMMENT '销售员业绩统计表';
-- 插入测试数据(6条数据,覆盖2个销售员、3天时间)
INSERT INTO sales (sale_name, sale_date, sale_amount)
VALUES
('吴十', '2025-01-01', 5000.00),
('吴十', '2025-01-02', 3000.00),
('吴十', '2025-01-03', 4500.00),
('郑十一', '2025-01-01', 2000.00),
('郑十一', '2025-01-02', 2500.00),
('郑十一', '2025-01-03', 3200.00);
三、窗口函数分类
MySQL窗口函数分为四大类:排名类、聚合类、跨行取值类、分布类。
3.1 排名类窗口函数(榜单/TopN核心)
排名类窗口函数是开发中最常用的窗口函数,核心作用是对分区内的数据按指定字段排序,生成对应的排名序号,适配业绩榜单、分组TopN、数据分层等高频场景。
MySQL内置4个排名类窗口函数,分别是ROW_NUMBER()、RANK()、DENSE_RANK()、NTILE(n),以下逐个拆解。
3.1.1 ROW_NUMBER():连续唯一排名函数(TopN首选)
核心定义:对分区内的数据按指定字段排序,生成连续、唯一的排名序号,即使有相同值,也不会出现并列排名,序号始终连续递增,是分组取TopN、数据去重、序号生成的首选函数。
语法细节:
-- 语法(无参数,必须搭配ORDER BY)
ROW_NUMBER() OVER (
PARTITION BY 分区字段 -- 可选,不写则全表排名
ORDER BY 排序字段 [ASC/DESC] -- 必选,否则排名混乱
) AS 排名别名
底层逻辑:先按PARTITION BY划分分区,每个分区内按ORDER BY指定的字段排序,然后为每个分区内的每行数据分配一个唯一的连续序号,从1开始递增,相同值的行序号不同,无跳跃。
使用注意事项:
- 必须搭配ORDER BY子句,否则排名结果完全混乱,无实际意义;若需按多个字段排序,可在ORDER BY后添加多个字段(例如ORDER BY salary DESC, bonus DESC)。
- PARTITION BY可选,省略时,整张表作为一个分区,生成全表唯一连续排名。
- 适合场景:分组取TopN(如每个部门薪资前2名)、数据去重(如保留每个分组内最新的一条数据)、生成连续序号(如订单序号、员工序号)。
实战案例:按部门统计员工薪资排名(生成连续唯一排名)
-- 需求:按部门分组,统计每个部门内员工的薪资排名(降序),生成连续唯一排名
SELECT
emp_name,
dept_name,
salary,
ROW_NUMBER() OVER (PARTITION BY dept_name ORDER BY salary DESC) AS row_num_rank
FROM emp;
全量执行结果:
| emp_name | dept_name | salary | row_num_rank |
|---|---|---|---|
| 李四 | 技术部 | 28000.00 | 1 |
| 张三 | 技术部 | 15000.00 | 2 |
| 王五 | 技术部 | 15000.00 | 3 |
| 赵六 | 产品部 | 22000.00 | 1 |
| 钱七 | 产品部 | 14000.00 | 2 |
| 孙八 | 运营部 | 12000.00 | 1 |
| 周九 | 运营部 | 9000.00 | 2 |
| 吴十 | 销售部 | 18000.00 | 1 |
| 郑十一 | 销售部 | 10000.00 | 2 |
逐行结果解析:
- 技术部有3名员工,李四薪资最高(28000),排名1;张三和王五薪资相同(15000),但ROW_NUMBER()生成连续唯一排名,分别为2和3,无并列。
- 产品部、运营部、销售部均按薪资降序生成连续唯一排名,每个部门的排名从1开始递增,互不干扰(分区独立特性)。
- 若省略PARTITION BY dept_name,会生成全表连续排名,李四薪资最高,排名1,赵六排名2,以此类推。
3.1.2 RANK():跳跃并列排名函数(业绩榜单专用)
核心定义:对分区内的数据按指定字段排序,支持并列排名,但并列之后会跳过对应的名次,例如两个并列第2名,下一个名次直接为第4名,适合业绩榜单、竞赛排名等允许并列且跳过名次的场景。
语法细节:
-- 语法(无参数,必须搭配ORDER BY)
RANK() OVER (
PARTITION BY 分区字段 -- 可选,不写则全表排名
ORDER BY 排序字段 [ASC/DESC] -- 必选,否则排名混乱
) AS 排名别名
底层逻辑:与ROW_NUMBER()类似,先分区、再排序,不同之处在于:当分区内有相同值的行时,会为这些行分配相同的排名序号,后续的排名序号会跳过并列的名次,保持排名的"真实位次"。
使用注意事项:
- 必须搭配ORDER BY子句,否则排名无意义;排序方向(ASC/DESC)直接影响排名结果,降序时数值越大排名越靠前,升序则相反。
- 并列排名后会跳过名次,例如3个并列第1名,下一个名次为第4名,这是与DENSE_RANK()的核心区别。
- 适合场景:业绩榜单(如月度销售排名)、竞赛排名(如考试排名),允许并列,且需要体现"真实位次"。
实战案例:按部门统计员工薪资排名(跳跃并列排名)
-- 需求:按部门分组,统计每个部门内员工的薪资排名(降序),支持并列,并列后跳过名次
SELECT
emp_name,
dept_name,
salary,
RANK() OVER (PARTITION BY dept_name ORDER BY salary DESC) AS rank_rank
FROM emp;
全量执行结果:
| emp_name | dept_name | salary | rank_rank |
|---|---|---|---|
| 李四 | 技术部 | 28000.00 | 1 |
| 张三 | 技术部 | 15000.00 | 2 |
| 王五 | 技术部 | 15000.00 | 2 |
| 赵六 | 产品部 | 22000.00 | 1 |
| 钱七 | 产品部 | 14000.00 | 2 |
| 孙八 | 运营部 | 12000.00 | 1 |
| 周九 | 运营部 | 9000.00 | 2 |
| 吴十 | 销售部 | 18000.00 | 1 |
| 郑十一 | 销售部 | 10000.00 | 2 |
逐行结果解析:
- 技术部张三和王五薪资相同(15000),RANK()为二者分配相同的排名2,由于有2个并列第2名,下一个名次直接跳过3,技术部无排名3的员工,体现"真实位次"。
- 其他部门无相同薪资的员工,排名与ROW_NUMBER()一致,从1开始递增,无跳跃。
- 若技术部有3名员工薪资相同(如均为15000),则3人并列第1名,下一个员工排名直接为4,以此类推。
3.1.3 DENSE_RANK():连续并列排名函数(层级划分专用)
核心定义:对分区内的数据按指定字段排序,支持并列排名,且并列之后不会跳过对应的名次,排名序号始终连续递增,例如两个并列第2名,下一个名次仍为第3名,适合数据分层、等级划分等需要并列且连续排名的场景。
语法细节:
-- 语法(无参数,必须搭配ORDER BY)
DENSE_RANK() OVER (
PARTITION BY 分区字段 -- 可选,不写则全表排名
ORDER BY 排序字段 [ASC/DESC] -- 必选,否则排名混乱
) AS 排名别名
底层逻辑:与RANK()、ROW_NUMBER()逻辑一致,先分区、再排序,核心差异在于并列排名后的处理------DENSE_RANK()不会跳过并列名次,无论有多少个并列名次,后续排名均按连续序号递增,确保排名序号无跳跃,仅体现"等级差异"。
使用注意事项:
- 必须搭配ORDER BY子句,排序方向直接决定排名高低,降序时数值越大排名越靠前,升序则相反;多字段排序可在ORDER BY后添加多个字段,按优先级排序。
- 与RANK()的核心区别:并列后是否跳过名次------DENSE_RANK()连续,RANK()跳跃;例如3个并列第1名,DENSE_RANK()下一个名次为2,RANK()下一个名次为4。
- 适合场景:员工等级划分(如S、A、B级对应不同排名区间)、数据分层统计(如高、中、低薪资分层)、无需体现"真实位次",仅需体现等级的场景。
实战案例:按部门统计员工薪资排名(连续并列排名)
-- 需求:按部门分组,统计每个部门内员工的薪资排名(降序),支持并列,并列后不跳过名次
SELECT
emp_name,
dept_name,
salary,
DENSE_RANK() OVER (PARTITION BY dept_name ORDER BY salary DESC) AS dense_rank_rank
FROM emp;
全量执行结果:
| emp_name | dept_name | salary | dense_rank_rank |
|---|---|---|---|
| 李四 | 技术部 | 28000.00 | 1 |
| 张三 | 技术部 | 15000.00 | 2 |
| 王五 | 技术部 | 15000.00 | 2 |
| 赵六 | 产品部 | 22000.00 | 1 |
| 钱七 | 产品部 | 14000.00 | 2 |
| 孙八 | 运营部 | 12000.00 | 1 |
| 周九 | 运营部 | 9000.00 | 2 |
| 吴十 | 销售部 | 18000.00 | 1 |
| 郑十一 | 销售部 | 10000.00 | 2 |
逐行结果解析:
- 技术部张三和王五薪资相同(15000),DENSE_RANK()为二者分配相同的排名2,与RANK()不同的是,后续无跳过名次,技术部3名员工的排名为1、2、2,序号连续,仅体现薪资等级差异。
- 若技术部有3名员工薪资相同(如均为15000),则3人并列第2名,下一个员工排名仍为3,排名序号始终连续,这是DENSE_RANK()最核心的特点。
- 对比ROW_NUMBER()和RANK():同一部门相同薪资,ROW_NUMBER()生成唯一连续排名,RANK()跳跃排名,DENSE_RANK()连续并列排名,三者按需选择即可。
3.1.4 NTILE(n):分组排名函数(数据均分专用)
核心定义:将分区内的数据按指定字段排序后,均匀划分为n个组,每个组分配一个唯一的组序号(从1到n),若数据无法被n整除,会将多余的行依次分配到前面的组中,每组行数相差不超过1,适合数据均分、分层抽样、区间划分等场景。
语法细节:
-- 语法(n为正整数,必须搭配ORDER BY)
NTILE(n) OVER (
PARTITION BY 分区字段 -- 可选,不写则全表均分
ORDER BY 排序字段 [ASC/DESC] -- 必选,否则分组无意义
) AS 组序号别名
底层逻辑:先按PARTITION BY划分分区,每个分区内按ORDER BY排序,然后计算每个分区的总行数,用总行数除以n,得到每组的基础行数;若有余数,多余的行从第1组开始依次分配1行,确保每组行数相差不超过1,最终每个行分配对应的组序号。
使用注意事项:
- n必须是正整数,例如NTILE(3)表示划分为3组,NTILE(5)表示划分为5组;若n大于分区内总行数,每个行将单独成为一组,组序号从1到总行数。
- 必须搭配ORDER BY子句,排序方向决定分组的数值分布,例如按薪资降序分组,第1组为薪资最高的行,第n组为薪资最低的行。
- 适合场景:数据分层抽样(如将用户按消费金额均分3组,抽取每组10%用户)、业绩区间划分(如将销售员业绩均分5组,对应5个业绩等级)、大数据量均分统计。
实战案例:按部门将员工薪资均分3组(高、中、低薪资分层)
-- 需求:按部门分组,将每个部门内的员工按薪资降序均分3组,生成组序号(1=高薪组,2=中薪组,3=低薪组)
SELECT
emp_name,
dept_name,
salary,
NTILE(3) OVER (PARTITION BY dept_name ORDER BY salary DESC) AS salary_group
FROM emp;
全量执行结果:
| emp_name | dept_name | salary | salary_group |
|---|---|---|---|
| 李四 | 技术部 | 28000.00 | 1 |
| 张三 | 技术部 | 15000.00 | 2 |
| 王五 | 技术部 | 15000.00 | 3 |
| 赵六 | 产品部 | 22000.00 | 1 |
| 钱七 | 产品部 | 14000.00 | 2 |
| 孙八 | 运营部 | 12000.00 | 1 |
| 周九 | 运营部 | 9000.00 | 2 |
| 吴十 | 销售部 | 18000.00 | 1 |
| 郑十一 | 销售部 | 10000.00 | 2 |
逐行结果解析:
- 技术部有3名员工,NTILE(3)将其均匀划分为3组,每组1人,李四(高薪)为组1,张三(中薪)为组2,王五(中薪)为组3,符合"薪资降序分层"需求。
- 产品部、运营部、销售部均只有2名员工,NTILE(3)无法均匀划分(2<3),此时每组最多1人,多余的组序号不生成,因此这3个部门的组序号仅为1和2,无组3。
- 若某部门有7名员工,NTILE(3)划分后,每组行数分别为3、2、2(7÷3=2余1,多余1行分配到第1组),确保每组行数相差不超过1,体现"均匀划分"的核心逻辑。
3.2 聚合类窗口函数(明细+汇总同屏)
聚合类窗口函数是将普通聚合函数(SUM、AVG、COUNT等)与窗口函数结合,核心作用是对分区内的指定窗口进行聚合计算,保留原表明细数据,同时在每行新增聚合结果,实现"单行明细+分组汇总"同屏展示,无需额外关联表,大幅简化SQL编写,是报表开发、数据统计的高频函数。
MySQL支持的聚合类窗口函数:SUM()、AVG()、COUNT()、MAX()、MIN()。
3.2.1 SUM():窗口求和函数(累计/分组求和专用)
核心定义:对分区内指定窗口的目标字段进行求和计算,可实现全分区求和、累计求和、移动求和等多种场景,保留原表明细,每行返回对应窗口的求和结果,是销售额统计、薪资总额统计、累计数据统计的核心函数。
语法细节:
-- 语法(参数为求和字段,可选搭配帧边界)
SUM(目标字段) OVER (
PARTITION BY 分区字段 -- 可选,不写则全表求和
ORDER BY 排序字段 [ASC/DESC] -- 可选,影响累计求和结果
[ROWS | RANGE] BETWEEN 起始边界 AND 结束边界 -- 可选,定义求和窗口范围
) AS 求和结果别名
底层逻辑:先按PARTITION BY划分分区,每个分区内按ORDER BY排序(无ORDER BY则不排序),再按帧边界定义的范围,对目标字段进行求和,每行返回当前窗口的求和结果;无帧边界时,按默认规则确定求和范围(前文已详细说明)。
使用注意事项:
- 目标字段需为数值类型(INT、DECIMAL等),若字段包含NULL值,SUM()会自动忽略NULL值,不影响求和结果。
- 搭配ORDER BY时,默认帧边界为"RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW",即累计求和;不搭配ORDER BY时,默认帧边界为"RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING",即全分区求和。
- 适合场景:每日销售额累计求和、每个部门薪资总额统计、员工薪资与部门薪资总和对比、近N天金额移动求和。
实战案例1:全分区求和(每个部门薪资总额,明细+汇总同屏)
-- 需求:查询员工明细,同时显示每个部门的薪资总额(全分区求和)
SELECT
emp_name,
dept_name,
salary,
-- 每个部门薪资总额(全分区求和,同部门所有行返回相同结果)
SUM(salary) OVER (PARTITION BY dept_name) AS dept_salary_total
FROM emp;
全量执行结果:
| emp_name | dept_name | salary | dept_salary_total |
|---|---|---|---|
| 李四 | 技术部 | 28000.00 | 58000.00 |
| 张三 | 技术部 | 15000.00 | 58000.00 |
| 王五 | 技术部 | 15000.00 | 58000.00 |
| 赵六 | 产品部 | 22000.00 | 36000.00 |
| 钱七 | 产品部 | 14000.00 | 36000.00 |
| 孙八 | 运营部 | 12000.00 | 21000.00 |
| 周九 | 运营部 | 9000.00 | 21000.00 |
| 吴十 | 销售部 | 18000.00 | 28000.00 |
| 郑十一 | 销售部 | 10000.00 | 28000.00 |
逐行结果解析:
- 按dept_name分区,每个分区内所有行的dept_salary_total(部门薪资总额)相同,例如技术部3名员工的薪资总和为28000+15000+15000=58000,因此技术部所有行均返回58000。
- 无需使用GROUP BY分组后再关联原表,仅用SUM() OVER()即可实现"明细+汇总"同屏,大幅简化SQL编写,提升开发效率。
- 若省略PARTITION BY dept_name,会计算全表薪资总和(58000+36000+21000+28000=143000),所有行均返回143000。
实战案例2:累计求和(每日订单金额累计统计)
-- 需求:按用户分组,按下单时间升序,统计每个用户的订单金额累计求和(从首单到当前单)
SELECT
user_id,
create_time,
order_amount,
-- 累计求和:从分区首行(用户首单)到当前行
SUM(order_amount) OVER (
PARTITION BY user_id
ORDER BY create_time ASC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumulative_amount
FROM order_detail;
全量执行结果:
| user_id | create_time | order_amount | cumulative_amount |
|---|---|---|---|
| 101 | 2025-01-01 | 200.00 | 200.00 |
| 101 | 2025-01-02 | 300.00 | 500.00 |
| 101 | 2025-01-03 | 150.00 | 650.00 |
| 102 | 2025-01-01 | 500.00 | 500.00 |
| 102 | 2025-01-02 | 400.00 | 900.00 |
| 103 | 2025-01-01 | 800.00 | 800.00 |
| 103 | 2025-01-03 | 260.00 | 1060.00 |
| 104 | 2025-01-02 | 350.00 | 350.00 |
| 104 | 2025-01-04 | 180.00 | 530.00 |
逐行结果解析:
- 按user_id分区,每个用户作为独立窗口,按create_time升序排序,帧边界指定为"从分区首行到当前行",实现累计求和。
- 用户101的3笔订单,累计金额依次为200(首单)、200+300=500(前两单)、500+150=650(三单合计),符合累计统计需求。
- 若省略帧边界,因搭配了ORDER BY,默认帧边界也是"RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW",结果与指定帧边界一致;若需修改累计范围,可调整帧边界参数(如近2单累计)。
3.2.2 AVG():窗口平均函数(分组/移动平均专用)
核心定义:对分区内指定窗口的目标字段进行平均值计算,可实现全分区平均、累计平均、移动平均等场景,保留原表明细,每行返回对应窗口的平均结果,适合薪资平均、订单金额平均、数据趋势分析等场景。
语法细节:
-- 语法(参数为求平均字段,可选搭配帧边界)
AVG(目标字段) OVER (
PARTITION BY 分区字段 -- 可选,不写则全表求平均
ORDER BY 排序字段 [ASC/DESC] -- 可选,影响累计平均结果
[ROWS | RANGE] BETWEEN 起始边界 AND 结束边界 -- 可选,定义求平均窗口范围
) AS 平均结果别名
底层逻辑:与SUM()窗口函数一致,先分区、再排序(可选)、再按帧边界定义的范围求平均,每行返回当前窗口的平均值;NULL值会被自动忽略,不影响平均结果。
使用注意事项:
- 目标字段需为数值类型,计算结果默认保留MySQL默认小数位数,可搭配ROUND()函数保留指定小数位数(如ROUND(AVG(salary), 2)保留2位小数)。
- 移动平均场景(如近3天订单金额平均),需使用ROWS帧边界,精准控制行数范围,避免使用RANGE(性能差且不符合需求)。
- 适合场景:每个部门薪资平均值、员工薪资与部门平均薪资对比、近N天数据移动平均、数据趋势平滑分析。
实战案例:员工薪资与部门平均薪资对比(全分区平均)
-- 需求:查询员工明细,显示每个部门的薪资平均值,同时计算员工薪资与部门平均薪资的差值
SELECT
emp_name,
dept_name,
salary,
-- 部门薪资平均值(保留2位小数)
ROUND(AVG(salary) OVER (PARTITION BY dept_name), 2) AS dept_salary_avg,
-- 员工薪资与部门平均薪资的差值(保留2位小数)
ROUND(salary - AVG(salary) OVER (PARTITION BY dept_name), 2) AS salary_diff
FROM emp;
全量执行结果:
| emp_name | dept_name | salary | dept_salary_avg | salary_diff |
|---|---|---|---|---|
| 李四 | 技术部 | 28000.00 | 19333.33 | 8666.67 |
| 张三 | 技术部 | 15000.00 | 19333.33 | -4333.33 |
| 王五 | 技术部 | 15000.00 | 19333.33 | -4333.33 |
| 赵六 | 产品部 | 22000.00 | 18000.00 | 4000.00 |
| 钱七 | 产品部 | 14000.00 | 18000.00 | -4000.00 |
| 孙八 | 运营部 | 12000.00 |
逐行结果解析:
- 按dept_name分区计算部门薪资平均值,技术部3名员工薪资平均为58000÷3≈19333.33,李四薪资高于平均值8666.67,张三和王五低于平均值4333.33,差值计算精准。
- 产品部2名员工薪资平均为36000÷2=18000.00,赵六薪资高于平均值4000.00,钱七低于平均值4000.00,符合实际计算逻辑。
- 搭配ROUND()函数将结果保留2位小数,符合报表数据展示规范;若不使用ROUND(),MySQL会默认返回多位小数,影响可读性。
实战案例2:移动平均(近2天订单金额移动平均)
-- 需求:按商品品类分组,按下单时间升序,计算每个品类近2天(当前行+前1行)的订单金额移动平均
SELECT
goods_category,
create_time,
order_amount,
-- 移动平均:当前行向前1行 + 当前行(近2天)
ROUND(AVG(order_amount) OVER (
PARTITION BY goods_category
ORDER BY create_time ASC
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
), 2) AS moving_avg_2d
FROM order_detail;
全量执行结果:
| goods_category | create_time | order_amount | moving_avg_2d |
|---|---|---|---|
| 电子产品 | 2025-01-01 | 200.00 | 200.00 |
| 电子产品 | 2025-01-01 | 500.00 | 350.00 |
| 电子产品 | 2025-01-01 | 800.00 | 650.00 |
| 电子产品 | 2025-01-02 | 300.00 | 550.00 |
| 电子产品 | 2025-01-02 | 350.00 | 325.00 |
| 家居用品 | 2025-01-02 | 400.00 | 400.00 |
| 家居用品 | 2025-01-03 | 150.00 | 275.00 |
| 家居用品 | 2025-01-03 | 260.00 | 205.00 |
| 家居用品 | 2025-01-04 | 180.00 | 220.00 |
逐行结果解析:
- 按goods_category分区,每个品类作为独立窗口,按create_time升序排序,帧边界指定为"当前行向前1行+当前行",实现近2天移动平均。
- 电子产品首行(2025-01-01 200.00)无前1行,移动平均即为自身数值200.00;第二行(500.00)移动平均为(200+500)÷2=350.00,第三行(800.00)移动平均为(500+800)÷2=650.00,以此类推,符合移动平均逻辑。
- 移动平均常用于数据趋势平滑分析,例如通过近N天订单金额平均,消除单日数据波动,更清晰看到品类销售趋势;若需调整移动天数,修改"1 PRECEDING"为对应数值即可(如近3天改为"2 PRECEDING")。
3.2.3 COUNT():窗口计数函数(分组/累计计数专用)
核心定义:对分区内指定窗口的行进行计数,可实现全分区计数、累计计数、移动计数等场景,保留原表明细,每行返回对应窗口的计数结果,适合员工数量统计、订单数量统计、数据行数统计等场景。
语法细节:
-- 语法(参数可指定字段或用*,可选搭配帧边界)
COUNT(字段/*) OVER (
PARTITION BY 分区字段 -- 可选,不写则全表计数
ORDER BY 排序字段 [ASC/DESC] -- 可选,影响累计计数结果
[ROWS | RANGE] BETWEEN 起始边界 AND 结束边界 -- 可选,定义计数窗口范围
) AS 计数结果别名
底层逻辑:与SUM()、AVG()窗口函数一致,先分区、再排序(可选)、再按帧边界定义的范围计数;COUNT(*)计数包含NULL值,COUNT(字段)计数忽略该字段为NULL的行,这是核心使用差异。
使用注意事项:
- COUNT(*)适用于"统计窗口内总行数",无论字段是否为NULL;COUNT(字段)适用于"统计窗口内该字段非NULL的行数",例如COUNT(bonus)统计有奖金的员工数量。
- 累计计数场景(如按时间累计订单数),搭配ORDER BY后,默认按"分区首行到当前行"计数,无需额外指定帧边界。
- 适合场景:每个部门员工数量、每个用户累计订单数、有奖金的员工数量统计、近N天订单数量移动计数。
实战案例:每个部门员工数量+每个用户累计订单数(双场景覆盖)
-- 场景1:查询员工明细,统计每个部门的员工总数(全分区计数)
SELECT
emp_name,
dept_name,
COUNT(*) OVER (PARTITION BY dept_name) AS dept_emp_count
FROM emp;
-- 场景2:查询订单明细,按用户分组、下单时间升序,统计每个用户累计订单数(累计计数)
SELECT
user_id,
create_time,
order_amount,
COUNT(*) OVER (
PARTITION BY user_id
ORDER BY create_time ASC
) AS cumulative_order_count
FROM order_detail;
场景1全量执行结果:
| emp_name | dept_name | dept_emp_count |
|---|---|---|
| 李四 | 技术部 | 3 |
| 张三 | 技术部 | 3 |
| 王五 | 技术部 | 3 |
| 赵六 | 产品部 | 2 |
| 钱七 | 产品部 | 2 |
| 孙八 | 运营部 | 2 |
| 周九 | 运营部 | 2 |
| 吴十 | 销售部 | 2 |
| 郑十一 | 销售部 | 2 |
场景2全量执行结果:
| user_id | create_time | order_amount | cumulative_order_count |
|---|---|---|---|
| 101 | 2025-01-01 | 200.00 | 1 |
| 101 | 2025-01-02 | 300.00 | 2 |
| 101 | 2025-01-03 | 150.00 | 3 |
| 102 | 2025-01-01 | 500.00 | 1 |
| 102 | 2025-01-02 | 400.00 | 2 |
| 103 | 2025-01-01 | 800.00 | 1 |
| 103 | 2025-01-03 | 260.00 | 2 |
| 104 | 2025-01-02 | 350.00 | 1 |
| 104 | 2025-01-04 | 180.00 | 2 |
逐行结果解析:
- 场景1:COUNT(*)按dept_name分区计数,技术部3名员工,计数结果为3;其他部门各2名员工,计数结果为2,准确反映每个部门的员工总数,且保留员工明细。
- 场景2:COUNT(*)按user_id分区、create_time升序排序,默认帧边界为"分区首行到当前行",实现累计订单数统计;用户101的3笔订单,累计计数依次为1、2、3,清晰体现用户订单累积过程。
- 若需统计"有奖金的员工数量",可将COUNT(*)改为COUNT(bonus),此时会忽略bonus为0.00的员工(若bonus为NULL则忽略,本文测试数据bonus默认0.00,需结合实际场景调整)。
3.2.4 MAX()/MIN():窗口极值函数(分组/区间极值专用)
核心定义:MAX()用于获取分区内指定窗口的目标字段最大值,MIN()用于获取最小值,可实现全分区极值、累计极值、移动极值等场景,保留原表明细,每行返回对应窗口的极值结果,适合薪资最高/最低、订单金额最大/最小、区间极值统计等场景。
语法细节(MAX()与MIN()用法一致,仅逻辑相反):
-- MAX()语法
MAX(目标字段) OVER (
PARTITION BY 分区字段 -- 可选,不写则全表求最大
ORDER BY 排序字段 [ASC/DESC] -- 可选,影响累计极值结果
[ROWS | RANGE] BETWEEN 起始边界 AND 结束边界 -- 可选,定义极值窗口范围
) AS 最大值别名
-- MIN()语法
MIN(目标字段) OVER (
PARTITION BY 分区字段 -- 可选,不写则全表求最小
ORDER BY 排序字段 [ASC/DESC] -- 可选,影响累计极值结果
[ROWS | RANGE] BETWEEN 起始边界 AND 结束边界 -- 可选,定义极值窗口范围
) AS 最小值别名
底层逻辑:与其他聚合类窗口函数一致,先分区、再排序(可选)、再按帧边界定义的范围获取极值;NULL值会被自动忽略,不影响极值结果。
使用注意事项:
- 目标字段需为数值类型,MAX()和MIN()可搭配ORDER BY调整极值统计逻辑,例如按薪资升序排序,累计MAX()会逐步更新最大值,累计MIN()则保持不变。
- 全分区极值场景(如每个部门最高薪资),无需搭配ORDER BY和帧边界,直接用"MAX(字段) OVER (PARTITION BY 分区字段)"即可,简洁高效。
- 适合场景:每个部门最高/最低薪资、每个用户最大/最小订单金额、近N天数据极值、累计极值跟踪。
实战案例:每个部门最高/最低薪资+每个用户最大订单金额(双场景覆盖)
-- 场景1:查询员工明细,显示每个部门的最高薪资和最低薪资(全分区极值)
SELECT
emp_name,
dept_name,
salary,
MAX(salary) OVER (PARTITION BY dept_name) AS dept_max_salary,
MIN(salary) OVER (PARTITION BY dept_name) AS dept_min_salary
FROM emp;
-- 场景2:查询订单明细,按用户分组、下单时间升序,统计每个用户累计最大订单金额(累计极值)
SELECT
user_id,
create_time,
order_amount,
MAX(order_amount) OVER (
PARTITION BY user_id
ORDER BY create_time ASC
) AS cumulative_max_amount
FROM order_detail;
场景1全量执行结果:
| emp_name | dept_name | salary | dept_max_salary | dept_min_salary |
|---|---|---|---|---|
| 李四 | 技术部 | 28000.00 | 28000.00 | 15000.00 |
| 张三 | 技术部 | 15000.00 | 28000.00 | 15000.00 |
| 王五 | 技术部 | 15000.00 | 28000.00 | 15000.00 |
| 赵六 | 产品部 | 22000.00 | 22000.00 | 14000.00 |
| 钱七 | 产品部 | 14000.00 | 22000.00 | 14000.00 |
| 孙八 | 运营部 | 12000.00 | 12000.00 | 9000.00 |
| 周九 | 运营部 | 9000.00 | 12000.00 | 9000.00 |
| 吴十 | 销售部 | 18000.00 | 18000.00 | 10000.00 |
| 郑十一 | 销售部 | 10000.00 | 18000.00 | 10000.00 |
场景2全量执行结果:
场景2全量执行结果:
| user_id | create_time | order_amount | cumulative_max_amount |
|---|---|---|---|
| 101 | 2025-01-01 | 200.00 | 200.00 |
| 101 | 2025-01-02 | 300.00 | 300.00 |
| 101 | 2025-01-03 | 150.00 | 300.00 |
| 102 | 2025-01-01 | 500.00 | 500.00 |
| 102 | 2025-01-02 | 400.00 | 500.00 |
| 103 | 2025-01-01 | 800.00 | 800.00 |
| 103 | 2025-01-03 | 260.00 | 800.00 |
| 104 | 2025-01-02 | 350.00 | 350.00 |
| 104 | 2025-01-04 | 180.00 | 350.00 |
逐行结果解析:
- 场景1:MAX(salary)和MIN(salary)按dept_name分区获取极值,技术部最高薪资28000.00(李四)、最低薪资15000.00(张三、王五),其他部门也精准返回对应极值,且保留员工明细,便于对比员工薪资与部门极值的差距。
- 场景2:MAX(order_amount)按user_id分区、create_time升序排序,默认帧边界为"分区首行到当前行",实现累计最大订单金额跟踪;用户101首单金额200.00,累计最大为200.00,第二单300.00超过首单,累计最大更新为300.00,第三单150.00未超过,累计最大保持300.00,符合累计极值逻辑。
- 若需统计"近3天订单最大金额",可添加帧边界"ROWS BETWEEN 2 PRECEDING AND CURRENT ROW",实现移动极值统计,精准捕捉区间内的最大值变化。
3.3 跨行取值类窗口函数(同比环比/上下行对比核心)
跨行取值类窗口函数核心作用是"获取同一分区内,当前行的上一行、下一行,或指定行数的字段值",无需关联表,即可实现上下行数据对比、同比环比计算、数据差值统计等场景,大幅简化SQL编写,提升开发效率。
MySQL内置3个核心跨行取值函数:LAG()、LEAD()、FIRST_VALUE()/LAST_VALUE(),以下逐个拆解。
3.3.1 LAG():取上一行数据函数(环比/上周期对比专用)
核心定义:在分区内按指定字段排序后,获取当前行的上N行(默认上1行)的指定字段值,若上N行不存在,返回NULL;适合环比计算(如当日销售额对比昨日)、上周期数据对比、上下行差值统计等高频场景。
语法细节:
-- 语法(3个参数,后2个可选)
LAG(目标字段, N, 默认值) OVER (
PARTITION BY 分区字段 -- 可选,不写则全表跨行取值
ORDER BY 排序字段 [ASC/DESC] -- 必选,否则跨行逻辑混乱
) AS 上一行字段别名
-- 参数说明:
-- 目标字段:要获取的上一行字段(必须)
-- N:要获取的上N行,默认1(可选,如N=2表示上2行)
-- 默认值:上N行不存在时返回的值,默认NULL(可选)
底层逻辑:先按PARTITION BY划分分区,每个分区内按ORDER BY排序,然后为每个行定位到"当前行向上N行"的位置,提取该位置的目标字段值;若向上N行超出分区范围(如首行取上1行),则返回默认值(NULL或自定义值)。
使用注意事项:
- 必须搭配ORDER BY子句,排序方向决定"上一行"的逻辑(如按时间升序,上一行是前一天;按时间降序,上一行是后一天)。
- N必须是正整数,默认值需与目标字段类型一致(如目标字段是DECIMAL,默认值可设为0.00;是VARCHAR,可设为'无')。
- 适合场景:销售额环比(当日对比昨日)、员工薪资对比上一位员工、订单金额对比上一笔订单、上周期数据统计。
实战案例:销售员每日销售额环比(当日对比昨日)
-- 需求:按销售员分组,按销售日期升序,计算每日销售额、昨日销售额、环比差值、环比增长率
SELECT
sale_name,
sale_date,
sale_amount,
-- 取上1行(昨日)销售额,无昨日数据返回0.00
LAG(sale_amount, 1, 0.00) OVER (
PARTITION BY sale_name
ORDER BY sale_date ASC
) AS yesterday_amount,
-- 环比差值(当日 - 昨日)
sale_amount - LAG(sale_amount, 1, 0.00) OVER (
PARTITION BY sale_name
ORDER BY sale_date ASC
) AS month_on_month_diff,
-- 环比增长率(保留2位小数,避免除以0)
ROUND(
(sale_amount - LAG(sale_amount, 1, 0.00) OVER (
PARTITION BY sale_name
ORDER BY sale_date ASC
)) / NULLIF(LAG(sale_amount, 1, 0.00) OVER (
PARTITION BY sale_name
ORDER BY sale_date ASC
), 0) * 100,
2
) AS month_on_month_rate
FROM sales;
全量执行结果:
| sale_name | sale_date | sale_amount | yesterday_amount | month_on_month_diff | month_on_month_rate |
|---|---|---|---|---|---|
| 吴十 | 2025-01-01 | 5000.00 | 0.00 | 5000.00 | NULL |
| 吴十 | 2025-01-02 | 3000.00 | 5000.00 | -2000.00 | -40.00 |
| 吴十 | 2025-01-03 | 4500.00 | 3000.00 | 1500.00 | 50.00 |
| 郑十一 | 2025-01-01 | 2000.00 | 0.00 | 2000.00 | NULL |
| 郑十一 | 2025-01-02 | 2500.00 | 2000.00 | 500.00 | 25.00 |
| 郑十一 | 2025-01-03 | 3200.00 | 2500.00 | 700.00 | 28.00 |
逐行结果解析:
- 按sale_name分区,按sale_date升序排序,LAG(sale_amount, 1, 0.00)获取每个销售员上1天的销售额,首天(2025-01-01)无昨日数据,返回默认值0.00。
- 吴十2025-01-02销售额3000.00,昨日销售额5000.00,环比差值-2000.00,环比增长率-40.00%(下降40%);2025-01-03销售额4500.00,昨日3000.00,环比增长50.00%,计算精准。
- 使用NULLIF()函数避免环比增长率计算时"除以0"的错误(首天昨日销售额为0,除以0会报错,NULLIF将0转为NULL,ROUND函数遇到NULL返回NULL),符合开发规范。
3.3.2 LEAD():取下一行数据函数(下周期/后续数据对比专用)
核心定义:与LAG()逻辑相反,在分区内按指定字段排序后,获取当前行的下N行(默认下1行)的指定字段值,若下N行不存在,返回NULL;适合下周期数据对比、后续订单预测、上下行数据联动等场景。
语法细节:
-- 语法(3个参数,后2个可选,与LAG()完全一致)
LEAD(目标字段, N, 默认值) OVER (
PARTITION BY 分区字段 -- 可选,不写则全表跨行取值
ORDER BY 排序字段 [ASC/DESC] -- 必选,否则跨行逻辑混乱
) AS 下一行字段别名
-- 参数说明与LAG()一致:目标字段(必须)、N(默认1,可选)、默认值(默认NULL,可选)
底层逻辑:与LAG()对称,先分区、再排序,为每个行定位到"当前行向下N行"的位置,提取该位置的目标字段值;若向下N行超出分区范围(如末行取下1行),则返回默认值。
使用注意事项:
- 必须搭配ORDER BY子句,排序方向直接影响"下一行"的逻辑(如按时间升序,下一行是后一天;按时间降序,下一行是前一天)。
- 与LAG()的核心区别:LAG()取上一行,LEAD()取下一行,二者可搭配使用,实现上下行双向对比。
- 适合场景:销售额对比明日、员工薪资对比下一位员工、订单金额对比下一笔订单、后续数据提前预览。
实战案例:销售员每日销售额对比明日(下一行数据对比)
-- 需求:按销售员分组,按销售日期升序,计算每日销售额、明日销售额、差值
SELECT
sale_name,
sale_date,
sale_amount,
-- 取下1行(明日)销售额,无明日数据返回0.00
LEAD(sale_amount, 1, 0.00) OVER (
PARTITION BY sale_name
ORDER BY sale_date ASC
) AS tomorrow_amount,
-- 差值(当日 - 明日)
sale_amount - LEAD(sale_amount, 1, 0.00) OVER (
PARTITION BY sale_name
ORDER BY sale_date ASC
) AS diff_with_tomorrow
FROM sales;
全量执行结果:
| sale_name | sale_date | sale_amount | tomorrow_amount | diff_with_tomorrow |
|---|---|---|---|---|
| 吴十 | 2025-01-01 | 5000.00 | 3000.00 | 2000.00 |
| 吴十 | 2025-01-02 | 3000.00 | 4500.00 | -1500.00 |
| 吴十 | 2025-01-03 | 4500.00 | 0.00 | 4500.00 |
| 郑十一 | 2025-01-01 | 2000.00 | 2500.00 | -500.00 |
| 郑十一 | 2025-01-02 | 2500.00 | 3200.00 | -700.00 |
| 郑十一 | 2025-01-03 | 3200.00 | 0.00 | 3200.00 |
逐行结果解析:
- 按sale_name分区、sale_date升序排序,LEAD(sale_amount, 1, 0.00)获取每个销售员下1天的销售额,末天(2025-01-03)无明日数据,返回默认值0.00。
- 吴十2025-01-01销售额5000.00,明日销售额3000.00,差值2000.00(当日高于明日);2025-01-02销售额3000.00,明日4500.00,差值-1500.00(当日低于明日),逻辑清晰。
- LEAD()与LAG()搭配,可同时获取当前行的上一行和下一行数据,实现"昨日-今日-明日"三方对比,满足更复杂的数据分析需求(如趋势预判)。
3.3.3 FIRST_VALUE()/LAST_VALUE():取分区首尾行数据函数(区间首尾对比专用)
核心定义:FIRST_VALUE()用于获取分区内按指定字段排序后首行 的指定字段值,LAST_VALUE()用于获取分区内排序后末行的指定字段值;无需手动定位首尾行,直接获取区间首尾数据,适合首尾对比、区间极值确认、首末数据统计等场景。
语法细节(二者用法一致,仅获取位置不同):
-- FIRST_VALUE():取分区首行数据
FIRST_VALUE(目标字段) OVER (
PARTITION BY 分区字段 -- 可选,不写则全表取首行
ORDER BY 排序字段 [ASC/DESC] -- 必选,否则首尾行无意义
[ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW] -- 可选,调整窗口范围
) AS 首行字段别名
-- LAST_VALUE():取分区末行数据
LAST_VALUE(目标字段) OVER (
PARTITION BY 分区字段 -- 可选,不写则全表取末行
ORDER BY 排序字段 [ASC/DESC] -- 必选,否则首尾行无意义
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING -- 必选,否则默认窗口范围错误
) AS 末行字段别名
底层逻辑:
- FIRST_VALUE():先分区、再排序,默认窗口范围为"分区首行到当前行",因此所有行返回的都是分区首行的目标字段值(无需调整窗口范围)。
- LAST_VALUE():默认窗口范围为"分区首行到当前行",若不调整为"当前行到分区末行",会返回"首行到当前行"的末行(即当前行),无法获取整个分区的末行,因此必须指定帧边界。
使用注意事项:
- 二者均必须搭配ORDER BY子句,排序方向决定首尾行的逻辑(如按薪资降序,首行是最高薪资,末行是最低薪资)。
- LAST_VALUE()必须指定帧边界"ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING",否则结果错误(返回当前行自身),这是最容易踩坑的点。
- 适合场景:每个部门最高/最低薪资(替代MAX/MIN,更直观)、每个用户首单/末单金额、区间首尾数据对比、首末时间统计。
实战案例:每个部门首行(最高薪资)、末行(最低薪资)员工及薪资对比
-- 需求:按部门分组,按薪资降序排序,获取每个部门首行(最高薪资)、末行(最低薪资)的员工姓名和薪资
SELECT
emp_name,
dept_name,
salary,
-- 取分区首行(最高薪资)员工姓名和薪资
FIRST_VALUE(emp_name) OVER (
PARTITION BY dept_name
ORDER BY salary DESC
) AS dept_first_emp,
FIRST_VALUE(salary) OVER (
PARTITION BY dept_name
ORDER BY salary DESC
) AS dept_first_salary,
-- 取分区末行(最低薪资)员工姓名和薪资,必须指定帧边界
LAST_VALUE(emp_name) OVER (
PARTITION BY dept_name
ORDER BY salary DESC
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
) AS dept_last_emp,
LAST_VALUE(salary) OVER (
PARTITION BY dept_name
ORDER BY salary DESC
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
) AS dept_last_salary
FROM emp;
全量执行结果:
| emp_name | dept_name | salary | dept_first_emp | dept_first_salary | dept_last_emp | dept_last_salary |
|---|---|---|---|---|---|---|
| 李四 | 技术部 | 28000.00 | 李四 | 28000.00 | 王五 | 15000.00 |
| 张三 | 技术部 | 15000.00 | 李四 | 28000.00 | 王五 | 15000.00 |
| 王五 | 技术部 | 15000.00 | 李四 | 28000.00 | 王五 | 15000.00 |
| 赵六 | 产品部 | 22000.00 | 赵六 | 22000.00 | 钱七 | 14000.00 |
| 钱七 | 产品部 | 14000.00 | 赵六 | 22000.00 | 钱七 | 14000.00 |
| 孙八 | 运营部 | 12000.00 | 孙八 | 12000.00 | 周九 | 9000.00 |
| 周九 | 运营部 | 9000.00 | 孙八 | 12000.00 | 周九 | 9000.00 |
| 吴十 | 销售部 | 18000.00 | 吴十 | 18000.00 | 郑十一 | 10000.00 |
| 郑十一 | 销售部 | 10000.00 | 吴十 | 18000.00 | 郑十一 | 10000.00 |
逐行结果解析:
- FIRST_VALUE()按dept_name分区、salary降序排序,每个部门所有行均返回分区首行(最高薪资)的员工姓名和薪资,例如技术部首行是李四(28000.00),所有技术部员工的dept_first_emp均为李四,dept_first_salary均为28000.00。
- LAST_VALUE()指定了帧边界"ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING",每个部门所有行均返回分区末行(最低薪资)的员工姓名和薪资,例如技术部末行是王五(15000.00),所有技术部员工的dept_last_emp均为王五,dept_last_salary均为15000.00。
- 若LAST_VALUE()不指定帧边界,会返回"首行到当前行"的末行(即当前行自身),例如技术部张三的dept_last_emp会是张三,而非王五,这是关键易错点,务必注意。
3.4 分布类窗口函数(数据占比/分位数统计专用)
分布类窗口函数核心作用是"分析数据在整个分区内的分布情况",包括数据占比、累积占比、分位数、排名占比等,适合数据占比统计、分位数分析、业绩分布、用户分层等场景。
MySQL内置4个核心分布类窗口函数:CUME_DIST()、PERCENT_RANK()、PERCENTILE_CONT()、PERCENTILE_DISC(),以下逐个拆解。
3.4.1 CUME_DIST():累积分布函数(累积占比专用)
核心定义:在分区内按指定字段排序后,计算当前行的"累积行数占分区总行数的比例",返回值范围为(0,1],相同值的行返回相同的累积分布值;适合累积占比统计(如前N名员工薪资累积占比)、数据分布趋势分析等场景。
语法细节:
-- 语法(无参数,必须搭配ORDER BY)
CUME_DIST() OVER (
PARTITION BY 分区字段 -- 可选,不写则全表计算累积分布
ORDER BY 排序字段 [ASC/DESC] -- 必选,否则分布无意义
) AS 累积分布别名
底层逻辑:先按PARTITION BY划分分区,每个分区内按ORDER BY排序,计算"当前行及之前的行数"除以"分区总行数"的比值,即为当前行的累积分布值;相同值的行,累积行数会合并计算,因此返回相同的分布值。
公式:CUME_DIST() = (当前行及之前的行数) / 分区总行数
使用注意事项:
- 必须搭配ORDER BY子句,排序方向决定累积分布的计算逻辑(如按薪资降序,累积分布值越大,薪资越低;按薪资升序,累积分布值越大,薪资越高)。
- 返回值保留多位小数(MySQL默认6位),可通过ROUND()函数调整小数位数,贴合统计需求(如保留2位小数,体现百分比)。
- 适合场景:部门薪资累积占比、销售额累积占比、用户消费金额累积分布、数据排名累积趋势分析。
实战案例:按部门统计员工薪资累积分布(降序,体现薪资从高到低的累积占比)
-- 需求:按部门分组,按薪资降序排序,计算每个员工薪资的累积分布值(保留2位小数),并转换为百分比格式
SELECT
emp_name,
dept_name,
salary,
-- 计算累积分布值,保留2位小数
ROUND(CUME_DIST() OVER (
PARTITION BY dept_name
ORDER BY salary DESC
), 2) AS cume_dist_value,
-- 转换为百分比格式(拼接%符号)
CONCAT(ROUND(CUME_DIST() OVER (
PARTITION BY dept_name
ORDER BY salary DESC
) * 100, 2), '%') AS cume_dist_percent
FROM emp;
全量执行结果:
| emp_name | dept_name | salary | cume_dist_value | cume_dist_percent |
|---|---|---|---|---|
| 李四 | 技术部 | 28000.00 | 0.33 | 33.33% |
| 张三 | 技术部 | 15000.00 | 1.00 | 100.00% |
| 王五 | 技术部 | 15000.00 | 1.00 | 100.00% |
| 赵六 | 产品部 | 22000.00 | 0.50 | 50.00% |
| 钱七 | 产品部 | 14000.00 | 1.00 | 100.00% |
| 孙八 | 运营部 | 12000.00 | 0.50 | 50.00% |
| 周九 | 运营部 | 9000.00 | 1.00 | 100.00% |
| 吴十 | 销售部 | 18000.00 | 0.50 | 50.00% |
| 郑十一 | 销售部 | 10000.00 | 1.00 | 100.00% |
逐行结果解析:
- 技术部共3名员工,按薪资降序排序:李四(28000.00)是第1行,当前行及之前行数为1,累积分布值=1/3≈0.33(33.33%);张三和王五薪资相同(15000.00),当前行及之前行数为3,累积分布值=3/3=1.00(100.00%),符合相同值返回相同分布值的规则。
- 产品部、运营部、销售部各2名员工,首行员工(薪资最高)的累积分布值均为0.50(50.00%),末行员工(薪资最低)的累积分布值均为1.00(100.00%),逻辑清晰,精准体现薪资从高到低的累积占比。
- 若按薪资升序排序,累积分布值会从低薪资到高薪资累积,例如技术部王五(15000.00)首行,累积分布值为0.33,李四(28000.00)末行,累积分布值为1.00,可根据统计需求调整排序方向。
3.4.2 PERCENT_RANK():百分比排名函数(相对排名占比专用)
核心定义:在分区内按指定字段排序后,计算当前行的"相对排名占比",返回值范围为[0,1],相同值的行返回相同的百分比排名;与CUME_DIST()的区别的是,PERCENT_RANK()基于排名计算,而非行数,适合相对排名占比、数据相对位置分析等场景。
语法细节:
-- 语法(无参数,必须搭配ORDER BY)
PERCENT_RANK() OVER (
PARTITION BY 分区字段 -- 可选,不写则全表计算百分比排名
ORDER BY 排序字段 [ASC/DESC] -- 必选,否则排名无意义
) AS 百分比排名别名
底层逻辑:先按PARTITION BY划分分区,每个分区内按ORDER BY排序,计算当前行的RANK()排名,再通过公式计算百分比排名;相同值的行,RANK()排名相同,因此百分比排名也相同。
公式:PERCENT_RANK() = (当前行RANK()排名 - 1) / (分区总行数 - 1)
使用注意事项:
- 必须搭配ORDER BY子句,排序方向直接影响排名和百分比排名结果;若分区总行数为1,分母为0,返回值为0(无意义,需避免此类场景)。
- 与CUME_DIST()的核心区别:CUME_DIST()基于"行数"计算累积占比,PERCENT_RANK()基于"排名"计算相对占比,前者范围(0,1],后者范围[0,1]。
- 适合场景:员工薪资相对排名占比、销售员业绩相对排名、数据相对位置评估(如某员工薪资排名处于部门前30%)。
实战案例:按部门统计员工薪资百分比排名(降序,体现相对排名占比)
-- 需求:按部门分组,按薪资降序排序,计算每个员工的百分比排名(保留3位小数),并转换为百分比格式
SELECT
emp_name,
dept_name,
salary,
-- 计算RANK()排名,用于验证百分比排名逻辑
RANK() OVER (PARTITION BY dept_name ORDER BY salary DESC) AS emp_rank,
-- 计算百分比排名,保留3位小数
ROUND(PERCENT_RANK() OVER (
PARTITION BY dept_name
ORDER BY salary DESC
), 3) AS percent_rank_value,
-- 转换为百分比格式(保留1位小数)
CONCAT(ROUND(PERCENT_RANK() OVER (
PARTITION BY dept_name
ORDER BY salary DESC
) * 100, 1), '%') AS percent_rank_percent
FROM emp;
全量执行结果:
| emp_name | dept_name | salary | emp_rank | percent_rank_value | percent_rank_percent |
|---|---|---|---|---|---|
| 李四 | 技术部 | 28000.00 | 1 | 0.000 | 0.0% |
| 张三 | 技术部 | 15000.00 | 2 | 0.500 | 50.0% |
| 王五 | 技术部 | 15000.00 | 2 | 0.500 | 50.0% |
| 赵六 | 产品部 | 22000.00 | 1 | 0.000 | 0.0% |
| 钱七 | 产品部 | 14000.00 | 2 | 1.000 | 100.0% |
| 孙八 | 运营部 | 12000.00 | 1 | 0.000 | 0.0% |
| 周九 | 运营部 | 9000.00 | 2 | 1.000 | 100.0% |
| 吴十 | 销售部 | 18000.00 | 1 | 0.000 | 0.0% |
| 郑十一 | 销售部 | 10000.00 | 2 | 1.000 | 100.0% |
逐行结果解析:
- 技术部共3名员工(分区总行数=3),李四RANK()排名=1,百分比排名=(1-1)/(3-1)=0.000(0.0%);张三和王五RANK()排名=2,百分比排名=(2-1)/(3-1)=0.500(50.0%),完全符合公式逻辑。
- 产品部共2名员工(分区总行数=2),赵六RANK()排名=1,百分比排名=(1-1)/(2-1)=0.000(0.0%);钱七RANK()排名=2,百分比排名=(2-1)/(2-1)=1.000(100.0%),清晰体现相对排名占比。
- 对比CUME_DIST():技术部李四的CUME_DIST()为33.33%,PERCENT_RANK()为0.0%,前者体现"行数累积占比",后者体现"相对排名占比",二者适用场景不同,按需选择即可。
3.4.3 PERCENTILE_CONT():连续分位数函数(插值计算专用)
核心定义:在分区内按指定字段排序后,计算指定分位数(如中位数、四分位数)的连续插值结果,返回值可能是分区内不存在的数值,基于线性插值计算;适合需要精准分位数、连续数据分布分析的场景(如统计部门薪资中位数、四分位数)。
语法细节:
-- 语法(2个参数,必须搭配ORDER BY)
PERCENTILE_CONT(分位数) WITHIN GROUP (
ORDER BY 排序字段 [ASC/DESC]
) OVER (PARTITION BY 分区字段) AS 分位数别名
-- 参数说明:
-- 分位数:取值范围[0,1],如0.5(中位数)、0.25(下四分位数)、0.75(上四分位数)
-- WITHIN GROUP:固定语法,用于指定排序字段,不可省略
-- PARTITION BY:可选,不写则全表计算分位数
底层逻辑:先按PARTITION BY划分分区,每个分区内按ORDER BY排序,根据指定分位数,通过线性插值公式计算分位数结果;若分位数对应的数据位置不是整数,会在相邻两个数据之间进行插值,因此返回值可能是分区内不存在的数值。
使用注意事项:
- 必须搭配WITHIN GROUP (ORDER BY 排序字段),否则语法报错;排序字段需为数值类型(如薪资、金额),无法对字符串类型排序。
- 分位数取值范围必须是[0,1],超出范围会报错;常用分位数:0.5(中位数)、0.25(Q1)、0.75(Q3)、0.1(10分位数)。
- 适合场景:薪资中位数统计、用户消费金额四分位数分析、连续数据分布的分位数计算,需要精准插值结果的场景。
实战案例:按部门统计员工薪资的中位数(分位数0.5)和四分位数(0.25、0.75)
-- 需求:按部门分组,统计每个部门薪资的下四分位数(0.25)、中位数(0.5)、上四分位数(0.75)
SELECT
dept_name,
-- 下四分位数(Q1)
ROUND(PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY salary DESC) OVER (PARTITION BY dept_name), 2) AS q1_salary,
-- 中位数(Q2)
ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary DESC) OVER (PARTITION BY dept_name), 2) AS median_salary,
-- 上四分位数(Q3)
ROUND(PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY salary DESC) OVER (PARTITION BY dept_name), 2) AS q3_salary
FROM emp
GROUP BY dept_name; -- 按部门分组,只显示每个部门的分位数结果
全量执行结果:
| dept_name | q1_salary | median_salary | q3_salary |
|---|---|---|---|
| 技术部 | 21500.00 | 15000.00 | 15000.00 |
| 产品部 | 20000.00 | 18000.00 | 16000.00 |
| 运营部 | 11250.00 | 10500.00 | 9750.00 |
| 销售部 | 16000.00 | 14000.00 | 12000.00 |
逐行结果解析:
- 技术部薪资排序(降序):28000.00、15000.00、15000.00,共3行;中位数(0.5)对应位置=3 0.5=1.5,线性插值计算:28000.00 + (15000.00 - 28000.00)(1.5-1) = 15000.00,与实际结果一致。
- 产品部薪资排序(降序):22000.00、14000.00,共2行;中位数对应位置=2 0.5=1,插值计算:22000.00 + (14000.00 - 22000.00)(1-1) = 18000.00(介于两者之间),体现连续插值特性。
- PERCENTILE_CONT()返回的分位数可能是分区内不存在的数值(如产品部中位数18000.00),适合需要精准插值、连续数据分布分析的场景,如财务统计、薪资分析等。
3.4.4 PERCENTILE_DISC():离散分位数函数(取实际值专用)
核心定义:与PERCENTILE_CONT()逻辑类似,在分区内按指定字段排序后,计算指定分位数的离散结果,返回值必须是分区内实际存在的数值,不进行插值计算;适合需要分位数为实际数据、离散数据分布分析的场景(如用户消费金额分位数,需取实际消费值)。
语法细节(与PERCENTILE_CONT()完全一致,仅计算逻辑不同):
-- 语法(2个参数,必须搭配ORDER BY)
PERCENTILE_DISC(分位数) WITHIN GROUP (
ORDER BY 排序字段 [ASC/DESC]
) OVER (PARTITION BY 分区字段) AS 分位数别名
-- 参数说明与PERCENTILE_CONT()一致,分位数取值范围[0,1]
底层逻辑:先按PARTITION BY划分分区,每个分区内按ORDER BY排序,根据指定分位数,找到对应位置的行,返回该行的目标字段值;若分位数对应的数据位置不是整数,会向上取整(或取不小于该位置的最小整数),确保返回值是分区内实际存在的数值。
使用注意事项:
- 必须搭配WITHIN GROUP (ORDER BY 排序字段),排序字段需为数值类型;分位数取值范围[0,1],超出范围报错。
- 与PERCENTILE_CONT()的核心区别:PERCENTILE_DISC()返回分区内实际存在的数值(离散),PERCENTILE_CONT()返回插值结果(连续);前者适合离散数据,后者适合连续数据。
- 适合场景:用户消费金额分位数(取实际消费值)、员工薪资分位数(取实际薪资)、离散数据的分位数统计,不需要插值的场景。
实战案例:按部门统计员工薪资的离散分位数(对比连续分位数,突出差异)
-- 需求:按部门分组,统计每个部门薪资的中位数(0.5),同时展示连续分位数和离散分位数,对比差异
SELECT
dept_name,
-- 连续分位数(插值,可能为非实际值)
ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary DESC) OVER (PARTITION BY dept_name), 2) AS cont_median,
-- 离散分位数(取实际值)
ROUND(PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY salary DESC) OVER (PARTITION BY dept_name), 2) AS disc_median
FROM emp
GROUP BY dept_name;
全量执行结果:
| dept_name | cont_median | disc_median |
|---|---|---|
| 技术部 | 15000.00 | 15000.00 |
| 产品部 | 18000.00 | 22000.00 |
| 运营部 | 10500.00 | 12000.00 |
| 销售部 | 14000.00 | 18000.00 |
逐行结果解析:
- 技术部共3行数据,中位数位置=3*0.5=1.5,PERCENTILE_DISC()向上取整为2,返回第2行的薪资15000.00(实际存在);PERCENTILE_CONT()插值计算结果也为15000.00,二者一致。
- 产品部共2行数据,中位数位置=2*0.5=1,PERCENTILE_DISC()返回第1行的薪资22000.00(实际存在);PERCENTILE_CONT()插值计算结果为18000.00(非实际存在),二者差异明显,体现核心区别。
- 选择建议:若需要分位数为实际业务数据(如薪资、消费金额),用PERCENTILE_DISC();若需要精准的插值分位数(如统计分析、学术研究),用PERCENTILE_CONT()。
本文由mdnice多平台发布