Получить верхнюю 1 строку каждой группы
у меня есть стол, который я хочу получить последнюю запись для каждой группы. Вот таблица:
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 |
обновление Я только что узнал, как использовать "применить", что облегчает решение таких проблем.
15 ответов:
;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 местах
- сохранить историю статусов
- ...
как она стоит, вы сохраняете историю. Если вы хотите, чтобы последний статус в родительской таблице тоже (что является денормализацией), вам понадобится триггер, чтобы поддерживайте "статус" в родителе. или удалите эту таблицу истории состояния.
Я только что научился использовать
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
Я сделал некоторые тайминги по различным рекомендациям здесь, и результаты действительно зависят от размера таблицы, но наиболее последовательным решением является использование перекрестного применения эти тесты были запущены против SQL Server 2008-R2, используя таблицу с 6500 записями и другую (идентичную схему) с 137 миллионами записей. Запрашиваемые столбцы являются частью первичного ключа таблицы, а ширина таблицы очень мала (около 30 байт). "Таймс" сообщает SQL-серверу из фактического плана выполнения.
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
Я думаю, что действительно удивительная вещь заключалась в том, насколько последовательным было время для перекрестного применения независимо от количества строк.
SELECT * FROM DocumentStatusLogs JOIN ( SELECT DocumentID, MAX(DateCreated) DateCreated FROM DocumentStatusLogs GROUP BY DocumentID ) max_date USING (DocumentID, DateCreated)
какой сервер баз данных? Этот код не работает на всех из них.
что касается второй половины вашего вопроса, мне представляется разумным включить статус в колонке. Вы можете уйти
DocumentStatusLogs
как журнал, но по-прежнему хранить последнюю информацию в главной таблице.кстати, если у вас уже есть
DateCreated
столбец в таблице документов вы можете просто присоединитьсяDocumentStatusLogs
используя это (покаDateCreated
- Это уникальный вDocumentStatusLogs
).Edit: MsSQL не делает поддержка использования, поэтому измените его на:
ON DocumentStatusLogs.DocumentID = max_date.DocumentID AND DocumentStatusLogs.DateCreated = max_date.DateCreated
Если вы беспокоитесь о производительности, вы также можете сделать это с максимальной():
SELECT * FROM DocumentStatusLogs D WHERE DateCreated = (SELECT MAX(DateCreated) FROM DocumentStatusLogs WHERE ID = D.ID)
ROW_NUMBER () требует сортировки всех строк в операторе SELECT, в то время как MAX-нет. Должно резко ускорить ваш запрос.
Это довольно старая тема, но я думал, что брошу свои два цента точно так же, как принятый ответ не работал особенно хорошо для меня. Я попробовал решение gbn на большом наборе данных и обнаружил, что оно ужасно медленное (>45 секунд на 5 миллионах плюс записи в SQL Server 2012). Глядя на план выполнения, очевидно, что проблема заключается в том, что он требует операции сортировки, которая значительно замедляет работу.
вот альтернатива, которую я поднял из сущности платформа, которая не требует операции сортировки и выполняет некластеризованный Поиск индекса. Это сокращает время выполнения до
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]
теперь я предполагаю, что что-то не совсем указано в исходном вопросе, но если ваш дизайн таблицы таков, что ваш столбец ID является идентификатором автоматического приращения, а DateCreated установлен на текущую дату с каждой вставкой, то даже без запуска с моим запросом выше вы могли бы получить значительный размер повышение производительности до решения gbn (около половины времени выполнения) только от заказ на ID вместо того, чтобы заказ на выполните команду поскольку это обеспечит идентичный порядок сортировки, и это более быстрая сортировка.
Я знаю, что это старый нить, но
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)
подробнее о верхнем предложении можно найти здесь.
мой код, чтобы выбрать топ-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 )
Это один из самых легко найденных вопросов по теме, поэтому я хотел дать современный ответ на него (как для моей справки, так и для помощи другим). С помощью 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 и выше. Первое значение можно рассматривать как способ выполнения select top 1 при использовании предложения over. Over позволяет группировать в списке выбора, поэтому вместо написания вложенных подзапросов (как и многие из них существующие ответы делают), это делает его более читаемым способом. Надеюсь, это поможет.
проверка удивительный и правильный ответ Клинта сверху:
производительность между двумя приведенными ниже запросами интересна. 52% - самый высокий. И 48% - второй. 4% улучшение производительности с использованием DISTINCT вместо ORDER BY. Но ORDER BY имеет преимущество для сортировки по нескольким столбцам.
IF (OBJECT_ID('tempdb..#DocumentStatusLogs') IS NOT NULL) BEGIN DROP TABLE #DocumentStatusLogs END CREATE TABLE #DocumentStatusLogs ( [ID] int NOT NULL, [DocumentID] int NOT NULL, [Status] varchar(20), [DateCreated] datetime ) INSERT INTO #DocumentStatusLogs([ID], [DocumentID], [Status], [DateCreated]) VALUES (2, 1, 'S1', '7/29/2011 1:00:00') INSERT INTO #DocumentStatusLogs([ID], [DocumentID], [Status], [DateCreated]) VALUES (3, 1, 'S2', '7/30/2011 2:00:00') INSERT INTO #DocumentStatusLogs([ID], [DocumentID], [Status], [DateCreated]) VALUES (6, 1, 'S1', '8/02/2011 3:00:00') INSERT INTO #DocumentStatusLogs([ID], [DocumentID], [Status], [DateCreated]) VALUES (1, 2, 'S1', '7/28/2011 4:00:00') INSERT INTO #DocumentStatusLogs([ID], [DocumentID], [Status], [DateCreated]) VALUES (4, 2, 'S2', '7/30/2011 5:00:00') INSERT INTO #DocumentStatusLogs([ID], [DocumentID], [Status], [DateCreated]) VALUES (5, 2, 'S3', '8/01/2011 6:00:00') INSERT INTO #DocumentStatusLogs([ID], [DocumentID], [Status], [DateCreated]) VALUES (6, 3, 'S1', '8/02/2011 7:00:00')
Вариант 1:
SELECT [Extent1].[ID], [Extent1].[DocumentID], [Extent1].[Status], [Extent1].[DateCreated] FROM #DocumentStatusLogs AS [Extent1] OUTER APPLY ( SELECT TOP 1 [Extent2].[ID], [Extent2].[DocumentID], [Extent2].[Status], [Extent2].[DateCreated] FROM #DocumentStatusLogs AS [Extent2] WHERE [Extent1].[DocumentID] = [Extent2].[DocumentID] ORDER BY [Extent2].[DateCreated] DESC, [Extent2].[ID] DESC ) AS [Project2] WHERE ([Project2].[ID] IS NULL OR [Project2].[ID] = [Extent1].[ID])
Вариант 2:
SELECT [Limit1].[DocumentID] AS [ID], [Limit1].[DocumentID] AS [DocumentID], [Limit1].[Status] AS [Status], [Limit1].[DateCreated] AS [DateCreated] FROM ( SELECT DISTINCT [Extent1].[DocumentID] AS [DocumentID] FROM #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 #DocumentStatusLogs AS [Extent2] WHERE [Distinct1].[DocumentID] = [Extent2].[DocumentID] ) AS [Project2] ORDER BY [Project2].[ID] DESC ) AS [Limit1]
M$'S Management Studio: после выделения и запуска первого блок, выделите как вариант 1, так и Вариант 2, щелкните правой кнопкой мыши -> [отобразить расчетный план выполнения]. Затем запустите все это, чтобы увидеть результаты.
Вариант 1 Результаты:
ID DocumentID Status DateCreated 6 1 S1 8/2/11 3:00 5 2 S3 8/1/11 6:00 6 3 S1 8/2/11 7:00
Вариант 2 Результаты:
ID DocumentID Status DateCreated 6 1 S1 8/2/11 3:00 5 2 S3 8/1/11 6:00 6 3 S1 8/2/11 7:00
Примечание:
Я обычно использую APPLY, когда я хочу, чтобы соединение было 1-к-(1 из многих).
Я использую соединение, если я хочу, чтобы соединение было 1-ко-многим или много-ко-многим.
Я избегаю CTE с ROW_NUMBER () если мне не нужно чтобы сделать что-то продвинутое, и я в порядке с штрафом за производительность окна.
Я также избегаю EXISTS / IN подзапросов в предложении WHERE или ON, поскольку я испытал это, вызывая некоторые ужасные планы выполнения. Но пробег варьируется. Просмотрите план выполнения и производительность профиля, где и когда это необходимо!
в сценариях, где вы хотите избежать использования row_count (), вы также можете использовать левое соединение:
select ds.DocumentID, ds.Status, ds.DateCreated from DocumentStatusLogs ds left join DocumentStatusLogs filter ON ds.DocumentID = filter.DocumentID -- Match any row that has another row that was created after it. AND ds.DateCreated < filter.DateCreated -- then filter out any rows that matched where filter.DocumentID is null
для примера схемы вы также можете использовать "не в подзапросе", который обычно компилируется в тот же вывод, что и левое соединение:
select ds.DocumentID, ds.Status, ds.DateCreated from DocumentStatusLogs ds WHERE ds.ID NOT IN ( SELECT filter.ID FROM DocumentStatusLogs filter WHERE ds.DocumentID = filter.DocumentID AND ds.DateCreated < filter.DateCreated)
обратите внимание, что шаблон подзапроса не будет работать, если в таблице нет хотя бы одного одностолбцового уникального ключа/ограничения/индекса, в данном случае первичного ключа "Id".
оба этих запроса, как правило, больше "дорого", чем запрос row_count () (как измеряется анализатором запросов). Однако вы можете столкнуться со сценариями, в которых они быстрее возвращают результаты или включают другие оптимизации.
попробуйте это:
SELECT [DocumentID], [tmpRez].value('/x[2]','varchar(20)') as [Status], [tmpRez].value('/x[3]','datetime') as [DateCreated] FROM ( SELECT [DocumentID], cast('<x>'+max(cast([ID] as varchar(10))+'</x><x>'+[Status]+'</x><x>' +cast([DateCreated] as varchar(20)))+'</x>' as XML) as [tmpRez] FROM DocumentStatusLogs GROUP by DocumentID) as [tmpQry]
SELECT o.* FROM `DocumentStatusLogs` o LEFT JOIN `DocumentStatusLogs` b ON o.DocumentID = b.DocumentID AND o.DateCreated < b.DateCreated WHERE b.DocumentID is NULL ;
Если вы хотите вернуть только недавний порядок документов по датам, он вернет только верхний 1 документ по DocumentID
Это самый ванильный TSQL, который я могу придумать
SELECT * FROM DocumentStatusLogs D1 JOIN ( SELECT DocumentID,MAX(DateCreated) AS MaxDate FROM DocumentStatusLogs GROUP BY DocumentID ) D2 ON D2.DocumentID=D1.DocumentID AND D2.MaxDate=D1.DateCreated
в SQLite проверяется, что вы можете использовать следующий простой запрос с ГРУППЫ ПО
SELECT MAX(DateCreated), * FROM DocumentStatusLogs GROUP BY DocumentID
здесь макс помогите получить максимум DateCreated от каждой группы.
но кажется, что MYSQL не связывает * - столбцы со значением max DateCreated : (