文章目录
-
- [一、为什么窗口函数是 SQL 的"核武器"?](#一、为什么窗口函数是 SQL 的“核武器”?)
-
- [1.1 传统聚合的局限](#1.1 传统聚合的局限)
- [1.2 窗口函数 vs 普通聚合](#1.2 窗口函数 vs 普通聚合)
- [1.3 优化点 checklist](#1.3 优化点 checklist)
- [1.4 窗口函数语法:三大核心组件](#1.4 窗口函数语法:三大核心组件)
- [1.5 窗口函数使用建议](#1.5 窗口函数使用建议)
- 二、五大类窗口函数实战
-
- [2.1 聚合函数(最常用)](#2.1 聚合函数(最常用))
- [2.2 排名函数(Top-N 分析)](#2.2 排名函数(Top-N 分析))
- [2.3 偏移函数(时间序列分析)](#2.3 偏移函数(时间序列分析))
- [2.4 分布函数(百分位分析)](#2.4 分布函数(百分位分析))
- [2.5 值函数(高级分析)](#2.5 值函数(高级分析))
- [三、Frame Clause(帧范围)深度解析](#三、Frame Clause(帧范围)深度解析)
-
- [3.1 默认帧范围规则](#3.1 默认帧范围规则)
- [3.2 帧类型详解](#3.2 帧类型详解)
- [3.3 经典案例:修复 LAST_VALUE()](#3.3 经典案例:修复 LAST_VALUE())
- 四、高性能窗口函数技巧
-
- [4.1 索引优化](#4.1 索引优化)
- [4.2 避免全表扫描](#4.2 避免全表扫描)
- 五、综合实战案例
-
- [5.1 案例 1:用户留存分析(经典漏斗)](#5.1 案例 1:用户留存分析(经典漏斗))
- [5.2 案例 2:股票技术指标(移动平均线)](#5.2 案例 2:股票技术指标(移动平均线))
- [5.3 案例 3:会话化用户行为](#5.3 案例 3:会话化用户行为)
- 六、常见陷阱与避坑指南
-
- [6.1 陷阱 1:混淆 ROWS 和 RANGE](#6.1 陷阱 1:混淆 ROWS 和 RANGE)
- [6.2 陷阱 2:在 WHERE/HAVING 中使用窗口函数](#6.2 陷阱 2:在 WHERE/HAVING 中使用窗口函数)
- [七、PostgreSQL 特色功能](#七、PostgreSQL 特色功能)
-
- [7.1 FILTER 子句(条件聚合)](#7.1 FILTER 子句(条件聚合))
- [7.2 IGNORE NULLS(PostgreSQL 11+)](#7.2 IGNORE NULLS(PostgreSQL 11+))
适用版本 :PostgreSQL 9.0+(推荐 12+)
目标读者 :数据分析师、后端开发、DBA
核心价值:掌握窗口函数,用一行 SQL 替代百行代码,解锁高级分析能力
一、为什么窗口函数是 SQL 的"核武器"?
1.1 传统聚合的局限
sql
-- 需求:计算每个部门的平均工资,同时保留员工明细
SELECT
dept,
AVG(salary) -- 错误!非聚合列不能与聚合函数共存
FROM employees;
解决方案:
- 方案1:子查询(嵌套复杂)
- 方案2:应用层处理(性能差)
- 方案3:窗口函数(优雅高效)
sql
SELECT
name,
dept,
salary,
AVG(salary) OVER (PARTITION BY dept) AS dept_avg -- 保留明细 + 聚合
FROM employees;
1.2 窗口函数 vs 普通聚合
| 特性 | 普通聚合 (GROUP BY) |
窗口函数 (OVER()) |
|---|---|---|
| 行数 | 减少(每组1行) | 不变(每行保留) |
| 明细数据 | 丢失 | 保留 |
| 多维度分析 | 需多次查询 | 单次查询完成 |
| 排序敏感 | 否 | 是(支持 ORDER BY) |
1.3 优化点 checklist
执行计划检查:
sql
EXPLAIN ANALYZE
SELECT ..., ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary)
FROM employees;
关键优化点:
-
PARTITION BY列有索引 -
ORDER BY列包含在索引中 - 避免在大表上无分区的窗口函数
- 使用
ROWS而非RANGE(除非需要逻辑范围) - 复杂窗口用 CTE 分步计算
1.4 窗口函数语法:三大核心组件
sql
function_name (expression) OVER (
[PARTITION BY partition_expression, ...]
[ORDER BY sort_expression [ASC|DESC], ...]
[frame_clause]
)
(1) PARTITION BY:分组(类似 GROUP BY)
- 定义窗口的分组边界
- 空值视为同一组
(2) ORDER BY:排序(窗口内排序)
- 决定计算顺序 和默认帧范围
- 对排名类函数至关重要
(3) Frame Clause:帧范围(计算窗口大小)
- 定义当前行关联的行范围
- 默认行为因
ORDER BY存在与否而异
1.5 窗口函数使用建议
| 场景 | 推荐函数 | 关键要点 |
|---|---|---|
| 分组聚合+明细 | SUM() OVER (PARTITION BY ...) |
保留原始行 |
| Top-N 分析 | ROW_NUMBER() + CTE |
避免 RANK() 跳跃 |
| 时间序列 | LAG()/LEAD() |
处理 NULL 值 |
| 分布分析 | PERCENT_RANK()/NTILE() |
理解分位含义 |
| 移动计算 | AVG() OVER (ROWS ...) |
显式指定帧范围 |
建议 :
"当你发现自己写了很多子查询或应用层循环时,停下来想想------窗口函数能解决吗?"掌握窗口函数,你将从 SQL 初学者进阶为数据处理大师!
二、五大类窗口函数实战
2.1 聚合函数(最常用)
支持所有聚合函数:SUM(), AVG(), COUNT(), MAX(), MIN(), STDDEV()...
场景:部门工资分析
sql
SELECT
name,
dept,
salary,
-- 部门总工资
SUM(salary) OVER (PARTITION BY dept) AS dept_total,
-- 部门平均工资
ROUND(AVG(salary) OVER (PARTITION BY dept), 2) AS dept_avg,
-- 公司总人数
COUNT(*) OVER () AS company_size
FROM employees;
💡 技巧 :
OVER ()表示整个结果集为一个窗口
2.2 排名函数(Top-N 分析)
| 函数 | 特点 | 并列处理 |
|---|---|---|
ROW_NUMBER() |
连续唯一 | 无并列(强制排序) |
RANK() |
跳跃排名 | 相同值同排名,下一名跳过 |
DENSE_RANK() |
密集排名 | 相同值同排名,下一名连续 |
场景:销售排行榜
sql
SELECT
salesperson,
region,
amount,
ROW_NUMBER() OVER (ORDER BY amount DESC) AS row_num,
RANK() OVER (ORDER BY amount DESC) AS rank_num,
DENSE_RANK() OVER (ORDER BY amount DESC) AS dense_rank_num
FROM sales;
输出示例:
amount | row_num | rank_num | dense_rank_num
-------|---------|----------|---------------
5000 | 1 | 1 | 1
4000 | 2 | 2 | 2
4000 | 3 | 2 | 2 ← 并列
3000 | 4 | 4 | 3 ← RANK跳过3, DENSE_RANK连续
实战:每个区域 Top 3 销售
sql
WITH ranked_sales AS (
SELECT
*,
ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount DESC) AS rn
FROM sales
)
SELECT * FROM ranked_sales WHERE rn <= 3;
2.3 偏移函数(时间序列分析)
| 函数 | 作用 |
|---|---|
LAG(column, offset, default) |
获取前N行的值 |
LEAD(column, offset, default) |
获取后N行的值 |
FIRST_VALUE(column) |
窗口第一行的值 |
LAST_VALUE(column) |
窗口最后一行的值 |
场景:计算日环比增长率
sql
SELECT
date,
revenue,
LAG(revenue) OVER (ORDER BY date) AS prev_revenue,
ROUND(
(revenue - LAG(revenue) OVER (ORDER BY date))
/ NULLIF(LAG(revenue) OVER (ORDER BY date), 0) * 100,
2
) AS mom_growth_pct
FROM daily_revenue
ORDER BY date;
场景:用户会话识别(事件间隔 > 30分钟 = 新会话)
sql
SELECT
user_id,
event_time,
-- 判断是否新会话起点
CASE WHEN
EXTRACT(EPOCH FROM (
event_time - LAG(event_time) OVER (
PARTITION BY user_id
ORDER BY event_time
)
)) > 1800 -- 30分钟=1800秒
OR LAG(event_time) OVER (PARTITION BY user_id ORDER BY event_time) IS NULL
THEN 1 ELSE 0 END AS is_new_session
FROM user_events;
2.4 分布函数(百分位分析)
| 函数 | 作用 |
|---|---|
PERCENT_RANK() |
当前行的相对排名(0~1) |
CUME_DIST() |
累积分布(≤当前值的比例) |
NTILE(n) |
将窗口分为N个桶(等深分箱) |
场景:学生成绩分位分析
sql
SELECT
student,
score,
-- 百分等级(0=最低, 1=最高)
ROUND(PERCENT_RANK() OVER (ORDER BY score), 4) AS pct_rank,
-- 累积分布(如0.8=80%学生分数≤当前)
ROUND(CUME_DIST() OVER (ORDER BY score), 4) AS cume_dist,
-- 四分位分组(1=最低25%, 4=最高25%)
NTILE(4) OVER (ORDER BY score) AS quartile
FROM exam_scores;
2.5 值函数(高级分析)
| 函数 | 作用 |
|---|---|
NTH_VALUE(column, n) |
窗口第N行的值 |
RATIO_TO_REPORT(expression) |
当前行值占窗口总和的比例 |
场景:计算产品销售额占比
sql
SELECT
product,
sales,
ROUND(
RATIO_TO_REPORT(sales) OVER (),
4
) AS sales_ratio
FROM products;
三、Frame Clause(帧范围)深度解析
帧范围定义当前行参与计算的行集合,是窗口函数最易混淆的部分。
3.1 默认帧范围规则
| 条件 | 默认帧范围 |
|---|---|
有 ORDER BY |
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW |
无 ORDER BY |
RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING |
陷阱 :
LAST_VALUE()在默认帧下永远返回当前行!
3.2 帧类型详解
(1) ROWS:基于物理行数
sql
-- 移动平均(前2天+当天+后1天)
AVG(price) OVER (
ORDER BY date
ROWS BETWEEN 2 PRECEDING AND 1 FOLLOWING
)
(2) RANGE:基于逻辑值范围
sql
-- 所有相同日期的平均价格(处理重复日期)
AVG(price) OVER (
ORDER BY date
RANGE BETWEEN INTERVAL '1 day' PRECEDING AND CURRENT ROW
)
(3) GROUPS:基于对等组(PostgreSQL 11+)
sql
-- 每个排名组的平均值
AVG(salary) OVER (
ORDER BY rank
GROUPS BETWEEN 1 PRECEDING AND 1 FOLLOWING
)
3.3 经典案例:修复 LAST_VALUE()
sql
-- 错误:默认帧导致 LAST_VALUE = 当前行
SELECT
id,
value,
LAST_VALUE(value) OVER (ORDER BY id) -- 总是返回value本身!
FROM t;
-- 正确:显式指定完整帧
SELECT
id,
value,
LAST_VALUE(value) OVER (
ORDER BY id
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS last_val
FROM t;
四、高性能窗口函数技巧
4.1 索引优化
PARTITION BY列 → 建复合索引ORDER BY列 → 索引需包含排序字段
sql
-- 查询:按部门分组,按工资排序
CREATE INDEX idx_emp_dept_salary ON employees(dept, salary DESC);
-- 窗口函数
SELECT *, ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC)
FROM employees;
4.2 避免全表扫描
sql
-- 低效:先计算全表窗口,再过滤
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
FROM employees
) t WHERE rn <= 3;
-- 高效:用 LATERAL JOIN(PostgreSQL 9.3+)
SELECT e.*
FROM (SELECT DISTINCT dept FROM employees) d
CROSS JOIN LATERAL (
SELECT *
FROM employees e2
WHERE e2.dept = d.dept
ORDER BY salary DESC
LIMIT 3
) e;
💡 何时用 LATERAL:当分组数量少但每组数据量大时
五、综合实战案例
5.1 案例 1:用户留存分析(经典漏斗)
需求:计算次日/7日/30日留存率
sql
WITH first_login AS (
SELECT
user_id,
MIN(login_date) AS first_date
FROM logins
GROUP BY user_id
),
retention_days AS (
SELECT
f.user_id,
f.first_date,
l.login_date,
l.login_date - f.first_date AS days_since_first
FROM first_login f
JOIN logins l ON f.user_id = l.user_id
),
cohort_size AS (
SELECT
first_date,
COUNT(DISTINCT user_id) AS cohort_users
FROM first_login
GROUP BY first_date
),
retention_counts AS (
SELECT
r.first_date,
r.days_since_first,
COUNT(DISTINCT r.user_id) AS retained_users
FROM retention_days r
WHERE r.days_since_first IN (0, 1, 7, 30)
GROUP BY r.first_date, r.days_since_first
)
SELECT
c.first_date,
c.cohort_users,
MAX(CASE WHEN r.days_since_first = 1 THEN r.retained_users END) AS day1_retained,
ROUND(100.0 * MAX(CASE WHEN r.days_since_first = 1 THEN r.retained_users END) / c.cohort_users, 2) AS day1_rate,
-- ... 类似计算7日/30日
FROM cohort_size c
LEFT JOIN retention_counts r ON c.first_date = r.first_date
GROUP BY c.first_date, c.cohort_users
ORDER BY c.first_date;
5.2 案例 2:股票技术指标(移动平均线)
sql
SELECT
date,
close_price,
-- 5日简单移动平均
AVG(close_price) OVER (
ORDER BY date
ROWS BETWEEN 4 PRECEDING AND CURRENT ROW
) AS ma5,
-- 20日指数移动平均(需递归CTE,此处简化)
AVG(close_price) OVER (
ORDER BY date
ROWS BETWEEN 19 PRECEDING AND CURRENT ROW
) AS ma20
FROM stock_prices
ORDER BY date;
5.3 案例 3:会话化用户行为
sql
WITH sessionized AS (
SELECT
user_id,
event_time,
-- 标记新会话(间隔>30分钟)
SUM(CASE WHEN
EXTRACT(EPOCH FROM (event_time - LAG(event_time) OVER w)) > 1800
OR LAG(event_time) OVER w IS NULL
THEN 1 ELSE 0 END) OVER w AS session_id
FROM user_events
WINDOW w AS (PARTITION BY user_id ORDER BY event_time)
)
SELECT
user_id,
session_id,
MIN(event_time) AS session_start,
MAX(event_time) AS session_end,
COUNT(*) AS events_count
FROM sessionized
GROUP BY user_id, session_id;
六、常见陷阱与避坑指南
6.1 陷阱 1:混淆 ROWS 和 RANGE
sql
-- 数据:相同日期多条记录
date | price
-----------|------
2023-01-01 | 100
2023-01-01 | 200 -- 同一天两条记录
-- ROWS:基于行数
AVG(price) OVER (ORDER BY date ROWS UNBOUNDED PRECEDING)
-- 第二行结果 = (100+200)/2 = 150
-- RANGE:基于值范围
AVG(price) OVER (ORDER BY date RANGE UNBOUNDED PRECEDING)
-- 第二行结果 = (100+200)/2 = 150 (相同)
-- 但如果 ORDER BY 多列:
ORDER BY date, price
-- ROWS:严格按行
-- RANGE:相同(date,price)视为一组
6.2 陷阱 2:在 WHERE/HAVING 中使用窗口函数
sql
-- 错误!窗口函数在 SELECT 阶段计算,WHERE 无法访问
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (...) AS rn
FROM t
) WHERE rn <= 10; -- 必须用子查询或CTE
七、PostgreSQL 特色功能
7.1 FILTER 子句(条件聚合)
sql
-- 计算部门中高薪员工(>10000)的比例
SELECT
dept,
COUNT(*) FILTER (WHERE salary > 10000) * 100.0 / COUNT(*) AS high_earner_pct
FROM employees
GROUP BY dept;
-- 结合窗口函数
SELECT
name,
dept,
salary,
COUNT(*) FILTER (WHERE salary > 10000) OVER (PARTITION BY dept) AS high_earners_in_dept
FROM employees;
7.2 IGNORE NULLS(PostgreSQL 11+)
sql
-- 跳过空值获取前一个非空值
LAG(salary IGNORE NULLS) OVER (ORDER BY date)