Отримайте записи з найвищим / найменшим <wwhat> на групу


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зазвичай використовується для рангу, але для мене він виглядає підозрілим, коли використовується в 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

Спасибі заздалегідь!


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)

( EDIT від 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, використовуючи дамп даних переповнення стека. Різниця надзвичайна: у моєму тесті метод приєднання працював у 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 повільним.


Це може виявитися цілком корисним (для ОП також), але, на жаль, не відповідає жодному з двох запитаних питань.
Андрій М

Дякую Білл, це гарна ідея, як уникнути звань, але ... чи не буде приєднання повільним? Приєднання (без обмеження пропозиції where) було б набагато більшим, ніж у моїх запитах. У будь-якому разі, дякую за ідею! Але мені також було б цікаво оригінальне питання, тобто якби ряди працювали так.
TMS

Дякую за чудову відповідь, Білл. Однак що, якби я використав @Rank1і @Rank2, по одному для кожного підзапиту? Це вирішить проблему? Це було б швидше, ніж ваше рішення?
TMS

Використання @Rank1і не @Rank2мало б значення.
Білл Карвін,

2
Дякую за чудове рішення. Я довго боровся з цією проблемою. Для людей, які хочуть додати фільтри для інших полів, наприклад "foo", вам потрібно додати їх до умови приєднання, ... AND t1.foo = t2.fooщоб пізніше отримати правильні результати дляWHERE ... AND foo='bar'
володіння
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.