记一次有意思的 SQL 实现 → 分组后取每组的第一条记录
开心一刻
今天,朋友气冲冲的走到我面前
朋友:我不是谈了个女朋友,谈了三个月嘛,昨天我偷看她手机,你猜她给我备注什么
我:备注什么?
朋友:舔狗 2 号!
我一听,气就上来了,说道:走,找她去,这婆娘确实该骂,臭不要脸的
朋友拉住我,劝到:哎哎,不是去骂她,是找她理论,叫她改成舔狗1号,是我先来的!
我:滚,我不认识你

需求背景
环境
MySQL 版本:8.0.27

有四张表:业务信息表、任务表、业务任务表、任务执行日志表

CREATE TABLE `t_business` (
`business_id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '业务id',
`business_name` VARCHAR(100) NOT NULL COMMENT '业务名',
`note` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '备注',
`create_user` BIGINT(20) NOT NULL COMMENT '创建人',
`create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
`modify_user` BIGINT(20) NOT NULL COMMENT '最终修改人',
`modify_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最终修改时间',
PRIMARY KEY (`business_id`) USING BTREE
) ENGINE=InnoDB COMMENT='业务信息'; CREATE TABLE `t_task` (
`task_id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '任务id',
`task_name` VARCHAR(100) NOT NULL COMMENT '业务名',
`note` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '备注',
`create_user` BIGINT(20) NOT NULL COMMENT '创建人',
`create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
`modify_user` BIGINT(20) NOT NULL COMMENT '最终修改人',
`modify_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最终修改时间',
PRIMARY KEY (`task_id`) USING BTREE
) ENGINE=InnoDB COMMENT='任务信息'; CREATE TABLE `t_business_task` (
`id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键id',
`business_id` BIGINT(20) UNSIGNED NOT NULL COMMENT '业务id',
`task_id` BIGINT(20) UNSIGNED NOT NULL COMMENT '任务id',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB COMMENT='业务任务关系'; CREATE TABLE `t_task_exec_log` (
`log_id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '日志id',
`task_id` BIGINT(20) UNSIGNED NOT NULL COMMENT '任务id',
`exec_status` VARCHAR(50) NOT NULL COMMENT '执行状态, 失败:fail,成功:success',
`data_date` DATE NOT NULL COMMENT '数据日期',
`note` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '备注',
`create_user` BIGINT(20) NOT NULL COMMENT '创建人',
`create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
`modify_user` BIGINT(20) NOT NULL COMMENT '最终修改人',
`modify_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最终修改时间',
PRIMARY KEY (`log_id`) USING BTREE
) ENGINE=InnoDB COMMENT='任务执行日志';
它们关系如下

一个业务下有多个任务,一个任务又可以属于不同的业务;同个业务下,一个任务最多关联一次
任务每执行一次就会生成一条执行日志;执行日志的数据日期 小于等于 任务执行的当前日期,比如昨天执行的任务的数据日期可以是前天的
四张表的数据量分别如下

需求
按业务分页,每个业务可以展开显示关联的任务信息以及任务最新的执行成功信息
任务最新的执行成功信息:状态成功,数据日期最大的那条执行日志信息;如果数据日期一致,则取最终修改时间最大的
后端返回的 JSON 数据类似如下

实现方式
先分页查业务和任务,再根据任务id循环查最新的执行成功信息
1、关联查询业务和任务
如果查询条件带任务信息(任务ID,任务名),那么 t_business 需要关联 t_business_task 、 t_task 来查
因为这三张表的数据量都比较小,联表查没什么问题
2、根据上一步查到的 task_id 集逐个去查 t_task_exec_log
SQL 类似如下

