Última linha por cliente: empates e planos eficientes
Compare ROW_NUMBER e OUTER APPLY, preserve clientes sem eventos e defina uma regra determinística para desempate.
A última ordem de cada cliente parece exigir apenas MAX. MAX encontra o maior horário, mas não uma linha completa única quando existem empates. Um join posterior pode retornar várias ordens. MAX independentes para outras colunas podem combinar valores que nunca pertenceram ao mesmo registro.
Definir a escolha
O exemplo ordena OccurredAt e depois OrderId de forma decrescente. O segundo critério único desempata e escolhe uma linha quando há ordens. Uma identidade maior é somente desempate; não prova horário de negócio posterior nem ordem de commit.
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;
O cliente 1 tem duas ordens simultâneas, então OrderId 12 vence. O cliente 2 possui uma e o 3 nenhuma. LEFT JOIN mantém o cliente 3 com campos NULL. rn = 1 deve permanecer na condição do join; movê-lo para WHERE removeria o cliente sem correspondência.
Se precisa de todas as ordens empatadas no último horário, use RANK ou DENSE_RANK somente pelo horário e aceite várias linhas. Adicionar OrderId desfaria o empate de negócio. Defina primeiro se deseja um representante ou todos os eventos igualmente recentes.
Comparar caminhos de acesso
ROW_NUMBER é conveniente para conjuntos amplos porque permite processar muitas ordens juntas. OUTER APPLY expressa uma busca por cliente que pode parar na primeira linha com um índice adequado.
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;
Nenhuma formulação é sempre superior. Poucos clientes podem favorecer buscas direcionadas; quase todos podem favorecer processamento conjunto. O otimizador transforma planos. Examine execução real, leituras lógicas e estimativas em vez de deduzir o caminho pelo texto SQL.
O índice começa por CustomerId, segue a ordem decrescente solicitada e inclui Amount para cobertura. Isso pode evitar sort ou lookup adicional. Mais colunas também aumentam armazenamento e custo de escrita. Sem índice adequado, APPLY pode repetir grandes scans para cada cliente.
Validar o significado
A última ordem bem-sucedida não equivale à última ordem caso ela seja bem-sucedida. Filtrar sucesso antes do ranking encontra o sucesso mais recente. Classificar tudo e filtrar depois pode retornar nada quando o último evento falhou. As duas interpretações são plausíveis, mas respondem perguntas diferentes.
Um relatório em determinada data precisa aplicar o limite aos candidatos. Filtre OccurredAt menor que a fronteira exclusiva antes de escolher o vencedor. Quando várias instruções precisam compartilhar uma visão, defina isolamento apropriado: consultas independentes podem observar mudanças entre etapas.
Teste clientes sem ordens, horários empatados, um cliente enorme, muitos pequenos e ambas as regras de estado. Confira a quantidade de clientes e se cada Amount pertence ao OrderId apresentado. Compare planos com parâmetros e semântica equivalentes. Inclua ainda um evento exatamente na fronteira temporal para confirmar sua exclusão. Uma consulta mais rápida que seleciona outra ordem não melhora o contrato original do relatório.
Para eventos atrasados, determine também se vale o horário do evento ou o horário de recebimento. Preserve ambos quando a diferença importar. Caso contrário, uma importação tardia pode alterar inesperadamente o último resultado apresentado ao cliente.
Referências técnicas: Microsoft Learn: ROW_NUMBER · Microsoft Learn: TOP.