Практика SQL Server

Последняя строка клиента: совпадения и планы запросов

Сравните ROW_NUMBER и OUTER APPLY, сохраните клиентов без событий и задайте однозначное правило разрешения одинакового времени.

Последняя запись клиента кажется простой задачей для MAX. Но максимальное время не определяет единственную полную строку при совпадениях. Обратное соединение по времени может вернуть несколько заказов. Независимые MAX для остальных полей способны собрать значения, которые никогда не принадлежали одному заказу.

Однозначное определение последней строки

Пример сортирует OccurredAt по убыванию, затем OrderId по убыванию для разрешения совпадений. Второй уникальный критерий даёт одну строку при наличии заказов. Большая identity здесь лишь разрешает равенство: она не доказывает более позднее бизнес-время или фиксацию транзакции.

CREATE TABLE #Customers(CustomerId int PRIMARY KEY);
INSERT #Customers VALUES(1),(2),(3);
CREATE TABLE #Orders(OrderId int PRIMARY KEY, CustomerId int NOT NULL,
 OccurredAt datetime2(0) NOT NULL, Amount decimal(10,2) NOT NULL);
INSERT #Orders VALUES(11,1,'20230101',10),(12,1,'20230101',20),
(13,2,'20230102',30);
CREATE INDEX IX_Latest ON #Orders(CustomerId,OccurredAt DESC,OrderId DESC)
INCLUDE(Amount);
;WITH ranked AS
(SELECT *, ROW_NUMBER() OVER(PARTITION BY CustomerId
 ORDER BY OccurredAt DESC,OrderId DESC) AS rn FROM #Orders)
SELECT c.CustomerId,r.OrderId,r.OccurredAt,r.Amount
FROM #Customers AS c LEFT JOIN ranked AS r
ON r.CustomerId=c.CustomerId AND r.rn=1
ORDER BY c.CustomerId;

У клиента 1 два заказа с одинаковым временем, поэтому выигрывает OrderId 12. У клиента 2 один заказ, у клиента 3 заказов нет. LEFT JOIN сохраняет клиента 3 с NULL в полях заказа. Условие rn = 1 должно оставаться в соединении. Перенос в WHERE удалит клиента без совпадения.

Если нужны все заказы с последним временем, примените RANK или DENSE_RANK только по времени и допустите несколько строк. Добавление OrderId устранит бизнес-равенство. Сначала определите, нужен один представитель или все одинаково последние события.

Сравнение способов доступа

ROW_NUMBER удобен для широкого набора клиентов, поскольку позволяет совместно обработать поток заказов. OUTER APPLY выражает отдельный поиск клиента, который при подходящем индексе может остановиться на первом результате.

SELECT c.CustomerId,o.OrderId,o.OccurredAt,o.Amount
FROM #Customers AS c
OUTER APPLY
(SELECT TOP(1) OrderId,OccurredAt,Amount FROM #Orders AS o
 WHERE o.CustomerId=c.CustomerId
 ORDER BY OccurredAt DESC,OrderId DESC) AS o
ORDER BY c.CustomerId;

Универсально быстрого варианта нет. Несколько клиентов часто выгодно искать отдельными обращениями; почти всех клиентов может быть дешевле обработать совместно. Оптимизатор преобразует планы. Смотрите фактическое выполнение, логические чтения и оценки, а не выводите физический алгоритм из синтаксиса.

Индекс начинается с CustomerId, затем соответствует требуемой убывающей сортировке; Amount включён для покрытия. Это может убрать сортировку и дополнительные обращения. Однако дополнительные поля увеличивают размер и стоимость записи. Без подходящего индекса APPLY способен повторять большие сканирования для каждого клиента.

Проверка бизнес-смысла

Последний успешный заказ отличается от последнего заказа при условии его успешности. Фильтрация успеха до ранжирования выбирает последний успех. Ранжирование всего потока с последующей фильтрацией может ничего не вернуть, если самое новое событие завершилось ошибкой. Обе трактовки допустимы, но отвечают разным вопросам.

Отчёт на момент времени должен ограничивать кандидатов. Примените условие OccurredAt меньше исключающей границы до выбора победителя. Если несколько команд должны видеть одно состояние базы, определите изоляцию: независимые чтения могут наблюдать изменения между шагами.

Проверьте отсутствие заказов, совпадение времени, одного очень крупного клиента, множество небольших и обе трактовки статуса. Сопоставьте количество клиентов и принадлежность Amount возвращённому OrderId. Добавьте событие точно на временной границе и убедитесь, что оно исключено. Для сравнения производительности используйте одинаковые параметры и бизнес-правила. Отдельно проверьте распределение данных: быстрый план на одном клиенте не доказывает эффективность полного отчёта. Если время допускает NULL, определите, считается ли такая запись кандидатом вообще, вместо неявного доверия порядку сортировки. Более быстрый запрос, выбравший другой заказ, не является оптимизацией исходного требования.

Для опоздавших событий дополнительно решите, что определяет новизну: время самого события или момент его получения. Если различие важно, сохраняйте обе отметки. Иначе запоздавшая загрузка способна неожиданно изменить последнюю запись отчёта. Зафиксируйте это правило в тестовых данных и проверьте повторный запуск после такой загрузки.

Техническая документация: Microsoft Learn: ROW_NUMBER · Microsoft Learn: TOP.

Вопрос по статье

Есть вопрос по этой теме?

Расскажите, что вы оцениваете или с какой проблемой столкнулись. Мы ответим с практической рекомендацией.

Inquiries are not enabled in this preview.

Задать вопрос по этой статье