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

88

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

Прежнее название этого вопроса было « использование ранга (@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обычно используется для ранжирования, но для меня это выглядит подозрительно, когда используется в двух подзапросах, но инициализируется только один раз. Так будет ли работать?

И во-вторых, будет ли он работать с одним подзапросом, который выполняется несколько раз? Как подзапрос в предложении 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

Заранее спасибо!

ТМС
источник
2
более сложный вопрос здесь stackoverflow.com/questions/9841093/…
TMS

Ответы:

174

Итак, вы хотите получить строку с наибольшим значением для 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)

( РЕДАКТИРОВАТЬ Томас: если в одной группе есть больше записей с одним и тем же 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).


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

Мы можем показать, что ранг увеличивается до трех для первой группы и до шести для второй группы, и внутренний запрос возвращает их правильно:

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.

Билл Карвин
источник
Это может оказаться весьма полезным (в том числе и для ОП), но, к сожалению, не отвечает ни на один из двух заданных вопросов.
Andriy M
Спасибо, Билл, это хорошая идея, как избежать рядов, но ... разве присоединение не будет медленным? Соединение (без ограничения предложения where) будет иметь гораздо больший размер, чем в моих запросах. В любом случае спасибо за идею! Но мне также было бы интересно узнать о первоначальном вопросе, то есть о том, будут ли так работать ранги.
TMS
Спасибо за отличный ответ, Билл. Однако что, если бы я использовал @Rank1и @Rank2, по одному для каждого подзапроса? Это решило бы проблему? Будет ли это быстрее вашего решения?
TMS
Использование @Rank1и не @Rank2имело бы никакого значения.
Билл Карвин,
2
Спасибо за отличное решение. Я долго боролся с этой проблемой. Для людей, которые хотят добавить фильтры для других полей, например "foo", вам нужно добавить их в условие соединения, ... AND t1.foo = t2.fooчтобы позже получить правильные результаты дляWHERE ... AND foo='bar'
ownking