Oracle窗口函数是SQL中极其强大的工具,它允许你在不折叠分组行的前提下,对一组相关的行(一个“窗口”)进行计算。理解它能将你的数据分析能力提升一个维度。
一、核心概念:它是什么?
想象一下普通的GROUP BY:它将多行合并成一行。窗口函数恰恰相反:它为查询结果的每一行都返回一个值,这个值是基于与该行相关的一组行计算而来的。
关键区别:
GROUP BY+ 聚合函数:多行输入,一行输出(每个分组)。- 窗口函数 + 聚合/分析函数:多行输入,多行输出(每行都附加计算结果)。
二、核心语法结构
function_name([arguments])
OVER ([PARTITION BY partition_expression, ...] -- 定义窗口分区(分组)[ORDER BY sort_expression [ASC | DESC], ...] -- 定义窗口内排序[ROWS/RANGE window_frame_clause] -- 定义窗口框架(计算范围)
)
三、三大核心组成部分详解
1. PARTITION BY
将整个结果集划分为更小的、独立的“窗口”或分区。计算在每个分区内独立进行。
- 示例:
PARTITION BY department_id会按部门创建独立窗口。
2. ORDER BY
定义窗口内行的顺序。这对于排名、累计计算等至关重要。
- 示例:
ORDER BY sale_date DESC会让窗口按销售日期降序排列。
3. 窗口框架 (ROWS/RANGE)
当使用ORDER BY时,可以进一步限定计算的具体范围。这是窗口函数最精细的控制部分。
ROWS:基于物理行的偏移。ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING:当前行、前一行、后一行。ROWS UNBOUNDED PRECEDING:从分区第一行到当前行(常用于累计)。
RANGE:基于ORDER BY列的值的逻辑偏移。RANGE BETWEEN INTERVAL '1' DAY PRECEDING AND CURRENT ROW:值在(当前日期-1天)到当前日期之间的所有行。
四、常用窗口函数分类与示例
我们用一个员工薪资表 emp_salary 来举例:
| EMP_ID | NAME | DEPT | SALARY | HIRE_DATE |
|---|---|---|---|---|
| 101 | 张三 | 销售 | 8000 | 2020-01-01 |
| 102 | 李四 | 销售 | 9000 | 2019-06-01 |
| 103 | 王五 | 技术 | 12000 | 2021-03-01 |
| 104 | 赵六 | 技术 | 11000 | 2020-08-01 |
| 105 | 钱七 | 销售 | 8500 | 2021-05-01 |
1. 排名函数
ROW_NUMBER():连续的、唯一的序号(即使值相同)。
SELECT emp_id, name, dept, salary,ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) as dept_salary_rank_row
FROM emp_salary;
结果:技术部,王五(12000)排1,赵六(11000)排2。销售部,李四(9000)排1,钱七(8500)排2,张三(8000)排3。
RANK() 与 DENSE_RANK():处理并列排名。
RANK():并列会占用名次。如:1, 2, 2, 4, ...DENSE_RANK():并列不占用名次。如:1, 2, 2, 3, ...
2. 聚合函数(用作窗口函数)
SUM(), AVG(), COUNT(), MAX(), MIN()
SELECT emp_id, name, dept, salary,SUM(salary) OVER (PARTITION BY dept) as dept_total_salary, -- 部门总薪资AVG(salary) OVER (PARTITION BY dept) as dept_avg_salary, -- 部门平均薪资SUM(salary) OVER (PARTITION BY dept ORDER BY hire_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) as dept_cumulative_salary -- 部门按入职累计薪资
FROM emp_salary;
3. 分布函数
PERCENT_RANK():(当前行的RANK值 - 1) / (总行数 - 1),结果在0~1之间。
CUME_DIST():小于等于当前行值的行数 / 分区总行数。
4. 前后行取值函数(极其实用)
LAG(column, n):获取窗口内当前行之前第n行的值。
LEAD(column, n):获取窗口内当前行之后第n行的值。
-- 查看每位员工与其部门内上一名员工的薪资差
SELECT emp_id, name, dept, salary,LAG(salary, 1) OVER (PARTITION BY dept ORDER BY salary) as prev_salary,salary - LAG(salary, 1) OVER (PARTITION BY dept ORDER BY salary) as salary_diff
FROM emp_salary;
5. 首尾取值函数
FIRST_VALUE(column):返回窗口第一行的值。
LAST_VALUE(column):返回窗口最后一行的值。
注意:使用LAST_VALUE时,通常需要指定完整的窗口框架,否则默认范围是RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,可能不是你想要的结果。
五、一个综合性的复杂示例
-- 查询:找出每个部门薪资排名前2的员工,并展示该员工薪资与部门最高、平均薪资的对比,以及比部门内上一位入职员工的薪资增长情况。
SELECT dept,name,salary,hire_date,dept_salary_rank,dept_max_salary,ROUND(salary / dept_avg_salary * 100, 2) || '%' as vs_avg_pct, -- 薪资 vs 部门平均salary - prev_emp_salary as salary_increase_from_prev -- 比前一位员工高多少
FROM (SELECT dept,name,salary,hire_date,ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) as dept_salary_rank,MAX(salary) OVER (PARTITION BY dept) as dept_max_salary,AVG(salary) OVER (PARTITION BY dept) as dept_avg_salary,LAG(salary, 1) OVER (PARTITION BY dept ORDER BY hire_date) as prev_emp_salaryFROM emp_salary
)
WHERE dept_salary_rank <= 2
ORDER BY dept, dept_salary_rank;
六、性能与最佳实践要点
- 性能:窗口函数通常在数据库内部进行优化,性能远优于使用自连接或相关子查询实现相同逻辑。但复杂的窗口框架(尤其是
RANGE)可能开销较大。 - 索引是朋友:在
PARTITION BY和ORDER BY涉及的列上建立索引,能大幅提升窗口函数的执行速度。 - 执行顺序:记住,窗口函数是在
WHERE,GROUP BY,HAVING子句之后执行的。因此,你不能在WHERE子句中直接引用窗口函数别名。如果需要过滤窗口函数结果,必须使用子查询或WITH(CTE)。 - 适用版本:Oracle 8i开始引入基础分析函数,后续版本功能不断增强。本文所述功能在Oracle 9i及以上版本均支持,部分高级功能(如
LISTAGG)在11g后更完善。
窗口函数初看复杂,但它是编写清晰、高效分析SQL的基石。建议你从ROW_NUMBER()和SUM() OVER (PARTITION BY ...)开始练习,逐步尝试LAG/LEAD和窗口框架,很快就能掌握其精髓。