在复杂数据分析、报表统计、排名计算等场景中经常使用。窗口函数是 SQL 中非常强大的功能,可以在不改变行数 的情况下对每一行进行计算,同时保留原始数据。
下面我从核心概念、语法结构、常用函数分类、实战案例、性能优化 几个维度来系统说明。
🎯 窗口函数是什么? 窗口函数 是在一个“窗口”(记录集合)上执行计算,但不会将多行合并为一行 ,每行都保留自己的身份,同时可以获得聚合结果。
对比聚合函数 特性 普通聚合函数 窗口函数 行数变化 多行 → 一行 行数不变 典型函数 SUM(),AVG(),COUNT()ROW_NUMBER(),RANK(),SUM() OVER()使用场景 汇总统计 排名、累计、移动平均、环比同比
-- ❌ 聚合函数:多行合并成一行 SELECT department, AVG ( salary) FROM employeesGROUP BY department; -- ✅ 窗口函数:每行保留,同时显示部门平均工资 SELECT name, department, salary, AVG ( salary) OVER ( PARTITION BY department) AS dept_avgFROM employees; 📖 语法结构 窗口函数OVER ( [ PARTITION BY 分区字段] -- 可选:将数据分组 [ ORDER BY 排序字段] -- 可选:定义排序,影响排名类函数 [ ROWS / RANGE 窗口范围] -- 可选:定义帧(滑动窗口) ) 三种核心组成部分 📚 窗口函数分类 1️⃣ 排名类函数 函数 说明 示例结果 ROW_NUMBER()连续排名,不跳号 1,2,3,4 RANK()跳跃排名,相同值同排名 1,2,2,4 DENSE_RANK()密集排名,相同值同排名,连续 1,2,2,3 NTILE(n)分成 n 组 1,1,2,2,3,3
SELECT name, department, salary, ROW_NUMBER( ) OVER ( ORDER BY salaryDESC ) AS row_num, RANK( ) OVER ( ORDER BY salaryDESC ) AS rank_num, DENSE_RANK( ) OVER ( ORDER BY salaryDESC ) AS dense_rank_num, NTILE( 4 ) OVER ( ORDER BY salaryDESC ) AS quartileFROM employees; 结果示例 :
name | department | salary | row_num | rank_num | dense_rank_num | quartile -------|------------|--------|---------|----------|----------------|---------- 张三 | 技术部 | 50000 | 1 | 1 | 1 | 1 李四 | 技术部 | 45000 | 2 | 2 | 2 | 1 王五 | 销售部 | 45000 | 3 | 2 | 2 | 2 赵六 | 销售部 | 40000 | 4 | 4 | 3 | 22️⃣ 聚合类窗口函数 在窗口上使用聚合函数,相当于“移动汇总”或“分组汇总但不合并行”。
函数 说明 SUM() OVER()累计求和 AVG() OVER()移动平均 COUNT() OVER()累计计数 MAX() / MIN() OVER()窗口内最大/最小值
-- 累计销售额(按日期) SELECT order_date, amount, SUM ( amount) OVER ( ORDER BY order_date) AS cumulative_amount, AVG ( amount) OVER ( ORDER BY order_dateROWS BETWEEN 6 PRECEDING AND CURRENT ROW ) AS ma_7dFROM orders; 3️⃣ 取值类窗口函数 获取窗口内指定位置的值。
函数 说明 LAG(column, n)获取当前行前第 n 行 的值 LEAD(column, n)获取当前行后第 n 行 的值 FIRST_VALUE(column)窗口内第一行 的值 LAST_VALUE(column)窗口内最后一行 的值 NTH_VALUE(column, n)窗口内第 n 行 的值
-- 环比增长计算 SELECT order_date, amount, LAG( amount, 1 ) OVER ( ORDER BY order_date) AS prev_day_amount, ( amount- LAG( amount, 1 ) OVER ( ORDER BY order_date) ) / LAG( amount, 1 ) OVER ( ORDER BY order_date) * 100 AS growth_rate_pctFROM daily_sales; 🚀 实战案例 案例 1:员工工资排名(部门内) -- 需求:查询每个部门工资前 3 名的员工 WITH ranked_employeesAS ( SELECT name, department, salary, DENSE_RANK( ) OVER ( PARTITION BY departmentORDER BY salaryDESC ) AS rank_in_deptFROM employees) SELECT * FROM ranked_employeesWHERE rank_in_dept<= 3 ; 案例 2:用户购买累计金额(时间序列) -- 需求:计算每个用户的累计消费金额 SELECT user_id, order_date, amount, SUM ( amount) OVER ( PARTITION BY user_idORDER BY order_dateROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS cumulative_amountFROM orders; 案例 3:7 日移动平均(平滑曲线) -- 需求:计算每日销售额的 7 日移动平均 SELECT order_date, daily_amount, AVG ( daily_amount) OVER ( ORDER BY order_dateROWS BETWEEN 6 PRECEDING AND CURRENT ROW ) AS ma_7daysFROM daily_sales; 案例 4:同比环比计算 -- 需求:计算每月销售额的环比和同比增长 WITH monthly_salesAS ( SELECT DATE_FORMAT( order_date, '%Y-%m' ) AS month , SUM ( amount) AS total_amountFROM ordersGROUP BY DATE_FORMAT( order_date, '%Y-%m' ) ) SELECT month , total_amount, LAG( total_amount, 1 ) OVER ( ORDER BY month ) AS prev_month_amount, ( total_amount- LAG( total_amount, 1 ) OVER ( ORDER BY month ) ) / LAG( total_amount, 1 ) OVER ( ORDER BY month ) * 100 AS mom_growth, LAG( total_amount, 12 ) OVER ( ORDER BY month ) AS prev_year_amount, ( total_amount- LAG( total_amount, 12 ) OVER ( ORDER BY month ) ) / LAG( total_amount, 12 ) OVER ( ORDER BY month ) * 100 AS yoy_growthFROM monthly_sales; 案例 5:填充空值(使用 LAG/LEAD) -- 需求:将 NULL 值填充为前一个非 NULL 值 SELECT date , original_value, COALESCE ( original_value, LAG( original_value) OVER ( ORDER BY date ) ) AS filled_valueFROM incomplete_data; 🎨 窗口帧(Frame)详解 窗口帧定义了滑动窗口的范围 ,是高级窗口函数的关键。
语法 {ROWS | RANGE }BETWEEN frame_startAND frame_end-- frame_start 可以是: UNBOUNDED PRECEDING -- 从分区第一行开始 NPRECEDING -- 当前行前 N 行 CURRENT ROW -- 当前行 -- frame_end 可以是: CURRENT ROW -- 到当前行 NFOLLOWING -- 当前行后 N 行 UNBOUNDED FOLLOWING -- 到分区最后一行 常见窗口帧模式 模式 写法 用途 累计到当前 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW累计求和 移动窗口 ROWS BETWEEN 6 PRECEDING AND CURRENT ROW7 日移动平均 中心滑动 ROWS BETWEEN 3 PRECEDING AND 3 FOLLOWING平滑算法 到末尾 ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING剩余累计
-- 累计求和(从开始到当前) SUM ( amount) OVER ( ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) -- 7日移动平均 AVG ( amount) OVER ( ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW ) -- 未来3天预测基准 AVG ( amount) OVER ( ORDER BY date ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING ) 🔧 不同数据库的差异 特性 MySQL 8.0+ PostgreSQL SQL Server Oracle 窗口函数支持 ✅ 完整 ✅ 完整 ✅ 完整 ✅ 完整 窗口帧 ROWS/RANGE ✅ ✅ ✅ ✅ PERCENT_RANK ✅ ✅ ✅ ✅ 窗口函数在 UPDATE 中 ❌ ✅ ✅ ✅ 命名窗口 ❌ ✅ ✅ ✅
MySQL 8.0 前的替代方案(使用变量) -- MySQL 5.7 模拟 ROW_NUMBER() SELECT name, salary, @row_num := @row_num + 1 AS row_numFROM employees, ( SELECT @row_num := 0 ) rORDER BY salaryDESC ; ⚡ 性能优化建议 1. 合理使用 PARTITION BY -- ✅ 好:分区字段有索引 ROW_NUMBER( ) OVER ( PARTITION BY department_idORDER BY salaryDESC ) -- ❌ 差:分区过多或分区字段无索引,可能导致大量排序 2. 减少不必要的窗口函数 -- ❌ 同一窗口重复计算 SELECT ROW_NUMBER( ) OVER ( ORDER BY salary) AS rn1, RANK( ) OVER ( ORDER BY salary) AS rn2FROM employees; -- ✅ 使用命名窗口(部分数据库支持) SELECT ROW_NUMBER( ) OVER wAS rn1, RANK( ) OVER wAS rn2FROM employees WINDOW wAS ( ORDER BY salary) ; 3. 使用索引优化 -- 窗口函数需要排序,创建相应索引 CREATE INDEX idx_dept_salaryON employees( department_id, salaryDESC ) ; 4. 避免大窗口下的 ROWS BETWEEN -- 大表避免使用过大的窗口范围 -- ROWS BETWEEN 1000 PRECEDING AND CURRENT ROW 可能很慢 📊 实际业务场景 场景 窗口函数方案 排行榜 ROW_NUMBER() OVER (ORDER BY score DESC)分组 Top N ROW_NUMBER() OVER (PARTITION BY category ORDER BY score DESC)累计统计 SUM(amount) OVER (ORDER BY date)环比/同比 LAG(amount) OVER (ORDER BY date)移动平均 AVG(amount) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)去重保留最新 ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC)用户行为序列 LAG(page) OVER (PARTITION BY session_id ORDER BY event_time)
💡 总结 问题 答案 窗口函数是什么? 在保持行数不变的情况下,对每一行进行窗口内计算 核心语法是什么? 函数() OVER (PARTITION BY ... ORDER BY ... 窗口帧)什么时候用? 排名、累计、移动平均、环比同比、分组 Top N 与 GROUP BY 的区别? GROUP BY 合并行,窗口函数保留行 性能如何? 比子查询/自连接快,但需要合理使用索引