SQL排名问题,100% leetcode答案大公开!

(首先原谅我最近新番看多了,起了一个中二的名字)sql

最近在找实习,因此打算系统总结(复习)一下sql中常常遇到问题。不论是刷leetcode仍是牛客的sql题,有一个问题老是绕不开的,那就是排名问题。其实对于MySql8.0以上版原本说,排名问题已经很容易解决了。由于MySql8.0以后开始支持三个窗口函数,分别是rank(),dense_rank()以及row_number()。这三个窗口函数对应了排名问题中最多见的三种状况。而对于以前的版本,则须要模拟这几个函数。函数

网上也有不少相关的文章,但实际上他们给出的代码都没法100%经过leetcode的样例。因而就想在这里从新总结一下sql中的排名问题。咱们以分数排名为例,假设有一个数据表scores包括两个字段id和score,须要对score进行排名。code

id score
1 0
2 15
3 15
4 15
5 17
6 18
7 20

不一样的排名需求适合不一样的场景,但为了方便比较,就不分开举例了。对于排名的结果,通常会包括如下几种状况:leetcode

1. 一样的分数不一样的名次,且排名连续

若是是这样的排名需求,排名的结果应该是:字符串

id score rank
7 20 1
6 18 2
5 17 3
2 15 4
3 15 5
4 15 6
1 0 7
  • 窗口函数 row_number()table

    SELECT id, score, row_number() over (order by score desc) as 'rank'
    FROM Scores;
  • 模拟窗口函数ast

    SET @curRank = 0;
    SELECT id, Score, (@currank := @currank + 1) As 'rank' 
    From Scores
    ORDER BY score DESC;

    自行模拟窗口函数的关键就在于要设置一个变量来保存当前的排名。class

2. 一样的分数相同的名次,且排名连续(Leetcode 178)

若是是这样的排名需求,排名的结果应该是:变量

id score rank
7 20 1
6 18 2
5 17 3
2 15 4
3 15 4
4 15 4
1 0 5
  • 使用窗口函数 dense_rank()select

    SELECT id, score, dense_rank() over (order by score desc) as 'rank'
    FROM Scores;
  • 使用变量模拟窗口函数

    对于这种排名,网上给出的代码大可能是这样的:

    SELECT tmp.score, 
           @ranking := case
              when @lastscore = tmp.score then @ranking
              when @lastscore := tmp.score then @ranking +1
           end as 'rank'
    FROM (select * from scores order by score desc) tmp,
         (select @ranking := 0, @lastscore := null) r;

    这段代码可能在通常的数据表中都没问题,但在leetcode上是不能彻底ac的。主要有两个问题:

    • 返回的rank排名是字符串,而不是数字,会致使样例失败
    • 若是表中score有值为0,排名结果就会是null。

    这种作法的思想是:

    • 若是当前的score跟上一行score(@lastscore)相等,则@ranking不增长(@ranking := @ranking);
    • 不然就 @lastscore := tmp.score (这一句在score不为0的时候永远为真),给 @lastscore 赋新值的同时,@ranking增长1(@ranking := @ranking + 1)。

    可是当score为0时,两个when中的表达式都是False,因此什么都不执行,致使0值的结果为null。

    针对这种状况,能够稍微改进一下:

    SELECT tmp.score, 
           @ranking := case
              when @lastscore = tmp.score then @ranking +0
              when @lastscore := tmp.score then @ranking +1
              else @ranking := @ranking + 1
           end as 'rank'
    FROM (select * from scores order by score desc) tmp,
    	 (select @ranking := 0, @lastscore := null) r;
    • 第一点是在第一个when处增长了 +0 ,进行类型转换
    • 第二点是增长了 else @ranking := @ranking + 1 ,保证出现0值的时候,仍然能够进行排名。

    这样的写法是能够经过leetcode的全部样例的。

    leetcode_ac

  • 使用联结模拟窗口函数

    SET @currank := 0;
    Select os.id, r.Score, r.rank
    From Scores os 
    LEFT JOIN (SELECT *, (@currank := @currank + 1) As 'rank'
               From (Select *
                     	From Scores
                     	Group BY Score
                     	ORDER BY score DESC
                     ) AS ra
               ) AS r
    ON os.score = r.score
    ORDER BY r.score DESC;

    这种查询其实能够看作两步,第一步是使用Group BY分组,进行了一个无重复值的排名(其实就是第一种状况),以后再把这个排名表Left Join到原始的表中,而后对组合表在进行一次排名。

3. 一样的分数相同的名次,且排名不连续

这种状况应该是最符合现实中分数排名的。排名结果以下:

id score rank
7 20 1
6 18 2
5 17 3
2 15 4
3 15 4
4 15 4
1 0 7
  • 使用窗口函数rank()

    SELECT id, score, rank() over (order by score desc) as 'rank'
    FROM Scores;
  • 使用变量模拟窗口函数

    SELECT tmp2.id, tmp2.score, tmp2.ranking AS 'rank'
    FROM (SELECT tmp.*,
                 @rownum := @rownum+1 AS rownum,
                 @ranking := case
                    when @lastscore = tmp.score then @ranking + 0
                    when @lastscore := tmp.score then @rownum + 0
                    else @rownum + 0
                 end as ranking
          FROM (select * from scores order by score desc) tmp,
               (select @rownum := 0, @lastscore := null, @ranking := 0) r
          ) AS tmp2

    这里的作法其实至关于第一种状况和第二种状况的结合,用两个变量分别存储排名和当前的行数。

    • 若是当前的score跟上一行score(@lastscore)相等,则@ranking不增长(@ranking := @ranking +0 );
    • 不然就 @lastscore := tmp.score ,这里跟第二种状况不一样在于@ranking再也不是增长1,而是赋值为行数(@rownum)

以上就是遇到比较多的排名问题的解法啦。关于窗口函数,其实还有更多的用途,好比分组排名。后面可能专门写一篇来介绍这几个排名窗口函数。

最后惯例:

能力有限,若有错漏,多多包涵,欢迎指正!

相关文章
相关标签/搜索