mysql 8.0之后可以使用内置的ROW_NUMBER()函数
mysql 8.0以前没有这样的函数,因此我们需要手动的来实现这个。
其实实现的方法也很简单。
问题
假设我们现在有一张数据量很大的表(千万以上),比如说一张活动表。表里记录了每一种活动每次发奖的明细,现在我们想知道每个活动获得积分最高的10个用户。
create table (
`id` #主键
`activity_id` #活动id
`uid` #用户id
`money` #发奖金额
`insert_time` #发奖时间
······
)
解决方案
拿到问题第一时间肯定想的是根据uid和activity_id来group by 然后sum(money)。
这样可以得到每个用户在每个活动在我们规定范围内的发奖金额,接下来就是想办法找出每个group 里面top 10的用户。
既然要按照金额取top 10,那么肯定需要对金额排序,order by sum(money) desc是肯定需要的。
但是这样会导致一个问题,比如活动1和活动2的产出量级类似,那很有可能会出现这种情况:
activity_id | uid | money |
2 | 10004| 100 |
1 | 10003| 100 |
2 | 10002| 100 |
1 | 10001| 101 |
3 | 10001| 100 |
1 | 10001| 100 |
1 | 10001| 100 |
2 | 10001| 100 |
··· ···
在整个的大排序下,活动是乱序的,因此我们要先order by activity_id,这样得到的就是
activity_id | uid | money |
1 | 10003| 100 |
1 | 10003| 100 |
1 | 10001| 100 |
1 | 10001| 100 |
1 | 10001| 101 |
2 | 10004| 100 |
2 | 10002| 100 |
2 | 10001| 100 |
3 | 10001| 100 |
···· ···
这样处理之后在整个大的排序中金额并不是有序的,而是分成了每个活动内是有序的。那么要怎么取出每个活动中的top n呢。
既然每个活动的组内是有序的,那么如果我们可以给每个组内的数据从1开始依次编号,编号记为num,那么num <= n的数据就是我们需要的数据,我们需要实现这种效果:
num | activity_id | uid | money |
1 | 1 | 10003| 100 |
2 | 1 | 10003| 100 |
3 | 1 | 10001| 100 |
4 | 1 | 10001| 100 |
5 | 1 | 10001| 101 |
1 | 2 | 10004| 100 |
2 | 2 | 10002| 100 |
3 | 2 | 10001| 100 |
1 | 3 | 10001| 100 |
···· ···
这个时候我们只需要套一层select ,找出满足num <= n的即可,比如我们需要的是每组前2的数据,只需要满足num <= 2就可以了
所以我们现在要实现这种效果。实现的方式很简单,其实如果在正常的代码中大家都知道该怎么做。
- 判断当前的activity_id有没有变化
- 如果没有变化,那么num++,如果不再是上一次拿到的activity_id了,那么num=1
所以需要用到两个变量来分别存储上一个activity_id和num
最终的完成的代码:
SELECT num, activity_id, uid, money
FROM (
SELECT @row_number := IF(@last_activity_id = activity_id, @row_number + 1, 1) AS num
, @last_activity_id := activity_id AS activity_id, uid, money
FROM (
SELECT activity_id, uid, sum(money) AS money
FROM my_table
WHERE insert_time >= xxxxxx
AND insert_time < xxxxxx
GROUP BY activity_id, uid
ORDER BY activity_id, sum(money) DESC
) b
) a
WHERE a.num <= 10
遇到的小问题
这段代码我直接在mysql命令行执行的时候是没有问题的,但是在项目中嵌入代码之后执行有问题,查出来的所有数据的num都=1,检查了之后发现是因为定义的这两个局部变量@row_number, @last_activity_id取不到。
具体原因没有查到,有大佬知道可能原因的可以分享一下
解决的办法是在进行这个查询之前先对这两个局部变量初始化一下:
select @row_number := 1 , @last_activity_id := ''
每次拿到的数据库连接是一个session级别的,只要连接不close掉就可以取到之前创建的变量,可以解决上面的问题.
还有一种方式是以事务的形式,
begin
设置变量(set @row_num = 1, last_activity_id = ‘’)
select查询
end