【问题标题】:Determine Rank based on Multiple Columns in MySQL根据 MySQL 中的多列确定排名
【发布时间】:2018-11-25 06:11:57
【问题描述】:

我有一个包含 3 个字段的表,我想根据 user_id 和 game_id 对列进行排名。

这里是 SQL 小提琴: http://sqlfiddle.com/#!9/883e9d/1

我已经有这张桌子了:

 user_id | game_id |   game_detial_sum  |
 --------|---------|--------------------|
 6       | 10      |  1000              |   
 6       | 11      |  260               |
 7       | 10      |  1200              |
 7       | 11      |  500               |
 7       | 12      |  360               |
 7       | 13      |  50                | 

预期输出:

user_id  | game_id |   game_detial_sum  |  user_game_rank  |
 --------|---------|--------------------|------------------|
 6       | 10      |  1000              |   1              |
 6       | 11      |  260               |   2              |
 7       | 10      |  1200              |   1              |
 7       | 11      |  500               |   2              |
 7       | 12      |  360               |   3              |
 7       | 13      |  50                |   4              |

到目前为止我的努力:

SET @s := 0; 
SELECT user_id,game_id,game_detail, 
       CASE WHEN user_id = user_id THEN (@s:=@s+1) 
            ELSE @s = 0 
       END As user_game_rank 
FROM game_logs

编辑:(来自 OP Comments):排序基于 game_detail 的降序排列

game_detail 的顺序

【问题讨论】:

  • game_id的升序还是game_detail的降序?
  • game_detail 的顺序

标签: mysql sql sql-rank


【解决方案1】:

Derived TableFROM 子句中的子查询)中,我们对数据进行排序,以使具有相同user_id 值的所有行聚集在一起,并根据game_detail 以降序对它们进行进一步排序。

现在,我们使用这个结果集并使用条件CASE..WHEN 表达式来评估行编号。它就像一种循环技术(我们在应用程序代码中使用,例如:PHP)。我们会将前一行的值存储在用户定义的变量中,然后根据前一行检查当前行的值。最终,我们将相应地分配行号。

编辑:基于 MySQL docs 和 @Gordon Linoff 的观察:

涉及用户变量的表达式的求值顺序是 不明确的。例如,不能保证 SELECT @a, @a:=@a+1 先计算@a,然后执行赋值。

我们需要计算行号并将user_id 值分配给同一表达式中的@u 变量。

SET @r := 0, @u := 0; 
SELECT
  @r := CASE WHEN @u = dt.user_id 
                  THEN @r + 1
             WHEN @u := dt.user_id /* Notice := instead of = */
                  THEN 1 
        END AS user_game_rank, 
  dt.user_id, 
  dt.game_detail, 
  dt.game_id 

FROM 
( SELECT user_id, game_id, game_detail
  FROM game_logs 
  ORDER BY user_id, game_detail DESC 
) AS dt 

结果

| user_game_rank | user_id | game_detail | game_id |
| -------------- | ------- | ----------- | ------- |
| 1              | 6       | 260         | 11      |
| 2              | 6       | 100         | 10      |
| 1              | 7       | 1200        | 10      |
| 2              | 7       | 500         | 11      |
| 3              | 7       | 260         | 12      |
| 4              | 7       | 50          | 13      |

View on DB Fiddle


我最近发现的来自 MySQL Docs 的一个有趣的注释:

以前的 MySQL 版本可以将值分配给 SET 以外的语句中的用户变量。这个功能是 MySQL 8.0 支持向后兼容,但受制于 在 MySQL 的未来版本中删除。

另外,感谢一位 SO 成员,我偶然发现了 MySQL 团队的这个博客:https://mysqlserverteam.com/row-numbering-ranking-how-to-use-less-user-variables-in-mysql-queries/

一般观察是,在同一查询块中使用 ORDER BY 和用户变量的评估,并不能确保值总是正确的。因为,MySQL 优化器可能到位并改变我们假定的评估顺序。

解决此问题的最佳方法是升级到 MySQL 8+ 并利用 Row_Number() 功能:

架构 (MySQL v8.0)

SELECT user_id, 
       game_id, 
       game_detail, 
       ROW_NUMBER() OVER (PARTITION BY user_id 
                          ORDER BY game_detail DESC) AS user_game_rank 
FROM game_logs 
ORDER BY user_id, user_game_rank;

结果

| user_id | game_id | game_detail | user_game_rank |
| ------- | ------- | ----------- | -------------- |
| 6       | 11      | 260         | 1              |
| 6       | 10      | 100         | 2              |
| 7       | 10      | 1200        | 1              |
| 7       | 11      | 500         | 2              |
| 7       | 12      | 260         | 3              |
| 7       | 13      | 50          | 4              |

View on DB Fiddle

【讨论】:

    【解决方案2】:

    MySQL 8.0 之前的最佳解决方案如下:

    select gl.*, 
           (@rn := if(@lastUserId = user_id, @rn + 1,
                      if(@lastUserId := user_id, 1, 1)
                     )
            ) as user_game_rank
    from (select gl.*
          from game_logs gl
          order by gl.user_id, gl.game_detail desc
         ) gl cross join
         (select @rn := 0, @lastUserId := 0) params;
    

    排序是在子查询中完成的。从 MySQL 5.7 开始,这是必需的。变量赋值都在一个表达式中,因此不同的表达式求值顺序无关紧要(并且 MySQL 不保证表达式的求值顺序)。

    【讨论】:

    • @lastUserId := user_id 仍然可以在 if() 表达式之前求值。 if(user_id := @lastUserId 中肯定有一些技巧,但无法理解。这是如何运作的 ?一些解释会很方便。
    • @MadhurBhaiya。 . .那条线是一个错误,已被删除。它不用于计算。
    • 现在说得通了。 +1
    【解决方案3】:
    SELECT user_id, game_id, game_detail, 
           CASE WHEN user_id = @lastUserId 
                THEN @rank := @rank + 1 
                ELSE @rank := 1 
           END As user_game_rank,
           @lastUserId := user_id
    FROM game_logs
    cross join (select @rank := 0, @lastUserId := 0) r
    order by user_id, game_detail desc
    

    SQLFiddle Demo

    【讨论】:

    • @GordonLinoff 来自 MySQL 团队的特定博客可能会有所帮助:mysqlserverteam.com/… 2) 这个答案是错误的,因为它没有根据game_detail 按降序确定行编号。它似乎有效,因为(不幸的是)OP 的 sample 数据本身不足(它已经排序)。第三,afaik,在这个和另一个答案(我的)中对用户变量的评估是在两个不同的表达式中发生的(用逗号分隔)。如果可以展示具体的差异,我们会很高兴
    • @MadhurBhaiya:我添加了一个order by
    • @juergend 我还请你读一下这个博客:mysqlserverteam.com/… 基本上 MySQL 不保证评估是在order by 之前还是之后进行(由于它自己的优化启动)。
    • @juergend 我和尼克聊了聊这类问题,以及某些场景中的意外行为等,在另一个问题的 cmets 中。您可能对此感兴趣:stackoverflow.com/questions/53404473/…
    • @MadhurBhaiya。 . .你是对的。这会犯同样的错误。
    【解决方案4】:

    您可以使用一个非常简单的相关子查询:

    SELECT *, (
        SELECT COUNT(DISTINCT game_detail) + 1
        FROM game_logs AS x
        WHERE user_id = t.user_id AND game_detail > t.game_detail
    ) AS user_game_rank
    FROM game_logs AS t
    ORDER BY user_id, user_game_rank
    

    DB Fiddle

    它比用户变量更慢但更可靠。只需一次 JOIN 即可破坏它们。

    【讨论】:

      猜你喜欢
      • 2013-02-06
      • 1970-01-01
      • 2019-12-18
      • 2014-12-16
      • 1970-01-01
      • 2017-08-31
      • 1970-01-01
      • 1970-01-01
      • 2020-04-23
      相关资源
      最近更新 更多