【问题标题】:How to perform grouped ranking in MySQL如何在 MySQL 中进行分组排名
【发布时间】:2010-10-06 16:21:26
【问题描述】:

所以我有一张如下表:

ID_STUDENT | ID_CLASS | GRADE
-----------------------------
   1       |    1     |  90
   1       |    2     |  80
   2       |    1     |  99
   3       |    1     |  80
   4       |    1     |  70
   5       |    2     |  78
   6       |    2     |  90
   6       |    3     |  50
   7       |    3     |  90

然后我需要对它们进行分组、排序和排序:

ID_STUDENT | ID_CLASS | GRADE | RANK
------------------------------------
    2      |    1     |  99   |  1
    1      |    1     |  90   |  2
    3      |    1     |  80   |  3
    4      |    1     |  70   |  4
    6      |    2     |  90   |  1
    1      |    2     |  80   |  2
    5      |    2     |  78   |  3
    7      |    3     |  90   |  1
    6      |    3     |  50   |  2

现在我知道你可以使用一个临时变量来进行排名,like here,但是对于分组集我该如何做呢?感谢您的任何见解!

【问题讨论】:

标签: mysql sql window-functions


【解决方案1】:
SELECT id_student, id_class, grade,
   @student:=CASE WHEN @class <> id_class THEN 0 ELSE @student+1 END AS rn,
   @class:=id_class AS clset
FROM
  (SELECT @student:= -1) s,
  (SELECT @class:= -1) c,
  (SELECT *
   FROM mytable
   ORDER BY id_class, id_student
  ) t

这很简单:

  1. 初始查询首先按id_class 排序,其次是id_student
  2. @student@class 被初始化为 -1
  3. @class 用于测试是否输入了下一组。如果id_class 的先前值(存储在@class 中)不等于当前值(存储在id_class 中),则@student 被归零。否则递增。
  4. @class 被分配了新值id_class,它将用于下一行第 3 步的测试。

【讨论】:

  • 它在“设置”上给了我一个错误。我对其进行了一些修改并使其正常工作。我将其发布为下面的答案。有什么办法可以优化吗?另外,你能解释一下它是如何工作的吗?感谢您的帮助!
  • 这能保证按预期工作吗? MySQL 的documentation 说:“作为一般规则,您永远不应该为用户变量赋值并在同一语句中读取该值”
  • @YouvalBronicki:不,不是。为了安全起见,您应该在单独的语句中分配@student@class 和/或将所有内容包装到存储过程中。但是,并非所有框架都支持连接持久性和存储过程。
  • @Quassnoi 我对我在 FROM 子查询中的 ORDER BY 上阅读的这些链接感到好奇。显然,返回的行不一定是有序的。是否会在您编写的查询中出现上述问题,因为我看到 FROM 子查询中有一个 ORDER BY ?链接:mariadb.com/kb/en/mariadb/…dba.stackexchange.com/questions/82930/…
  • 排序不应该是ORDER BY id_class, grade DESC
【解决方案2】:

Quassnoi 的解决方案有问题(标记为最佳答案)。

我也有同样的问题(即在 MySQL 中模拟 SQL Window Function),我曾经实现 Quassnoi 的解决方案,使用用户定义的变量来存储之前的行值...

但是,也许在 MySQL 升级或其他之后,我的查询不再起作用。这是因为不能保证 SELECT 中字段的评估顺序。 @class 分配可以在@student 分配之前进行评估,即使它放在 SELECT 之后。

这在 MySQL 文档中提到如下:

作为一般规则,您永远不应该为用户变量赋值 并读取同一语句中的值。你可能会得到 您期望的结果,但这不能保证。的顺序 涉及用户变量的表达式的评估是未定义的,并且 可能会根据给定语句中包含的元素而更改; 另外,这个顺序不保证在 MySQL 服务器的版本。

来源:http://dev.mysql.com/doc/refman/5.5/en/user-variables.html

最后我使用了类似的技巧来确保在阅读后分配@class:

