Получать записи с наивысшим/наименьшим <безотносительно> к каждой группе

Как это сделать?

Раннее название этого вопроса заключалось в "использовании ранга (@Rank: = @Rank + 1) в сложном запросе с подзапросами - будет ли он работать?" потому что я искал решение с использованием рангов, но теперь я вижу, что решение, отправленное Биллом, намного лучше.

Оригинальный вопрос:

Я пытаюсь составить запрос, который будет принимать последнюю запись из каждой группы с определенным порядком:

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

Expression @Rank := @Rank + 1 обычно используется для ранжирования, но для меня он выглядит подозрительным при использовании в двух подзапросах, но инициализируется только один раз. Будет ли это работать таким образом?

И во-вторых, будет ли он работать с одним подзапросом, который оценивается несколько раз? Подобно подзапросу, в котором (или имеющем) предложение (другим способом, как написать выше):

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

Спасибо заранее!

Ответ 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

конец редактирования.)

Иными словами, верните строку t1, для которой нет другой строки t2 с тем же GroupId и большим OrderField. Когда t2.* равно NULL, это означает, что левое внешнее соединение не нашло такого совпадения, и поэтому t1 имеет наибольшее значение OrderField в группе.

Нет рангов, нет подзапросов. Это должно выполняться быстро и оптимизировать доступ к t2 с помощью "Использование индекса", если у вас есть составной индекс на (GroupId, OrderField).


Что касается производительности, см. мой ответ на Получение последней записи в каждой группе. Я попробовал метод подзапроса и метод join, используя дамп данных Stack Overflow. Различие замечательно: метод соединения в моем тесте выполнялся в 278 раз быстрее.

Важно, чтобы у вас был правильный индекс, чтобы получить наилучшие результаты!

Что касается вашего метода с использованием переменной @Rank, он не будет работать так, как вы его написали, потому что значения @Rank не будут reset равны нулю после того, как запрос обработает первую таблицу. Я покажу вам пример.

Я вставил некоторые фиктивные данные, с дополнительным полем, которое является нулевым, кроме строки, которую мы знаем, является наибольшей для каждой группы:

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 с reset равняться нулю между обработкой двух таблиц (и надеемся, что оптимизатор не изменит порядок, в котором он оценивает таблицы, или же используйте 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                  |
+----+-------------+------------+--------+---------------+------+---------+------+------+---------------------------------+

В то время как мое решение, использующее левое внешнее соединение, оптимизируется намного лучше. Он не использует таблицу temp и даже отчеты "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 медленным.