Получить первую строку каждой группы

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

DocumentStatusLogs Table

|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            |

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

+446
27 июл. '11 в 8:41
источник поделиться
17 ответов
;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-х местах.
  • сохранить историю состояния
  • ...

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

+641
27 июл. '11 в 8:44
источник

Связанные вопросы


Похожие вопросы

Я только что научился использовать 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
+144
30 авг. '12 в 6:10
источник

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

+47
07 мар. '15 в 14:57
источник
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
+26
27 июл. '11 в 8:44
источник

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

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

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

+23
15 янв. '13 в 20:57
источник

Я знаю, что это старая ветка, но решения 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 можно узнать здесь.

+23
24 янв. '18 в 0:14
источник

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

+9
03 июн. '14 в 8:34
источник

Это один из наиболее легко обнаруживаемых вопросов по этой теме, поэтому я хотел дать современный ответ на него (как для справки, так и для помощи другим). Используя 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 позволяет группировать в списке выбора, поэтому вместо написания вложенных подзапросов (как это делают многие из существующих ответов) это делается более читабельно. Надеюсь это поможет.

+6
18 янв. '18 в 0:55
источник

Мой код, чтобы выбрать топ 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
)
+5
23 сент. '12 в 11:22
источник
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

+2
19 дек. '16 в 15:10
источник

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

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

Я использую JOIN, если я хочу, чтобы соединение было 1-to-many или many-to-many.

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

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

+2
28 окт. '15 в 22:10
источник

Попробуйте следующее:

        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
05 нояб. '16 в 11:57
источник

В сценариях, в которых вы хотите избежать использования 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
04 сент. '12 в 20:47
источник
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
16 окт. '18 в 8:40
источник

Вот 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
17 июн. '19 в 5:18
источник

Это самый ванильный 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
-1
30 июл. '15 в 12:25
источник

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

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

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

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

-2
22 янв. '14 в 18:07
источник

Посмотрите другие вопросы по меткам или Задайте вопрос