Получить записи с наибольшим / наименьшим <whatever>на группу - PullRequest
84 голосов
/ 06 января 2012

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

Прежний заголовок этого вопроса был " с использованием ранга (@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

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

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

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

161 голосов
/ 06 января 2012

То есть вы хотите получить строку с самым высоким 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).


Что касается производительности, см. Мой ответ на Получение последней записи в каждомгруппа .Я попробовал метод подзапроса и метод соединения, используя дамп данных переполнения стека.Разница замечательная: метод соединения в моем тесте работал в 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 медленным.

...