获取每组最高/最小 <whatever> 的记录

Posted

技术标签:

【中文标题】获取每组最高/最小 <whatever> 的记录【英文标题】:Get records with highest/smallest <whatever> per group 【发布时间】:2012-02-03 15:50:36 【问题描述】:

怎么做?

这个问题的前标题是“在带有子查询的复杂查询中使用排名 (@Rank := @Rank + 1) - 会起作用吗?”因为我一直在寻找使用排名的解决方案,但是现在我看到 Bill 发布的解决方案要好得多。

原问题:

我正在尝试编写一个查询,该查询将在给定特定顺序的情况下从每个组中获取最后一条记录:

SET @Rank=0;

select s.*
from (select GroupId, max(Rank) AS MaxRank
      from (select GroupId, @Rank := @Rank + 1 AS Rank 
            from Table
            order by OrderField
            ) as t
      group by GroupId) as t 
  join (
      select *, @Rank := @Rank + 1 AS Rank
      from Table
      order by OrderField
      ) as s 
  on t.GroupId = s.GroupId and t.MaxRank = s.Rank
order by OrderField

表达式@Rank := @Rank + 1 通常用于排名,但对我来说,在 2 个子查询中使用它看起来很可疑,但只初始化了一次。它会这样工作吗?

其次,它是否适用于一个被多次评估的子查询?就像 where (or having) 子句中的子查询(上面的另一种写法):

SET @Rank=0;

select Table.*, @Rank := @Rank + 1 AS Rank
from Table
having Rank = (select max(Rank) AS MaxRank
              from (select GroupId, @Rank := @Rank + 1 AS Rank 
                    from Table as t0
                    order by OrderField
                    ) as t
              where t.GroupId = table.GroupId
             )
order by OrderField

提前致谢!

【问题讨论】:

这里有更高级的问题***.com/questions/9841093/… 这能回答你的问题吗? Fetch the row which has the Max value for a column 【参考方案1】:

所以您想获得每个组中OrderField 最高的行吗?我会这样做:

SELECT t1.*
FROM `Table` AS t1
LEFT OUTER JOIN `Table` AS t2
  ON t1.GroupId = t2.GroupId AND t1.OrderField < t2.OrderField
WHERE t2.GroupId IS NULL
ORDER BY t1.OrderField; // not needed! (note by Tomas)

(Tomas 编辑:如果同一组中有更多具有相同 OrderField 的记录,而您恰好需要其中之一,则可能需要扩展条件:

SELECT t1.*
FROM `Table` AS t1
LEFT OUTER JOIN `Table` AS t2
  ON t1.GroupId = t2.GroupId 
        AND (t1.OrderField < t2.OrderField 
         OR (t1.OrderField = t2.OrderField AND t1.Id < t2.Id))
WHERE t2.GroupId IS NULL

编辑结束。)

换句话说,返回不存在其他行t2 的行t1,具有相同的GroupId 和更大的OrderField。当t2.* 为NULL 时,表示左外连接没有找到这样的匹配,因此t1 在组中具有OrderField 的最大值。

没有等级,没有子查询。如果您在 (GroupId, OrderField) 上有一个复合索引,这应该会运行得很快并通过“使用索引”优化对 t2 的访问。


关于性能,请参阅我对Retrieving the last record in each group 的回答。我尝试了使用 Stack Overflow 数据转储的子查询方法和连接方法。差异是显着的:在我的测试中,join 方法的运行速度提高了 278 倍。

拥有正确的索引以获得最佳结果非常重要!

关于使用@Rank 变量的方法,它不会像您编写的那样工作,因为在查询处理完第一个表后@Rank 的值不会重置为零。我给你举个例子。

我插入了一些虚拟数据,除了我们知道每组最大的行之外,还有一个为空的额外字段:

select * from `Table`;

+---------+------------+------+
| GroupId | OrderField | foo  |
+---------+------------+------+
|      10 |         10 | NULL |
|      10 |         20 | NULL |
|      10 |         30 | foo  |
|      20 |         40 | NULL |
|      20 |         50 | NULL |
|      20 |         60 | foo  |
+---------+------------+------+

我们可以证明第一组的排名增加到三,第二组的排名增加到六,并且内部查询正确返回了这些:

select GroupId, max(Rank) AS MaxRank
from (
  select GroupId, @Rank := @Rank + 1 AS Rank
  from `Table`
  order by OrderField) as t
group by GroupId

+---------+---------+
| GroupId | MaxRank |
+---------+---------+
|      10 |       3 |
|      20 |       6 |
+---------+---------+

现在运行没有连接条件的查询,强制所有行的笛卡尔积,我们还获取所有列:

select s.*, t.*
from (select GroupId, max(Rank) AS MaxRank
      from (select GroupId, @Rank := @Rank + 1 AS Rank 
            from `Table`
            order by OrderField
            ) as t
      group by GroupId) as t 
  join (
      select *, @Rank := @Rank + 1 AS Rank
      from `Table`
      order by OrderField
      ) as s 
  -- on t.GroupId = s.GroupId and t.MaxRank = s.Rank
order by OrderField;

+---------+---------+---------+------------+------+------+
| GroupId | MaxRank | GroupId | OrderField | foo  | Rank |
+---------+---------+---------+------------+------+------+
|      10 |       3 |      10 |         10 | NULL |    7 |
|      20 |       6 |      10 |         10 | NULL |    7 |
|      10 |       3 |      10 |         20 | NULL |    8 |
|      20 |       6 |      10 |         20 | NULL |    8 |
|      20 |       6 |      10 |         30 | foo  |    9 |
|      10 |       3 |      10 |         30 | foo  |    9 |
|      10 |       3 |      20 |         40 | NULL |   10 |
|      20 |       6 |      20 |         40 | NULL |   10 |
|      10 |       3 |      20 |         50 | NULL |   11 |
|      20 |       6 |      20 |         50 | NULL |   11 |
|      20 |       6 |      20 |         60 | foo  |   12 |
|      10 |       3 |      20 |         60 | foo  |   12 |
+---------+---------+---------+------------+------+------+