SELECT id_student, id_class, grade,
   @student:=CASE WHEN @class <> id_class THEN concat(left(@class:=id_class, 0), 0) ELSE @student+1 END AS rn
FROM
  (SELECT @student:= -1) s,
  (SELECT @class:= -1) c,
  (SELECT *
   FROM mytable
   ORDER BY id_class, grade desc
  ) t

使用left() 函数只是用来设置@class 变量。然后,将 left() 的结果(等于 NULL)连接到预期的结果是透明的。

不是很优雅,但很有效!

【讨论】:

  • concat(left(@class:=id_class, 0), 0) 中,您可以简单地使用if(@class:=id_class, 1, 1)。 if-condition 中的赋值仍然有点 hacky,但感觉更容易理解。
  • 另外,我会把整个事情写成函数:@student:=if(@class &lt;&gt; id_class, if(@class:=id_class, 1, 1), @student+1)。但是整个事情有一个很大的缺点,它不能在视图中使用,因为那里不允许使用临时变量,因此必须使用连接的答案
【解决方案3】:
SELECT g1.student_id
     , g1.class_id
     , g1.grade
     , COUNT(*) AS rank
  FROM grades   AS g1
  JOIN grades   AS g2
    ON (g2.grade, g2.student_id) >= (g1.grade, g1.student_id)
   AND g1.class_id = g2.class_id
 GROUP BY g1.student_id
        , g1.class_id
        , g1.grade
 ORDER BY g1.class_id
        , rank
 ;

结果:

+------------+----------+-------+------+
| student_id | class_id | grade | rank |
+------------+----------+-------+------+
|          2 |        1 |    99 |    1 |
|          1 |        1 |    90 |    2 |
|          3 |        1 |    80 |    3 |
|          4 |        1 |    70 |    4 |
|          6 |        2 |    90 |    1 |
|          1 |        2 |    80 |    2 |
|          5 |        2 |    78 |    3 |
|          7 |        3 |    90 |    1 |
|          6 |        3 |    50 |    2 |
+------------+----------+-------+------+

【讨论】:

  • 如果是这样的优势,那就是可以在视图中使用。在 MySQL 中无法在视图中使用临时变量
【解决方案4】:

从上面修改,这可行,但它比我认为的要复杂:

SELECT ID_STUDENT, ID_CLASS, GRADE, RANK
FROM
    (SELECT ID_STUDENT, ID_CLASS, GRADE,
        @student:=CASE WHEN @class <> id_class THEN 1 ELSE @student+1 END AS RANK,
        @class:=id_class AS CLASS
    FROM
        (SELECT @student:= 0) AS s,
        (SELECT @class:= 0) AS c,
        (SELECT * 
            FROM Students
            ORDER BY ID_CLASS, GRADE DESC
        ) AS temp
    ) AS temp2