可以建个组合索引 idx_status_task_date_modify(exec_status,task_id,data_date,modify_time)
3、将第 1、2 步的数据进行组合
将任务的最新执行成功信息添加到任务信息中
逻辑非常清晰,代码实现起来也非常简单
但是,一个任务id就查一次数据库,这显然是有很大性能问题的(一般,公司的开发规范内都会有一条:禁止循环查数据库)
先分页查业务和任务,再根据任务id批量查最新的执行成功信息
1、关联查询业务和任务
2、根据第 1 步查到的任务id集批量查 t_task_exec_log
因为这是多个任务一起查,也就没法用 LIMIT 1 了
那如何查出每个任务的最新执行成功的那一条记录了?
这里也就对应了文章的标题:分组后取每组的第 1 条记录
实现方式其实有很多,我这里提供一种,如下

结合索引 idx_status_task_date_modify(exec_status,task_id,data_date,modify_time) ,查询速度还行
大家细看这个 SQL ,是不是发现了有意思的东西:GROUP_CONCAT(log_id ORDER BY data_date DESC,modify_time DESC)
是不是知识盲区,是不是有点东西?

3、将第 1、2 步的数据进行组合
新增任务最新执行成功记录表
一般而言,大数据量的日志表是不参与复杂查询的,所以单独拎出来一个表,专门记录任务最新执行成功信息

一个任务最多只有一条记录,不存在则直接插入表中,存在则根据 data_date DESC,modify_time DESC 与表中记录做比较,看是否需要进行表中记录更新
因为一个任务最多只有一条记录,那么 t_task_latest_exec_log 的数据量是 小于等于 t_task 的数据量的,也就是说数据量不大
那么用一个 SQL 就可以实现业务(直接联表 t_business 、 t_business_task 、 t_task 、 t_task_latest_exec_log )
然后在后端代码中进行数据格式的处理,返回前端需要的格式
新增表后,其初始数据该如何导入了?

总结
1、大家写 SQL 的时候,一定要多结合执行计划来写
神奇的 SQL 之 MySQL 执行计划 → EXPLAIN,让我们了解 SQL 的执行过程!
2、 t_task_latest_exec_log 初始数据的导入
其实比较简单, 如下所示

INSERT INTO t_task_latest_exec_log(task_id,data_date,create_user,create_time,modify_user,modify_time)
SELECT t2.task_id, t2.data_date, t2.create_user, t2.create_time, t2.modify_user,t2.modify_time FROM (
SELECT SUBSTRING_INDEX(GROUP_CONCAT(log_id ORDER BY data_date DESC,modify_time DESC),',',1) log_id
FROM t_task_exec_log
WHERE exec_status = 'success'
GROUP BY task_id
) t INNER JOIN t_task_exec_log t2 ON t.log_id = t2.log_id;

