Получить записи с самым высоким / самым маленьким в группе


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

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

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

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

1 76

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)

(EDIT 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 с помощью "Using index", если у вас есть составной индекс на (GroupId, OrderField).


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

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

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 медленным.