一文搞懂 SQL 窗口函数:告别子查询和临时表,解锁高级数据分析的利器
在 SQL 的世界里,我们经常需要对数据进行聚合计算,比如计算总和、平均值、最大最小值等。最常用的工具是 GROUP BY
子句,它可以将数据按某个或多个列分组,然后对每个组进行聚合。然而,GROUP BY
的一个固有局限性是,它会将原始行合并成组,我们无法在同一查询结果中既看到聚合值,又保留原始的明细行数据。
想象一下这样的场景:
- 你想计算每个部门的平均工资, 同时 显示每个员工的工资以及他们与部门平均工资的差距。
- 你想计算一个用户历史订单的总金额, 同时 列出用户的每一笔订单。
- 你想对每个销售区域内的销售额进行排名, 同时 保留每一条销售记录。
- 你想计算每天的销售额以及前一天的销售额,以便分析趋势或计算环比增长。
- 你想计算一个用户从注册至今的累计消费金额。
这些任务用传统的 GROUP BY
很难甚至不可能在一个查询中优雅地完成。你可能需要使用子查询、临时表、甚至多次查询,这会让 SQL 代码变得复杂、难以阅读,并且可能影响性能。
幸运的是,SQL 提供了一个强大的功能来解决这些问题——窗口函数(Window Functions)。窗口函数在某种程度上类似于聚合函数,但它们的独特之处在于,它们计算的是与当前行“相关”的一组行(一个“窗口”)的数据,并且不会像 GROUP BY
那样将行合并。这意味着,使用窗口函数,你可以在保留原始行明细的同时,为每一行添加一个基于窗口计算出的值。
本文将带你深入了解 SQL 窗口函数,从基本概念到高级用法,助你彻底掌握这一数据分析利器。
什么是 SQL 窗口函数?
简单来说,窗口函数是在与当前查询行相关的一组行上执行计算的函数。这组行被称为“窗口”。
与普通聚合函数的区别:
- 不合并行: 窗口函数不会减少输出的行数。输出结果集的行数与输入结果集的行数相同。
- 针对窗口计算: 计算是针对窗口内的行进行的,而不是整个结果集或
GROUP BY
分组。 - 可以在
SELECT
和ORDER BY
子句中使用: 这使得我们可以在选取数据的同时进行复杂的分析计算。
窗口函数的通用语法如下:
sql
window_function(arg1, arg2, ...) OVER ([PARTITION BY column1, column2, ...] [ORDER BY column3 ASC/DESC, ...] [frame_clause])
让我们分解一下这个语法中的各个组成部分:
window_function(arg1, arg2, ...)
: 这是具体的窗口函数名称,比如SUM()
、AVG()
、ROW_NUMBER()
、RANK()
、LAG()
、LEAD()
等。它可以接受参数,就像普通函数一样。OVER()
: 这个关键字是窗口函数的标志。它告诉数据库,前面的函数是一个窗口函数,而不是一个普通的聚合函数或标量函数。OVER()
括号内的部分定义了“窗口”:[PARTITION BY column1, column2, ...]
: 分区子句。 它将结果集划分为独立的、互不影响的区域(分区)。窗口函数在每个分区内独立计算。这非常类似于GROUP BY
的分组概念,但它不合并行。如果没有PARTITION BY
子句,整个结果集将被视为一个单一的分区。[ORDER BY column3 ASC/DESC, ...]
: 排序子句。 它定义了在每个分区内(或整个结果集内,如果没有PARTITION BY
)行的顺序。对于某些窗口函数(如排名函数、顺序敏感的聚合函数如累计和、以及LAG
/LEAD
函数),ORDER BY
子句是必需的或非常重要的。它决定了窗口函数在处理行时的逻辑顺序。[frame_clause]
: 窗口帧子句。 这是窗口函数中最灵活(也可能是最复杂)的部分。它定义了当前行的“窗口”的具体范围。这个范围是基于ORDER BY
子句定义的顺序来确定的。默认的窗口帧取决于是否有ORDER BY
子句。
OVER()
子句的组成部分详解
理解 OVER()
子句的各个部分是掌握窗口函数的关键。
1. OVER()
– 窗口函数标识符
这是最基础的部分。只要在函数后面加上 OVER()
,它就变成了一个窗口函数。
示例:
计算所有员工的平均工资,并将这个平均值显示在每一行员工记录旁边。
假设我们有一个 Employees
表:
EmployeeID | Name | Department | Salary |
---|---|---|---|
1 | Alice | Sales | 50000 |
2 | Bob | IT | 60000 |
3 | Charlie | Sales | 55000 |
4 | David | IT | 62000 |
5 | Eve | Sales | 52000 |
sql
SELECT
EmployeeID,
Name,
Department,
Salary,
AVG(Salary) OVER() AS TotalAverageSalary
FROM
Employees;
结果:
EmployeeID | Name | Department | Salary | TotalAverageSalary |
---|---|---|---|---|
1 | Alice | Sales | 50000 | 55800.00 |
2 | Bob | IT | 60000 | 55800.00 |
3 | Charlie | Sales | 55000 | 55800.00 |
4 | David | IT | 62000 | 55800.00 |
5 | Eve | Sales | 52000 | 55800.00 |
可以看到,AVG(Salary) OVER()
计算了整个结果集的平均工资(50000+60000+55000+62000+52000 / 5 = 55800),并将这个值复制到了每一行。这里 OVER()
是空的,表示窗口是整个结果集。
2. PARTITION BY
– 划分窗口
PARTITION BY
将结果集切分成独立的分区,窗口函数在每个分区内独立操作。
示例:
计算每个部门的平均工资,并将这个部门平均值显示在每个员工记录旁边。
sql
SELECT
EmployeeID,
Name,
Department,
Salary,
AVG(Salary) OVER(PARTITION BY Department) AS DepartmentAverageSalary
FROM
Employees;
结果:
EmployeeID | Name | Department | Salary | DepartmentAverageSalary |
---|---|---|---|---|
1 | Alice | Sales | 50000 | 52333.33 |
3 | Charlie | Sales | 55000 | 52333.33 |
5 | Eve | Sales | 52000 | 52333.33 |
2 | Bob | IT | 60000 | 61000.00 |
4 | David | IT | 62000 | 61000.00 |
这里 PARTITION BY Department
将数据按部门分成了两组(Sales和IT)。AVG(Salary)
窗口函数在 Sales 分区内计算平均值 (50000+55000+52000 / 3 = 52333.33),并在 Sales 部门的每一行显示这个值。在 IT 分区内独立计算平均值 (60000+62000 / 2 = 61000),并在 IT 部门的每一行显示这个值。
这是 PARTITION BY
与 GROUP BY
的核心区别:GROUP BY
会把 Sales 的三行合并成一行,IT 的两行合并成一行,最终输出两行;而使用窗口函数,原始的五行记录都被保留了下来。
3. ORDER BY
– 定义窗口内的顺序
ORDER BY
子句定义了窗口内行的处理顺序。这对于需要顺序敏感的计算(如排名、累计、前后行比较)至关重要。
示例:
计算每个部门内员工的工资排名。
sql
SELECT
EmployeeID,
Name,
Department,
Salary,
RANK() OVER(PARTITION BY Department ORDER BY Salary DESC) AS DepartmentRank
FROM
Employees;
结果:
EmployeeID | Name | Department | Salary | DepartmentRank |
---|---|---|---|---|
3 | Charlie | Sales | 55000 | 1 |
5 | Eve | Sales | 52000 | 2 |
1 | Alice | Sales | 50000 | 3 |
4 | David | IT | 62000 | 1 |
2 | Bob | IT | 60000 | 2 |
这里,PARTITION BY Department
先将数据按部门分区。ORDER BY Salary DESC
在每个部门内部按工资降序排列。RANK()
函数则根据这个顺序在每个分区内生成排名。Charlie 在 Sales 部门工资最高,排名1;David 在 IT 部门工资最高,排名1。
4. frame_clause
– 窗口帧
窗口帧子句进一步细化了窗口的范围,它决定了在当前行计算时,哪些行会被包含进来。窗口帧是基于 ORDER BY
子句定义的顺序来确定的。它通常用于聚合类的窗口函数(如 SUM
, AVG
, COUNT
, MIN
, MAX
),以便计算移动平均、累计总和等。
窗口帧的语法通常是 RANGE BETWEEN ... AND ...
或 ROWS BETWEEN ... AND ...
。
ROWS
基于物理行的偏移量。RANGE
基于当前行ORDER BY
列的值的偏移量。
常见的帧定义关键词:
UNBOUNDED PRECEDING
: 从分区的开始到当前行。CURRENT ROW
: 当前行本身。UNBOUNDED FOLLOWING
: 从当前行到分区的结束。<N> PRECEDING
: 当前行之前的 N 行。<N> FOLLOWING
: 当前行之后的 N 行。
默认窗口帧:
- 如果
OVER()
子句包含ORDER BY
,则默认帧是RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
。这意味着窗口从分区开始到当前行,基于ORDER BY
列的值范围。对于相同的ORDER BY
值,所有行都被包含在内。 - 如果
OVER()
子句没有ORDER BY
,则默认帧是ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
。这意味着窗口是整个分区(或整个结果集如果没有PARTITION BY
),这也就是前面AVG() OVER()
和AVG() OVER(PARTITION BY Department)
示例中的默认行为。
理解默认帧非常重要,尤其是在计算累计值时。
示例:计算员工的累计工资(按部门和工资排序):
sql
SELECT
EmployeeID,
Name,
Department,
Salary,
-- 默认帧是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
-- 在ORDER BY Salary DESC时,对同一个Salary值会包含所有相同值之前的和它们自身
SUM(Salary) OVER(PARTITION BY Department ORDER BY Salary DESC) AS CumulativeSalary_DefaultFrame
FROM
Employees;
结果:
EmployeeID | Name | Department | Salary | CumulativeSalary_DefaultFrame |
---|---|---|---|---|
3 | Charlie | Sales | 55000 | 55000 |
5 | Eve | Sales | 52000 | 107000 |
1 | Alice | Sales | 50000 | 157000 |
4 | David | IT | 62000 | 62000 |
2 | Bob | IT | 60000 | 122000 |
在这个例子中,由于 ORDER BY Salary DESC
,默认帧是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
。
- Charlie (Sales, 55000): 窗口包含 Sales 分区中工资 >= 55000 的行。只有她自己。累计 55000。
- Eve (Sales, 52000): 窗口包含 Sales 分区中工资 >= 52000 的行 (Charlie, Eve)。累计 55000 + 52000 = 107000。
- Alice (Sales, 50000): 窗口包含 Sales 分区中工资 >= 50000 的行 (Charlie, Eve, Alice)。累计 55000 + 52000 + 50000 = 157000。
如果希望累计是严格基于行的物理顺序(在 ORDER BY
下),可以使用 ROWS
帧:
示例:计算基于行的累计工资:
sql
SELECT
EmployeeID,
Name,
Department,
Salary,
SUM(Salary) OVER(PARTITION BY Department ORDER BY Salary DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS CumulativeSalary_RowsFrame
FROM
Employees;
结果:
EmployeeID | Name | Department | Salary | CumulativeSalary_RowsFrame |
---|---|---|---|---|
3 | Charlie | Sales | 55000 | 55000 |
5 | Eve | Sales | 52000 | 107000 |
1 | Alice | Sales | 50000 | 157000 |
4 | David | IT | 62000 | 62000 |
2 | Bob | IT | 60000 | 122000 |
在这个特定的数据和排序下,RANGE
和 ROWS
的默认行为产生了相同的结果,因为没有工资完全相同的员工。但在有相同 ORDER BY
值(tie)的情况下,它们可能会产生不同的累计结果。
示例:计算基于行的 2 行移动平均(当前行及前 1 行):
“`sql
— 假设有一个包含日期和销售额的 Sales 表
— SalesDate | Amount
— ———-|——-
— 2023-01-01| 100
— 2023-01-02| 150
— 2023-01-03| 120
— 2023-01-04| 200
— 2023-01-05| 180
SELECT
SalesDate,
Amount,
AVG(Amount) OVER(ORDER BY SalesDate ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS TwoDayMovingAverage
FROM
Sales;
“`
结果(示例):
SalesDate | Amount | TwoDayMovingAverage |
---|---|---|
2023-01-01 | 100 | 100.00 |
2023-01-02 | 150 | 125.00 |
2023-01-03 | 120 | 135.00 |
2023-01-04 | 200 | 160.00 |
2023-01-05 | 180 | 190.00 |
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
定义了窗口帧包含当前行以及紧挨着它的前一行(根据 ORDER BY SalesDate
的顺序)。
常见的窗口函数类型
窗口函数大致可以分为几类:
- 聚合函数 (Aggregate Functions): 在窗口内执行聚合计算。
- 排名函数 (Ranking Functions): 在窗口内为每一行生成排名。
- 值函数 (Value Functions): 获取窗口内特定位置或偏移量的行的值。
让我们详细看看这些函数。
1. 聚合函数作为窗口函数
几乎所有的标准聚合函数 (SUM
, AVG
, COUNT
, MIN
, MAX
) 都可以用作窗口函数,只需要在它们后面加上 OVER()
子句。
语法: AGG_FUNCTION(column) OVER ([PARTITION BY ...] [ORDER BY ...] [frame_clause])
示例:
- 部门总销售额 (同时显示明细):
sql
SELECT
OrderID,
Region,
Amount,
SUM(Amount) OVER(PARTITION BY Region) AS TotalSalesPerRegion
FROM
Orders; - 按日期累计销售额:
sql
SELECT
OrderDate,
Amount,
SUM(Amount) OVER(ORDER BY OrderDate ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS CumulativeSales
FROM
DailySales; - 过去 7 天滚动平均销售额:
sql
SELECT
OrderDate,
Amount,
AVG(Amount) OVER(ORDER BY OrderDate ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS SevenDayMovingAvg
FROM
DailySales;
2. 排名函数
排名函数用于在窗口内为每一行生成一个排名。它们通常需要 ORDER BY
子句来确定排名依据。
-
ROW_NUMBER()
: 为分区内的每一行分配一个唯一的、连续的整数。排名不考虑并列。
sql
-- 给每个部门工资从高到低编号
SELECT
Name,
Department,
Salary,
ROW_NUMBER() OVER(PARTITION BY Department ORDER BY Salary DESC) AS RowNum
FROM
Employees;
结果(示例):
| Name | Department | Salary | RowNum |
| :—— | :——— | :—– | :—– |
| Charlie | Sales | 55000 | 1 |
| Eve | Sales | 52000 | 2 |
| Alice | Sales | 50000 | 3 |
| David | IT | 62000 | 1 |
| Bob | IT | 60000 | 2 | -
RANK()
: 为分区内的每一行分配一个排名。如果存在并列,并列的行会获得相同的排名,下一个排名会跳过相应数量的位置。
sql
-- 给每个部门工资从高到低排名 (有跳跃)
SELECT
Name,
Department,
Salary,
RANK() OVER(PARTITION BY Department ORDER BY Salary DESC) AS RankNum
FROM
Employees;
结果(示例,假设Charlie和Eve工资相同):
| Name | Department | Salary | RankNum |
| :—— | :——— | :—– | :—— |
| Charlie | Sales | 55000 | 1 |
| Eve | Sales | 55000 | 1 |
| Alice | Sales | 50000 | 3 | — 跳过了排名 2
| … | … | … | … | -
DENSE_RANK()
: 为分区内的每一行分配一个排名。如果存在并列,并列的行会获得相同的排名,下一个排名是紧挨着的下一个整数,不会跳过位置。
sql
-- 给每个部门工资从高到低排名 (无跳跃)
SELECT
Name,
Department,
Salary,
DENSE_RANK() OVER(PARTITION BY Department ORDER BY Salary DESC) AS DenseRankNum
FROM
Employees;
结果(示例,假设Charlie和Eve工资相同):
| Name | Department | Salary | DenseRankNum |
| :—— | :——— | :—– | :———– |
| Charlie | Sales | 55000 | 1 |
| Eve | Sales | 55000 | 1 |
| Alice | Sales | 50000 | 2 | — 没有跳过排名 2
| … | … | … | … | -
NTILE(n)
: 将分区内的行尝试平均分成n
个组,并为每一行分配一个组号(从 1 到n
)。
sql
-- 将所有员工按工资分成 3 组
SELECT
Name,
Salary,
NTILE(3) OVER(ORDER BY Salary) AS SalaryGroup
FROM
Employees;
结果(示例):
| Name | Salary | SalaryGroup |
| :—— | :—– | :———- |
| Alice | 50000 | 1 |
| Eve | 52000 | 1 |
| Charlie | 55000 | 2 |
| Bob | 60000 | 2 |
| David | 62000 | 3 |
3. 值函数
值函数用于访问窗口内相对于当前行的其他行的值。它们非常适合进行前后行比较、获取分区内的第一行/最后一行等操作。它们通常需要 ORDER BY
子句。
-
LAG(column, offset, default)
: 获取当前行之前第offset
行的column
值。如果offset
处的行不存在,返回default
值(如果指定),否则返回NULL
。offset
默认为 1。
sql
-- 计算员工工资与部门内上一位(按工资排序)员工的工资差
SELECT
Name,
Department,
Salary,
LAG(Salary, 1, 0) OVER(PARTITION BY Department ORDER BY Salary) AS PreviousSalary, -- 没有前一位时返回 0
Salary - LAG(Salary, 1, Salary) OVER(PARTITION BY Department ORDER BY Salary) AS SalaryDifference -- 没有前一位时差值为 0
FROM
Employees;
结果(示例):
| Name | Department | Salary | PreviousSalary | SalaryDifference |
| :—— | :——— | :—– | :————- | :————— |
| Alice | Sales | 50000 | 0 | 0 |
| Eve | Sales | 52000 | 50000 | 2000 |
| Charlie | Sales | 55000 | 52000 | 3000 |
| Bob | IT | 60000 | 0 | 0 |
| David | IT | 62000 | 60000 | 2000 | -
LEAD(column, offset, default)
: 获取当前行之后第offset
行的column
值。如果offset
处的行不存在,返回default
值(如果指定),否则返回NULL
。offset
默认为 1。
sql
-- 获取员工部门内下一位(按工资排序)员工的工资
SELECT
Name,
Department,
Salary,
LEAD(Salary, 1, 0) OVER(PARTITION BY Department ORDER BY Salary) AS NextSalary
FROM
Employees;
结果(示例):
| Name | Department | Salary | NextSalary |
| :—— | :——— | :—– | :——— |
| Alice | Sales | 50000 | 52000 |
| Eve | Sales | 52000 | 55000 |
| Charlie | Sales | 55000 | 0 |
| Bob | IT | 60000 | 62000 |
| David | IT | 62000 | 0 | -
FIRST_VALUE(column)
: 获取窗口帧内第一行的column
值。
sql
-- 获取每个部门工资最低(排序第一)员工的工资
SELECT
Name,
Department,
Salary,
FIRST_VALUE(Salary) OVER(PARTITION BY Department ORDER BY Salary) AS LowestSalaryInDept
FROM
Employees;
结果(示例):
| Name | Department | Salary | LowestSalaryInDept |
| :—— | :——— | :—– | :—————– |
| Alice | Sales | 50000 | 50000 |
| Eve | Sales | 52000 | 50000 |
| Charlie | Sales | 55000 | 50000 |
| Bob | IT | 60000 | 60000 |
| David | IT | 62000 | 60000 |
注意:FIRST_VALUE
和LAST_VALUE
受窗口帧影响。默认帧是RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
,所以FIRST_VALUE
通常表现良好(取分区开始的第一行)。但LAST_VALUE
在默认帧下通常只取到当前行,可能不是你想要的。如果你想取到分区 真正 的最后一行,需要明确指定帧为ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
或类似能包含最后一行的帧。 -
LAST_VALUE(column)
: 获取窗口帧内最后一行的column
值。
sql
-- 尝试获取每个部门工资最高(排序最后)员工的工资
SELECT
Name,
Department,
Salary,
-- 使用明确的帧来确保窗口包含分区最后一行
LAST_VALUE(Salary) OVER(PARTITION BY Department ORDER BY Salary ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS HighestSalaryInDept
FROM
Employees;
结果(示例):
| Name | Department | Salary | HighestSalaryInDept |
| :—— | :——— | :—– | :—————— |
| Alice | Sales | 50000 | 55000 |
| Eve | Sales | 52000 | 55000 |
| Charlie | Sales | 55000 | 55000 |
| Bob | IT | 60000 | 62000 |
| David | IT | 62000 | 62000 | -
NTH_VALUE(column, n)
: 获取窗口帧内第n
行的column
值(基于ORDER BY
顺序)。
sql
-- 获取每个部门工资第二高员工的工资 (可能需要处理并列)
SELECT
Name,
Department,
Salary,
-- 使用明确的帧确保窗口包含足够多的行
NTH_VALUE(Salary, 2) OVER(PARTITION BY Department ORDER BY Salary DESC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS SecondHighestSalaryInDept
FROM
Employees;
结果(示例):
| Name | Department | Salary | SecondHighestSalaryInDept |
| :—— | :——— | :—– | :———————— |
| Charlie | Sales | 55000 | 52000 |
| Eve | Sales | 52000 | 52000 |
| Alice | Sales | 50000 | 52000 |
| David | IT | 62000 | 60000 |
| Bob | IT | 60000 | 60000 |
窗口函数与 GROUP BY
的根本区别再回顾
这是初学者最容易混淆的地方,再次强调:
GROUP BY
: 用于将行聚合成组,并为每个组生成一个输出行。它减少了结果集的行数。聚合函数(如SUM
,AVG
)在每个组上计算,结果是每个组的一个值。- 窗口函数: 在一个与当前行相关的“窗口”上执行计算,但不合并行。它为输入结果集的每一行生成一个输出值。窗口函数的计算可以基于整个结果集、一个分区或一个窗口帧。
何时使用哪个?
- 如果你需要一个结果集,其中每一行代表一个组的汇总信息(例如,每个部门的总销售额,不需要看到每个订单),使用
GROUP BY
。 - 如果你需要在原始的明细行旁边显示一些聚合或排名信息(例如,每个订单旁边显示该订单所属区域的总销售额,或者每个员工旁边显示他在部门内的排名),使用窗口函数。
当然,在复杂的查询中,你可以结合使用它们。例如,你可能先用 GROUP BY
计算出一些中间聚合结果,然后在这些聚合结果上使用窗口函数进行进一步分析。或者,你可以在使用窗口函数后,在一个外部查询中使用 WHERE
子句(或者更常见的,将其放入 CTE 或子查询,然后在外部查询中筛选)来过滤基于窗口函数结果的行(比如找出每个部门工资最高的员工,这需要先排名再筛选)。
窗口函数的应用场景
窗口函数在数据分析领域有着广泛的应用,包括但不限于:
- 排名和分级: 计算销售额、分数、排名的前 N 名。
- 累计和和移动平均: 分析随时间变化的趋势,如累计销售额、滚动平均值。
- 前后行比较: 计算环比增长、差异、间隔时间。
- 组内百分比: 计算某个值占其所属分组总和或平均值的百分比。
- 填充缺失值: 使用
LAG
或LEAD
等函数填充基于序列的缺失值。 -
复杂过滤: 虽然不能直接在
WHERE
中使用窗口函数,但可以通过子查询或 CTE 先计算出窗口函数结果,然后在外部查询中过滤。例如,找出每个部门销售额最高的员工:sql
WITH RankedEmployees AS (
SELECT
EmployeeID,
Name,
Department,
Salary,
RANK() OVER(PARTITION BY Department ORDER BY Salary DESC) as rn
FROM Employees
)
SELECT
EmployeeID,
Name,
Department,
Salary
FROM RankedEmployees
WHERE rn = 1;
窗口函数的性能考虑
窗口函数虽然强大,但计算成本可能比简单的聚合或过滤更高。数据库需要为每一行确定其所属的窗口,并在该窗口上执行计算。PARTITION BY
和 ORDER BY
子句中涉及的列通常需要数据库进行排序和分组操作。
性能建议:
- 索引: 在
PARTITION BY
和ORDER BY
子句中使用的列上建立索引通常有助于提高性能。 - 限制数据量: 在使用窗口函数之前,尽可能先使用
WHERE
子句过滤掉不需要的行,减少参与窗口计算的数据量。 - 避免不必要的复杂性: 只有在确实需要窗口函数时才使用它。简单的聚合使用
GROUP BY
更高效。 - 理解窗口帧: 精确定义窗口帧可以避免对不必要的行进行计算。
总结
SQL 窗口函数是进行高级数据分析的强大工具。它们允许你在不合并原始数据行的情况下,对相关行集执行计算。通过掌握 OVER()
子句中的 PARTITION BY
、ORDER BY
和窗口帧(ROWS
/RANGE BETWEEN
),你可以执行排名、累计计算、前后行比较等复杂的分析任务,从而写出更简洁、更高效、更易读的 SQL 代码。
从聚合函数到排名函数再到值函数,每种类型的窗口函数都有其独特的用途。理解它们的工作原理以及如何通过 OVER()
子句控制它们的行为,将极大地提升你的 SQL 数据处理能力。
不要被最初的复杂性吓倒,窗口函数值得投入时间学习。从简单的例子开始练习,逐步尝试更复杂的场景,你将很快能驾驭这一强大的 SQL 特性,解锁更深层次的数据洞察。
现在,你已经具备了理解 SQL 窗口函数所需的所有基本知识。勇敢地在你的查询中实践它们吧!