Получить 1 верхний ряд каждой группы - PullRequest
449 голосов
/ 27 июля 2011

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

DocumentStatusLogs Таблица

|ID| DocumentID | Status | DateCreated |
| 2| 1          | S1     | 7/29/2011   |
| 3| 1          | S2     | 7/30/2011   |
| 6| 1          | S1     | 8/02/2011   |
| 1| 2          | S1     | 7/28/2011   |
| 4| 2          | S2     | 7/30/2011   |
| 5| 2          | S3     | 8/01/2011   |
| 6| 3          | S1     | 8/02/2011   |

Таблица будет сгруппирована по DocumentID и отсортирована по DateCreated в порядке убывания.Для каждого DocumentID я хочу получить последний статус.

Мой предпочтительный вывод:

| DocumentID | Status | DateCreated |
| 1          | S1     | 8/02/2011   |
| 2          | S3     | 8/01/2011   |
| 3          | S1     | 8/02/2011   |
  • Существует ли какая-либо агрегатная функция для получения только вершины из каждой группы?См. Псевдокод GetOnlyTheTop ниже:

    SELECT
      DocumentID,
      GetOnlyTheTop(Status),
      GetOnlyTheTop(DateCreated)
    FROM DocumentStatusLogs
    GROUP BY DocumentID
    ORDER BY DateCreated DESC
    
  • Если такой функции не существует, можно ли каким-либо образом добиться желаемого результата?

  • Или, во-первых, это может быть вызвано ненормализованной базой данных?Я думаю, так как то, что я ищу, это всего лишь одна строка, должен ли status также находиться в родительской таблице?

Пожалуйста, смотрите родительскую таблицу для получения дополнительной информации:

Текущий Documents Таблица

| DocumentID | Title  | Content  | DateCreated |
| 1          | TitleA | ...      | ...         |
| 2          | TitleB | ...      | ...         |
| 3          | TitleC | ...      | ...         |

Должна ли родительская таблица быть такой, чтобы я мог легко получить доступ к ее состоянию?

| DocumentID | Title  | Content  | DateCreated | CurrentStatus |
| 1          | TitleA | ...      | ...         | s1            |
| 2          | TitleB | ...      | ...         | s3            |
| 3          | TitleC | ...      | ...         | s1            |

ОБНОВЛЕНИЕ Я только что узнал, как использовать «применить», что облегчает решение таких проблем.

Ответы [ 17 ]

645 голосов
/ 27 июля 2011
;WITH cte AS
(
   SELECT *,
         ROW_NUMBER() OVER (PARTITION BY DocumentID ORDER BY DateCreated DESC) AS rn
   FROM DocumentStatusLogs
)
SELECT *
FROM cte
WHERE rn = 1

Если вы ожидаете 2 записи в день, тогда она будет произвольно выбрана. Чтобы получить обе записи за день, используйте DENSE_RANK вместо

Что касается нормализации или нет, это зависит от того, хотите ли вы:

  • сохранить статус в 2 местах
  • сохранить историю статусов
  • ...

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

144 голосов
/ 30 августа 2012

Я только что научился использовать cross apply.Вот как использовать его в этом сценарии:

 select d.DocumentID, ds.Status, ds.DateCreated 
 from Documents as d 
 cross apply 
     (select top 1 Status, DateCreated
      from DocumentStatusLogs 
      where DocumentID = d.DocumentId
      order by DateCreated desc) as ds
47 голосов
/ 07 марта 2015

Я провел несколько периодов времени по различным рекомендациям здесь, и результаты действительно зависят от размера используемой таблицы, но наиболее согласованным решением является использование CROSS APPLY. Эти тесты выполнялись на SQL Server 2008-R2 с использованием таблица с 6500 записями и другая (идентичная схема) с 137 миллионами записей. Запрашиваемые столбцы являются частью первичного ключа таблицы, а ширина таблицы очень мала (около 30 байт). Время сообщается SQL Server из фактического плана выполнения.

Query                                  Time for 6500 (ms)    Time for 137M(ms)

CROSS APPLY                                    17.9                17.9
SELECT WHERE col = (SELECT MAX(COL)…)           6.6               854.4
DENSE_RANK() OVER PARTITION                     6.6               907.1

Я думаю, что действительно удивительно то, насколько стабильно было время для CROSS APPLY независимо от количества задействованных строк.

26 голосов
/ 27 июля 2011
SELECT * FROM
DocumentStatusLogs JOIN (
  SELECT DocumentID, MAX(DateCreated) DateCreated
  FROM DocumentStatusLogs
  GROUP BY DocumentID
  ) max_date USING (DocumentID, DateCreated)

Какой сервер базы данных?Этот код не работает на всех из них.

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

