mysql在group by之后如何取出每个group下的topN?在mysql 8之前,使用max/min函数能够取出group后某列的top1的值,但是面对topN的问题无能为力,通过子查询的方式进行解决;mysql 8引入的窗口函数对于此类组内统计很实用
取topN的情况并不少见,以下我们给出一个示例——实际上这个问题也是在编写查询zabbix监控数据的sql时遇到的,示例使用一个简化的表说明核心问题
一个用于存储监控数据的表t_monitor_history:
1 | CREATE TABLE `t_monitor_history` ( |
第一个需求:查询每个监控项的最近一次采集值。直接对monitor_key列group后,对clock取max,即可找出每组的monitor_key和对应的最近一次采集值的clock,通过这两个列的值筛选即可:
1 | SELECT |
然后进一步,让我们进入正题:如何查询每个监控项的最近2条采集值记录(注意,我们希望只取2条,即使clock出现并列的情况)?
union all合并多个查询的结果集
分别查询每个group的top2(where group = xx order by … limit 2),使用union all合并结果集——这一方法在小型数据集的分析上是比较简洁的,但需要预先确定数据特征,并不是一个通用解
定义session内变量循环查找
如何解决?在StackOverflow上等网站上有热心网友提供了通过在sql语句内定义session级变量进行循环查找的方式,但本人实测性能下降显著。
这类回答不少,贴个链接作为参考
where条件使用子查询筛选group内topN
1 | SELECT |
这一方法简洁易实现,但是测试中发现在特定情况下存在问题:如果某个group内的排序列clock的第2名并列,那么只会返回第1位的结果,如下,为memory.used这一个监控项添加一个clock并列第2位的记录:
1 | INSERT INTO `sql_test`.`t_monitor_history` (`clock`, `monitor_key`, `value`) VALUES (631126700, 'memory.used', -1); |
执行结果成为了:
1 | -- ------ RESULT --------------------------------- |
使用group_concat和find_in_set取group内某列topN
group_concat用于列转行,将多行的值合并到一行内,默认使用’,’连接为一个字符串;find_in_set可在’,’连接的多个字符串中查找出指定的字符串,返回值为0(查找的字符串不存在),或查找的字符串在字符串列表中的索引值(索引值从1开始)
sql整体采取与取max同样的思路:对monitor_key列group后,找出top2的clock值,通过这两个列的值进行筛选。问题就来到了如何找出每个group内top2的clock值,结合使用group_concat和find_in_set即可实现
1 | SELECT |
这里在子查询中,首先通过GROUP_CONCAT函数以降序拼接clock值到一行中,然后使用SUBSTRING_INDEX按照分隔符’,’进行截取得到子字符串,分割2次即保留了每个group内最大的两个clock值;之后在外部的筛选中,通过FIND_IN_SET函数过滤出所有子查询结果集提供的clock值,即得到了每个group中clock最大的top2记录
然而与上一条类似,因为外层查询使用top2的clock进行筛选,这一方法在clock列出现并列时会将查询所有并列的记录。插入一条并列记录:
1 | INSERT INTO `sql_test`.`t_monitor_history` (`clock`, `monitor_key`, `value`) VALUES (631126700, 'memory.used', -1); |
执行结果变成了:
1 | -- ------ RESULT --------------------------------- |
在实际的查询中,通过在子查询和外部查询添加where条件过滤clock和monitor_key值,可以更充分的利用索引,达到更高的效率
mysql 8使用窗口函数
mysql 8引入了窗口函数,关于窗口函数的使用示例可以参考网友的这一篇介绍文章
使用窗口函数对clock列按照monitor_key分组后排序,并使用ROW_NUMBER()排序生成排序次序列r,在外层查询中使用r筛选即可轻松取到每个分组内按照clock取topN的行:
1 | SELECT |
ROW_NUMBER()不会考虑并列值,而是为每行生成一个序号,因此group内clock值出现重复时也能准确的取出topN——如果希望取出并列值,可以使用RANK函数