Выберите одно значение из группы по заказу из других столбцов

Проблема

Предположим, что у меня есть эта таблица tab (fiddle).

| g | a | b |     v |
---------------------
| 1 | 3 | 5 |   foo |
| 1 | 4 | 7 |   bar |
| 1 | 2 | 9 |   baz |
| 2 | 1 | 1 |   dog |
| 2 | 5 | 2 |   cat |
| 2 | 5 | 3 | horse |
| 2 | 3 | 8 |   pig |

Я группирую строки по g, и для каждой группы я хочу одно значение из столбца v. Тем не менее, я не хочу никакой ценности, но мне нужно значение из строки с максимальным a и из всех тех, которые имеют максимальный b. Другими словами, мой результат должен быть

| 1 |   bar |
| 2 | horse |

Текущее решение

Я знаю запрос для достижения этого:

SELECT grps.g,
(SELECT v FROM tab
 WHERE g = grps.g
 ORDER BY a DESC, b DESC
 LIMIT 1) AS r
FROM (SELECT DISTINCT g FROM tab) grps

Вопрос

Но я считаю этот запрос скорее уродливым. В основном потому, что он использует зависимый подзапрос , который похож на реального убийцы производительности. Поэтому я задаюсь вопросом, есть ли более легкое решение этой проблемы.

Ожидаемые ответы

Самый вероятный ответ, который я ожидаю в этом вопросе, будет своего рода дополнением или патчем для MySQL (или MariaDB), который предоставляет для этого функцию. Но я также приветствую другие полезные вдохновения. Все, что работает без зависимого подзапроса, будет квалифицироваться как ответ.

Если ваше решение работает только для одного столбца заказа, то есть не может различать cat и horse, не стесняйтесь предлагать этот ответ, а также я ожидаю, что он будет по-прежнему полезен для большинства случаев использования, Например, 100*a+b был бы вероятным способом упорядочить вышеуказанные данные обоими столбцами, все еще используя только одно выражение.

У меня есть несколько довольно хакерских решений, и я мог бы добавить их через некоторое время, но сначала я посмотрю и посмотрю, сначала ли в них появляются новые новые.


Результаты тестов

Как трудно сравнивать различные ответы, просто взглянув на них, я запустил некоторые тесты. Это было выполнено на моем собственном рабочем столе, используя MySQL 5.1. Числа не будут сравниваться ни с одной другой системой, только друг с другом. Вы, вероятно, должны делать свои собственные тесты с вашими реальными данными, если производительность имеет решающее значение для вашего приложения. Когда появятся новые ответы, я могу добавить их в мой script и повторно запустить все тесты.

  • 100 000 наименований, 1000 групп на выбор, InnoDb:
  • 100 000 наименований, 50 000 групп на выбор, InnoDb:
  • 100 000 наименований, 100 групп на выбор, InnoDb:

Итак, кажется, что мое собственное решение пока не так уж плохо, даже с зависимым подзапросом. Удивительно, но решение acatt, которое также использует зависимый подзапрос и который я бы рассмотрел примерно так же, намного хуже. Вероятно, оптимизатор MySQL не может справиться. Решение, предложенное RichardTheKiwi, похоже, имеет хорошую общую производительность. Два других решения в значительной степени зависят от структуры данных. Со многими группами небольших групп подход xdazz превосходит все остальные, тогда как решение Dems лучше всего работает (хотя и не очень хорошо) для нескольких больших групп.

Ответ 1

SELECT g, a, b, v
  FROM (
            SELECT *, 
                   @rn := IF(g = @g, @rn + 1, 1) rn, 
                   @g := g
              FROM (select @g := null, @rn := 0) x, 
                   tab
          ORDER BY g, a desc, b desc, v
       ) X
 WHERE rn = 1;

Одиночный проход. Все остальные решения выглядят O (n ^ 2) для меня.

Ответ 2

Этот способ не использует подзапрос.

