7

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

2

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

Таблица DocumentStatusLogs | ID | DocumentID | Status | DateCreated | |----|------------|--------|-------------| | 2 | 1 | S1 | 29.07.2011 | | 3 | 1 | S2 | 30.07.2011 | | 6 | 1 | S1 | 02.08.2011 | | 1 | 2 | S1 | 28.07.2011 | | 4 | 2 | S2 | 30.07.2011 | | 5 | 2 | S3 | 01.08.2011 | | 6 | 3 | S1 | 02.08.2011 |

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

Предпочитаемый вывод: | DocumentID | Status | DateCreated | |------------|--------|-------------| | 1 | S1 | 02.08.2011 | | 2 | S3 | 01.08.2011 | | 3 | S1 | 02.08.2011 |

Вопросы:

  1. Существует ли агрегатная функция, которая позволит получить только верхнюю запись из каждой группы? Например, как в псевдокоде GetOnlyTheTop ниже:
    SELECT
        DocumentID,
        GetOnlyTheTop(Status),
        GetOnlyTheTop(DateCreated)
    FROM DocumentStatusLogs
    GROUP BY DocumentID
    ORDER BY DateCreated DESC
    
  2. Если такой функции не существует, есть ли способ получить желаемый вывод?
  3. Может ли это быть связано с ненормализованной базой данных? Я думаю, что так как мне нужна всего одна запись, не должно ли поле 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 |

Обновление Я только что научился использовать "apply", что упрощает решение подобных задач.

5 ответ(ов)

9

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

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

  • хотите ли вы поддерживать статус в двух местах?
  • хотите ли вы сохранять историю статусов?
  • ...

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

2

Вы можете использовать cross apply для получения последнего статуса документа из таблицы DocumentStatusLogs. Вот как это сделать в вашем случае:

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

В этом запросе CROSS APPLY позволяет для каждой строки из таблицы Documents выполнить подзапрос, который возвращает последний статус документа на основе даты создания (DateCreated). Подзапрос выбирает верхнюю запись (т.е. самую последнюю) из DocumentStatusLogs, используя сортировку по DateCreated в порядке убывания. Таким образом, вы получаете актуальную информацию о статусах документов.

0

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

Запрос                                   Время для 6500 (мс)    Время для 137M (мс)

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 независимо от количества вовлеченных строк.

0

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

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

Использование ROW_NUMBER() требует сортировки всех строк в вашем SELECT запросе, тогда как MAX() этого не требует. Это должно значительно ускорить выполнение вашего запроса.

0

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

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 2012 и выше. Функцию first_value можно рассматривать как способ реализации SELECT TOP 1, когда используется конструкция over. Over позволяет группировать результаты в списке select, поэтому вместо написания вложенных подзапросов (как это делают многие существующие ответы), такой подход делает код более читаемым.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь