最近遇到一个问题,需要根据两张表(一对一)的查询结果进行分组并按照时间倒序排序,取每组最新的一条记录;问题是分组的字段和排序的字段还不在一张表,而且因为执行顺序group by是在order by之前的,先group by分组再order by排序发现记录不是每组排序的第一条;把查询结果作为子查询先order by排序,最后在对整体记录group by分组也得不到想要的结果,搞得很头疼

        通过对官方文档的查询、网上资料的研究以及自己的验证,我总结了几种解决方案,既可以作为解决类似问题的快速回忆,又可以帮助其他遇到这类问题的人节省时间。以下是几种方案:

方案一:利用distinct关键字,这里distinct其实起到了一个固定排序的作用

SELECT
	*
FROM
	(
		SELECT DISTINCT
			t1.*, 
			t2.type,
			t2.price
		FROM
			t1
		LEFT JOIN t2 ON t1.type_id = t2.id
		ORDER BY
			t1.end_time DESC
	) t
GROUP BY
	t.type

方案二:利用limit关键字,这里limit其实也是起到了一个固定排序的作用,这种因为要尽可能的使limit的数量比较大,所以不是很推荐,如果是对一些已知且数据量小的表的话也是可以用的

SELECT
	*
FROM
	(
		SELECT
			t1.*,
			t2.type,
			t2.price
		FROM
			t1
		LEFT JOIN t2 ON t1.type_id = t2.id
		ORDER BY
			t1.end_time DESC
		LIMIT 99999(超过表记录数或者可预知的最大记录数)
	) t
GROUP BY
	t.type

方案三:先对一个唯一列分组排序,一般是主键,最后再对查询的记录进行实际字段的分组,其实原理还是固定排序,防止最后分组的时候破坏原有的排序

SELECT
	*
FROM
	(
		SELECT
			t1.*, 
			t2.type,
			t2.price
		FROM
			t1
		LEFT JOIN t2 ON t1.type_id = t2.id
		GROUP BY
			t1.id
		ORDER BY
			t1.end_time DESC
	) t
GROUP BY
	t.type

方案四:利用SUBSTRING_INDEX函数截取主键然后再分组,跟方案三其实思路一样,实现方式不同

SELECT
	t1.*, 
	t2.type,
	t2.price
FROM
	t1
LEFT JOIN t2 ON t1.type_id = t2.id
WHERE
	t1.id IN (
		SELECT
			SUBSTRING_INDEX(GROUP_CONCAT(t.id ORDER BY t.end_time DESC), ',', 1)
		FROM
			(
				SELECT
					t3.*, 
					t4.type,
					t4.price
				FROM
					t3
				LEFT JOIN t4 ON t3.type_id = t4.id
				ORDER BY
					t3.end_time DESC
			) t
		GROUP BY
			t.type
	)

方案五:sql不分组,在代码利用循环中比较结束时间,取出每种分组最后一条记录放入新集合中,这种是最容易理解的方案,但是代码略显臃肿

方案六:sql不分组,在代码中利用stream的sorted排序然后groupingBy分组,这样的结果是分组的字段为key,分组的数据为list的一个map,在声明返回结果类型和中间类型类型时注意要返回一个有序的map,TreeMap或者LinkedHashMap,这样你根据key取每一组的第一条数据就是你最开始sorted排序的第一条,下边简单写一个例子:

List<StudentVO> studentVOList = new ArrayList<>();
LinkedHashMap<String, List<StudentVO>> studentVOListMap = studentVOList.stream()
        .sorted(Comparator.comparing(StudentVO::getEndTime).reversed())
        .collect(Collectors.groupingBy(s -> s.getType(), LinkedHashMap::new,        Collectors.toList()));
        
studentVOListMap.forEach((type, list) -> {
        StudentVO studentVO = list.get(0);
});

方案七:sql不分组,在代码中利用stream的groupingBy分组,然后再对返回的map中的value进行排序,这个对返回的map类型没有具体要求,其实思路都差不多,看个人习惯和偏好吧,下边也是一个简单的例子:

List<StudentVO> studentVOList = new ArrayList<>();
HashMap<String, StudentVO> map = list.stream()
        .collect(Collectors.groupingBy(StudentVO::getType, HashMap::new, 
                        Collectors.collectingAndThen(Collectors.toList(), list -> list.stream()
                        .sorted(Comparator.comparing(StudentVO::getEndTime).reversed())
                        .findFirst().get())));
        
map.forEach((key, value) -> {
        StudentVO studentVO = value;
});

这个其实理解起来很好理解,但是代码读起来有点费劲,需要对Java的steam流有一定的了解,也可以直接套用自己检验,嘿嘿~~

        以上的方案我都实际试过,最后采用的是第五种,复杂度和可读性我个人理解相对好一些,sql的写法被老大噶了,说数据量大的时候性能不行,各位根据自身情况采用或者自己研究,我上边的例子是阉割版的,如果是理解不深入的话,套用的时候最好多试试,别出问题,总结不易,觉得有用请点赞支持下,有其他好的解决方案的也可以评论留言,互相交流!

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