Mysql分组查询每组最新的一条数据(五种实现方法)

本文主要是介绍Mysql分组查询每组最新的一条数据(五种实现方法),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

MySQL分组查询每组最新的一条数据

    • 前言
    • 注意事项
    • 准备SQL
    • 错误查询
      • 错误原因
    • 方法一
    • 方法二(适用于自增ID和创建时间排序一致)
    • 方法三(适用于自增ID和创建时间排序一致,查询性能最优)
    • 方法四(通过DISTINCT关键字打破MySQL语句优化使排序生效)
    • 方法五(以创建时间为基准获取每个用户最新的一条数据,必须要添加对应字段的索引 最好是覆盖索引)
    • 总结
      • MAX()函数和MIN()这一类函数和GROUP BY配合使用存在问题

前言

在写报表功能时遇到一个需要根据用户id分组查询最新一条钱包明细数据的需求,在写sql测试时遇到一个有趣的问题,开始使用子查询根据时间倒序+group by customer_id发现查询出来的数据一直都是最旧的一条,而不是我需要的最新一条数据我明明已经倒序排了,后来总结出了五种解决方案如下。

注意事项

  • 数据库版本 Mysql5.7+
  • 执行 GROUP BY 语句的时候出现 sql_mode=only_full_group_by 解决方法(这里是Mysql8的解决方案,Mysql5.7也差不多,具体实现可以查看 解决MySQL-this is incompatible with sql_mode=only_full_group_by 问题)
    • 1、执行 select @@sql_mode; 查看sql模式

      select @@sql_mode;
      

      在这里插入图片描述

    • 2、将sql_mode中的only_full_group_by模式剔除 重新设置sql_mode值,如果是使用JDBC连接需要重启项目才能生效。

      set global sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';
      set session sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';
      

准备SQL

这里模拟一个sql

