掌握 SQL 窗口函数:分组、排名与最新记录获取的最佳实践

文章目录

在写 SQL 时都会遇到一个经典需求:

按某个字段分组(比如设备、用户、订单),取每组最新一条 / Top N 条记录

同时还想做:排名、累计统计、取上一条/下一条记录

但又不想把数据 GROUP BY 之后"压扁"

这时就轮到 窗口函数(Window Function) 出场了。


一、窗口函数是什么

窗口函数 = 在"分组后的行集合"上做计算,但不合并行

它和 GROUP BY 最大的区别:

  • GROUP BY:把多行汇总成一行(明细没了)
  • 窗口函数:明细保留,但能在每一行上得到组内计算结果

二、窗口函数的统一写法

窗口函数都有这个结构:

sql 复制代码
函数名(...) OVER (
    PARTITION BY ...
    ORDER BY ...
    [ROWS / RANGE ...]
)

你可以理解为:

  • PARTITION BY:按什么分组(逻辑分组,不合并行)
  • ORDER BY:组内按什么排序
  • OVER:声明这是窗口函数

三、最常见需求:每组取最新一条记录

假设你有一张表:

  • resource_key:分组字段(比如设备ID/用户ID)
  • createtime:时间字段

✅ 推荐写法(最稳)

sql 复制代码
SELECT id, resource_key, longitude, latitude, createtime
FROM (
    SELECT *,
           ROW_NUMBER() OVER (
               PARTITION BY resource_key
               ORDER BY createtime DESC, id DESC
           ) AS rn
    FROM track_data
) t
WHERE rn = 1;

为什么要加 id DESC

因为实际数据里经常出现:

  • 同一组内
  • 同一时间戳 createtime
  • 有多条记录

只写 ORDER BY createtime DESC 时,"哪条排第一"可能不稳定。

加上 id DESC 就能保证:同时间戳时取 id 最大那条,结果稳定。


四、窗口函数四大类

① 排名类(最常用)

1️⃣ ROW_NUMBER(每行唯一编号)

sql 复制代码
ROW_NUMBER() OVER (
  PARTITION BY key
  ORDER BY createtime DESC
)

特点:不管值是否相同,编号都递增:1、2、3...

用途:

  • 每组最新 1 条(rn=1)
  • 每组 Top N 条(rn<=N)

2️⃣ RANK(允许并列,会跳号)

sql 复制代码
RANK() OVER (...)

例子:

createtime RANK
10:00:05 1
10:00:05 1
10:00:03 3

用途:排名场景、允许并列名次


3️⃣ DENSE_RANK(允许并列,不跳号)

sql 复制代码
DENSE_RANK() OVER (...)

用途:等级分层、分段统计


② 聚合类窗口函数(统计但不合并行)

COUNT / SUM / AVG / MAX / MIN OVER

sql 复制代码
COUNT(*) OVER (PARTITION BY key) AS cnt

用途:

  • 每行旁边展示"该组一共有多少条"
  • 不需要 GROUP BY,明细仍保留

③ 偏移类(上一条/下一条)

⭐ LAG:上一条记录的值

sql 复制代码
LAG(longitude) OVER (
  PARTITION BY key
  ORDER BY createtime
) AS prev_lng

用途:

  • 取上一条数据做对比
  • 计算差值(如金额差、数值变化)

LEAD:下一条记录的值

sql 复制代码
LEAD(createtime) OVER (...) AS next_time

用途:

  • 计算"本条到下一条"的间隔
  • 分段分析

④ 首尾值函数(组内第一/最后)

FIRST_VALUE

sql 复制代码
FIRST_VALUE(createtime) OVER (
  PARTITION BY key
  ORDER BY createtime DESC
) AS newest_time

LAST_VALUE(注意窗口范围)

默认容易踩坑,建议显式指定窗口范围:

sql 复制代码
LAST_VALUE(createtime) OVER (
  PARTITION BY key
  ORDER BY createtime
  ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS last_time

五、为什么 GROUP BY 不能直接查出整条最新记录?

很多人会写:

sql 复制代码
SELECT key, MAX(createtime)
FROM t
GROUP BY key;

这只能得到 每组最大时间 ,但拿不到这条时间对应的其它字段(经度、纬度等)。

你如果强行把其它字段也 select 出来:

sql 复制代码
SELECT key, createtime, longitude
FROM t
GROUP BY key;

数据库会报错(标准 SQL 不允许),原因是:

同一个 key 有多行,longitude 有多个候选值,数据库不知道该选哪个

所以要么:

  • GROUP BY + MAX + JOIN(能拿回整行,但写法更绕)
  • 用窗口函数(更直观、可拓展 TopN)

六、实用的几个模板

1)每组 Top 3

sql 复制代码
SELECT *
FROM (
  SELECT *,
         ROW_NUMBER() OVER (PARTITION BY key ORDER BY createtime DESC) rn
  FROM t
) x
WHERE rn <= 3;

2)每组统计条数 + 保留明细

sql 复制代码
SELECT *,
       COUNT(*) OVER (PARTITION BY key) AS group_cnt
FROM t;

3)对比上一条数据

sql 复制代码
SELECT *,
       LAG(value) OVER (PARTITION BY key ORDER BY createtime) AS prev_value
FROM t;

七、总结

ROW_NUMBER 只是窗口函数的入口

窗口函数真正强大之处在于:
既能做分组计算,又能保留每一行明细

常用记忆法:

  • 取最新 / TopNROW_NUMBER
  • 排名RANK / DENSE_RANK
  • 统计但保留明细COUNT/SUM/AVG OVER
  • 上一条/下一条LAG / LEAD
  • 组内首尾FIRST_VALUE / LAST_VALUE
相关推荐
剩下了什么4 小时前
MySQL JSON_SET() 函数
数据库·mysql·json
山峰哥4 小时前
数据库工程与SQL调优——从索引策略到查询优化的深度实践
数据库·sql·性能优化·编辑器
较劲男子汉5 小时前
CANN Runtime零拷贝传输技术源码实战 彻底打通Host与Device的数据传输壁垒
运维·服务器·数据库·cann
java搬砖工-苤-初心不变5 小时前
MySQL 主从复制配置完全指南:从原理到实践
数据库·mysql
WangYaolove13146 小时前
基于python的在线水果销售系统(源码+文档)
python·mysql·django·毕业设计·源码
山岚的运维笔记7 小时前
SQL Server笔记 -- 第18章:Views
数据库·笔记·sql·microsoft·sqlserver
roman_日积跬步-终至千里7 小时前
【LangGraph4j】LangGraph4j 核心概念与图编排原理
java·服务器·数据库
汇智信科7 小时前
打破信息孤岛,重构企业效率:汇智信科企业信息系统一体化运营平台
数据库·重构
野犬寒鸦8 小时前
从零起步学习并发编程 || 第六章:ReentrantLock与synchronized 的辨析及运用
java·服务器·数据库·后端·学习·算法
霖霖总总8 小时前
[小技巧66]当自增主键耗尽:MySQL 主键溢出问题深度解析与雪花算法替代方案
mysql·算法