Abrufen der Gesamtzeilenanzahl von OFFSET / FETCH NEXT

89

Ich habe also eine Funktion, die eine Reihe von Datensätzen zurückgibt, für die ich Paging auf meiner Website implementieren möchte. Es wurde mir vorgeschlagen, in SQL Server 2012 Offset / Fetch Next zu verwenden, um dies zu erreichen. Auf unserer Website haben wir einen Bereich, in dem die Gesamtzahl der Datensätze und die Seite aufgeführt sind, auf der Sie sich gerade befinden.

Zuvor hatte ich den gesamten Datensatz erhalten und konnte das Paging programmgesteuert darauf aufbauen. Wenn ich jedoch die SQL-Methode nur mit FETCH NEXT X ROWS verwende, werden mir nur X Zeilen zurückgegeben, sodass ich nicht weiß, wie hoch mein Gesamtdatensatz ist und wie ich meine minimalen und maximalen Seiten berechne. Die einzige Möglichkeit, dies zu erkennen, besteht darin, die Funktion zweimal aufzurufen, die erste Zeile zu zählen und die zweite mit FETCH NEXT auszuführen. Gibt es eine bessere Möglichkeit, die Abfrage nicht zweimal auszuführen? Ich versuche, die Leistung zu beschleunigen, nicht zu verlangsamen.

Kristallblau
quelle

Antworten:

111

Sie können verwenden COUNT(*) OVER()... hier ist ein kurzes Beispiel mit 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;

Dies sollte jedoch für kleine Datenmengen reserviert werden. Bei größeren Sets kann die Leistung miserabel sein. In diesem Artikel von Paul White finden Sie bessere Alternativen , einschließlich der Verwaltung indizierter Ansichten (die nur funktionieren, wenn das Ergebnis ungefiltert ist oder Sie WHEREKlauseln im Voraus kennen) und der Verwendung von ROW_NUMBER()Tricks.

Aaron Bertrand
quelle
44
In einer Tabelle mit 3.500.000 Datensätzen dauerte COUNT (*) OVER () 1 Minute und 3 Sekunden. Der unten von James Moberg beschriebene Ansatz dauerte 13 Sekunden, um denselben Datensatz abzurufen. Ich bin sicher, dass der Count Over-Ansatz für kleinere Datensätze gut funktioniert, aber wenn Sie anfangen, wirklich groß zu werden, verlangsamt er sich erheblich.
Matthew_360
Oder Sie können einfach COUNT (1) OVER () verwenden, was sehr viel schneller ist, da es nicht die tatsächlichen Daten aus der Tabelle lesen muss, wie es count (*) tut
ldx
1
@ AaronBertrand Wirklich? Das muss bedeuten, dass Sie entweder einen Index haben, der alle Spalten enthält, oder dass dieser seit 2008R2 stark verbessert wurde. In dieser Version arbeitet die Anzahl (*) nacheinander, was bedeutet, dass zuerst * (wie in: alle Spalten) ausgewählt und dann gezählt wird. Wenn Sie gezählt haben (1), wählen Sie einfach eine Konstante aus, was viel schneller ist als das Lesen der tatsächlichen Daten.
ldx
5
@idx Nein, so hat das auch in 2008 R2 nicht funktioniert, sorry. Ich verwende SQL Server seit 6.5 und erinnere mich nicht an eine Zeit, in der die Engine nicht intelligent genug war, um nur den engsten Index nach COUNT (*) oder COUNT (1) zu durchsuchen. Sicher nicht seit 2000. Aber hey, ich habe eine Instanz von 2008 R2. Können Sie einen Repro auf SQLfiddle einrichten, der diesen Unterschied demonstriert, von dem Sie behaupten, dass er existiert? Ich versuche es gerne.
Aaron Bertrand
2
Bei einer SQL Server 2016-Datenbank, bei der in einer Tabelle mit etwa 25 Millionen Zeilen gesucht und über 3000 Ergebnisse ausgelagert wurden (mit mehreren Verknüpfungen, einschließlich einer Funktion mit Tabellenwert), dauerte dies Millisekunden - großartig!
Jkerak
138

Bei der Verwendung der COUNT ( ) OVER () -Methode sind einige Leistungsprobleme aufgetreten . (Ich bin nicht sicher, ob es der Server war, da es 40 Sekunden dauerte, um 10 Datensätze zurückzugeben, und später keine Probleme hatte.) Diese Technik funktionierte unter allen Bedingungen, ohne COUNT ( ) OVER () verwenden zu müssen, und führt das aus gleiche Sache:

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
quelle
31
Es wäre wirklich großartig, wenn es die Möglichkeit gäbe, den Wert COUNT (*) in einer Variablen zu speichern. Ich könnte es als OUTPUT-Parameter meiner gespeicherten Prozedur festlegen. Irgendwelche Ideen?
Zu Ka
1
Gibt es eine Möglichkeit, die Zählung in einer separaten Tabelle abzurufen? Anscheinend können Sie "TempResult" nur für die erste vorhergehende SELECT-Anweisung verwenden.
Matthew_360
declare @result table ( ID int, Name nvarchar(max), counter int ) dann kann auf der obigen Aussage das endgültige außerhalb von CTE select sein: SELECT * into @result FROM TempResult, TempCount ORDER BY TempResult.Name OFFSET (@PageNum-1)*@PageSize ROWS FETCH NEXT @PageSize ROWS ONLY falls Sie
Nathan Teague
4
Warum funktioniert das so gut? Im ersten CTE werden alle Zeilen ausgewählt und dann durch den Abruf reduziert. Ich hätte gedacht, dass die Auswahl aller Zeilen im ersten CTE die Dinge erheblich verlangsamen würde. Auf jeden Fall danke dafür!
jbd
1
in meinem Fall wurde es langsamer als COUNT (1) OVER () .. vielleicht weil eine Funktion in der Auswahl.
Tiju John
1

Basierend auf James Mobergs Antwort :

Dies ist eine Alternative Row_Number(), wenn Sie nicht über SQL Server 2012 verfügen und OFFSET nicht verwenden können

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
quelle