Получить строки с максимальным значением в одном столбце для каждого уникального значения другого столбца
Описание проблемы
В таблице находятся следующие поля:
UserId, Value, Date.
Мне нужно получить UserId и Value для максимальной Date для каждого UserId. То есть, мне необходим Value для каждого UserId, который соответствует самой последней дате.
Как это можно сделать в SQL? (Предпочтительно на Oracle.)
Важно, чтобы я получил все UserId, но для каждого UserId только ту строку, где у пользователя самая последняя дата.
5 ответ(ов)
Я вижу, что многие люди используют подзапросы или оконные функции для выполнения такого запроса, но я часто делаю это без использования подзапросов следующим образом. Это простой, стандартный SQL, который должен работать в любом реляционном СУБД.
SELECT t1.*
FROM mytable t1
LEFT OUTER JOIN mytable t2
ON (t1.UserId = t2.UserId AND t1."Date" < t2."Date")
WHERE t2.UserId IS NULL;
Другими словами: получаем строки из t1, где не существует другой строки с тем же UserId и более поздней датой.
(Я обернул идентификатор "Date" в кавычки, потому что это зарезервированное слово SQL.)
В случае, если t1."Date" = t2."Date", может происходить дублирование. Обычно в таблицах есть ключ с автоинкрементом, например, id. Чтобы избежать дублирования, можно использовать следующий подход:
SELECT t1.*
FROM mytable t1
LEFT OUTER JOIN mytable t2
ON t1.UserId = t2.UserId AND ((t1."Date" < t2."Date")
OR (t1."Date" = t2."Date" AND t1.id < t2.id))
WHERE t2.UserId IS NULL;
Что касается комментария от @Farhan:
Вот более подробное объяснение:
Внешнее соединение пытается объединить t1 с t2. По умолчанию возвращаются все результаты из t1, и если существует совпадение в t2, оно также возвращается. Если для заданной строки из t1 нет совпадения в t2, запрос все равно возвращает строку из t1, и используется NULL в качестве плейсхолдера для всех столбцов из t2. Так работают внешние соединения в общем случае.
Хитрость этого запроса заключается в том, чтобы задать условие совпадения соединения так, чтобы t2 должно совпадать с тем же UserId, и иметь более позднюю дату. Идея в том, что если в t2 существует строка с более поздней датой, то строка в t1, с которой она сравнивается, не может быть самой поздней датой для этого UserId. Но если совпадения нет, т.е. если в t2 нет строки с более поздней датой, чем строка в t1, мы знаем, что строка в t1 имеет самую позднюю дату для данного UserId.
В этих случаях (когда совпадений нет) столбцы t2 будут равны NULL — даже столбцы, указанные в условии соединения. Поэтому мы используем условие WHERE t2.UserId IS NULL, потому что мы ищем случаи, когда не была найдена строка с более поздней датой для данного UserId.
Этот запрос извлекает все строки, для которых значение в столбце my_date равно максимальному значению my_date для данного userid. Это может вернуть несколько строк для userid, если максимальная дата встречается в нескольких записях.
select userid,
my_date,
...
from
(
select userid,
my_date,
...
max(my_date) over (partition by userid) max_my_date
from users
)
where my_date = max_my_date
"Аналитические функции крутят!"
Правка: В ответ на первый комментарий...
"Использование аналитических запросов и самообъединение противоречит смыслу аналитических запросов."
В этом коде нет самообъединения. Здесь применяется предикат к результату вложенного запроса, который содержит аналитическую функцию, что является совершенно стандартной практикой.
"По умолчанию окно в Oracle идет от первой строки в партиции до текущей."
Клаузула окна применима только при наличии клаузулы order by. Без нее клауза окон не применяется по умолчанию, и ее нельзя явно указать.
Код работает.
Этот SQL-запрос выбирает userid и максимальное значение value, соответствующее самой последней дате для каждого пользователя из таблицы. Давайте рассмотрим его подробнее:
SELECT userid, MAX(value) KEEP (DENSE_RANK FIRST ORDER BY date DESC): здесь мы выбираемuseridи максимальное значениеvalue, используя конструкциюKEEP. Она позволяет сохранить максимальное значение для первых строк, упорядоченных поdateв порядке убывания. То есть, при наличии нескольких одинаковых максимальных значенийvalueдля одной даты, будет выбрано первое из них.FROM table: указывает, что данные выбираются из указанной таблицы.GROUP BY userid: эта часть группирует результаты поuserid, так что для каждого уникальногоuseridбудет возвращён один результат с максимальным значениемvalueдля самой последней даты.
Таким образом, данный запрос возвращает userid и соответствующее максимальное значение value для каждой записи, основываясь на самой последней дате. Если у вас есть дополнительные вопросы или нужна помощь с другим SQL-запросом, не стесняйтесь спрашивать!
Я не знаю точных названий ваших столбцов, но скорее всего это будет выглядеть примерно так:
SELECT userid, value
FROM users u1
WHERE date = (
SELECT MAX(date)
FROM users u2
WHERE u1.userid = u2.userid
)
Этот запрос выбирает userid и value из таблицы users, где дата совпадает с максимальной датой для соответствующего пользователя. Убедитесь, что вы подставили свои названия столбцов и таблиц.
Не будучи на работе, у меня нет доступа к Oracle, но я помню, что Oracle позволяет использовать множественные столбцы в предложении IN, что, по крайней мере, избавляет от вариантов с коррелированными подзапросами, которые редко бывают хорошей идеей.
Что-то вроде этого, возможно (не помню, нужно ли заключать список столбцов в круглые скобки):
SELECT *
FROM MyTable
WHERE (User, Date) IN
( SELECT User, MAX(Date) FROM MyTable GROUP BY User)
ИЗМЕНЕНИЕ: Я только что попробовал это на практике:
SQL> create table MyTable (usr char(1), dt date);
SQL> insert into mytable values ('A','01-JAN-2009');
SQL> insert into mytable values ('B','01-JAN-2009');
SQL> insert into mytable values ('A', '31-DEC-2008');
SQL> insert into mytable values ('B', '31-DEC-2008');
SQL> select usr, dt from mytable
2 where (usr, dt) in
3 ( select usr, max(dt) from mytable group by usr)
4 /
U DT
- ---------
A 01-JAN-09
B 01-JAN-09
Так что это работает, хотя некоторые из более современных решений, упомянутых в других источниках, могут быть более производительными.
SQL: выбрать только строки с максимальным значением в столбце
Как выбрать строки с MAX(значение колонки), используя PARTITION по другой колонке в MySQL?
Тип данных Oracle Timestamp
Как сравнить строки в SQL, игнорируя регистр?
Как использовать константу пакета в SQL-запросе SELECT?