Paginação no SQL Server sem lentidão nas páginas distantes
Implemente paginação confiável no SQL Server com ordenação única, índice adequado e regras explícitas para cursores e alterações simultâneas.
Um cliente abre o histórico de pedidos. A primeira página aparece imediatamente, mas a página 8.000 demora vários segundos. A aplicação continua devolvendo apenas 25 linhas. Adicionar servidores talvez não resolva: OFFSET exige que o SQL Server percorra as entradas anteriores antes de entregar o trecho solicitado.
Um índice ordenado pode evitar ordenar a tabela inteira, mas as entradas descartadas ainda representam trabalho. Compare as leituras lógicas de uma página inicial e de outra distante usando os mesmos filtros. Se elas crescem com o deslocamento, revise o contrato de paginação. Isso é diferente de uma consulta lenta já na primeira página por ordenar uma expressão sem índice apropriado.
Definir uma fronteira inequívoca
Na paginação por chave, a próxima chamada recebe os últimos valores de ordenação da resposta anterior. Para pedidos do mais recente ao mais antigo, ela busca datas anteriores e identificadores menores quando a data é igual. A segunda condição é indispensável. Um horário raramente é único; excluir todas as linhas daquele horário pode fazer pedidos desaparecerem da navegação.
O exemplo usa somente uma tabela temporária. A primeira consulta retorna 105 e 104; a segunda deve retornar 103 e 102. Assim, a condição de fronteira pode ser examinada antes de aplicar a técnica em milhões de registros.
CREATE TABLE #Orders
(
OrderId bigint NOT NULL PRIMARY KEY,
CreatedAt datetime2(3) NOT NULL,
Amount decimal(12,2) NOT NULL
);
INSERT #Orders VALUES
(105,'2022-01-10T09:00:00',25),
(104,'2022-01-10T09:00:00',40),
(103,'2022-01-09T12:00:00',15),
(102,'2022-01-08T08:00:00',80),
(101,'2022-01-07T08:00:00',30);
CREATE INDEX IX_Orders_Page
ON #Orders(CreatedAt DESC, OrderId DESC)
INCLUDE(Amount);
SELECT TOP (2) OrderId, CreatedAt, Amount
FROM #Orders
ORDER BY CreatedAt DESC, OrderId DESC;
DECLARE @LastTime datetime2(3) = '2022-01-10T09:00:00';
DECLARE @LastId bigint = 104;
SELECT TOP (2) OrderId, CreatedAt, Amount
FROM #Orders
WHERE CreatedAt < @LastTime
OR (CreatedAt = @LastTime AND OrderId < @LastId)
ORDER BY CreatedAt DESC, OrderId DESC;
DROP TABLE #Orders;
O índice usa as colunas de navegação como chave e inclui Amount para atender à resposta. Em uma aplicação com vários clientes, TenantId, CreatedAt, OrderId costuma ser um início adequado quando TenantId está fixado em um valor. Uma tela global, com todos os clientes, apresenta outro padrão de acesso. Não presuma que o mesmo índice atende igualmente aos dois.
Tratar o cursor como contrato da API
Preserve a precisão exata do datetime2 e do identificador. Arredondar o horário em um objeto de data do cliente pode deslocar a fronteira. Vincule também cliente, filtros, direção de ordenação e versão do formato. Um cursor criado para pedidos pendentes não deve ser reutilizado silenciosamente para todos os pedidos. Uma assinatura detecta adulteração, mas a autorização ainda precisa ser aplicada em cada chamada.
Use uma consulta sem fronteira para a primeira página. Uma condição opcional como “cursor nulo OU ...” pode produzir um plano reutilizável menos seletivo. Compare os planos reais. O próprio OR da comparação lexicográfica pode aparecer parcialmente como filtro residual. Verifique linhas lidas e leituras lógicas; um operador Index Seek não garante sozinho uma navegação eficiente.
Busque uma linha adicional para descobrir se há outra página, mas entregue apenas a quantidade combinada. O próximo cursor deve vir da última linha efetivamente entregue. Usar a linha adicional com uma comparação estritamente menor faria essa linha ser ignorada. Um COUNT exato de todo o resultado pode custar mais do que a própria página. Muitos históricos não precisam recalcular esse total em toda requisição.
Definir o efeito das alterações concorrentes
Pedidos novos inseridos antes da fronteira normalmente não deslocam a próxima página. Alterar valores usados na ordenação pode mover linhas através da fronteira e causar duplicações ou omissões. Prefira colunas de ordenação imutáveis. Para um relatório de auditoria que exige um conjunto congelado, leituras independentes não bastam: considere uma transação snapshot com duração controlada, um resultado materializado ou uma fronteira estável de exportação.
Teste horários repetidos, exclusão da linha de fronteira, uma última página vazia e clientes com volumes muito diferentes. Excluir a linha de fronteira não é um problema quando o cursor guarda seus valores antigos; a consulta não precisa reencontrá-la. Para voltar, inverta a comparação e a ordenação, recupere um conjunto limitado e depois restaure a ordem de exibição.
A abordagem privilegia navegação sequencial previsível. Saltos arbitrários para números de página ainda podem justificar OFFSET ou marcadores pré-calculados. Escolha conforme a interação real e demonstre, com dados representativos, que páginas distantes mantêm leituras limitadas. O resultado desejado combina completude, ordenação correta e consumo estável, não apenas uma primeira tela rápida.
Referências técnicas: Microsoft Learn: ORDER BY · Microsoft Learn: Pagination.