【MySQL】函数:窗口函数


一、核心基础

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 计算结果别名

关键字逐句拆解(结合开发场景,说明使用注意事项):

  1. 窗口函数名:MySQL内置的窗口函数分为四大类(排名类、聚合类、跨行取值类、分布类),不同函数的参数要求不同,例如排名类函数无需参数,分位数函数需要指定分位值参数,后续会详细讲解。
  2. OVER关键字 :窗口函数的**唯一标识,必须存在!**如果省略OVER子句,该函数会变成普通聚合函数,这是区分窗口函数与普通聚合函数的关键。
  3. PARTITION BY子句:分区关键字,核心作用是将整个数据集按指定字段拆分为多个独立的子窗口,每个子窗口内的计算相互独立。例如"PARTITION BY dept_name"表示按部门拆分窗口,技术部、产品部、销售部分别作为独立窗口计算。省略PARTITION BY时,整张表作为一个统一窗口计算。
  4. ORDER BY子句:分区内排序关键字,用于指定每个子窗口内的数据排序规则,直接影响排名类、累计类、跨行取值类窗口函数的结果。注意:排名类函数(ROW_NUMBER()、RANK()等)必须搭配ORDER BY使用,否则排名结果混乱;累计类函数(SUM()、AVG()等)搭配ORDER BY时,默认按"分区首行到当前行"累计计算。
  5. ROWS/RANGE子句(帧边界):用于精确控制窗口内参与计算的行范围,是窗口函数精细化计算的核心,分为ROWS和RANGE两种类型,二者差异显著,开发中需按需选择:
  6. ROWS:基于物理行定位,精准控制行数,例如"ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING"表示"当前行向前1行 + 当前行 + 当前行向后1行",共3行数据参与计算。开发首选ROWS,性能极高,适合明确行数范围的场景(如移动平均、近N行统计)。
  7. 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多平台发布

相关推荐
青柠代码录21 小时前
【MySQL】增删改查(CRUD)手册
程序人生
郝学胜-神的一滴1 天前
【技术实战】500G单行大文件读取难题破解!生成器+自定义函数最优方案解析
开发语言·python·程序人生·面试
婷婷_1721 天前
【PCIe验证每日学习·阶段复盘01】Day1~Day7 纯理论深度复盘
网络·程序人生·芯片·每日学习·pcie 验证·ic 验证·pcie学习
oi..1 天前
python Get/Post请求练习
开发语言·经验分享·笔记·python·程序人生·安全·网络安全
码上生存指南1 天前
技术栈要不要追新?我为此换过一次工作,结论是……
java·程序人生
一次旅行2 天前
今日心理学知识分享(三)
开发语言·javascript·程序人生·ecmascript
郝学胜-神的一滴2 天前
Pytorch张量核心运算精讲:从类型转换到数值操作全解析
开发语言·人工智能·pytorch·python·深度学习·程序人生·机器学习
敲代码的嘎仔3 天前
Java后端开发——真实面试汇总(持续更新)
java·开发语言·程序人生·面试·职场和发展·八股
郝学胜-神的一滴3 天前
深入解析:生成器在UserList中的应用与Python可迭代对象实现原理
开发语言·python·程序人生·算法