窗口函数:SQL界的“任意门”,带你玩转复杂数据分析 🧙♂️
各位观众老爷们,大家好!欢迎来到“SQL魔法学院”,我是你们的魔法导师——“数据挖掘老司机”。今天,我们要一起探索SQL世界里一个非常酷炫的魔法——窗口函数(Window Functions)。
如果你还在用GROUP BY苦苦挣扎,为复杂的报表统计抓耳挠腮,那么,恭喜你,今天你将开启一扇通往新世界的大门!窗口函数就像SQL界的“任意门”,能让你在不改变原始数据颗粒度的前提下,进行各种复杂的数据分析,简直是程序员居家旅行、升职加薪的必备良药!
一、初识窗口函数:打破聚合的“次元壁”
想象一下,你在一家电商公司工作,老板突然甩给你一个需求: “小伙子,给我统计一下每个用户的订单金额,还要知道他每次订单金额占总订单金额的比例!”
你眉头一皱,心想: “这还不简单,GROUP BY一下就完事了!”
SELECT
user_id,
order_id,
order_amount,
SUM(order_amount) OVER (PARTITION BY user_id) AS total_order_amount,
order_amount / SUM(order_amount) OVER (PARTITION BY user_id) AS proportion
FROM
orders;
当你正准备沾沾自喜的时候,老板又发话了:“不对不对,我还要看到每个订单金额相对于该用户平均订单金额的偏差,还要知道这个用户在所有用户中的订单排名!”
What?! 😱
这时候,GROUP BY就显得力不从心了。因为它会将数据分组,聚合后只保留分组后的结果,原始的订单信息就丢失了。
而窗口函数,就像一位优雅的舞者,它在原始数据的基础上,开辟了一个“窗口”,在这个窗口内进行计算,然后将结果返回到每一行数据中,而不会改变原始数据的结构。
简单来说,窗口函数既能进行聚合计算,又能保留原始数据的细节。 这就是它与GROUP BY的最大区别,也是它强大的根源。
二、窗口函数的基本语法:解锁“任意门”的咒语
窗口函数的语法结构并不复杂,掌握了几个关键的咒语,就能轻松驾驭它:
函数名 (参数) OVER (
[PARTITION BY 列名1, 列名2, ...]
[ORDER BY 列名 [ASC | DESC]]
[ROWS | RANGE BETWEEN 边界规则1 AND 边界规则2]
)
我们来逐一解读这些咒语:
- 函数名 (参数):这是窗口函数的核心,可以是聚合函数(SUM, AVG, COUNT, MAX, MIN),也可以是排名函数(RANK, DENSE_RANK, ROW_NUMBER),甚至是偏移函数(LAG, LEAD)。
- OVER(): 这是窗口函数的标志,告诉SQL引擎,这是一个窗口函数,而不是普通的聚合函数。
- PARTITION BY 列名1, 列名2, …:这是分组咒语,它将数据按照指定的列进行分组,每个分组就是一个窗口。类似于GROUP BY,但不会改变原始数据的结构。
- ORDER BY 列名 [ASC | DESC]:这是排序咒语,它指定了窗口内数据的排序方式,通常与排名函数一起使用。
- ROWS | RANGE BETWEEN 边界规则1 AND 边界规则2:这是窗口大小咒语,它定义了窗口的范围,也就是在哪些行之间进行计算。这个咒语比较复杂,我们稍后会详细讲解。
三、窗口函数家族:各有千秋的魔法师
窗口函数家族非常庞大,但我们常用的主要有以下几种:
-
聚合函数 (Aggregate Functions):
- SUM():计算窗口内数值的总和。
- AVG():计算窗口内数值的平均值。
- COUNT():计算窗口内非空值的数量。
- MAX():计算窗口内数值的最大值。
- MIN():计算窗口内数值的最小值。
-
排名函数 (Ranking Functions):
- RANK():为窗口内的每一行数据分配一个排名,如果有并列的排名,则跳过后续排名。
- DENSE_RANK():为窗口内的每一行数据分配一个排名,如果有并列的排名,则不跳过后续排名。
- ROW_NUMBER():为窗口内的每一行数据分配一个唯一的行号,从1开始递增。
- NTILE(n):将窗口内的数据分成n组,并为每一行数据分配一个组号。
-
偏移函数 (Offset Functions):
- LAG(column, n, default):返回窗口内当前行之前第n行的指定列的值。如果前面没有n行,则返回default值。
- LEAD(column, n, default):返回窗口内当前行之后第n行的指定列的值。如果后面没有n行,则返回default值。
-
值函数 (Value Functions):
- FIRST_VALUE(column):返回窗口内第一行的指定列的值。
- LAST_VALUE(column):返回窗口内最后一行的指定列的值。
- NTH_VALUE(column, n):返回窗口内第n行的指定列的值。
四、窗口大小:控制魔法的范围
窗口大小咒语(ROWS | RANGE BETWEEN 边界规则1 AND 边界规则2
)是窗口函数中最复杂的部分,它决定了窗口的范围,也就是在哪些行之间进行计算。
- ROWS: 基于行号定义窗口范围。
- RANGE: 基于值定义窗口范围。
边界规则可以是以下几种:
- UNBOUNDED PRECEDING:窗口从第一行开始。
- UNBOUNDED FOLLOWING:窗口到最后一行结束。
- CURRENT ROW:窗口包含当前行。
- n PRECEDING:窗口包含当前行之前的n行。
- n FOLLOWING:窗口包含当前行之后的n行。
举个例子:
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
:窗口包含从第一行到当前行的所有行。ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
:窗口包含当前行以及前一行和后一行,也就是滑动窗口。RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
: 窗口包含分区起始行到当前行,如果当前行有相同值的行,都包含进来。RANGE BETWEEN 10 PRECEDING AND 10 FOLLOWING
:窗口包含订单金额在当前订单金额前后10元范围内的所有订单。
注意: RANGE
只能用于数值型或日期型数据,并且必须与 ORDER BY
子句一起使用。
五、实战演练:让魔法照进现实
理论讲了这么多,是时候来点真枪实弹了!我们继续用电商公司的例子,看看窗口函数如何解决老板提出的各种复杂需求。
假设我们有以下订单数据表 orders
:
user_id | order_id | order_date | order_amount |
---|---|---|---|
1 | 101 | 2023-01-01 | 100 |
1 | 102 | 2023-01-05 | 150 |
1 | 103 | 2023-01-10 | 200 |
2 | 201 | 2023-01-02 | 50 |
2 | 202 | 2023-01-07 | 75 |
3 | 301 | 2023-01-03 | 250 |
3 | 302 | 2023-01-08 | 300 |
-
计算每个用户的订单总金额和订单数量:
SELECT user_id, order_id, order_amount, COUNT(*) OVER (PARTITION BY user_id) AS total_orders, SUM(order_amount) OVER (PARTITION BY user_id) AS total_order_amount FROM orders;
-
计算每个订单金额占用户总订单金额的比例:
SELECT user_id, order_id, order_amount, order_amount / SUM(order_amount) OVER (PARTITION BY user_id) AS proportion FROM orders;
-
计算每个订单金额相对于该用户平均订单金额的偏差:
SELECT user_id, order_id, order_amount, order_amount - AVG(order_amount) OVER (PARTITION BY user_id) AS deviation FROM orders;
-
计算每个用户在所有用户中的订单排名(按照订单总金额排序):
SELECT user_id, SUM(order_amount) AS total_order_amount, RANK() OVER (ORDER BY SUM(order_amount) DESC) AS order_rank FROM orders GROUP BY user_id;
-
计算每个用户上一次和下一次的订单金额:
SELECT user_id, order_id, order_date, order_amount, LAG(order_amount, 1, 0) OVER (PARTITION BY user_id ORDER BY order_date) AS previous_order_amount, LEAD(order_amount, 1, 0) OVER (PARTITION BY user_id ORDER BY order_date) AS next_order_amount FROM orders;
-
计算每个用户的订单金额的移动平均值(窗口大小为3):
SELECT user_id, order_id, order_date, order_amount, AVG(order_amount) OVER (PARTITION BY user_id ORDER BY order_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS moving_average FROM orders;
通过这些例子,我们可以看到,窗口函数可以轻松解决各种复杂的报表统计和数据分析问题。
六、窗口函数的性能优化:让魔法更快更强
虽然窗口函数很强大,但如果使用不当,也会带来性能问题。以下是一些窗口函数性能优化的建议:
- 合理使用索引: 窗口函数通常需要排序和分组,因此,在相关的列上建立索引可以提高查询效率。
- 避免过度使用窗口函数: 窗口函数的计算成本较高,应尽量避免在同一个查询中使用过多的窗口函数。
- 选择合适的窗口大小: 窗口大小会影响计算的复杂程度,应根据实际需求选择合适的窗口大小。
- 利用物化视图: 对于一些常用的窗口函数计算结果,可以将其物化为视图,以避免重复计算。
- 了解数据库的优化器: 不同的数据库对窗口函数的优化策略不同,了解数据库的优化器可以帮助你编写更高效的SQL语句。
七、总结:掌握窗口函数,开启数据分析新篇章
窗口函数是SQL中一个非常重要的特性,它可以让你在不改变原始数据颗粒度的前提下,进行各种复杂的数据分析。掌握窗口函数,可以大大提高你的数据分析能力,让你在职场上更加游刃有余。
希望今天的讲解能够帮助你理解窗口函数的原理和应用。记住,熟能生巧,多加练习,你也能成为SQL魔法师!🧙♂️
最后,送大家一句至理名言: “数据在手,天下我有!”
感谢大家的观看,我们下期再见! 👋