Извлечь строку, которая имеет значение Max для столбца - PullRequest
538 голосов
/ 23 сентября 2008

Таблица:

UserId, Value, Date.

Я хочу получить UserId, значение для max (Date) для каждого UserId. То есть значение для каждого идентификатора пользователя, который имеет самую последнюю дату. Есть ли способ сделать это просто в SQL? (Желательно Oracle)

Обновление: Извинения за любую двусмысленность: мне нужно получить ВСЕ UserIds. Но для каждого UserId только та строка, в которой у этого пользователя самая последняя дата.

Ответы [ 34 ]

423 голосов
/ 24 сентября 2008

Я вижу, что многие люди используют для этого подзапросы или специфичные для поставщика функции, но я часто делаю этот тип запросов без подзапросов следующим образом. Он использует простой стандартный 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 и большей датой.

(Я поместил идентификатор «Дата» в разделители, потому что это зарезервированное слово SQL.)

В случае, если t1."Date" = t2."Date", появляется удвоение. Обычно таблицы имеют ключ auto_inc(seq), например, 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, также возвращается. Если в t2 нет совпадения для данной строки t1, то запрос по-прежнему возвращает строку t1 и использует NULL в качестве заполнителя для всех столбцов t2. Так работают внешние соединения.

Хитрость в этом запросе состоит в том, чтобы спроектировать условие сопоставления объединения таким образом, чтобы t2 должно совпадать с таким же userid и большим date. Идея состоит в том, что если в t2 есть строка, имеющая большее date, то строка в t1, с которой она сравнивается с , не может быть самой большой date для этого userid. Но если нет совпадений - т.е. если в t2 нет строки с большим значением date, чем в t1 - мы знаем, что строка в t1 была строкой с наибольшим значением date для данного userid.

В этих случаях (когда совпадений нет) столбцы t2 будут NULL - даже столбцы, указанные в условии соединения. Вот почему мы используем WHERE t2.UserId IS NULL, потому что мы ищем случаи, когда не было найдено ни одной строки с большим date для данного userid.

373 голосов
/ 23 сентября 2008

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

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. Без указания по порядку, по умолчанию не применяется ни одно оконное предложение, и ни одно из них не может быть указано явно.

Код работает.

154 голосов
/ 23 сентября 2008
SELECT userid, MAX(value) KEEP (DENSE_RANK FIRST ORDER BY date DESC)
  FROM table
  GROUP BY userid
48 голосов
/ 23 сентября 2008

Я не знаю ваших точных имен столбцов, но это будет примерно так:

    select userid, value
      from users u1
     where date = (select max(date)
                     from users u2
                    where u1.userid = u2.userid)
35 голосов
/ 24 сентября 2008

Не будучи на работе, у меня нет 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

Так что это работает, хотя некоторые из новеньких вещей, упомянутых в другом месте, могут быть более производительными.

13 голосов
/ 23 сентября 2008

Я знаю, что вы спрашивали об Oracle, но в SQL 2005 мы теперь используем это:


-- Single Value
;WITH ByDate
AS (
SELECT UserId, Value, ROW_NUMBER() OVER (PARTITION BY UserId ORDER BY Date DESC) RowNum
FROM UserDates
)
SELECT UserId, Value
FROM ByDate
WHERE RowNum = 1

-- Multiple values where dates match
;WITH ByDate
AS (
SELECT UserId, Value, RANK() OVER (PARTITION BY UserId ORDER BY Date DESC) Rnk
FROM UserDates
)
SELECT UserId, Value
FROM ByDate
WHERE Rnk = 1
6 голосов
/ 23 сентября 2008

У меня нет Oracle для его тестирования, но самое эффективное решение - использовать аналитические запросы. Это должно выглядеть примерно так:

SELECT DISTINCT
    UserId
  , MaxValue
FROM (
    SELECT UserId
      , FIRST (Value) Over (
          PARTITION BY UserId
          ORDER BY Date DESC
        ) MaxValue
    FROM SomeTable
  )

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

Если вы хотите узнать об аналитических запросах, я бы посоветовал прочитать http://www.orafaq.com/node/55 и http://www.akadia.com/services/ora_analytic_functions.html. Вот краткое резюме.

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

В этом случае вот что делает внутренний запрос. Весь набор данных сортируется по UserId, а затем по дате DESC. Затем он обрабатывает его за один проход. Для каждой строки вы возвращаете UserId и первую Date, увиденную для этого UserId (поскольку даты отсортированы DESC, это максимальная дата). Это дает вам ваш ответ с дублированными строками. Тогда внешний DISTINCT сдавливает дубликаты.

Это не особенно впечатляющий пример аналитических запросов. Для гораздо большего выигрыша рассмотрите возможность получения таблицы финансовых квитанций и расчета для каждого пользователя и квитанции, промежуточной суммы того, что они заплатили. Аналитические запросы решают это эффективно. Другие решения менее эффективны. Именно поэтому они являются частью стандарта SQL 2003 года. (К сожалению, у Postgres их пока нет. Grrr ...)

6 голосов
/ 19 октября 2011

Разве предложение QUALIFY не будет самым простым и лучшим?

select userid, my_date, ...
from users
qualify rank() over (partition by userid order by my_date desc) = 1

Для контекста, здесь, на Teradata, тест приличного размера проводится в 17-х годах с этой версией QUALIFY и в 23-х с «встроенным представлением» / решением Aldridge # 1.

5 голосов
/ 26 марта 2017

В Oracle 12c+ вы можете использовать Top n запросов вместе с аналитической функцией rank, чтобы достичь этого очень кратко без подзапросов:

select *
from your_table
order by rank() over (partition by user_id order by my_date desc)
fetch first 1 row with ties;

Вышеприведенное возвращает все строки с максимальным значением my_date для каждого пользователя.

Если вам нужна только одна строка с максимальной датой, замените rank на row_number:

select *
from your_table
order by row_number() over (partition by user_id order by my_date desc)
fetch first 1 row with ties; 
5 голосов
/ 01 ноября 2011

В PostgreSQL 8.4 или новее вы можете использовать это:

select user_id, user_value_1, user_value_2
  from (select user_id, user_value_1, user_value_2, row_number()
          over (partition by user_id order by user_date desc) 
        from users) as r
  where r.row_number=1
...