У меня есть эта таблица для документов (упрощенная версия здесь):
+------+-------+--------------------------------------+
| id | rev | content |
+------+-------+--------------------------------------+
| 1 | 1 | ... |
| 2 | 1 | ... |
| 1 | 2 | ... |
| 1 | 3 | ... |
+------+-------+--------------------------------------+
Как выбрать одну строку для каждого идентификатора и только наибольшее число оборотов?
С данными выше, результат должен содержать две строки: [1, 3, ...]
а также [2, 1, ..]
, я использую MySQL.
В настоящее время я использую чеки в while
цикл для обнаружения и перезаписи старых оборотов из набора результатов. Но является ли это единственным методом достижения результата? Разве нет SQL решение?
Обновить
Как показывают ответы, есть является решение SQL, и здесь демо sqlfiddle.
Обновление 2
Я заметил после добавления выше sqlfiddle, скорость, с которой вопрос поднимается, превзошла скорость ответов. Это не было намерением! Скрипка основана на ответах, особенно принятых ответах.
Все, что вам нужно, это GROUP BY
оговорка с MAX
агрегатная функция:
SELECT id, MAX(rev)
FROM YourTable
GROUP BY id
Я просто заметил, что вам нужно content
колонна также.
Это очень распространенный вопрос в SQL: найти все данные для строки с максимальным значением в столбце для некоторого идентификатора группы. Я много это слышал за свою карьеру. На самом деле, это был один из вопросов, на которые я ответил на техническом собеседовании.
На самом деле, это настолько распространено, что сообщество StackOverflow создало один тег, чтобы иметь дело с такими вопросами: Наибольший-н-в-группа.
По сути, у вас есть два подхода к решению этой проблемы:
group-identifier, max-value-in-group
Суб-запросПри таком подходе вы сначала находите group-identifier, max-value-in-group
(уже решено выше) в подзапросе. Затем вы присоединяете свою таблицу к подзапросу с равенством на обоих group-identifier
а также max-value-in-group
:
SELECT a.id, a.rev, a.contents
FROM YourTable a
INNER JOIN (
SELECT id, MAX(rev) rev
FROM YourTable
GROUP BY id
) b ON a.id = b.id AND a.rev = b.rev
При таком подходе вы оставляете за столом присоединение к себе. Равенство, конечно, идет в group-identifier
, Затем 2 умных хода:
NULL
в правой части (это LEFT JOIN
, Помните?). Затем мы фильтруем объединенный результат, показывая только те строки, где правая сторона NULL
,Итак, вы в конечном итоге:
SELECT a.*
FROM YourTable a
LEFT OUTER JOIN YourTable b
ON a.id = b.id AND a.rev < b.rev
WHERE b.id IS NULL;
Оба подхода дают одинаковый результат.
Если у вас есть две строки с max-value-in-group
за group-identifier
обе строки будут в результате в обоих подходах.
Оба подхода совместимы с SQL ANSI, поэтому будут работать с вашей любимой СУБД, независимо от ее «аромата».
Оба подхода также не влияют на производительность, однако ваш пробег может отличаться (СУБД, структура БД, индексы и т. Д.). Поэтому, когда вы выбираете один подход над другим, эталонный тест. И убедитесь, что вы выбрали тот, который имеет наибольшее значение для вас.
Я предпочитаю использовать как можно меньше кода …
Вы можете сделать это с помощью IN
попробуй это:
SELECT *
FROM t1 WHERE (id,rev) IN
( SELECT id, MAX(rev)
FROM t1
GROUP BY id
)
На мой взгляд, это менее сложно … легче читать и поддерживать.
Еще одно решение заключается в использовании коррелированного подзапроса:
select yt.id, yt.rev, yt.contents
from YourTable yt
where rev =
(select max(rev) from YourTable st where yt.id=st.id)
Наличие индекса (id, rev) делает подзапрос почти простым поиском …
Ниже приводятся сравнения с решениями в ответе @ AdrianCarneiro (подзапрос, левое соединение), основанный на измерениях MySQL с таблицей InnoDB ~ 1 миллион записей, размер группы: 1-3.
В то время как для полных сканирований таблицы подзапрос / оставленное соединение / коррелированные моменты времени относятся друг к другу как 6/8/9, когда речь идет о прямом поиске или пакетной обработке (id in (1,2,3)
), подзапрос намного медленнее остальных (из-за перезапуска подзапроса). Однако я не мог различить левое соединение и коррелированные решения по скорости.
И последнее замечание: поскольку leftjoin создает n * (n + 1) / 2 объединений в группах, его производительность может сильно зависеть от размера групп …
Я поражен, что ни один ответ не предложил решение для оконной функции SQL:
SELECT a.id, a.rev, a.contents
FROM (SELECT id, rev, contents,
ROW_NUMBER() OVER (PARTITION BY id ORDER BY rev DESC) rank
FROM YourTable) a
WHERE a.rank = 1
Добавленные в стандарт SQL ANSI / ISO Standard SQL: 2003 и более поздние, дополненные стандартом ANSI / ISO SQL: 2008, функции окна (или оконного режима) теперь доступны для всех основных поставщиков. Есть еще несколько типов функций ранга, доступных для решения проблемы связи: RANK, DENSE_RANK, PERSENT_RANK
,
Я не могу ручаться за производительность, но вот трюк, вдохновленный ограничениями Microsoft Excel. У этого есть несколько хороших особенностей
ХОРОШАЯ ВЕЩЬ
ПОДХОД
Это немного уродливо и требует, чтобы вы знали кое-что о диапазоне допустимых значений оборот колонка. Давайте предположим, что мы знаем оборот столбец — это число от 0,00 до 999, включая десятичные дроби, но справа от десятичной точки будут только две цифры (например, 34.17 будет допустимым значением).
Суть в том, что вы создаете один синтетический столбец путем объединения строк / упаковки основного поля сравнения вместе с данными, которые вы хотите. Таким образом, вы можете заставить агрегатную функцию MAX () SQL возвращать все данные (потому что они были упакованы в один столбец). Затем вы должны распаковать данные.
Вот как это выглядит на примере выше, написанном на SQL
SELECT id,
CAST(SUBSTRING(max(packed_col) FROM 2 FOR 6) AS float) as max_rev,
SUBSTRING(max(packed_col) FROM 11) AS content_for_max_rev
FROM (SELECT id,
CAST(1000 + rev + .001 as CHAR) || '---' || CAST(content AS char) AS packed_col
FROM yourtable
)
GROUP BY id
Упаковка начинается с принуждения оборот столбец должен быть числом известной длины символа независимо от значения оборот так что например
Если вы все сделаете правильно, сравнение строк двух чисел должно привести к тому же «max», что и сравнение чисел двух чисел, и его легко преобразовать обратно в исходное число с помощью функции подстроки (которая доступна в той или иной форме в значительной степени везде).
Я думаю, что это самое простое решение:
SELECT *
FROM
(SELECT *
FROM Employee
ORDER BY Salary DESC)
AS employeesub
GROUP BY employeesub.Salary;
Если вам нужен только один ряд, это еще проще:
SELECT *
FROM Employee
ORDER BY Employee.Salary DESC
LIMIT 1
Я также думаю, что проще всего разобраться, понять и изменить для других целей:
Понимание этого подхода позволяет решить любую из этих похожих проблем тривиально: получить сотрудника с наименьшей зарплатой (смените DESC на ASC), получить 10 лучших зарабатывающих сотрудников (измените LIMIT 1 на LIMIT 10), отсортировать с помощью другого поля (изменить ORDER BY Employee.Salary to ORDER BY Employee.Commission) и т. Д.
Что-то вроде этого?
SELECT yourtable.id, rev, content
FROM yourtable
INNER JOIN (
SELECT id, max(rev) as maxrev FROM yourtable
WHERE yourtable
GROUP BY id
) AS child ON (yourtable.id = child.id) AND (yourtable.rev = maxrev)
Так как это самый популярный вопрос в отношении этой проблемы, я еще раз отвечу на этот вопрос здесь:
Похоже, есть более простой способ сделать это (но только в MySQL):
select *
from (select * from mytable order by id, rev desc ) x
group by id
Пожалуйста, отметьте ответ пользователя Bohemian в этот вопрос за предоставление такого краткого и элегантного ответа на эту проблему.
РЕДАКТИРОВАТЬ: хотя это решение работает для многих людей, оно может быть нестабильным в долгосрочной перспективе, так как MySQL не гарантирует, что оператор GROUP BY будет возвращать значимые значения для столбцов, не входящих в список GROUP BY. Так что используйте это решение на свой страх и риск