分组排序取第一条数据 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
相关推荐
码界奇点18 小时前
灵活性与高性能兼得KingbaseES 对 JSON 数据的全面支持深度解析
数据库·json·es
2501_9418714518 小时前
面向微服务链路追踪与全局上下文管理的互联网系统可观测性设计与多语言工程实践分享
大数据·数据库·python
·云扬·18 小时前
MySQL单机多实例部署两种实用方法详解
数据库·mysql·adb
odoo中国18 小时前
Pgpool-II 在 PostgreSQL 中的用例场景与优势
数据库·postgresql·中间件·pgpool
男孩李18 小时前
postgres数据库常用命令介绍
数据库·postgresql
IvorySQL19 小时前
让源码安装不再困难:IvorySQL 一键安装脚本的实现细节解析
数据库·人工智能·postgresql·开源
云和数据.ChenGuang19 小时前
openGauss赋能新能源汽车智能制造——比亚迪MES系统数据库国产化升级案例
数据库·汽车·opengauss·gaussdb·数据库权限管理
洛小豆19 小时前
她问我:数据库还在存 Timestamp?我说:大人,时代变了
数据库·后端·mysql
邵伯19 小时前
为什么你的 SELECT 有时会阻塞?
数据库·后端
洛小豆19 小时前
代码修仙录 · 第1期:宗门大阵崩了?只因少了毫秒三魂!
数据库·后端·mysql