Кстати, если у вас уже есть столбец DateCreated в таблице документов, вы можете просто присоединиться к DocumentStatusLogs, используяэто (если DateCreated уникально в DocumentStatusLogs).

Редактировать: MsSQL не поддерживает ИСПОЛЬЗОВАНИЕ, поэтому измените его на:

ON DocumentStatusLogs.DocumentID = max_date.DocumentID AND DocumentStatusLogs.DateCreated = max_date.DateCreated
24 голосов
/ 24 января 2018

Я знаю, что это старая ветка, но решения TOP 1 WITH TIES довольно хороши и могут быть полезны при чтении решений.

select top 1 with ties
   DocumentID
  ,Status
  ,DateCreated
from DocumentStatusLogs
order by row_number() over (partition by DocumentID order by DateCreated desc)

Подробнее о предложении TOP можно найти здесь .

24 голосов
/ 16 января 2013

Если вы беспокоитесь о производительности, вы также можете сделать это с помощью MAX ():

SELECT *
FROM DocumentStatusLogs D
WHERE DateCreated = (SELECT MAX(DateCreated) FROM DocumentStatusLogs WHERE ID = D.ID)

ROW_NUMBER () требует сортировки всех строк в вашем операторе SELECT, а MAX - нет. Должно резко ускорить ваш запрос.

10 голосов
/ 03 июня 2014

Это довольно старая тема, но я подумал, что я бы скинул свои два цента точно так же, как принятый ответ не работал для меня особенно хорошо.Я попробовал решение gbn для большого набора данных и обнаружил, что оно ужасно медленное (> 45 секунд на 5 миллионов записей в SQL Server 2012).Глядя на план выполнения, становится очевидным, что проблема в том, что для него требуется операция SORT, которая значительно замедляет работу.

Вот альтернатива, которую я извлек из структуры сущностей, которая не требует операции SORT и выполняет некластеризациюИндекс поиска.Это сокращает время выполнения до <2 секунд для вышеупомянутого набора записей. </p>

SELECT 
[Limit1].[DocumentID] AS [DocumentID], 
[Limit1].[Status] AS [Status], 
[Limit1].[DateCreated] AS [DateCreated]
FROM   (SELECT DISTINCT [Extent1].[DocumentID] AS [DocumentID] FROM [dbo].[DocumentStatusLogs] AS [Extent1]) AS [Distinct1]
OUTER APPLY  (SELECT TOP (1) [Project2].[ID] AS [ID], [Project2].[DocumentID] AS [DocumentID], [Project2].[Status] AS [Status], [Project2].[DateCreated] AS [DateCreated]
    FROM (SELECT 
        [Extent2].[ID] AS [ID], 
        [Extent2].[DocumentID] AS [DocumentID], 
        [Extent2].[Status] AS [Status], 
        [Extent2].[DateCreated] AS [DateCreated]
        FROM [dbo].[DocumentStatusLogs] AS [Extent2]
        WHERE ([Distinct1].[DocumentID] = [Extent2].[DocumentID])
    )  AS [Project2]
    ORDER BY [Project2].[ID] DESC) AS [Limit1]

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

6 голосов
/ 18 января 2018

Это один из наиболее легко найденных вопросов по теме, поэтому я хотел дать современный ответ на него (как для справки, так и для помощи другим). Используя over и first value, вы можете быстро обработать вышеуказанный запрос:

select distinct DocumentID
  , first_value(status) over (partition by DocumentID order by DateCreated Desc) as Status
  , first_value(DateCreated) over (partition by DocumentID order by DateCreated Desc) as DateCreated
From DocumentStatusLogs

Это должно работать в SQL Server 2008 и выше. Первое значение можно рассматривать как способ выполнения выбора top 1 при использовании предложения over. Over позволяет группировать в списке выбора, поэтому вместо написания вложенных подзапросов (как это делают многие из существующих ответов) это делается более читабельно. Надеюсь, это поможет.

5 голосов
/ 23 сентября 2012

Мой код для выбора топ 1 из каждой группы

select a.* from #DocumentStatusLogs a where 
 datecreated in( select top 1 datecreated from #DocumentStatusLogs b
where 
a.documentid = b.documentid
order by datecreated desc
)
2 голосов
/ 19 декабря 2016
SELECT o.*
FROM `DocumentStatusLogs` o                   
  LEFT JOIN `DocumentStatusLogs` b                   
  ON o.DocumentID = b.DocumentID AND o.DateCreated < b.DateCreated
 WHERE b.DocumentID is NULL ;

Если вы хотите вернуть только последний порядок документов по DateCreated, он вернет только 1 верхний документ по DocumentID

...