DROP TABLE IF EXISTS `customer_wallet_detail`;
CREATE TABLE `customer_wallet_detail`  (`id` bigint(20) NOT NULL AUTO_INCREMENT,`customer_id` bigint(20) NULL DEFAULT NULL COMMENT '用户ID',`happen_amount` varchar(15)  NULL DEFAULT '0' COMMENT '发生金额 带-号的代表扣款',`balance_amount` varchar(15) NULL DEFAULT '0' COMMENT '可用余额',`create_time` bigint(20) NULL DEFAULT NULL COMMENT '发生时间',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB COMMENT = '用户钱包明细';INSERT INTO `customer_wallet_detail`(`id`, `customer_id`, `happen_amount`, `balance_amount`, `create_time`) VALUES (1, 1, '100', '100', 1670300656630);
INSERT INTO `customer_wallet_detail`(`id`, `customer_id`, `happen_amount`, `balance_amount`, `create_time`) VALUES (2, 1, '-10', '90', 1670300656640);
INSERT INTO `customer_wallet_detail`(`id`, `customer_id`, `happen_amount`, `balance_amount`, `create_time`) VALUES (3, 1, '5', '95', 1670300656650);
INSERT INTO `customer_wallet_detail`(`id`, `customer_id`, `happen_amount`, `balance_amount`, `create_time`) VALUES (4, 3, '998', '998', 1670300656660);
INSERT INTO `customer_wallet_detail`(`id`, `customer_id`, `happen_amount`, `balance_amount`, `create_time`) VALUES (5, 3, '-100', '898', 1670300656670);
INSERT INTO `customer_wallet_detail`(`id`, `customer_id`, `happen_amount`, `balance_amount`, `create_time`) VALUES (6, 3, '-98', '800', 1670300656680);
INSERT INTO `customer_wallet_detail`(`id`, `customer_id`, `happen_amount`, `balance_amount`, `create_time`) VALUES (7, 2, '666', '666', 1670300656690);
INSERT INTO `customer_wallet_detail`(`id`, `customer_id`, `happen_amount`, `balance_amount`, `create_time`) VALUES (8, 2, '-66', '600', 1670300656695);
INSERT INTO `customer_wallet_detail`(`id`, `customer_id`, `happen_amount`, `balance_amount`, `create_time`) VALUES (9, 2, '-600', '0', 1670300656699);

在这里插入图片描述

错误查询

SELECT* 
FROM( SELECT * FROM customer_wallet_detail ORDER BY create_time DESC ) t1 
GROUP BYt1.customer_id;

在这里插入图片描述

错误原因

mysql5.7以及之后的版本,如果GROUP BY的子查询中包含ORDER BY,但是 GROUP BY 不与 LIMIT 配合使用,ORDER BY会被忽略掉,所以子查询在 GROUP BY 时排序不会生效,可能是因为子查询大多数是作为一个结果给主查询使用,所以子查询不需要排序。

方法一

鉴于以上的原因我们可以添加上 LIMIT 条件来实现功能。
PS:这个LIMIT的数量可以先自行 COUNT 出你要遍历的数据条数(这个数据条数是所有满足查询条件的数据合,我这里共9条数据)

SELECT* 
FROM( SELECT * FROM customer_wallet_detail ORDER BY create_time DESC LIMIT 9 ) t1 
GROUP BYt1.customer_id;

在这里插入图片描述

方法二(适用于自增ID和创建时间排序一致)

方法一需要先 COUNT 查询然后将查询结果设置到 LIMIT 条件中比较麻烦,这里还可以使用 MAX() 函数来实现该功能。
PS:因为我这里的业务数据是有序插入的,使用主键自增id和create_time结果是一样的而且使用id查询效率更高,如果没有唯一且有序的id可以替代create_time那么就用方案一,不能直接使用 SELECT id,MAX(create_time) 这种操作来获取最新一条数据id,原因在总结中有详细描述。

SELECT*
FROMcustomer_wallet_detail 
WHEREid IN ( SELECT MAX( id ) FROM customer_wallet_detail GROUP BY customer_id ) 
ORDER BYcustomer_id;

在这里插入图片描述

方法三(适用于自增ID和创建时间排序一致,查询性能最优)

方法三和方法二实现逻辑基本一致只是将IN查询替换成了连接查询,本地20w条数据测试 方法三比方法二性能提升50%,有兴趣的可以增大数据集测试后续性能变化。

SELECTt1.* 
FROMcustomer_wallet_detail t1INNER JOIN ( SELECT MAX( id ) AS id FROM customer_wallet_detail GROUP BY customer_id ) t2 ON t1.id = t2.id

在这里插入图片描述

方法四(通过DISTINCT关键字打破MySQL语句优化使排序生效)

方法四实现起来比较简单,数据量小的时候查询性能也挺不错的,数据量大了之后查询性能也还可以,我本地测试了100w数据的查询,这个方法耗时0.9s左右,减少DISTINCT的字段能降到0.4s左右,不给customer_id字段加索引的情况下通过方法三查询耗时0.35s,加了索引耗时0.035s,有兴趣可以分析一下方法三和方法四的执行计划。

SELECT* 
FROM( SELECT DISTINCT * FROM `customer_wallet_detail` ORDER BY id DESC ) AS t1 
GROUP BYt1.customer_id;

在这里插入图片描述

方法五(以创建时间为基准获取每个用户最新的一条数据,必须要添加对应字段的索引 最好是覆盖索引)

有朋友在评论区提供了第四种方法,这种方法在表数据量少的时候是可行的,我的测试表还是20w数据,并且customer_id字段加了索引,全部查询出来耗时在180s左右,我本地MySQL性能会差一点,这种查询方式是将 b1 中的每一条数据 都和 b2 中的每一条数据进行比对取出满足条件的数据,b1 有20w条数据 b2 也有20w条数据,如果没有索引不计算io开销,只算cpu开销,这条sql需要进行 20w * 20w = 400亿次数据比对,在有索引的情况下数据比对次数会少一些但是也千万级的,如果考虑其它开销并且没索引的情况下那查询耗时可想而知。

  • 使用限制
    • 1、这种方式其实除了性能问题以外还有一个更加严重的问题,在一些业务里给用户余额明细添加数据时可能同一时间戳添加多条,这样count结果就大于1了,这个用户数据就查不出来了,还有一种情况如果开发人员事务没有控制好,我们在入库时一般会提前将create_time填充,但是我们用的是自增ID,入库时create_time 小的,数据ID可能还会大一些,选择那种方法还是需要看业务上怎么设计的
SELECTb1.* 
FROMcustomer_wallet_detail t1 
WHERE( SELECT COUNT( 1 ) FROM customer_wallet_detail t1 WHERE t2.customer_id = t1.customer_id AND t1.create_time <= t2.create_time ) <= 1;

在这里插入图片描述

PS:优化方案

  • 1、针对这条语句的查询特性,我们减少数据的查询条数,比如给 t1和t2 添加上筛选时间区间,减少遍历数组总数。
  • 2、使用覆盖索引,我自己在测试的时候发现如果使用组合索引包含两个字段 (customer_id,create_time) 性能会提升很多,20w数据查询出结果只用了40s,如果只使用customer_id字段索引会进行回表,使用覆盖索引没有额外的回表操作所以会快很多。

总结

结合我的业务经过测试,目前看来方案三是最合适的,sql简单性能适中,方案一比方案二性能更差而且实现麻烦,最终选择那个方案主要看业务而定。

MAX()函数和MIN()这一类函数和GROUP BY配合使用存在问题

MAX()函数和MIN()这一类函数和GROUP BY配合使用,GROUP BY拿到的数据永远都是这个分组排序最上面的一条,而MAX()函数和MIN()这一类函数会将这个分组中最大或最小的值取出来,这样会导致查询出来的数据对应不上。

  • 正确查询:
    在这里插入图片描述
  • 错误查询:这里的确拿到每个分组最新创建时间了但是拿的数据id还是排序的第一条
    在这里插入图片描述

在这里插入图片描述

这篇关于Mysql分组查询每组最新的一条数据(五种实现方法)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/337720

相关文章

Spring IoC 容器的使用详解(最新整理)

《SpringIoC容器的使用详解(最新整理)》文章介绍了Spring框架中的应用分层思想与IoC容器原理,通过分层解耦业务逻辑、数据访问等模块,IoC容器利用@Component注解管理Bean... 目录1. 应用分层2. IoC 的介绍3. IoC 容器的使用3.1. bean 的存储3.2. 方法注

MySQL 删除数据详解(最新整理)

《MySQL删除数据详解(最新整理)》:本文主要介绍MySQL删除数据的相关知识,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录一、前言二、mysql 中的三种删除方式1.DELETE语句✅ 基本语法: 示例:2.TRUNCATE语句✅ 基本语

golang中reflect包的常用方法

《golang中reflect包的常用方法》Go反射reflect包提供类型和值方法,用于获取类型信息、访问字段、调用方法等,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值... 目录reflect包方法总结类型 (Type) 方法值 (Value) 方法reflect包方法总结

MySQL中查找重复值的实现

《MySQL中查找重复值的实现》查找重复值是一项常见需求,比如在数据清理、数据分析、数据质量检查等场景下,我们常常需要找出表中某列或多列的重复值,具有一定的参考价值,感兴趣的可以了解一下... 目录技术背景实现步骤方法一:使用GROUP BY和HAVING子句方法二:仅返回重复值方法三:返回完整记录方法四:

IDEA中新建/切换Git分支的实现步骤

《IDEA中新建/切换Git分支的实现步骤》本文主要介绍了IDEA中新建/切换Git分支的实现步骤,通过菜单创建新分支并选择是否切换,创建后在Git详情或右键Checkout中切换分支,感兴趣的可以了... 前提:项目已被Git托管1、点击上方栏Git->NewBrancjsh...2、输入新的分支的

从入门到精通MySQL联合查询

《从入门到精通MySQL联合查询》:本文主要介绍从入门到精通MySQL联合查询,本文通过实例代码给大家介绍的非常详细,需要的朋友可以参考下... 目录摘要1. 多表联合查询时mysql内部原理2. 内连接3. 外连接4. 自连接5. 子查询6. 合并查询7. 插入查询结果摘要前面我们学习了数据库设计时要满

C# 比较两个list 之间元素差异的常用方法

《C#比较两个list之间元素差异的常用方法》:本文主要介绍C#比较两个list之间元素差异,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录1. 使用Except方法2. 使用Except的逆操作3. 使用LINQ的Join,GroupJoin

Python实现对阿里云OSS对象存储的操作详解

《Python实现对阿里云OSS对象存储的操作详解》这篇文章主要为大家详细介绍了Python实现对阿里云OSS对象存储的操作相关知识,包括连接,上传,下载,列举等功能,感兴趣的小伙伴可以了解下... 目录一、直接使用代码二、详细使用1. 环境准备2. 初始化配置3. bucket配置创建4. 文件上传到os

Qt QCustomPlot库简介(最新推荐)

《QtQCustomPlot库简介(最新推荐)》QCustomPlot是一款基于Qt的高性能C++绘图库,专为二维数据可视化设计,它具有轻量级、实时处理百万级数据和多图层支持等特点,适用于科学计算、... 目录核心特性概览核心组件解析1.绘图核心 (QCustomPlot类)2.数据容器 (QCPDataC

MySQL查询JSON数组字段包含特定字符串的方法

《MySQL查询JSON数组字段包含特定字符串的方法》在MySQL数据库中,当某个字段存储的是JSON数组,需要查询数组中包含特定字符串的记录时传统的LIKE语句无法直接使用,下面小编就为大家介绍两种... 目录问题背景解决方案对比1. 精确匹配方案(推荐)2. 模糊匹配方案参数化查询示例使用场景建议性能优