Получить строки с максимальным значением в одном столбце для каждого уникального значения другого столбца
Описание проблемы
В таблице находятся следующие поля:
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
Так что это работает, хотя некоторые из более современных решений, упомянутых в других источниках, могут быть более производительными.
Получение последней записи в каждой группе - MySQL
Выбрать первую строку в каждой группе GROUP BY?
SQL: выбрать только строки с максимальным значением в столбце
Как ограничить количество строк, возвращаемых запросом Oracle после сортировки?
Как выбрать строки с MAX(значение колонки), используя PARTITION по другой колонке в MySQL?