Obtenir le nombre total de lignes à partir de OFFSET / FETCH NEXT

90

Donc, j'ai une fonction qui renvoie un certain nombre d'enregistrements pour lesquels je souhaite implémenter la pagination sur mon site Web. Il m'a été suggéré d'utiliser Offset / Fetch Next dans SQL Server 2012 pour ce faire. Sur notre site Web, nous avons une zone qui répertorie le nombre total d'enregistrements et la page sur laquelle vous vous trouvez à ce moment-là.

Auparavant, je recevais l'intégralité du jeu d'enregistrements et j'étais en mesure de créer la pagination sur ce programme. Mais en utilisant la méthode SQL avec FETCH NEXT X ROWS UNIQUEMENT, je ne reçois que X lignes, donc je ne sais pas quel est mon jeu d'enregistrements total et comment calculer mes pages min et max. La seule façon dont je peux dire de faire cela est d'appeler la fonction deux fois et de compter les lignes sur la première, puis d'exécuter la seconde avec FETCH NEXT. Existe-t-il une meilleure façon de ne pas me faire exécuter la requête deux fois? J'essaie d'accélérer les performances, pas de les ralentir.

Cristal bleu
la source

Réponses:

112

Vous pouvez utiliser COUNT(*) OVER()... voici un exemple rapide utilisant 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;

Cependant, cela devrait être réservé aux petits ensembles de données; sur de plus grands ensembles, les performances peuvent être épouvantables. Consultez cet article de Paul White pour de meilleures alternatives , y compris le maintien des vues indexées (qui ne fonctionne que si le résultat n'est pas filtré ou si vous connaissez les WHEREclauses à l'avance) et l'utilisation d' ROW_NUMBER()astuces.

Aaron Bertrand
la source
44
Dans une table avec 3 500 000 enregistrements, le COUNT (*) OVER () a pris 1 minute et 3 secondes. L'approche décrite ci-dessous par James Moberg a pris 13 secondes pour récupérer le même ensemble de données. Je suis sûr que l'approche Count Over fonctionne bien pour les ensembles de données plus petits, mais lorsque vous commencez à devenir vraiment volumineux, cela ralentit considérablement.
matthew_360
Ou vous pouvez simplement utiliser COUNT (1) OVER () qui est un helluvalot plus rapide car il n'a pas à lire les données réelles de la table, comme le fait count (*)
ldx
1
@AaronBertrand Vraiment? cela doit signifier que vous avez soit un index qui inclut toutes les colonnes, soit que cela a été beaucoup amélioré depuis 2008R2. Dans cette version, le nombre (*) fonctionne de manière séquentielle, ce qui signifie que le premier * (comme dans: toutes les colonnes) est sélectionné, puis compté. Si vous avez compté (1), vous sélectionnez simplement une constante, ce qui est beaucoup plus rapide que la lecture des données réelles.
ldx
5
@idx Non, ce n'est pas non plus ainsi que cela fonctionnait en 2008 R2, désolé. J'utilise SQL Server depuis la version 6.5 et je ne me souviens pas d'une époque où le moteur n'était pas assez intelligent pour analyser simplement l'index le plus étroit pour COUNT (*) ou COUNT (1). Certainement pas depuis 2000. Mais bon, j'ai une instance de 2008 R2, pouvez-vous mettre en place une repro sur SQLfiddle qui démontre cette différence que vous prétendez existe? Je suis heureux de l'essayer.
Aaron Bertrand
2
sur une base de données SQL Server 2016, recherche sur une table avec environ 25 millions de lignes, pagination sur environ 3000 résultats (avec plusieurs jointures, y compris à une fonction table), cela a pris des millisecondes - génial!
jkerak
139

J'ai rencontré des problèmes de performances en utilisant la méthode COUNT ( ) OVER (). (Je ne sais pas si c'était le serveur car il a fallu 40 secondes pour renvoyer 10 enregistrements et ensuite n'a pas eu de problèmes.) Cette technique a fonctionné dans toutes les conditions sans avoir à utiliser COUNT ( ) OVER () et accomplit le même chose:

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
la source
31
Ce serait vraiment génial s'il y avait une possibilité de sauvegarder la valeur COUNT (*) dans une variable. Je pourrais le définir comme paramètre de SORTIE de ma procédure stockée. Des idées?
To Ka
1
Existe-t-il un moyen d'obtenir le décompte dans une table séparée? Il semble que vous ne pouvez utiliser "TempResult" que pour la première instruction SELECT précédente.
matthew_360
4
Pourquoi cela fonctionne-t-il si bien? Dans le premier CTE, toutes les lignes sont sélectionnées, puis réduites par l'extraction. J'aurais deviné que sélectionner toute la ligne dans le premier CTE ralentirait considérablement les choses. En tout cas, merci pour ça!
jbd
1
dans mon cas, il a ralenti que COUNT (1) OVER () .. peut-être parce qu'une fonction dans la sélection.
Tiju John
1
Cela fonctionne parfaitement pour les petites bases de données lorsque les lignes sont des millions, cela prend trop de temps.
Kiya
1

Basé sur la réponse de James Moberg :

Ceci est une alternative d'utilisation Row_Number(), si vous n'avez pas SQL Server 2012 et que vous ne pouvez pas utiliser 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
la source