文章目录
窗口函数
SQL Server
中的窗口函数(也称为分析函数)是一组非常强大的SQL
功能,**它们允许你在结果集的行上执行计算,而不需要将结果集分组为多个输出行。**窗口函数可以对一组行执行计算,这组行与当前行相关,被称为窗口。窗口函数可以执行排名、聚合、行号分配等操作,而无需改变查询结果中的行数。
<窗口函数> OVER (
PARTITION BY <列名> -- 可选,用于将结果集划分为多个分区
ORDER BY <列名> -- 可选,用于在每个分区内对行进行排序
ROWS|RANGE BETWEEN <开始范围> AND <结束范围> -- 可选,定义窗口帧
)
窗口函数分类
- 排名函数:如
ROW_NUMBER()、RANK()、DENSE_RANK()、NTILE(n)
等,用于对行进行排名或分组。 - 聚合函数:如
SUM()、AVG()、MIN()、MAX()、COUNT()
等,但在这里它们不是对整个结果集进行聚合,而是对每个窗口进行聚合。 - 分析函数:如
LEAD()、LAG()、FIRST_VALUE()、LAST_VALUE()
等,用于访问窗口中的其他行数据。
窗口函数示例
假设我们有一个名为Employees
的表,包含DepartmentID
和Salary
列,我们可以使用ROW_NUMBER()
窗口函数来为每个部门内的员工按薪水排序并分配一个唯一的行号:
SELECT
DepartmentID,
Salary,
ROW_NUMBER() OVER (PARTITION BY DepartmentID ORDER BY Salary DESC) AS SalaryRank
FROM
Employees;
这个查询将结果集按DepartmentID
分区,并在每个分区内按Salary
降序排列,为每个分区内的行分配一个唯一的行号(SalaryRank
)。
聚合函数示例
如果我们想计算每个部门内员工的平均薪水,但不想改变结果集中的行数(即仍然为每位员工显示数据),我们可以使用AVG()
窗口函数:
SELECT
DepartmentID,
Salary,
AVG(Salary) OVER (PARTITION BY DepartmentID) AS AvgDepartmentSalary
FROM
Employees;
这个查询将结果集按DepartmentID
分区,并计算每个分区内薪水的平均值(AvgDepartmentSalary
),但结果集中仍包含每位员工的记录。
LAG和LEAD
LAG
和LEAD
是在处理窗口函数时会用到的两个函数,它们能够让你访问表中多行数据,无需使用自连接。下面来详细了解一下这两个函数。
LAG函数
LAG
函数的作用是获取当前行之前的行的数据。其语法格式如下:
LAG(column_name, offset, default_value)
OVER (
[PARTITION BY partition_expression, ... ]
ORDER BY sort_expression [ASC | DESC], ...
)
- column_name:这是你想要获取值的列。
- offset:表示要往前偏移的行数,默认值为1。
- default_value:当偏移超出表的范围时返回的值,默认是
NULL
。
下面通过一个例子来更好地理解。假设有一个名为sales的表,其数据如下:
month | sales |
---|---|
January | 100 |
February | 150 |
March | 200 |
现在,我们使用LAG
函数来获取前一个月的销售额:
SELECT
month,
sales,
LAG(sales, 1) OVER (ORDER BY month) AS previous_month_sales
FROM sales;
得到的结果如下:
month | sales | previous_month_sales |
---|---|---|
January | 100 | NULL |
February | 150 | 100 |
March | 200 | 150 |
LEAD函数
LEAD
函数与LAG
函数相反,它用于获取当前行之后的行的数据。其语法格式为:
LEAD(column_name, offset, default_value)
OVER (
[PARTITION BY partition_expression, ... ]
ORDER BY sort_expression [ASC | DESC], ...
)
参数的含义和LAG
函数相同。
还是以sales
表为例,我们使用LEAD
函数来获取下一个月的销售额:
SELECT
month,
sales,
LEAD(sales, 1) OVER (ORDER BY month) AS next_month_sales
FROM sales;
结果如下:
month | sales | next_month_sales |
---|---|---|
January | 100 | 150 |
February | 150 | 200 |
March | 200 | NULL |
主要区别
- LAG:是向后看,用于访问之前行的数据。
- LEAD:是向前看,用于访问后续行的数据。
这两个函数在进行时间序列分析、计算增长率或者比较相邻行数据时非常实用。
注意事项
- 窗口函数不会减少结果集中的行数,它们只是为每行添加了额外的计算列。
- 窗口函数可以与
GROUP BY
子句结合使用,但通常它们用于那些不需要完全分组聚合的查询。 PARTITION BY
子句是可选的,如果不使用,则整个结果集被视为一个单一的分区。ORDER BY
子句在窗口函数中通常是必需的,尤其是在使用排名函数或需要定义窗口帧时。然而,它仅用于定义窗口内的排序顺序,并不改变结果集的最终排序。
是的,在使用LAG函数计算变化率的基础上,可以通过子查询或临时表来简化计算逻辑,使查询更清晰。此外,要呈现百分号,可以使用字符串格式化函数将变化率转换为百分比形式。
使用子查询计算变化率并显示百分比
下面是一个使用子查询的示例,计算每月销售额的变化率并以百分比形式显示:
SELECT
month,
sales,
previous_month_sales,
CONCAT(ROUND(((sales - previous_month_sales) / previous_month_sales) * 100, 2), '%') AS sales_change_percentage
FROM (
SELECT
month,
sales,
LAG(sales, 1) OVER (ORDER BY month) AS previous_month_sales
FROM sales
) AS subquery
WHERE previous_month_sales IS NOT NULL; -- 过滤掉第一个月,因为没有前一个月的数据
使用CTE(公共表表达式)替代子查询
CTE
可以使查询更具可读性,尤其在处理复杂计算时:
WITH sales_with_previous AS (
SELECT
month,
sales,
LAG(sales, 1) OVER (ORDER BY month) AS previous_month_sales
FROM sales
)
SELECT
month,
sales,
previous_month_sales,
CONCAT(ROUND(((sales - previous_month_sales) / previous_month_sales) * 100, 2), '%') AS sales_change_percentage
FROM sales_with_previous
WHERE previous_month_sales IS NOT NULL;
关键说明:
-
子查询/CTE的作用:
- 先计算出
previous_month_sales
,将复杂的窗口函数逻辑封装在内部。 - 外层查询只需专注于变化率的计算,避免了嵌套窗口函数的复杂性。
- 先计算出
-
百分比格式化:
- 使用
CONCAT
函数将计算结果与%
符号拼接。 ROUND(..., 2)
将结果保留两位小数。- 计算逻辑:
((当前月销售额 - 上月销售额) / 上月销售额) * 100
- 使用
-
处理NULL值:
- 第一个月的
previous_month_sales
为NULL,通过WHERE
子句过滤掉这些行。
- 第一个月的
示例结果:
假设sales表数据如下:
month | sales |
---|---|
January | 100 |
February | 150 |
March | 200 |
查询结果将显示:
month | sales | previous_month_sales | sales_change_percentage |
---|---|---|---|
February | 150 | 100 | 50.00% |
March | 200 | 150 | 33.33% |
替代方案:临时表
如果需要多次使用计算结果,可以创建临时表:
-- 创建临时表存储带前月销售额的数据
CREATE TEMP TABLE sales_with_prev AS
SELECT
month,
sales,
LAG(sales, 1) OVER (ORDER BY month) AS previous_month_sales
FROM sales;
-- 查询临时表并计算变化率
SELECT
month,
sales,
previous_month_sales,
CONCAT(ROUND(((sales - previous_month_sales) / previous_month_sales) * 100, 2), '%') AS sales_change_percentage
FROM sales_with_prev
WHERE previous_month_sales IS NOT NULL;
这种方法在处理大型数据集或需要多次计算时效率更高。
FIRST_VALUE和LAST_VALUE
FIRST_VALUE
和LAST_VALUE
属于窗口函数,它们能让你从窗口内的第一行或最后一行获取值。下面为你详细介绍这两个函数的功能、语法和使用案例。
FIRST_VALUE函数
FIRST_VALUE
函数可返回窗口帧内的第一行值。其语法结构如下:
FIRST_VALUE(expression)
OVER (
[PARTITION BY partition_expression, ... ]
ORDER BY sort_expression [ASC | DESC], ...
[frame_clause]
)
- expression:指的是你想要获取值的列或者表达式。
- PARTITION BY:用于将结果集按照指定的列进行分组。
- ORDER BY:确定了窗口内的排序方式,这会影响到第一行和最后一行的定义。
- frame_clause:对窗口帧的范围进行定义,默认情况下是从窗口的起始行到当前行。
下面通过一个例子来理解。假设有一个名为employees
的表,记录了不同部门员工的工资情况:
department | employee_name | salary |
---|---|---|
IT | Alice | 80000 |
IT | Bob | 75000 |
HR | Charlie | 60000 |
HR | Dave | 65000 |
现在,我们使用FIRST_VALUE
函数来找出每个部门工资最高的员工:
SELECT
department,
employee_name,
salary,
FIRST_VALUE(employee_name) OVER (
PARTITION BY department
ORDER BY salary DESC
) AS highest_paid_employee
FROM employees;
得到的结果如下:
department | employee_name | salary | highest_paid_employee |
---|---|---|---|
IT | Alice | 80000 | Alice |
IT | Bob | 75000 | Alice |
HR | Dave | 65000 | Dave |
HR | Charlie | 60000 | Dave |
LAST_VALUE函数
LAST_VALUE
函数与FIRST_VALUE
函数相反,它返回窗口帧内的最后一行值。其语法结构为:
LAST_VALUE(expression)
OVER (
[PARTITION BY partition_expression, ... ]
ORDER BY sort_expression [ASC | DESC], ...
[frame_clause]
)
参数的含义和FIRST_VALUE
函数相同。不过需要注意的是,LAST_VALUE
函数默认的窗口帧范围是从窗口的起始行到当前行,这意味着如果不明确指定窗口帧,它可能无法返回你预期的最后一行值。
还是以employees
表为例,我们使用LAST_VALUE
函数来找出每个部门工资最低的员工:
SELECT
department,
employee_name,
salary,
LAST_VALUE(employee_name) OVER (
PARTITION BY department
ORDER BY salary DESC
RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS lowest_paid_employee
FROM employees;
结果如下:
department | employee_name | salary | lowest_paid_employee |
---|---|---|---|
IT | Alice | 80000 | Bob |
IT | Bob | 75000 | Bob |
HR | Dave | 65000 | Charlie |
HR | Charlie | 60000 | Charlie |
关键区别和注意事项
-
窗口帧的影响:
FIRST_VALUE
函数默认会返回窗口内的第一行,一般能得到预期结果。- 而
LAST_VALUE
函数默认的窗口帧范围是到当前行,所以需要使用RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
来明确指定窗口帧范围,使其包含整个分区。
-
处理NULL值:
- 如果第一行或最后一行的值是
NULL
,这两个函数会直接返回NULL
。你可以结合COALESCE
函数来处理这种情况。
- 如果第一行或最后一行的值是
-
替代方案:
- 除了使用
LAST_VALUE
函数,你还可以通过反向排序然后使用FIRST_VALUE
函数来实现相同的效果:FIRST_VALUE(employee_name) OVER ( PARTITION BY department ORDER BY salary ASC ) AS lowest_paid_employee
- 除了使用
这两个函数在进行排名分析、比较极端值等场景中非常有用。
流水表提取最新状态
通过窗口函数ROW_NUMBER()
对每家店铺进行分组,只提取更新时间最新的一条记录,之后再过滤valid_flag=1
的店铺作为需要预警的店铺。
WITH RankedStores AS (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY store_id ORDER BY update_time DESC) AS rn
FROM table1
WHERE type='通用' and platform = 'tmall')
SELECT STRING_AGG(store_id,',')
FROM RankedStores
WHERE rn = 1 and valid_flag = 1