Paginación de SQL Server rápida incluso en páginas profundas
Diseña paginación fiable en SQL Server con orden único, índices adecuados y reglas claras para cursores y cambios concurrentes.
Un cliente abre el historial de pedidos. La primera página aparece inmediatamente, pero la página 8.000 tarda varios segundos. La aplicación sigue devolviendo solamente 25 filas. Aumentar servidores puede no ayudar: OFFSET obliga a SQL Server a pasar por las entradas anteriores antes de entregar el fragmento solicitado.
Un índice ordenado puede evitar ordenar toda la tabla, pero las entradas descartadas también cuestan trabajo. Compara lecturas lógicas de páginas tempranas y tardías con filtros idénticos. Si aumentan con el desplazamiento, revisa el contrato de paginación. Es un problema distinto al de una consulta lenta desde la primera página porque ordena una expresión sin índice adecuado.
Definir un límite inequívoco
La paginación por clave transmite los últimos valores de ordenación de la respuesta anterior. Para pedidos del más reciente al más antiguo, la consulta siguiente busca fechas anteriores y, si la fecha coincide, identificadores menores. Esta segunda condición es fundamental: una fecha rara vez es única y excluir todas las filas con esa fecha puede omitir pedidos.
El ejemplo funciona exclusivamente con una tabla temporal. La primera consulta devuelve 105 y 104; la segunda debe devolver 103 y 102. Así puedes revisar el límite antes de llevar el patrón a millones 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;
El índice sitúa las columnas de navegación en la clave e incluye Amount para resolver la salida. En una aplicación con varios clientes, TenantId, CreatedAt, OrderId suele ser un punto de partida útil si TenantId queda fijado a un valor. Una vista global de todos los clientes tiene otro patrón de acceso. No supongas que un índice servirá igualmente para ambas.
Definir el contrato del cursor
Conserva la precisión exacta del datetime2 y del identificador. Redondear la fecha al convertirla en un objeto del cliente puede desplazar el límite. Vincula también cliente, filtros, dirección y versión del formato. Un cursor creado para pedidos pendientes no debe reutilizarse silenciosamente para todos los pedidos. Firmarlo ayuda a detectar manipulaciones, pero no reemplaza la autorización de cada solicitud.
Usa una consulta sin límite para la primera página. Una condición opcional como «cursor nulo O ...» puede producir un plan reutilizable menos selectivo. Compara los planes reales. Incluso el OR de la condición lexicográfica puede evaluarse parcialmente como filtro residual. Revisa filas leídas y lecturas lógicas; la presencia de un Index Seek no demuestra por sí sola que la navegación sea eficiente.
Solicita una fila adicional para detectar si existe otra página, pero entrega únicamente la cantidad acordada. El siguiente cursor debe usar la última fila realmente entregada. Si utiliza la fila adicional y luego aplica menor estricto, esa fila se perderá. Un COUNT exacto de todo el resultado puede ser más caro que obtener la página. Muchos historiales no necesitan recalcular ese total en cada solicitud.
Acordar qué significan los cambios simultáneos
Los pedidos nuevos que llegan antes del límite normalmente no desplazan la página siguiente. Cambiar valores de ordenación existentes sí puede mover filas a través del límite y producir duplicados u omisiones. Prefiere columnas de ordenación inmutables. Un informe de auditoría que exige un conjunto congelado necesita otra garantía: una transacción snapshot de duración controlada, un resultado materializado o un límite de exportación estable.
Prueba fechas iguales, eliminación de la fila límite, una última página vacía y clientes con volúmenes muy distintos. Eliminar la fila límite no causa problemas si el cursor conserva sus valores anteriores; la consulta no necesita encontrarla. Para retroceder, invierte comparación y orden, recupera un conjunto limitado y luego restablece el orden visual.
Este diseño favorece una navegación secuencial predecible. Los saltos arbitrarios a números de página pueden seguir requiriendo OFFSET o marcadores precalculados. Decide según la interacción real y demuestra con datos representativos que las páginas lejanas mantienen lecturas contenidas. El objetivo combina resultados completos, orden correcto y consumo estable, no simplemente una primera pantalla más rápida.
Referencias técnicas: Microsoft Learn: ORDER BY · Microsoft Learn: Pagination.