SQL窗口函数解决了什么
传统SQL做复杂分析(排名、累计、同比、占比)需要多重嵌套子查询,性能差且难维护。窗口函数让这些分析变成一行SQL,性能高、可读性强。本教程从基础到复杂实战完整讲解。
第一步:理解窗口函数
窗口函数对一组相关行(窗口)进行计算,结果不会合并行。这是与聚合函数的关键区别:
- 聚合函数——SUM/AVG等把多行合并成一行
- 窗口函数——对每行计算一个汇总值,保留原行
语法:
函数名() OVER (
[PARTITION BY 列]
[ORDER BY 列]
[ROWS/RANGE 子句]
)
三大组件:
- PARTITION BY——分组(类似GROUP BY但不合并)
- ORDER BY——窗口内排序
- ROWS/RANGE——窗口范围(前后几行)
第二步:排名函数
三种排名函数应用场景不同:
ROW_NUMBER()
SELECT name, score, ROW_NUMBER() OVER (ORDER BY score DESC) as rn
FROM students;
1,2,3,4,5连续排名,不并列。适合分页、每组取第一条。
RANK()
SELECT name, score, RANK() OVER (ORDER BY score DESC) as rk
FROM students;
1,2,2,4,5并列后跳跃。适合排名显示(销售排名、考试排名)。
DENSE_RANK()
SELECT name, score, DENSE_RANK() OVER (ORDER BY score DESC) as drk
FROM students;
1,2,2,3,4并列后连续。适合等级评定(A/B/C级)。
分组内排名
SELECT name, class, score, RANK() OVER (PARTITION BY class ORDER BY score DESC) as class_rank
FROM students;
每个班级单独排名,不是全校排名。
第三步:累计与移动计算
累计求和
SELECT date, amount, SUM(amount) OVER (ORDER BY date) as cumulative
FROM orders;
累计销售额,看业务增长趋势。
移动平均
SELECT date, amount, AVG(amount) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) as ma7
FROM orders;
7天移动平均线,看趋势更平滑。
移动求和
SELECT date, amount, SUM(amount) OVER (ORDER BY date ROWS BETWEEN 29 PRECEDING AND CURRENT ROW) as sum_30d
FROM orders;
30天累计销售。
窗口范围详解
- ROWS BETWEEN 2 PRECEDING AND CURRENT ROW——当前行+前2行
- ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW——从分区开始到当前
- ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING——从当前到分区结束
- RANGE BETWEEN——按值范围而非物理行
第四步:占比与对比
占总销售比例
SELECT product, sales, sales * 100.0 / SUM(sales) OVER () as pct
FROM product_sales;
组内占比
SELECT category, product, sales, sales * 100.0 / SUM(sales) OVER (PARTITION BY category) as cat_pct
FROM product_sales;
同比环比
SELECT
month, sales,
sales – LAG(sales) OVER (ORDER BY month) as mom_change,
(sales – LAG(sales) OVER (ORDER BY month)) * 100.0 / LAG(sales) OVER (ORDER BY month) as mom_pct
FROM monthly_sales;
LAG/LEAD函数取上N行/下N行的值,是同比环比的关键。
第五步:复杂实战案例
案例1:用户留存分析
WITH cohorts AS (
SELECT user_id, DATE_TRUNC(‘month’, signup_date) as cohort
FROM users
),
user_activity AS (
SELECT user_id, DATE_TRUNC(‘month’, event_date) as active_month
FROM events
)
SELECT
c.cohort,
a.active_month,
COUNT(DISTINCT a.user_id) as active_users,
COUNT(DISTINCT a.user_id) * 100.0 / COUNT(DISTINCT c.user_id) as retention_pct
FROM cohorts c
LEFT JOIN user_activity a ON c.user_id = a.user_id
GROUP BY c.cohort, a.active_month;
案例2:用户RFM分层
SELECT user_id,
NTILE(5) OVER (ORDER BY recency DESC) as R_score,
NTILE(5) OVER (ORDER BY frequency DESC) as F_score,
NTILE(5) OVER (ORDER BY monetary DESC) as M_score
FROM user_rfm;
NTILE把用户分成5层,组合RFM分数做用户分层。
案例3:连续登录天数
WITH dates AS (
SELECT user_id, login_date,
login_date – INTERVAL (ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date)) DAY as grp
FROM logins
)
SELECT user_id, MIN(login_date) as streak_start, MAX(login_date) as streak_end,
COUNT(*) as streak_days
FROM dates
GROUP BY user_id, grp;
经典的Gaps and Islands算法识别连续登录。
性能优化
- 索引——窗口函数的ORDER BY和PARTITION BY列建索引
- 避免大分区——PARTITION BY的列值不能太多(不超过1000)
- 减少计算——能用简单聚合的不要用窗口函数
- 分批计算——超大数据集考虑分批计算
常见数据库支持
- PostgreSQL——完整支持
- MySQL 8.0+——完整支持
- SQL Server——完整支持
- Oracle——完整支持
- SQLite——3.25+部分支持
- ClickHouse/Snowflake/BigQuery——云数仓都支持
常见问题与误区
- 窗口过大性能差——按时间或类别分区避免全表窗口
- NULL值处理——PARTITION BY/ORDER BY的NULL值默认排首位
- 结果非预期——窗口函数是结果列,不是过滤条件,WHERE要用子查询
- 过度使用——简单聚合能解决的不要用窗口函数
效率数据
实测:用户留存分析,传统嵌套子查询约5-8分钟,窗口函数约30秒。销售同比环比,传统需要3层子查询+自连接,窗口函数1层完成,可读性提升10倍。适合场景:业务报表、用户分析、销售分析、金融分析、数据探索。