/ / Отримати верхній 1 рядок кожної групи - sql, tsql, sql-server-2005, group-by, greatest-n-per-group

Отримайте перший рядок кожної групи - sql, tsql, sql-сервер-2005, group-by, great-n-per-group

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

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            |

UPDATE Я щойно дізнався, як використовувати "застосовувати", що полегшує вирішення таких проблем.

Відповіді:

550 для відповіді №1
;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 місцях
  • зберегти історію статусу
  • ...

У такому вигляді ви зберігаєте історію статусів.Якщо ви також хочете отримати останній статус у батьківській таблиці (денормалізація), вам потрібен тригер, щоб підтримувати "статус" у батьківській. Або скиньте цю таблицю історії стану.


116 за відповідь № 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

39 за відповідь № 3

Я зробив кілька синхронізацій над різнимирекомендацій, і результати дійсно залежать від розміру задіяної таблиці, але найбільш послідовним рішенням є використання 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

Я думаю, що справді дивовижно було те, наскільки послідовним був час КРЕСТНОГО ЗАЯВКИ незалежно від кількості задіяних рядків.


24 за відповідь № 4
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 не підтримує USING, тому змініть його на:

ON DocumentStatusLogs.DocumentID = max_date.DocumentID AND DocumentStatusLogs.DateCreated = max_date.DateCreated

19 за відповідь № 5

Якщо вас турбує продуктивність, ви також можете зробити це з MAX ():

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

ROW_NUMBER () вимагає сортування всіх рядків у вашому операторі SELECT, тоді як MAX - ні. Повинно різко прискорити ваш запит.


9 для відповіді № 6

Це досить стара тема, але я думав, що "дкинути мої два центи точно так само, як прийнята відповідь не спрацювала для мене особливо добре. Я спробував рішення gbn на великому наборі даних і виявив, що це надзвичайно повільно (> 45 секунд для 5 мільйонів записів у SQL Server 2012). Дивлячись на план виконання, очевидно, що проблема полягає в тому, що для цього потрібна операція SORT, яка значно уповільнює ситуацію.

Ось альтернатива, яку я підняв зсутність, яка не потребує операції SORT та виконує пошук без кластеризованого індексу. Це зменшує час виконання до <2 секунд на згаданому наборі записів.

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 за відповіддю № 7

Я знаю, що це стара тема, але 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 можна знайти тут.


5 для відповіді № 8

Мій код для вибору першого з кожної групи

виберіть a. * із #DocumentStatusПовідомляє a де дата створена у (виберіть перші 1 дату, створену з #DocumentStatusLogs b де a.documentid = b.documentid упорядкувати за датою створено опис )

2 для відповіді № 9

Перевірка чудової та правильної відповіді Клінта зверху:

Ефективність між двома наведеними нижче запитами:цікаво. 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 $ ":Після виділення та запуску першого блоку виділіть як варіант 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 з багатьох).

Я використовую JOIN, якщо хочу, щоб об’єднання було 1-до-багатьох або багато-до-багатьох.

Я уникаю CTE за допомогою ROW_NUMBER (), якщо мені не потрібно робити щось просунуте, і я в порядку зі штрафом за продуктивність вікон.

Я також уникаю підзапитів EXISTS / IN у WHEREабо клавіша ON, оскільки я переживав це, спричиняючи жахливі плани виконання. Але пробіг варіюється. Перегляньте план виконання та ефективність профілю, де і коли це потрібно!


2 для відповіді № 10

Це одне з найбільш легко знайдених питаньна тему, тому я хотів дати сучасну відповідь на неї (як для ознайомлення, так і для допомоги іншим). Використовуючи значення over і first, ви можете коротко опрацювати наведений вище запит:

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 і новіших версій.Перше значення можна розглядати як спосіб виконати вибір першого 1 при використанні речення over. Over дозволяє групувати у списку вибору, тож замість того, щоб писати вкладені підзапити (як це роблять багато існуючих відповідей), це робить це більш читабельним способом. Сподіваюся, це допомагає.


0 для відповіді № 11

У сценаріях, коли ви хочете уникати використання 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 () (як вимірюється за допомогою Query Analyzer). Однак ви можете зіткнутися зі сценаріями, коли вони швидше повертають результати або вмикають інші оптимізації.


0 для відповіді № 12

Спробуйте це:

        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]

0 для відповіді № 13
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


-1 за відповідь № 14

Це найбільш ванільний 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

-2 для відповіді № 15

У SQLite перевірено, що ви можете використовувати наступний простий запит за допомогою GROUP BY

SELECT MAX(DateCreated), *
FROM DocumentStatusLogs
GROUP BY DocumentID

Ось тут MAX допомогти отримати максимум Дата Створена ІЗ кожної групи.

Але, схоже, MYSQL не асоціює * -колонки зі значенням max DateCreated :(