各位观众,大家好!我是今天的主讲人,外号“SQL老司机”。今天咱们不聊那些虚头巴脑的理论,直接上干货,聊聊MySQL 8.0里让人眼前一亮的Window Functions
(窗口函数)。这玩意儿啊,就像给你的SQL加了个“透视镜”,让你不用写一大堆让人头疼的JOIN
,也能轻松搞定各种复杂的数据分析。
一、 窗口函数是啥? 别慌,我来给你“解剖”一下
简单来说,窗口函数就是在指定的数据“窗口”上进行计算的函数。这个“窗口”可不是你家窗户,而是由OVER()
子句定义的,它决定了函数计算的数据范围。
传统的聚合函数(比如SUM()
, AVG()
, COUNT()
)会把多行数据聚合成一行,但是窗口函数不会,它会为每一行都返回一个值,这个值是基于当前行所在的窗口计算出来的。听起来有点绕?别急,咱们用例子说话。
二、 窗口函数的基本语法:OVER()
子句
OVER()
子句是窗口函数的核心,它定义了窗口的规则。最简单的OVER()
子句就是OVER()
,表示整个结果集都是窗口。当然,这没什么意义,咱们一般会配合以下参数使用:
PARTITION BY
: 把数据分成多个“分区”,每个分区都是一个独立的窗口。ORDER BY
: 在每个分区内,按照指定的列进行排序。ROWS/RANGE BETWEEN
: 定义窗口的范围,也就是当前行的前N行到后M行。
三、 窗口函数家族大阅兵:常用函数介绍
MySQL 8.0提供了丰富的窗口函数,咱们挑几个最常用的来重点讲讲:
-
聚合函数 (Aggregates as Window Functions):
SUM()
,AVG()
,COUNT()
,MIN()
,MAX()
这些函数摇身一变,也能当窗口函数用。 它们在窗口内进行聚合计算,并为每一行返回聚合结果。
例子:计算每个部门的工资总额,并显示每个员工的工资和部门总工资。
假设我们有张员工表
employees
:CREATE TABLE employees ( id INT PRIMARY KEY, department VARCHAR(50), salary DECIMAL(10, 2) ); INSERT INTO employees (id, department, salary) VALUES (1, '销售部', 5000.00), (2, '销售部', 6000.00), (3, '技术部', 8000.00), (4, '技术部', 9000.00), (5, '行政部', 4000.00);
使用窗口函数:
SELECT id, department, salary, SUM(salary) OVER (PARTITION BY department) AS department_total_salary FROM employees;
结果:
id department salary department_total_salary 5 行政部 4000.00 4000.00 3 技术部 8000.00 17000.00 4 技术部 9000.00 17000.00 1 销售部 5000.00 11000.00 2 销售部 6000.00 11000.00 解释:
SUM(salary) OVER (PARTITION BY department)
的意思是,按照department
分组,然后在每个组内计算salary
的总和。 注意,每一行都显示了该行员工的工资以及其所在部门的工资总额。 -
排序函数 (Ranking Functions):
RANK()
,DENSE_RANK()
,ROW_NUMBER()
,NTILE()
这些函数可以为每一行分配一个排名,基于指定的排序规则。
RANK()
: 排名,如果有并列,会跳过后面的排名。DENSE_RANK()
: 排名,如果有并列,不会跳过后面的排名。ROW_NUMBER()
: 为每一行分配一个唯一的序号,即使有并列。NTILE(n)
: 将结果集分成n组,并为每一行分配组号。
例子:按照工资高低给员工排名。
SELECT id, department, salary, RANK() OVER (ORDER BY salary DESC) AS salary_rank, DENSE_RANK() OVER (ORDER BY salary DESC) AS salary_dense_rank, ROW_NUMBER() OVER (ORDER BY salary DESC) AS salary_row_number FROM employees;
结果:
id department salary salary_rank salary_dense_rank salary_row_number 4 技术部 9000.00 1 1 1 3 技术部 8000.00 2 2 2 2 销售部 6000.00 3 3 3 1 销售部 5000.00 4 4 4 5 行政部 4000.00 5 5 5 解释:
RANK() OVER (ORDER BY salary DESC)
: 按照工资降序排列,并计算排名。DENSE_RANK() OVER (ORDER BY salary DESC)
: 按照工资降序排列,并计算紧密排名(没有排名跳跃)。ROW_NUMBER() OVER (ORDER BY salary DESC)
: 按照工资降序排列,并分配唯一的行号。
例子:将员工按照工资分成3组。
SELECT id, department, salary, NTILE(3) OVER (ORDER BY salary DESC) AS salary_group FROM employees;
结果:
id department salary salary_group 4 技术部 9000.00 1 3 技术部 8000.00 1 2 销售部 6000.00 2 1 销售部 5000.00 2 5 行政部 4000.00 3 解释:
NTILE(3) OVER (ORDER BY salary DESC)
: 按照工资降序排列,并将员工分成3组。 -
值函数 (Value Functions):
LAG()
,LEAD()
,FIRST_VALUE()
,LAST_VALUE()
,NTH_VALUE()
这些函数可以访问窗口中其他行的值。
LAG(column, n, default)
: 返回当前行之前第n行的column
的值。 如果没有前n行,则返回default
。LEAD(column, n, default)
: 返回当前行之后第n行的column
的值。 如果没有后n行,则返回default
。FIRST_VALUE(column)
: 返回窗口中第一行的column
的值。LAST_VALUE(column)
: 返回窗口中最后一行的column
的值。 默认情况下,LAST_VALUE
会受到窗口帧的限制,需要显式指定帧的范围才能正确使用。NTH_VALUE(column, n)
: 返回窗口中第n行的column
的值。
例子:计算每个员工的工资与前一个员工的工资差。
SELECT id, department, salary, LAG(salary, 1, 0) OVER (ORDER BY id) AS prev_salary, salary - LAG(salary, 1, 0) OVER (ORDER BY id) AS salary_diff FROM employees;
结果:
id department salary prev_salary salary_diff 1 销售部 5000.00 0.00 5000.00 2 销售部 6000.00 5000.00 1000.00 3 技术部 8000.00 6000.00 2000.00 4 技术部 9000.00 8000.00 1000.00 5 行政部 4000.00 9000.00 -5000.00 解释:
LAG(salary, 1, 0) OVER (ORDER BY id)
:按照id
排序,返回当前行之前一行的salary
值。 如果当前行是第一行,则返回0。例子:获取每个部门工资最高的员工的工资。
SELECT id, department, salary, FIRST_VALUE(salary) OVER (PARTITION BY department ORDER BY salary DESC) AS highest_salary FROM employees;
结果:
id department salary highest_salary 5 行政部 4000.00 4000.00 4 技术部 9000.00 9000.00 3 技术部 8000.00 9000.00 2 销售部 6000.00 6000.00 1 销售部 5000.00 6000.00 解释:
FIRST_VALUE(salary) OVER (PARTITION BY department ORDER BY salary DESC)
: 按照department
分组,然后在每个组内按照salary
降序排列,并返回每个组的第一行的salary
值,也就是工资最高的员工的工资。
四、 窗口帧 (Window Framing):精细控制窗口范围
窗口帧允许你更精确地定义窗口的范围。 它使用ROWS
或RANGE
子句来指定窗口的起始和结束位置。
ROWS BETWEEN
: 基于行数定义窗口范围。RANGE BETWEEN
: 基于排序后的值定义窗口范围。
常用的窗口帧选项:
UNBOUNDED PRECEDING
: 窗口从分区的第一行开始。n PRECEDING
: 窗口从当前行之前的n行开始。CURRENT ROW
: 窗口从当前行开始或结束。n FOLLOWING
: 窗口从当前行之后的n行结束。UNBOUNDED FOLLOWING
: 窗口到分区的最后一行结束。
例子:计算每个员工及其前后一个员工的工资总和(滑动平均)。
SELECT
id,
department,
salary,
SUM(salary) OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS moving_average
FROM
employees;
结果:
id | department | salary | moving_average |
---|---|---|---|
1 | 销售部 | 5000.00 | 11000.00 |
2 | 销售部 | 6000.00 | 19000.00 |
3 | 技术部 | 8000.00 | 23000.00 |
4 | 技术部 | 9000.00 | 21000.00 |
5 | 行政部 | 4000.00 | 13000.00 |
解释:SUM(salary) OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING)
: 按照id
排序,然后计算当前行及其前一行和后一行的salary
总和。
五、 窗口函数 VS. GROUP BY
:谁更胜一筹?
GROUP BY
也能进行聚合计算,那窗口函数和它有什么区别呢?
特性 | GROUP BY |
窗口函数 |
---|---|---|
返回结果 | 将多行聚合成一行 | 为每一行都返回一个值 |
数据粒度 | 改变结果集的粒度 | 不改变结果集的粒度 |
适用场景 | 需要对数据进行分组聚合时 | 需要在不改变数据粒度的前提下进行分析时 |
是否需要JOIN |
某些复杂场景可能需要JOIN |
通常可以避免JOIN |
简单来说,GROUP BY
是“瘦身”,窗口函数是“透视”。 如果你只想知道每个组的汇总结果,用GROUP BY
。 如果你想在每一行数据旁边都显示一些聚合信息,用窗口函数。
六、 窗口函数的应用场景:让你的数据分析更上一层楼
窗口函数在数据分析领域应用广泛,可以解决各种复杂的问题。 比如:
- 计算累计总和、移动平均: 统计每日销售额的累计总和,或者计算最近7天的销售额平均值。
- 计算排名和百分比: 对销售人员进行业绩排名,或者计算每个客户的消费金额占总消费金额的百分比。
- 查找最大值、最小值: 找出每个部门工资最高的员工,或者找出每个月销售额最低的产品。
- 计算同比、环比: 计算今年销售额与去年同期的增长率,或者计算本月销售额与上个月的增长率。
- 复杂报表统计: 灵活生成各种复杂的报表,例如帕累托分析、ABC分析等。
七、 窗口函数的性能优化:别让它成为你的“猪队友”
窗口函数虽然强大,但如果使用不当,也会影响查询性能。 以下是一些优化建议:
- 尽量使用索引:
PARTITION BY
和ORDER BY
子句中使用的列,最好都有索引。 - 避免在窗口函数中使用复杂的表达式: 复杂的表达式会增加计算成本。
- 合理选择窗口帧: 窗口帧的范围越小,计算量越小。
- MySQL版本要给力: 升级到MySQL 8.0的最新版本,享受性能优化。
- 具体情况具体分析: 针对具体的查询,进行性能测试和优化。
八、 案例分析:不用JOIN
也能飞
假设我们有两张表:orders
(订单表) 和 customers
(客户表)。
orders
表:
CREATE TABLE orders (
order_id INT PRIMARY KEY,
customer_id INT,
order_date DATE,
amount DECIMAL(10, 2)
);
INSERT INTO orders (order_id, customer_id, order_date, amount) VALUES
(1, 101, '2023-01-01', 100.00),
(2, 101, '2023-01-05', 150.00),
(3, 102, '2023-01-10', 200.00),
(4, 101, '2023-02-01', 120.00),
(5, 102, '2023-02-15', 180.00),
(6, 103, '2023-03-01', 250.00);
customers
表:
CREATE TABLE customers (
customer_id INT PRIMARY KEY,
customer_name VARCHAR(50)
);
INSERT INTO customers (customer_id, customer_name) VALUES
(101, '张三'),
(102, '李四'),
(103, '王五');
需求:查询每个客户的订单总金额,以及该客户的订单总金额占所有客户订单总金额的百分比。
如果不用窗口函数,我们需要先用GROUP BY
计算每个客户的订单总金额,然后再JOIN
到总金额表,才能计算百分比。 代码如下:
SELECT
c.customer_name,
customer_total.total_amount,
customer_total.total_amount / (SELECT SUM(amount) FROM orders) * 100 AS percentage
FROM
customers c
JOIN
(SELECT customer_id, SUM(amount) AS total_amount FROM orders GROUP BY customer_id) AS customer_total
ON
c.customer_id = customer_total.customer_id;
是不是感觉有点绕? 现在,让我们用窗口函数来简化这个查询:
SELECT
c.customer_name,
SUM(o.amount) OVER (PARTITION BY c.customer_id) AS total_amount,
SUM(o.amount) OVER (PARTITION BY c.customer_id) / SUM(o.amount) OVER () * 100 AS percentage
FROM
customers c
JOIN
orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.customer_name;
结果:
customer_name | total_amount | percentage |
---|---|---|
张三 | 370.00 | 35.57692307692308 |
李四 | 380.00 | 36.53846153846153 |
王五 | 250.00 | 24.03846153846154 |
解释:
SUM(o.amount) OVER (PARTITION BY c.customer_id)
: 按照customer_id
分组,计算每个客户的订单总金额。SUM(o.amount) OVER ()
: 计算所有订单的总金额。-
SUM(o.amount) OVER (PARTITION BY c.customer_id) / SUM(o.amount) OVER () * 100
: 计算每个客户的订单总金额占所有订单总金额的百分比。这个查询避免了复杂的
JOIN
操作,代码更简洁,可读性也更高。
九、 总结:窗口函数,你的SQL超能力
今天我们一起探索了MySQL 8.0的窗口函数,学习了它的基本语法、常用函数和应用场景。 窗口函数就像给你的SQL注入了一剂“超能力”,让你能够以更优雅、更高效的方式进行数据分析。
记住,掌握窗口函数,你就能:
- 摆脱复杂的
JOIN
,简化你的SQL代码。 - 在不改变数据粒度的前提下,进行各种聚合计算。
- 轻松解决各种复杂的数据分析问题。
- 让你的数据分析报告更加清晰易懂。
所以,赶紧操练起来,让窗口函数成为你数据分析的得力助手吧!
希望今天的分享对大家有所帮助。 感谢各位的观看,咱们下次再见!