什么是SQL窗口函数
SQL窗口函数(Window Functions)是SQL标准中最强大的功能之一,它允许对一组相关行进行计算,同时保留每一行的原始细节。通俗说:传统GROUP BY会把多行压缩成一行,窗口函数保留每行的同时还能做”分组内的计算”。比如计算每个员工的部门内排名、每个月的累计销售、用户的前后行为对比,都是窗口函数的典型场景。本教程从实战角度详解10个可直接复用的高价值模板。
第一步:理解窗口函数语法
窗口函数基础语法:
函数名() OVER (
[PARTITION BY 分组列]
[ORDER BY 排序列]
[ROWS/RANGE 窗口范围]
) [AS 别名]
三个核心子句:
- PARTITION BY:分组(类似GROUP BY)
- ORDER BY:组内排序
- ROWS/RANGE:定义窗口边界
第二步:ROW_NUMBER()行号
ROW_NUMBER()给每行一个唯一序号:
语法:ROW_NUMBER() OVER (PARTITION BY 分组列 ORDER BY 排序列)
实战场景:每个部门工资最高的员工
SELECT * FROM (
SELECT
*,
ROW_NUMBER() OVER (
PARTITION BY department
ORDER BY salary DESC
) AS rn
FROM employees
) t
WHERE rn = 1;
输出每个部门工资最高的1位员工。
第三步:RANK和DENSE_RANK排名
两个排名函数的区别:
- RANK():相同值同名次,后续跳号(1,1,3)
- DENSE_RANK():相同值同名次,后续不跳号(1,1,2)
实战:销售排行榜
SELECT
salesperson,
sales_amount,
RANK() OVER (ORDER BY sales_amount DESC) AS rk,
DENSE_RANK() OVER (ORDER BY sales_amount DESC) AS drk
FROM sales;
提示:RANK用于颁奖(前三名都有奖),DENSE_RANK用于分级(前10%是A级)
第四步:LAG和LEAD前后行
LAG()取前面行的值,LEAD()取后面行的值。
语法:
LAG(列, offset, default) OVER (ORDER BY ...)LEAD(列, offset, default) OVER (ORDER BY ...)
实战:每日销售额环比
SELECT
date,
sales,
LAG(sales) OVER (ORDER BY date) AS prev_sales,
sales - LAG(sales) OVER (ORDER BY date) AS diff,
ROUND(
(sales - LAG(sales) OVER (ORDER BY date)) * 100.0 /
LAG(sales) OVER (ORDER BY date),
2
) AS growth_pct
FROM daily_sales;
第五步:SUM() OVER累计和
SUM() OVER实现累计求和(running total):
实战:累计销售达成率
SELECT
month,
sales,
SUM(sales) OVER (
ORDER BY month
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumulative_sales
FROM monthly_sales;
ROWS BETWEEN定义窗口边界:
- UNBOUNDED PRECEDING:从分组开始
- CURRENT ROW:当前行
- UNBOUNDED FOLLOWING:到分组结束
- n PRECEDING:前n行
- n FOLLOWING:后n行
第六步:NTILE分桶
NTILE(n)把数据等分为n桶:
实战:用户价值分层(RFM分析)
SELECT
user_id,
total_amount,
NTILE(4) OVER (ORDER BY total_amount DESC) AS quartile
FROM user_rfm;
quartile=1是头部用户,quartile=4是尾部用户。
第七步:FIRST_VALUE和LAST_VALUE
取窗口内第一/最后一个值:
实战:每个用户首次购买距今天数
SELECT
user_id,
purchase_date,
FIRST_VALUE(purchase_date) OVER (
PARTITION BY user_id
ORDER BY purchase_date
) AS first_purchase,
CURRENT_DATE - FIRST_VALUE(purchase_date) OVER (
PARTITION BY user_id
ORDER BY purchase_date
) AS days_since_first
FROM purchases;
第八步:移动平均
实战:7天移动平均销售额
SELECT
date,
sales,
AVG(sales) OVER (
ORDER BY date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS moving_avg_7d
FROM daily_sales;
第九步:用户留存分析
实战:用户次日/7日/30日留存
WITH first_purchase AS (
SELECT user_id, MIN(purchase_date) AS first_date
FROM purchases
GROUP BY user_id
)
SELECT
f.user_id,
f.first_date,
MAX(CASE WHEN p.purchase_date = f.first_date + INTERVAL '1 day' THEN 1 ELSE 0 END) AS d1_retained,
MAX(CASE WHEN p.purchase_date = f.first_date + INTERVAL '7 day' THEN 1 ELSE 0 END) AS d7_retained
FROM first_purchase f
LEFT JOIN purchases p ON f.user_id = p.user_id
GROUP BY f.user_id, f.first_date;
第十步:性能优化
窗口函数在大数据上的优化:
- 避免在窗口函数外再用GROUP BY(先聚合再用窗口)
- 用PostgreSQL/MySQL 8+ 优化器
- 建立合适的索引:
CREATE INDEX idx_user_date ON purchases(user_id, purchase_date) - 避免ROWS BETWEEN和RANGE混合使用
- 用EXPLAIN分析查询计划
三大数据库语法对比
PostgreSQL:
• 最完善,支持所有窗口函数
• 支持RANGE BETWEEN INTERVAL ‘7 day’ PRECEDING
MySQL 8+:
• 支持ROW_NUMBER/RANK/DENSE_RANK/LAG/LEAD/FIRST_VALUE/LAST_VALUE
• 早期版本(5.7)不支持窗口函数
SQL Server:
• 全部支持
• 支持OFFSET/FETCH NEXT分页
效率数据
对比子查询方案:
- 原方案(子查询):平均100ms,300行SQL
- 窗口函数方案:平均20ms,100行SQL
- 10倍以上效率提升
常见误区
- Q:窗口函数vs GROUP BY?A:GROUP BY压缩行,窗口保留行
- Q:能嵌套使用吗?A:可以,但要注意性能
- Q:WHERE子句里能用吗?A:不行,需要子查询包裹
- Q:MySQL 5.7能用吗?A:必须8.0+