Obtener el recuento total de filas de OFFSET / FETCH NEXT

92

Entonces, tengo una función que devuelve una cantidad de registros para los que quiero implementar la paginación en mi sitio web. Se me sugirió que use Offset / Fetch Next en SQL Server 2012 para lograr esto. En nuestro sitio web, tenemos un área que enumera el número total de registros y en qué página se encuentra en ese momento.

Antes, obtenía todo el conjunto de registros y pude construir la paginación en eso programáticamente. Pero usando la forma SQL con FETCH NEXT X ROWS ONLY, solo me devuelven X filas, por lo que no sé cuál es mi conjunto de registros total y cómo calcular mis páginas mínimas y máximas. La única forma en que puedo decir que hago esto es llamando a la función dos veces y haciendo un recuento de filas en la primera, luego ejecutando la segunda con FETCH NEXT. ¿Hay alguna manera mejor que no me permita ejecutar la consulta dos veces? Estoy intentando acelerar el rendimiento, no ralentizarlo.

Azul cristal
fuente

Respuestas:

115

Puede usar COUNT(*) OVER()... aquí hay un ejemplo rápido usando sys.all_objects:

DECLARE 
  @PageSize INT = 10, 
  @PageNum  INT = 1;

SELECT 
  name, object_id, 
  overall_count = COUNT(*) OVER()
FROM sys.all_objects
ORDER BY name
  OFFSET (@PageNum-1)*@PageSize ROWS
  FETCH NEXT @PageSize ROWS ONLY;

Sin embargo, esto debería reservarse para pequeños conjuntos de datos; en conjuntos más grandes, el rendimiento puede ser abismal. Consulte este artículo de Paul White para obtener mejores alternativas , incluido el mantenimiento de vistas indexadas (que solo funciona si el resultado no está filtrado o si conoce las WHEREcláusulas de antemano) y el uso de ROW_NUMBER()trucos.

Aaron Bertrand
fuente
44
En una tabla con 3,500,000 registros, el COUNT (*) OVER () tomó 1 minuto y 3 segundos. El enfoque descrito a continuación por James Moberg tomó 13 segundos para recuperar el mismo conjunto de datos. Estoy seguro de que el enfoque de Count Over funciona bien para conjuntos de datos más pequeños, pero cuando comienza a hacerse realmente grande, se ralentiza considerablemente.
matthew_360
O simplemente podría usar COUNT (1) OVER () que es muchísimo más rápido ya que no tiene que leer los datos reales de la tabla, como hace count (*)
ldx
1
@AaronBertrand ¿De verdad? eso debe significar que tiene un índice que incluye todas las columnas o que se ha mejorado mucho desde 2008R2. En esa versión, el recuento (*) funciona secuencialmente, lo que significa que primero se selecciona * (como en: todas las columnas) y luego se cuenta. Si hizo un recuento (1), simplemente seleccione una constante, que es mucho más rápida que leer los datos reales.
ldx
5
@idx No, eso tampoco funcionó en 2008 R2, lo siento. He estado usando SQL Server desde 6.5 y no recuerdo un momento en el que el motor no fuera lo suficientemente inteligente como para escanear el índice más estrecho para COUNT (*) o COUNT (1). Ciertamente no desde 2000. Pero bueno, tengo una instancia de 2008 R2, ¿puedes configurar una reproducción en SQLfiddle que demuestre esta diferencia que afirmas que existe? Estoy feliz de probarlo.
Aaron Bertrand
2
en una base de datos de SQL Server 2016, buscando en una tabla con aproximadamente 25 millones de filas, paginando alrededor de 3000 resultados (con varias uniones, incluida una función con valores de tabla), esto tomó milisegundos, ¡increíble!
jkerak
142

Encontré algunos problemas de rendimiento con el método COUNT ( ) OVER (). (No estoy seguro de si fue el servidor, ya que tardó 40 segundos en devolver 10 registros y luego no tuve ningún problema). Esta técnica funcionó en todas las condiciones sin tener que usar COUNT ( ) OVER () y logra el la misma cosa:

DECLARE 
    @PageSize INT = 10, 
    @PageNum  INT = 1;

WITH TempResult AS(
    SELECT ID, Name
    FROM Table
), TempCount AS (
    SELECT COUNT(*) AS MaxRows FROM TempResult
)
SELECT *
FROM TempResult, TempCount
ORDER BY TempResult.Name
    OFFSET (@PageNum-1)*@PageSize ROWS
    FETCH NEXT @PageSize ROWS ONLY
James Moberg
fuente
32
Sería realmente increíble si existiera la posibilidad de guardar COUNT (*) valor en una variable. Podría configurarlo como un parámetro de SALIDA de mi procedimiento almacenado. ¿Algunas ideas?
Hacia Ka
1
¿Hay alguna forma de obtener el recuento en una tabla separada? Parece que solo puede usar "TempResult" para la primera instrucción SELECT anterior.
matthew_360
4
¿Por qué esto funciona tan bien? En el primer CTE, se seleccionan todas las filas y luego se reducen mediante la recuperación. Habría adivinado que seleccionar toda la fila en el primer CTE ralentizaría significativamente las cosas. En cualquier caso, ¡gracias por esto!
jbd
1
en mi caso se ralentizó que COUNT (1) OVER () .. tal vez porque una función en la selección.
Tiju John
1
Esto funciona perfectamente para bases de datos pequeñas cuando las filas son millones y lleva demasiado tiempo.
Kiya
1

Basado en la respuesta de James Moberg :

Esta es una alternativa de uso Row_Number(), si no tiene SQL Server 2012 y no puede usar OFFSET

DECLARE 
    @PageNumEnd INT = 10, 
    @PageNum  INT = 1;

WITH TempResult AS(
    SELECT ID, NAME
    FROM Tabla
), TempCount AS (
    SELECT COUNT(*) AS MaxRows FROM TempResult
)

select * 
from
(
    SELECT
     ROW_NUMBER() OVER ( ORDER BY PolizaId DESC) AS 'NumeroRenglon', 
     MaxRows, 
     ID,
     Name
    FROM TempResult, TempCount

)resultados
WHERE   NumeroRenglon >= @PageNum
    AND NumeroRenglon <= @PageNumEnd
ORDER BY NumeroRenglon
elblogdelbeto
fuente