一定要去执行,你会发现大惊喜!
3、多和同事沟通,多和需求方沟通
多和同事沟通,集思广益,说不定就找到合适的解决方案了
多和需求方沟通,多谈谈个人的见解,也许需求改动一丢丢,但我们实现却容易很多
4、留疑
1、分组后如何取前 N 条
2、分组后如何取倒数 N 条
记一次有意思的 SQL 实现 → 分组后取每组的第一条记录的更多相关文章
- SQL获取分组后取某字段最大一条记录(求每个类别中最大的值的列表)
获取分组后取某字段最大一条记录 方法一:(效率最高) select * from test as a where typeindex = (select max(b.typeindex) from t ...
- MySQL 分组后取每组前N条数据
与oracle的 rownumber() over(partition by xxx order by xxx )语句类似,即:对表分组后排序 创建测试emp表 1 2 3 4 5 6 7 8 9 ...
- sql语句 查询分组后的每组的第一条或最后一条数据
select * from(select row_number() over(partition by IDCARD order by DATATM desc) as rownum,* from (S ...
- [mysql] 先按某字段分组再取每组中前N条记录
From: http://blog.chinaunix.net/uid-26729093-id-4294287.html 请参考:http://bbs.csdn.net/topics/33002126 ...
- SQL Server 分组后取Top N
SQL Server 分组后取Top N(转) 近日,工作中突遇一需求:将一数据表分组,而后取出每组内按一定规则排列的前N条数据.乍想来,这本是寻常查询,无甚难处.可提笔写来,终究是困住了笔者好一会儿 ...
- sql 分组后按时间降序排列再取出每组的第一条记录
原文:sql 分组后按时间降序排列再取出每组的第一条记录 竞价记录表: Aid 为竞拍车辆ID,uid为参与竞价人员ID,BidTime为参与竞拍时间 查询出表中某人参与的所有车辆的最新的一条的竞价记 ...
- SQL分组排序后取每组最新一条数据的另一种思路
在hibernate框架和mysql.oracle两种数据库兼容的项目中实现查询每个id最新更新的一条数据. 之前工作中一直用的mybatis+oracle数据库这种,一般写这类分组排序取每组最新一条 ...
- MSSQL 分组后取每组第一条(group by order by)
查询中经常遇到这种查询,分组后取每组第一条.分享下一个SQL语句: --根据 x 分组后.根据 y 排序后取第一条 select * from ( select ROW_NUMBER() over(p ...
- mysql单列去重复group by分组取每组前几条记录加order by排序
mysql分组取每组前几条记录(排名) 附group by与order by的研究,需要的朋友可以参考下 --按某一字段分组取最大(小)值所在行的数据 复制代码代码如下: /* 数据如下: name ...
随机推荐
- Flink使用Pod Template将状态快照(Checkpoint、Savepoint)存储在NFS
背景 Flink 版本 1.13.3,使用 native k8s 部署模式,原采用 HDFS 作为状态快照(Checkpoint.Savepoint)的存储地址,但是由于仅使用了其 HDFS 作为状态 ...
- 使用pip安装库或执行pip命令时报错解决方案
初次安装pip后执行安装升级一般不会有问题,但是国外的镜像源下载升级由于网速过慢会进行报错,提示需要升级 pip 或者下载速度很慢最后直接报了错如下图: 这个时候只需要修改镜像源即可,建议修改为永久镜 ...
- 解决python 导入selenium 库后自动化运行成功但是报错问题
本章节开始进入自动化的基础教学了,首先我们要对我们的工具有一定的熟练使用程度,做自动化常用的工具一个是搭建 RobotFramework自动化框架,另外一个便是我们最常用的python 工作原理是比较 ...
- torch.tensor(),torch.Tensor()
Pytorch tensor操作 https://www.cnblogs.com/jeshy/p/11366269.html 我们需要明确一下,torch.Tensor()是python类,更明 ...
- while循环、do..while循环
While循环 While循环呢它是更具条件来判断是否执行大括号里的内容 ,只要条件成立就会一值执行直到不满足条件它的语法格式: while(循环条件){ 执行语句 }那么我们来做一个小测试看看: p ...
- .NET中如何在同步代码块中调用异步方法
更新记录 本文迁移自Panda666原博客,原发布时间:2021年7月2日. 在同步代码块中调用异步方法,方法有很多. 一.对于有返回值的Task 在同步代码块中直接访问 Task 的 Result ...
- UiPath从入门到精通视频教程
匠厂出品,必属精品 Uipath中文社区qq交流群:465630324 微信小程序搜索RPA之家小店可以购买相关RPA的课程,现在联系有优惠 官网:https://rpazj.com uipath ...
- 开通博客-学习java之路
已被西南交通大学录取,毕设也已经进入末期.开始狂神说的Java学习之路,纪念一下!!!
- P1189 SEARCH—搜索
将这题加进来的原因 因为他的优化令人眼前一新! 题目传送门() 相似的题目之 血色先锋队 ↑这一题也要用到标记数组 优化!!! 对于一个位置, 如果他在同样的深度再一次被访问,那他接下来所走的路径,所 ...
- C语言整形转字符串的方法
今天写力扣第九题,里面用到了这个,就做个笔记. 1. char *itoa( int value, char *string,int radix);(stdlib.h) Windows特有 ...