什么是窗口函数?
窗口函数是 SQL 中一种强大的分析功能,它允许在对每一行进行计算时,能够访问到与当前行相关的多行数据。与 GROUP BY
不同,窗口函数不会将多行合并为一行,而是保留所有原始行,同时添加计算列。
直观理解
想象一个 Excel 表格:
姓名 | 部门 | 工资 |
---|---|---|
张三 | 技术部 | 8000 |
李四 | 技术部 | 9000 |
王五 | 技术部 | 7500 |
赵六 | 销售部 | 7000 |
普通 GROUP BY:
SELECT 部门, AVG(工资) as 平均工资 FROM 员工表 GROUP BY 部门;
结果:2行数据(部门聚合结果)
窗口函数:
SELECT 姓名, 部门, 工资, AVG(工资) OVER (PARTITION BY 部门) as 部门平均工资 FROM 员工表;
结果:4行数据(保留所有原始行,新增计算列)
窗口函数的核心概念
1. 窗口定义
函数名() OVER (PARTITION BY 分组字段ORDER BY 排序字段[窗口帧]
)
-
PARTITION BY:将数据分成多个窗口(类似 GROUP BY)
-
ORDER BY:在窗口内排序
-
窗口帧:定义计算范围(如前后几行)
2. 常用窗口函数分类
排名函数
-- 为每行分配唯一序号
ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) as rn-- 排名,相同值会有并列
RANK() OVER (PARTITION BY department ORDER BY salary DESC) as rank-- 密集排名,无间隔
DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) as dense_rank
聚合函数
-- 窗口内求和
SUM(salary) OVER (PARTITION BY department) as dept_total-- 窗口内平均值
AVG(salary) OVER (PARTITION BY department) as dept_avg-- 窗口内最大值
MAX(salary) OVER (PARTITION BY department) as dept_max
分布函数
-- 百分比排名
PERCENT_RANK() OVER (ORDER BY salary) as pct_rank-- 累计分布
CUME_DIST() OVER (ORDER BY salary) as cume_dist
实战案例:解决数据去重问题
问题场景
在区块链任务表中,每个 (start_block, end_block)
组合可能有多个版本,我们需要获取每个组合的最新版本(created_at
最大的记录)。
传统方案的局限性
-- GROUP BY 无法获取完整记录
SELECT start_block, end_block, MAX(created_at)
FROM block_tasks
GROUP BY start_block, end_block;
问题:只能返回分组字段和聚合值,无法获取其他字段的完整信息。
窗口函数解决方案
SELECT * FROM (SELECT *,ROW_NUMBER() OVER (PARTITION BY start_block, end_block ORDER BY created_at DESC) as rnFROM block_tasks WHERE status = 'init'
) WHERE rn = 1;
执行过程分解
原始数据:
id | start_block | end_block | status | created_at |
---|---|---|---|---|
1 | 1000 | 2000 | init | 2024-01-01 10:00:00 |
2 | 1000 | 2000 | init | 2024-01-02 15:00:00 |
3 | 1001 | 2001 | init | 2024-01-01 09:00:00 |
4 | 1001 | 2001 | init | 2024-01-03 14:00:00 |
窗口函数计算后:
id | start_block | end_block | created_at | rn | |
---|---|---|---|---|---|
1 | 1000 | 2000 | 2024-01-01 10:00:00 | 2 | |
2 | 1000 | 2000 | 2024-01-02 15:00:00 | 1 | ✅ |
3 | 1001 | 2001 | 2024-01-01 09:00:00 | 3 | |
4 | 1001 | 2001 | 2024-01-03 14:00:00 | 1 | ✅ |
5 | 1001 | 2001 | 2024-01-02 11:00:00 | 2 |
最终结果(rn = 1):
id | start_block | end_block | created_at |
---|---|---|---|
2 | 1000 | 2000 | 2024-01-02 15:00:00 |
4 | 1001 | 2001 | 2024-01-03 14:00:00 |
更多实用场景
场景1:计算移动平均
-- 计算每行及前2行的平均价格
SELECT date, price,AVG(price) OVER (ORDER BY date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) as moving_avg
FROM stock_prices;
场景2:计算累计和
-- 计算每月销售额的累计和
SELECT month, sales,SUM(sales) OVER (ORDER BY month ROWS UNBOUNDED PRECEDING) as cumulative_sales
FROM monthly_sales;
场景3:前后行比较
-- 比较当前行与前一行的差异
SELECT date, revenue,LAG(revenue) OVER (ORDER BY date) as prev_revenue,revenue - LAG(revenue) OVER (ORDER BY date) as growth
FROM daily_revenue;
ClickHouse 中的窗口函数
基本语法
function_name([expression]) OVER ([PARTITION BY expression1, expression2, ...][ORDER BY expression1 [ASC|DESC], expression2 [ASC|DESC], ...][frame_specification]
)
性能优化建议
-
利用索引:确保
PARTITION BY
和ORDER BY
的字段有合适索引 -
避免全表排序:使用
LIMIT
限制结果集大小 -
合理分区:数据分区可以减少单个窗口的数据量
ClickHouse 特定函数
-- 获取每个分组的第一个值
first_value(column) OVER (PARTITION BY group ORDER BY time)-- 获取每个分组的最后一个值
last_value(column) OVER (PARTITION BY group ORDER BY time)-- 计算分位数
quantile(0.5)(column) OVER (PARTITION BY group)
窗口函数 vs GROUP BY
特性 | GROUP BY | 窗口函数 |
---|---|---|
输出行数 | 分组数量 | 原始行数 |
字段访问 | 只能访问分组字段和聚合值 | 可以访问所有原始字段 |
多个聚合 | 需要多个查询或复杂JOIN | 单次查询可计算多个窗口 |
排序控制 | 无法控制选择哪条记录 | 明确指定排序和选择逻辑 |
性能 | 通常更快,但信息有限 | 稍慢,但功能更强大 |
实际代码示例
Go + GORM 实现
func GetLatestTasks(db *gorm.DB) ([]*BlockTask, error) {var tasks []*BlockTasksql := `SELECT * FROM (SELECT *,ROW_NUMBER() OVER (PARTITION BY start_block, end_block ORDER BY created_at DESC) as rnFROM block_tasks WHERE status = 'init') WHERE rn = 1ORDER BY start_block DESCLIMIT 100`err := db.Raw(sql).Scan(&tasks).Errorreturn tasks, err
}
总结
窗口函数是现代 SQL 中不可或缺的强大工具,它解决了 GROUP BY
的诸多限制:
-
✅ 保留原始数据:不丢失任何行信息
-
✅ 灵活分析:支持排名、聚合、分布等多种计算
-
✅ 性能优秀:相比多次查询或复杂 JOIN,通常更高效
-
✅ 代码简洁:用声明式语法替代复杂的过程逻辑
在数据去重、移动平均、排名计算、趋势分析等场景中,窗口函数都能提供优雅而高效的解决方案。掌握窗口函数,将极大提升你的数据分析能力!