从上面我们可以看出,每个组的最大排名是正确的,但是随着它处理第二个派生表,@Rank 会继续增加,达到 7 或更高。所以第二个派生表的排名永远不会与第一个派生表的排名重叠。

您必须添加另一个派生表以强制 @Rank 在处理两个表之间重置为零(并希望优化器不会更改它评估表的顺序,或者使用 STRAIGHT_JOIN 来防止这种情况发生):

select s.*
from (select GroupId, max(Rank) AS MaxRank
      from (select GroupId, @Rank := @Rank + 1 AS Rank 
            from `Table`
            order by OrderField
            ) as t
      group by GroupId) as t 
  join (select @Rank := 0) r -- RESET @Rank TO ZERO HERE
  join (
      select *, @Rank := @Rank + 1 AS Rank
      from `Table`
      order by OrderField
      ) as s 
  on t.GroupId = s.GroupId and t.MaxRank = s.Rank
order by OrderField;

+---------+------------+------+------+
| GroupId | OrderField | foo  | Rank |
+---------+------------+------+------+
|      10 |         30 | foo  |    3 |
|      20 |         60 | foo  |    6 |
+---------+------------+------+------+

但是这个查询的优化很糟糕。它不能使用任何索引,它创建两个临时表,对它们进行硬排序,甚至使用连接缓冲区,因为它在连接临时表时也不能使用索引。这是EXPLAIN 的示例输出:

+----+-------------+------------+--------+---------------+------+---------+------+------+---------------------------------+
| id | select_type | table      | type   | possible_keys | key  | key_len | ref  | rows | Extra                           |
+----+-------------+------------+--------+---------------+------+---------+------+------+---------------------------------+
|  1 | PRIMARY     | <derived4> | system | NULL          | NULL | NULL    | NULL |    1 | Using temporary; Using filesort |
|  1 | PRIMARY     | <derived2> | ALL    | NULL          | NULL | NULL    | NULL |    2 |                                 |
|  1 | PRIMARY     | <derived5> | ALL    | NULL          | NULL | NULL    | NULL |    6 | Using where; Using join buffer  |
|  5 | DERIVED     | Table      | ALL    | NULL          | NULL | NULL    | NULL |    6 | Using filesort                  |
|  4 | DERIVED     | NULL       | NULL   | NULL          | NULL | NULL    | NULL | NULL | No tables used                  |
|  2 | DERIVED     | <derived3> | ALL    | NULL          | NULL | NULL    | NULL |    6 | Using temporary; Using filesort |
|  3 | DERIVED     | Table      | ALL    | NULL          | NULL | NULL    | NULL |    6 | Using filesort                  |
+----+-------------+------------+--------+---------------+------+---------+------+------+---------------------------------+

而我使用左外连接的解决方案优化得更好。它不使用临时表,甚至报告"Using index",这意味着它可以仅使用索引来解析连接,而无需接触数据。

+----+-------------+-------+------+---------------+---------+---------+-----------------+------+--------------------------+
| id | select_type | table | type | possible_keys | key     | key_len | ref             | rows | Extra                    |
+----+-------------+-------+------+---------------+---------+---------+-----------------+------+--------------------------+
|  1 | SIMPLE      | t1    | ALL  | NULL          | NULL    | NULL    | NULL            |    6 | Using filesort           |
|  1 | SIMPLE      | t2    | ref  | GroupId       | GroupId | 5       | test.t1.GroupId |    1 | Using where; Using index |
+----+-------------+-------+------+---------------+---------+---------+-----------------+------+--------------------------+

您可能会读到有人在他们的博客上声称“联接使 SQL 变慢”,但这是无稽之谈。优化不佳导致 SQL 变慢。

【讨论】:

这可能证明非常有用(对于 OP 也是如此),但遗憾的是,这两个问题都没有回答。 谢谢比尔,如何避免排名是个好主意,但是......加入会不会很慢?连接(没有 where 子句限制)将比我的查询大得多。无论如何,谢谢你的想法!但我也会对最初的问题感兴趣,即排名是否会以这种方式工作。 感谢您的出色回答,比尔。但是,如果我使用@Rank1@Rank2,每个子查询一个呢?那能解决问题吗?这会比您的解决方案更快吗? 使用@Rank1@Rank2 没有区别。 感谢您提供的出色解决方案。我在这个问题上挣扎了很长时间。对于想要为其他字段添加过滤器的人,例如"foo" 您需要将它们添加到连接条件 ... AND t1.foo = t2.foo 以便稍后获得 WHERE ... AND foo='bar' 的正确结果【参考方案2】:

或者你可以使用order bylimit,即:

SELECT * FROM TABLE ORDER BY ORDERFIELD DESC LIMIT 1

【讨论】:

以上是关于获取每组最高/最小 <whatever> 的记录的主要内容,如果未能解决你的问题,请参考以下文章

jquery 从特定表单获取所有输入

获取每组中元素之间的最小差异

20: 求最高最低平均分

<!WHATEVER!> Kotlin 中的语法? (尖括号包裹感叹号)

PowerPivot DAX - 每组动态排名(每组最小值)

小程序for循环绑定每组数据的id,并通过id获取里面某个数组的值的方法