SELECT t1.g, t1.v
FROM tab t1
LEFT JOIN tab t2 ON t1.g = t2.g AND (t1.a < t2.a OR (t1.a = t2.a AND t1.b < t2.b))
WHERE t2.g IS NULL

Объяснение:

LEFT JOIN работает на том основании, что когда t1.a находится на своем максимальном значении, нет s2.a с большим значением, а значения строк s2 будут NULL.

Ответ 3

Это можно решить, используя коррелированный запрос:

SELECT g, v
FROM tab t
WHERE NOT EXISTS (
    SELECT 1
    FROM tab
    WHERE g = t.g
        AND a > t.a
        OR (a = t.a AND b > t.b)
    )

Ответ 4

Во многих РСУБД есть конструкции, которые особенно подходят для этой проблемы. MySQL не является одним из них.

Это приводит к трем основным подходам.

  • Проверьте каждую запись, чтобы убедиться, что она вам нужна, используя EXISTS и коррелированный подзапрос в предложении EXISTS. (@acatt ответ, но я понимаю, что MySQL не всегда оптимизирует это очень хорошо. Убедитесь, что у вас есть составной индекс на (g,a,b), прежде чем предположить, что MySQL не будет делать это очень хорошо.)

  • Сделайте половину декартова продукта, чтобы заполнить ту же проверку. Любая запись, которая не соединяется, является целевой записью. Если каждая группа ( "g" ) велика, это может быстро ухудшить производительность (если для каждого уникального значения g будет 10 записей, это даст ~ 50 записей и отбросит 49. Для группового размера 100 оно дает ~ 5000 записей и отбросить 4999), но отлично подходит для небольших групп. (@xdazz ответ.)

  • Или используйте несколько подзапросов для определения MAX (a), а затем MAX (b)...

Несколько последовательных подзапросов...

SELECT
  yourTable.*
FROM
  (SELECT g,    MAX(a) AS a FROM yourTable GROUP BY g   ) AS searchA
INNER JOIN
  (SELECT g, a, MAX(b) AS b FROM yourTable GROUP BY g, a) AS searchB
    ON  searchA.g = searchB.g
    AND searchA.a = searchB.a
INNER JOIN
  yourTable
    ON  yourTable.g = searchB.g
    AND yourTable.a = searchB.a
    AND yourTable.b = searchB.b

В зависимости от того, как MySQL оптимизирует второй подзапрос, это может быть или не быть более эффективным, чем другие параметры. Это, однако, самый длинный (и, возможно, наименее поддерживаемый) код для данной задачи.

Предполагая составной индекс во всех трех полях поиска (g, a, b), я предполагаю, что он лучше всего подходит для больших размеров группы g. Но это должно быть проверено.

Для небольших размеров группы g я бы пошел с ответом @xdazz.

ИЗМЕНИТЬ

Существует также подход грубой силы.

  • Создайте идентичную таблицу, но с столбцом AUTO_INCREMENT в качестве идентификатора.
  • Вставьте таблицу в этот клон, упорядоченный по g, a, b.
  • Затем идентификатор можно найти с помощью SELECT g, MAX(id).
  • Этот результат можно затем использовать для поиска нужных значений v.

Это вряд ли будет лучшим подходом. Если это так, это действительно затрудняет способность оптимизатора MySQL справляться с этой проблемой.

Тем не менее, у каждого двигателя есть слабые места. Итак, лично я стараюсь все, пока не думаю, что понимаю, как работает РСУБД и может сделать мой выбор:)

ИЗМЕНИТЬ

Пример с помощью ROW_NUMBER(). (Oracle, SQL Server, PostGreSQL и т.д.)

SELECT
  *
FROM
(
  SELECT
    ROW_NUMBER() OVER (PARTITION BY g ORDER BY a DESC, b DESC) AS sequence_id,
    *
  FROM
    yourTable
)
  AS data
WHERE
  sequence_id = 1