分组排序取第一条数据 SQL写法

1. 背景

在数据库查询过程中经常遇到需要分组排序查询第一条数据的情况。例如,在消息列表中需要展示每个联系人最近的一条信息。

2. 解决方案

目前我接触到的解决方案有两种,分别是开窗函数 row_number 和变量法。

2.1 开窗函数法

比较常用的解决方案是使用开窗函数 row_number() over(partition by xxx order by xxx) 。使用开窗函数比较简便,只需要两个步骤

2.1.1 分组+排序+标注序号

sql 复制代码
select *, 
	row_number()  over (partition by ${group_col} order by ${order_col} ) as rownum 
from ${table}

以上 SQL 中 ${group_col} 表示要分组的字段;${order_col} 表示排序字段;rownum 是为序号字段取的别名 。这条 SQL 的含义是将数据表 table 中的数据根据 group_col 分组后根据 order_col 进行排序,并为每条数据标注出在当前分组中的序号。

2.1.2 取第一条

sql 复制代码
select * from t1 where rownum = 1
  • t1分组+排序+标注序号 后的表,这里为了简洁就直接用 t1 代表子查询了。
  • rownum分组+排序+标注序号 中序号的别名。where rownum = 1 表示取 分组+排序+标注序号 后的第一条数据。

SQL 的含义是从 分组+排序+标注序号 后的表中查询序号为 1 的数据,这里的序号是各分组中排序后的序号。

3.1.3 Demo

将上述两个步骤连贯起来的 Demo 如下。

sql 复制代码
select * from (
	select *, 
	row_number() over (partition by ${group_col} order by ${order_col} )  rownum 
	from ${table}) t1 
where rownum = 1 

2.2 变量法

开窗函数的方法在 Hive 和高版本的 MySQL 里都是比较简单的查询方法,但是在低版本的 MySQL (比如 MySQL 5.7)中不支持开窗函数。所以只能换另一种方法,就是变量法。变量法需要通过变量来达成开窗函数的效果所以比较复杂,主要分为三部:数据排序、添加序号、取第一条。

2.2.1 数据排序

sql 复制代码
select * from ${table} order by ${group_col}, ${order_col}

首先对 table 表中的数据进行排序,排序的字段中第一个必须是想要用于分组的字段 group_col,后面才是需要排序的字段 order_col

2.2.2 添加序号

sql 复制代码
SELECT t.*, 
	IF(@x = ${group_col} OR (@x IS NULL AND ${group_col} IS NULL), 
		@rank:=@rank + 1, 
		@rank:=1 AND @x:= ${group_col}) as rownum 
	from (SELECT @x:=- 1) t0, t

这条 SQL 中定义了两个变量 xrank,整个 SQL 都围绕这两个变量展开,下面分别解释一下相关的内容。

  • x 变量用于保存当前分组的字段值,比如当前分组的字段值为 1,如果后面发现分组字段值不等于 1 了则说明换到另一个分组了,需要重新计数。
  • rank 变量用于保存当前分组中上一条记录的序号,如果上一条记录的序号是 1,则当前记录序号为 2。
  • (SELECT @x:=- 1) 的作用是为 x 变量初始化一个默认值,如果不初始化默认值则 x 默认为 nullx 的默认值必须是分组字段中没有的值,不建议设置为 null
  • IF(@x = ${group_col} OR (@x IS NULL AND ${group_col} IS NULL), @rank:=@rank + 1, @rank:=1 AND @x:=${group_col}) as rownum 这条语句就是序号赋值的关键所在。含义为如果 x 与当前记录的分组字段值相同则 rank = rank + 1(同分组内序号递增),否则 rank=1 (不同分组重新开始计数)并且 @x:=${group_col}(将 x 赋值为当前记录的分组字段值)。
  • t数据排序 后的表,这里为了简洁用 t 来代替子查询。

2.2.3 取第一条

sql 复制代码
select * from t2 where t2.rownum = 1

t2添加序号 后的表,这里为了简洁也是用 t2 代替子查询。

2.2.4 Demo

将上述三个步骤融合在一起如下。

