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

TMS*_*TMS 84 mysql subquery rank greatest-n-per-group

怎么做?

这个问题的前标题是" 在带有子查询的复杂查询中使用等级(@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
Run Code Online (Sandbox Code Playgroud)

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

第二,它是否适用于多次评估的子查询?像子查询在哪里(或有)子句(另一种方式如何写上面):

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
Run Code Online (Sandbox Code Playgroud)

提前致谢!

Bil*_*win 164

所以你想得到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)
Run Code Online (Sandbox Code Playgroud)

(由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
Run Code Online (Sandbox Code Playgroud)

编辑结束.)

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

没有排名,没有子查询.如果您打开了复合索引,则应该快速运行并使用"使用索引"优化对t2的访问(GroupId, OrderField).


关于性能,请参阅我在每组中检索最后一条记录的答案.我尝试使用Stack Overflow数据转储的子查询方法和join方法.差异非常显着:我的测试中连接方法的运行速度提高了278倍.

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

关于使用@Rank变量的方法,它将无法正常工作,因为在查询处理完第一个表后,@ Rank的值不会重置为零.我会告诉你一个例子.

我插入了一些虚拟数据,其中一个额外的字段为null,除了我们知道每行最大的行:

select * from `Table`;

+---------+------------+------+
| GroupId | OrderField | foo  |
+---------+------------+------+
|      10 |         10 | NULL |
|      10 |         20 | NULL |
|      10 |         30 | foo  |
|      20 |         40 | NULL |
|      20 |         50 | NULL |
|      20 |         60 | foo  |
+---------+------------+------+
Run Code Online (Sandbox Code Playgroud)

我们可以证明第一组的排名增加到3,第二组的排名增加到6,内部查询正确地返回这些:

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 |
+---------+---------+
Run Code Online (Sandbox Code Playgroud)

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

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 |
+---------+---------+---------+------------+------+------+
Run Code Online (Sandbox Code Playgroud)

我们可以从上面看到每组的最大等级是正确的,但随着处理第二个派生表,@ 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 |
+---------+------------+------+------+
Run Code Online (Sandbox Code Playgroud)

但是这个查询的优化很糟糕.它不能使用任何索引,它会创建两个临时表,以硬方式对它们进行排序,甚至使用连接缓冲区,因为它在连接临时表时也不能使用索引.这是以下示例输出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                  |
+----+-------------+------------+--------+---------------+------+---------+------+------+---------------------------------+
Run Code Online (Sandbox Code Playgroud)

而使用左外连接的解决方案优化得更好.它不使用临时表甚至报告"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 |
+----+-------------+-------+------+---------------+---------+---------+-----------------+------+--------------------------+
Run Code Online (Sandbox Code Playgroud)

你可能会读到那些在他们的博客上声称"加入使SQL缓慢"的人,但那是无稽之谈.优化不佳会使SQL变慢.

  • 谢谢你的出色解决方案.我一直在努力解决这个问题.对于想要为其他字段添加过滤器的人,例如"foo",你需要将它们添加到连接条件`...和t1.foo = t2.foo`以便稍后获得`WHERE的正确结果... AND foo ='bar' (2认同)