复杂的SQL排名查询

时间:2017-05-28 13:50:01

标签: mysql sql performance query-optimization

我有3个相当简单的表:

用户

user_id
1
2
3

radio_songs

song_id  song
1        SomeName
2        OtherName

radio_rates

user_id  song_id  rate (from 1 to 5)
1        1        5
2        1        4
1        2        2
2        2        2

我编写了相当复杂的查询目标MySQL,根据 lower bound of Wilson score confidence interval for a Bernoulli parameter 计算歌曲的当前“位置”(排名)。

SELECT rank FROM(
    SELECT x.song AS song, x.ci_lower_bound AS ci_lower_bound, (@row:= @row + 1) AS rank FROM(
        SELECT song, ((SUM((rate - 1) * 0.25) + 1.9208) / (SUM((rate - 1) * 0.25) + SUM((5 - rate) * 0.25)) - 1.96 * SQRT((SUM((rate - 1) * 0.25) * SUM((5 - rate) * 0.25)) / (SUM((rate - 1) * 0.25) + SUM((5 - rate) * 0.25)) + 0.9604) / (SUM((rate - 1) * 0.25) + SUM((5 - rate) * 0.25))) / (1 + 3.8416 / (SUM((rate - 1) * 0.25) + SUM((5 - rate) * 0.25))) AS ci_lower_bound
        FROM radio_rates
        INNER JOIN radio_songs ON radio_rates.song_id = radio_songs.song_id 
        GROUP BY radio_rates.song_id
        ORDER BY ci_lower_bound DESC
    ) x, (SELECT @row := 0) r
) xx WHERE xx.song = @song

此查询基本上接受@song参数和:

  • 按降序计算Wilson得分和订单的下限
  • 为每行添加行号,因为我在MySQL中找不到ROW_NUMBER()的任何方法
  • 最后获得我们正在寻找的歌曲的排名

查询工作正常,我对此非常满意,但是当我们有多个具有相同分数的歌曲时,由于排序结果排名可能会因同一SQL查询的执行而异。我希望通过从与目标歌曲具有相同分数的所有歌曲中获得MIN()等级来避免这种情况,但是查询变得如此复杂以至于我在没有临时表的情况下如何做到这一点 - 这甚至是可能的?

我很感谢帮助,以及上述查询的效果/优化方面的任何建议。

我知道考虑简单地在歌曲表中添加另一个分数列并通过触发器在每次插入/更新时计算它是值得的,但我想尽可能避免这种情况并按需计算排名。因此SQL查询本身对我来说最重要。

提前谢谢。

1 个答案:

答案 0 :(得分:1)

这可能对您有用:

SELECT rank FROM(
    SELECT x.song AS song,
           (@row:= @row + 1) AS rn,
           IF(@last_score = x.ci_lower_bound, @rank, @rank := @row) AS rank
           (@last_score := x.ci_lower_bound) AS ci_lower_bound
    FROM(
        SELECT song, ((SUM((rate - 1) * 0.25) + 1.9208) / (SUM((rate - 1) * 0.25) + SUM((5 - rate) * 0.25)) - 1.96 * SQRT((SUM((rate - 1) * 0.25) * SUM((5 - rate) * 0.25)) / (SUM((rate - 1) * 0.25) + SUM((5 - rate) * 0.25)) + 0.9604) / (SUM((rate - 1) * 0.25) + SUM((5 - rate) * 0.25))) / (1 + 3.8416 / (SUM((rate - 1) * 0.25) + SUM((5 - rate) * 0.25))) AS ci_lower_bound
        FROM radio_rates
        INNER JOIN radio_songs ON radio_rates.song_id = radio_songs.song_id 
        GROUP BY radio_rates.song_id
        ORDER BY ci_lower_bound DESC
    ) x, (SELECT @row := 0, @rank := null, @last_score := null) r
) xx WHERE xx.song = @song

变化是:

SELECT x.song AS song,
       (@row:= @row + 1) AS rn,
       IF(@last_score = x.ci_lower_bound, @rank, @rank := @row) AS rank
       (@last_score := x.ci_lower_bound) AS ci_lower_bound

(SELECT @row := 0, @rank := null, @last_score := null) r

在这一行

IF(@last_score = x.ci_lower_bound, @rank, @rank := @row) AS rank

仅当分数与最后一行相比发生变化时,才将排名设置为行号。如果得分相同,则使用最后一行的排名。

警告:以这种方式使用会话变量,在升级到新版本时,您的代码将始终面临返回意外结果的风险。如果它工作,那是因为引擎的实现方式。无法保证表达式将按预期顺序执行。

  

作为一般规则,除了在SET语句中,你永远不应该   为用户变量赋值并读取其中的值   声明。例如,要增加变量,这没关系:

SET @a = @a + 1;
     

对于其他语句,例如SELECT,您可能会得到结果   期待,但这不能保证。在以下声明中,您   可能会认为MySQL会首先评估@a然后做一个   第二个任务:

SELECT @a, @a:=@a+1, ...;
     

但是,涉及用户的表达式的评估顺序   变量未定义。

User-Defined Variables