sql 复制代码
select * from (
	SELECT t.*, 
		IF(@x = ${group_col} OR (@x IS NULL AND ${group_col} IS NULL), 		
		@rank:=@rank + 1, 
		@rank:=1 AND @x:= ${group_col}) as rownum 
	from (SELECT @x:=- 1) as t0, 
	(select * from ${table} order by ${group_col}, ${order_col}) as t) as t2 where t2.rownum = 1

3. 例子

3.1 需求

有一张留言记录表,表中记录的是每个用户的留言和管理员的回复。现需要取每个用户最新的一条留言记录

3.2 表结构

字段名 字段类型 说明
id Bigint Id
addtime Timestamp 添加时间
userid Bigint 用户 id
adminid Bigint 管理员 Id
ask Longtext 留言
reply Longtext 回复
isreply Int 是否回复

3.3 表数据概览

Id Addtime Userid Adminid Ask Reply Isreply
1 2024-04-15 13:05:00 1 1 提问1 0
142 2024-04-15 13:04:00 2 1 提问2 0
143 2024-04-15 13:03:00 3 1 提问3 0
144 2024-04-15 13:01:00 4 1 提问4 0
182 2024-04-22 14:31:27 1713703513670 1 123 0
183 2024-04-22 14:31:33 1713703513670 1 12312321 0
184 2024-04-22 14:31:36 1713703513670 1 1232133 0

3.4 查询语句

查询的思路就是根据用户 Id 分组,按添加时间倒序(从大到小)排,取每个用户 Id 分组中的第一条数据。为了方便查看,在查询最后再将记录根据添加时间倒序排列,将留言时间最近的用户放在前面。

3.4.1 开窗函数法

sql 复制代码
select * from (
	select *, 
		row_number()  over (partition by userid order by addtime desc)  rownum 
		from chat 
		where ask is not null ) t1 
	where rownum = 1 order by addtime desc

3.4.2 变量法

sql 复制代码
select addtime, userid, ask from (
    (select *, 
		IF(@x = userid OR (@x IS NULL AND userid IS NULL), 
			@rank:=@rank + 1, 
			@rank:=1 AND @x:=userid) AS rownum
    FROM
		(SELECT @x:=- 1) t0, 
			(SELECT * FROM chat 
				ORDER BY userid , addtime DESC) t) t2
	WHERE t2.rownum = 1 order by addtime desc; 

3.5 结果

Addtime Userid Ask
2024-04-22 14:31:36 1713703513670 1232133
2024-04-15 13:05:00 1 提问 1
2024-04-15 13:04:00 2 提问 2
2024-04-15 13:03:00 3 提问 3
2024-04-15 13:01:00 4 提问 4
相关推荐
蓝鸟19742 分钟前
Oracle 19c JSON_OBJECT 完全实战指南|嵌套、多行多列数组合并、空值踩坑、医保报文落地
数据库·oracle·json_arrayagg·多行转json数组·sql实战踩坑·数据库json拼装·json_object
霸道流氓气质3 分钟前
Mysql中慢 SQL 优化实战:减少不必要的 JOIN 关联
sql·mysql·mybatis
晴天165 分钟前
Agent 全栈学习笔记 1-Day14
数据库·笔记·学习
瀚高PG实验室5 分钟前
几种因网络波动导致应用与数据库操作异常的现象
运维·网络·数据库·postgresql·瀚高数据库
一个天蝎座 白勺 程序猿19 分钟前
SQL Server数据迁移金仓性能优化实战:从慢SQL到系统瓶颈,一套组合拳打下来
sql·oracle·性能优化·kingbasees
崖边看雾27 分钟前
同步上下文管理器规则
开发语言·数据库
.柒宇.30 分钟前
运维常见面试题_02_数据库
运维·数据库·sql·面试
名字还没想好☜9 小时前
Python f-string 进阶:数字格式化、对齐填充、调试 = 号与嵌套表达式
开发语言·数据库·python·字符串格式化·f-string
ltl9 小时前
Serverless 数据库弹性理论:Neon 与 Aurora Serverless v2
数据库
marvelyu14 小时前
每天10分钟学会OceanBase系列(Day 20):跨机房容灾实战——构建多数据中心高可用架构
java·大数据·数据库