Получить 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 ]

2 голосов
/ 29 октября 2015

Проверка Клинта удивительным и правильным ответом сверху:

Производительность между двумя запросами ниже интересна. 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 $ 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 из многих).

Я использую JOIN, если хочу, чтобы соединение было 1-ко-многим или многие-ко-многим.

Я избегаю CTE с помощью ROW_NUMBER (), если только мне не нужно делать что-то более сложное, и я не согласен с потерей производительности окна.

Я также избегаю подзапросов EXISTS / IN в предложении WHERE или ON, так как я столкнулся с некоторыми ужасными планами выполнения. Но пробег меняется. Просмотрите план выполнения и профиль производительности, где и когда это необходимо!

0 голосов
/ 17 июня 2019

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

Подход 1 : Использование ROW_NUMBER ().Если индекс хранилища строк не может повысить производительность, вы можете попробовать некластеризованный / кластеризованный индекс columnstore, как для запросов с агрегацией и группировкой, так и для таблиц, которые всегда упорядочены по разным столбцам, индекс columnstore обычно является лучшим выбором.

;WITH CTE AS
    (
       SELECT   *,
                RN = ROW_NUMBER() OVER (PARTITION BY DocumentID ORDER BY DateCreated DESC)
       FROM     DocumentStatusLogs
    )
    SELECT  ID      
        ,DocumentID 
        ,Status     
        ,DateCreated
    FROM    CTE
    WHERE   RN = 1;

Подход 2 : Использование FIRST_VALUE.Если индекс хранилища строк не может повысить производительность, вы можете попробовать некластеризованный / кластеризованный индекс columnstore, как для запросов с агрегацией и группировкой, так и для таблиц, которые всегда упорядочены по разным столбцам, индекс columnstore обычно является лучшим выбором.

SELECT  DISTINCT
    ID      = FIRST_VALUE(ID) OVER (PARTITION BY DocumentID ORDER BY DateCreated DESC)
    ,DocumentID
    ,Status     = FIRST_VALUE(Status) OVER (PARTITION BY DocumentID ORDER BY DateCreated DESC)
    ,DateCreated    = FIRST_VALUE(DateCreated) OVER (PARTITION BY DocumentID ORDER BY DateCreated DESC)
FROM    DocumentStatusLogs;

Подход 3 : Использование CROSS APPLY.Создание индекса хранилища строк в таблице DocumentStatusLogs, охватывающей столбцы, используемые в запросе, должно быть достаточным для покрытия запроса без необходимости в индексе columnstore.

SELECT  DISTINCT
    ID      = CA.ID
    ,DocumentID = D.DocumentID
    ,Status     = CA.Status 
    ,DateCreated    = CA.DateCreated
FROM    DocumentStatusLogs D
    CROSS APPLY (
            SELECT  TOP 1 I.*
            FROM    DocumentStatusLogs I
            WHERE   I.DocumentID = D.DocumentID
            ORDER   BY I.DateCreated DESC
            ) CA;
0 голосов
/ 16 октября 2018
SELECT doc_id,status,date_created FROM (
SELECT a.*,Row_Number() OVER(PARTITION BY doc_id ORDER BY date_created DESC ) AS rnk FROM doc a)
WHERE rnk=1;
0 голосов
/ 05 ноября 2016

Попробуйте это:

        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 голосов
/ 30 июля 2015

Это самый ванильный 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
0 голосов
/ 22 января 2014

В SQLite проверено, что вы можете использовать следующий простой запрос с GROUP BY

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

Здесь MAX помогают получить максимум DateCreated ОТ каждой группы.

Но, похоже, MYSQL не связывает * -колонки со значением max DateCreated: (

0 голосов
/ 05 сентября 2012

В сценариях, где вы хотите избежать использования 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)

Обратите внимание, шаблон подзапроса не будет работать, если в таблице не будет хотя бы одного уникального ключа / ограничения / индекса из одного столбца, в этомcase первичный ключ "Id".

Оба эти запроса, как правило, более "дорогие", чем запрос row_count () (по данным Query Analyzer).Однако вы можете столкнуться со сценариями, в которых они возвращают результаты быстрее или включают другие способы оптимизации.

...