【讨论】:

    【解决方案5】:
    SELECT ID_STUDENT, ID_CLASS, GRADE, RANK() OVER(
    PARTITION BY ID_CLASS
    ORDER BY GRADE ASC) AS 'Rank'
    FROM table
    ORDER BY ID_CLASS;
    

    我在家庭作业中遇到了类似的问题,发现 MySQL(不能代表任何其他 RDBMS)对其 RANK() 方法有一个分区参数。不明白为什么它不能解决这个问题。

    【讨论】:

    • 您需要PARTITION BY id_class ORDER BY grade DESC 而不是ASC。您可能还想ORDER BY id_class, grade DESC,而不仅仅是ID_CLASS
    • 请注意,这个问题是在 MySQL 中存在 RANK 函数之前提出的:如果你被困在低于 8 的版本上,那就没有运气了。
    【解决方案6】:

    虽然我没有足够的声望点来发表评论(有点幽默),但 MySQL 近年来已经取得了长足的进步。添加了窗口函数和 CTE(WITH 子句),这意味着现在支持 rank(和 row_number 等)。

    我还是以前的“Jon Armstrong - Xgc”,但那个帐户已经被旧电子邮件地址所淹没了。

    一条评论提出了一个关于 MySQL 是否支持排名窗口函数的问题。答:是的。

    我几年前的原始回复:

    SELECT p1.student_id
         , p1.class_id
         , p1.grade
         , COUNT(p2.student_id) AS rank
      FROM grades   AS p1
      JOIN grades   AS p2
        ON (p2.grade, p2.student_id) >= (p1.grade, p1.student_id)
       AND p1.class_id = p2.class_id
     GROUP BY p1.student_id, p1.class_id
     ORDER BY p1.class_id, rank
    ;
    

    结果:

    +------------+----------+-------+------+
    | student_id | class_id | grade | rank |
    +------------+----------+-------+------+
    |          2 |        1 |    99 |    1 |
    |          1 |        1 |    90 |    2 |
    |          3 |        1 |    80 |    3 |
    |          4 |        1 |    70 |    4 |
    |          6 |        2 |    90 |    1 |
    |          1 |        2 |    80 |    2 |
    |          5 |        2 |    78 |    3 |
    |          7 |        3 |    90 |    1 |
    |          6 |        3 |    50 |    2 |
    +------------+----------+-------+------+
    9 rows in set (0.001 sec)
    

    使用 ROW_NUMBER 窗口函数:

    WITH cte1 AS (
            SELECT student_id
                 , class_id
                 , grade
                 , ROW_NUMBER() OVER (PARTITION BY class_id ORDER BY grade DESC) AS rank
              FROM grades
         )
    SELECT *
      FROM cte1
     ORDER BY class_id, r
    ;
    

    结果:

    +------------+----------+-------+------+
    | student_id | class_id | grade | rank |
    +------------+----------+-------+------+
    |          2 |        1 |    99 |    1 |
    |          1 |        1 |    90 |    2 |
    |          3 |        1 |    80 |    3 |
    |          4 |        1 |    70 |    4 |
    |          6 |        2 |    90 |    1 |
    |          1 |        2 |    80 |    2 |
    |          5 |        2 |    78 |    3 |
    |          7 |        3 |    90 |    1 |
    |          6 |        3 |    50 |    2 |
    +------------+----------+-------+------+
    9 rows in set (0.002 sec)
    

    使用RANK窗口函数:

    WITH cte1 AS (
            SELECT student_id
                 , class_id
                 , grade
                 , RANK() OVER (PARTITION BY class_id ORDER BY grade DESC) AS rank
              FROM grades
         )
    SELECT *
      FROM cte1
     ORDER BY class_id, rank
    ;
    

    结果:

    +------------+----------+-------+------+
    | student_id | class_id | grade | rank |
    +------------+----------+-------+------+
    |          2 |        1 |    99 |    1 |
    |          1 |        1 |    90 |    2 |
    |          3 |        1 |    80 |    3 |
    |          4 |        1 |    70 |    4 |
    |          6 |        2 |    90 |    1 |
    |          1 |        2 |    80 |    2 |
    |          5 |        2 |    78 |    3 |
    |          7 |        3 |    90 |    1 |
    |          6 |        3 |    50 |    2 |
    +------------+----------+-------+------+
    9 rows in set (0.000 sec)
    

    【讨论】:

      【解决方案7】:

      我做了一些搜索,发现this article想出了这个解决方案:

      SELECT S2.*, 
      FIND_IN_SET(
      S2.GRADE
      , (
      SELECT GROUP_CONCAT(GRADE ORDER BY GRADE DESC)
      FROM Students S1
      WHERE S1.ID_CLASS = S2.ID_CLASS
      )
      ) AS RANK
      FROM Students S2 ORDER BY ID_CLASS, GRADE DESC;
      

      有什么更好的想法吗?

      【讨论】:

      • 我的当然更好 :) 这个会为每一行选择一个完整的类执行连接,这对性能不利。但是,在真实数据上,您几乎不会注意到任何差异。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-01-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多