Получить первую строку каждой группы
У меня есть таблица, из которой я хочу получить последние записи для каждой группы. Вот структура этой таблицы:
Таблица 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 |
Вопросы:
- Существует ли агрегатная функция, которая позволит получить только верхнюю запись из каждой группы? Например, как в псевдокоде
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 |
Обновление Я только что научился использовать "apply", что упрощает решение подобных задач.
5 ответ(ов)
Если вы ожидаете получить 2 записи за день, то данный запрос будет отбирать одну из них произвольно. Чтобы получить обе записи за день, вместо ROW_NUMBER используйте DENSE_RANK.
Что касается нормализации, то это зависит от ваших целей:
- хотите ли вы поддерживать статус в двух местах?
- хотите ли вы сохранять историю статусов?
- ...
На текущий момент вы сохраняете историю статусов. Если вам нужно также получить последний статус в родительской таблице (что является денормализацией), вам потребуется триггер для поддержания поля "status" в родительской таблице, или же стоит отказаться от таблицы истории статусов.
Вы можете использовать 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
в порядке убывания. Таким образом, вы получаете актуальную информацию о статусах документов.
Я провел некоторые замеры по различным рекомендациям, и результаты действительно зависят от размера таблицы, однако самым стабильным решением оказалось использование 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 независимо от количества вовлеченных строк.
Если вас беспокоит производительность, вы также можете сделать это с помощью MAX()
:
SELECT *
FROM DocumentStatusLogs D
WHERE DateCreated = (SELECT MAX(DateCreated) FROM DocumentStatusLogs WHERE ID = D.ID)
Использование ROW_NUMBER()
требует сортировки всех строк в вашем SELECT
запросе, тогда как MAX()
этого не требует. Это должно значительно ускорить выполнение вашего запроса.
Это один из самых часто задаваемых вопросов на эту тему, поэтому я хотел бы дать современный ответ (как для себя на будущее, так и чтобы помочь другим). Используя функции 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, поэтому вместо написания вложенных подзапросов (как это делают многие существующие ответы), такой подход делает код более читаемым.
Как выполнить UPDATE с использованием SELECT в SQL Server?
"Вставка результатов хранимой процедуры в временную таблицу"
Как экранировать одинарную кавычку в SQL Server?
Как выполнить оператор UPDATE с JOIN в SQL Server?
Как вывести все внешние ключи, ссылающиеся на заданную таблицу в SQL Server?