Effiziente Implementierung von Paging

118

Sollte ich LINQs Skip()und Take()Methoden für das Paging verwenden oder mein eigenes Paging mit einer SQL-Abfrage implementieren?

Welches ist am effizientesten? Warum sollte ich einen über den anderen wählen?

Ich verwende SQL Server 2008, ASP.NET MVC und LINQ.

Steinherz
quelle
Ich denke es kommt darauf an. An welcher App können Sie arbeiten? Welche Art von Last wird es haben?
BuddyJoe
Schauen Sie sich auch diese Antwort an: stackoverflow.com/a/10639172/416996
bekzbek
Schauen Sie sich dies auch an aspsnippets.com/Articles/…
Frank Myat Do

Antworten:

175

Wenn Sie versuchen, eine kurze Antwort auf Ihre Zweifel zu geben, verwendet skip(n).take(m)Ihre Abfrage die Select ROW_NUMBER() Over ...Anweisung , wenn Sie die Methoden unter linq ausführen (mit SQL 2005/2008 als Datenbankserver) , wobei es sich um ein direktes Paging in der SQL-Engine handelt.

Als Beispiel habe ich eine DB-Tabelle namens mtcityund habe die folgende Abfrage geschrieben (arbeite auch mit linq to entity):

using (DataClasses1DataContext c = new DataClasses1DataContext())
{
    var query = (from MtCity2 c1 in c.MtCity2s
                select c1).Skip(3).Take(3);
    //Doing something with the query.
}

Die resultierende Abfrage lautet:

SELECT [t1].[CodCity], 
    [t1].[CodCountry], 
    [t1].[CodRegion], 
    [t1].[Name],  
    [t1].[Code]
FROM (
    SELECT ROW_NUMBER() OVER (
        ORDER BY [t0].[CodCity], 
        [t0].[CodCountry], 
        [t0].[CodRegion], 
        [t0].[Name],
        [t0].[Code]) AS [ROW_NUMBER], 
        [t0].[CodCity], 
        [t0].[CodCountry], 
        [t0].[CodRegion], 
        [t0].[Name],
        [t0].[Code]
    FROM [dbo].[MtCity] AS [t0]
    ) AS [t1]
WHERE [t1].[ROW_NUMBER] BETWEEN @p0 + 1 AND @p0 + @p1
ORDER BY [t1].[ROW_NUMBER]

Das ist ein Fensterdatenzugriff (ziemlich cool, übrigens, weil er von Anfang an Daten zurückgibt und auf die Tabelle zugreift, solange die Bedingungen erfüllt sind). Dies wird sehr ähnlich sein zu:

With CityEntities As 
(
    Select ROW_NUMBER() Over (Order By CodCity) As Row,
        CodCity //here is only accessed by the Index as CodCity is the primary
    From dbo.mtcity
)
Select [t0].[CodCity], 
        [t0].[CodCountry], 
        [t0].[CodRegion], 
        [t0].[Name],
        [t0].[Code]
From CityEntities c
Inner Join dbo.MtCity t0 on c.CodCity = t0.CodCity
Where c.Row Between @p0 + 1 AND @p0 + @p1
Order By c.Row Asc

Mit der Ausnahme, dass diese zweite Abfrage schneller als das linq-Ergebnis ausgeführt wird, da ausschließlich der Index zum Erstellen des Datenzugriffsfensters verwendet wird. Dies bedeutet, wenn Sie eine Filterung benötigen, sollte die Filterung in der Entitätsliste (wo die Zeile erstellt wird) sein (oder müssen) und einige Indizes sollten ebenfalls erstellt werden, um die gute Leistung aufrechtzuerhalten.

Was ist besser?

Wenn Ihre Logik einen ziemlich soliden Workflow enthält, ist die Implementierung der richtigen SQL-Methode kompliziert. In diesem Fall ist LINQ die Lösung.

Wenn Sie diesen Teil der Logik direkt in SQL (in einer gespeicherten Prozedur) senken können, ist dies sogar noch besser, da Sie die zweite Abfrage, die ich Ihnen gezeigt habe (mithilfe von Indizes), implementieren und SQL erlauben können, den Ausführungsplan des zu generieren und zu speichern Abfrage (Verbesserung der Leistung).

rodrigoelp
quelle
2
Gute Antwort - ein allgemeiner Tabellenausdruck ist eine gute Möglichkeit, Paging durchzuführen.
Jarrod Dixon
Könnten Sie meine Frage überprüfen ( stackoverflow.com/questions/11100929/… )? Ich habe einen SP erstellt, den ich meinem EDMX hinzugefügt und in einer Linq-to-Entities-Abfrage verwendet habe.
Misi
2
+1, gute Antwort, ich schätze es, dass Sie die Leistungsvorteile des zweiten Beispiels erklären
Cohen
@Johan: Es gibt eine Alternative der genannten Suchverfahren , dass stark übertrifft Offsets für große Seitenzahl.
Lukas Eder
50

Versuchen Sie es mit

FROM [TableX]
ORDER BY [FieldX]
OFFSET 500 ROWS
FETCH NEXT 100 ROWS ONLY

um die Zeilen von 501 bis 600 auf dem SQL Server abzurufen, ohne sie in den Speicher zu laden. Beachten Sie, dass diese Syntax mit verfügbar geworden ist , SQL Server 2012 nur

d.popov
quelle
Ich denke das ist falsch. Das angezeigte SQL zeigt Zeilen von 502-601 (es sei denn, Sie indizieren null?)
Smudge202
Nein, es gibt Zeilen von 501 bis 600
Volkan Sen
12

Während LINQ-to-SQL eine OFFSETKlausel generiert (möglicherweise ROW_NUMBER() OVER() wie von anderen erwähnt emuliert ), gibt es eine völlig andere, viel schnellere Möglichkeit, Paging in SQL durchzuführen. Dies wird oft als "Suchmethode" bezeichnet, wie in diesem Blog-Beitrag hier beschrieben .

SELECT TOP 10 first_name, last_name, score
FROM players
WHERE (score < @previousScore)
   OR (score = @previousScore AND player_id < @previousPlayerId)
ORDER BY score DESC, player_id DESC

Die Werte @previousScoreund @previousPlayerIdsind die jeweiligen Werte des letzten Datensatzes von der vorherigen Seite. Auf diese Weise können Sie die "nächste" Seite abrufen. Wenn die ORDER BYRichtung ist ASC, verwenden Sie >stattdessen einfach .

Mit der obigen Methode können Sie nicht sofort zu Seite 4 springen, ohne zuvor die vorherigen 40 Datensätze abgerufen zu haben. Aber oft will man sowieso nicht so weit springen. Stattdessen erhalten Sie eine viel schnellere Abfrage, mit der Daten je nach Indizierung möglicherweise in konstanter Zeit abgerufen werden können. Außerdem bleiben Ihre Seiten "stabil", unabhängig davon, ob sich die zugrunde liegenden Daten ändern (z. B. auf Seite 1, während Sie sich auf Seite 4 befinden).

Dies ist der beste Weg, um Paging zu implementieren, wenn beispielsweise mehr Daten in Webanwendungen verzögert geladen werden.

Beachten Sie, dass die " Suchmethode " auch als Keyset-Paging bezeichnet wird .

Lukas Eder
quelle
5

LinqToSql konvertiert automatisch einen .Skip (N1) .Take (N2) für Sie in die TSQL-Syntax. Tatsächlich erstellt jede "Abfrage", die Sie in Linq ausführen, nur eine SQL-Abfrage für Sie im Hintergrund. Um dies zu testen, führen Sie einfach SQL Profiler aus, während Ihre Anwendung ausgeführt wird.

Die Skip / Take-Methode hat für mich und andere nach dem, was ich gelesen habe, sehr gut funktioniert.

Welche Art von Self-Paging-Abfrage haben Sie aus Neugier, die Ihrer Meinung nach effizienter ist als das Überspringen / Nehmen von Linq?

mandreko
quelle
4

Wir verwenden einen in Dynamic SQL eingeschlossenen CTE (da unsere Anwendung eine dynamische Sortierung der Datenserverseite erfordert) innerhalb einer gespeicherten Prozedur. Ich kann ein einfaches Beispiel liefern, wenn Sie möchten.

Ich hatte keine Gelegenheit, mir das von LINQ erzeugte T / SQL anzusehen. Kann jemand eine Probe posten?

Wir verwenden weder LINQ noch direkten Zugriff auf die Tabellen, da wir die zusätzliche Sicherheitsebene benötigen (vorausgesetzt, das dynamische SQL unterbricht dies etwas).

So etwas sollte den Trick machen. Sie können parametrisierte Werte für Parameter usw. hinzufügen.

exec sp_executesql 'WITH MyCTE AS (
    SELECT TOP (10) ROW_NUMBER () OVER ' + @SortingColumn + ' as RowID, Col1, Col2
    FROM MyTable
    WHERE Col4 = ''Something''
)
SELECT *
FROM MyCTE
WHERE RowID BETWEEN 10 and 20'
mrdenny
quelle
2
@mrdenny - Ein Hinweis für das von Ihnen bereitgestellte Beispiel : Mit haben sp_executesqlSie die Möglichkeit, Parameter auf sichere Weise zu übergeben, z EXECUTE sp_executesql 'WITH myCTE AS ... WHERE Col4=@p1) ...', '@p1 nvarchar(max)', @ValueForCol4. Sicher bedeutet in diesem Zusammenhang, dass es robust gegen SQL-Injection ist - Sie können jeden möglichen Wert innerhalb der Variablen übergeben @ValueForCol4- sogar '--', und die Abfrage funktioniert weiterhin!
Matt
1
@ Mrdenny Hallo, anstatt die Abfrage zu verketten, verwenden wir so etwas: SELECT ROW_NUMBER() OVER (ORDER BY CASE WHEN @CampoId = 1 THEN Id WHEN @CampoId = 2 THEN field2 END)
Ezequiel
Das kann einige schreckliche SQL-Ausführungspläne erzeugen.
Mrdenny
@mrdenny: Für große Seitenzahlen, die Suchverfahren viel schneller sein kann als ROW_NUMBER() OVER()Offset - Emulation. Siehe auch: 4guysfromrolla.com/webtech/042606-1.shtml
Lukas Eder
2

In SQL Server 2008:

DECLARE @PAGE INTEGER = 2
DECLARE @TAKE INTEGER = 50

SELECT [t1].*
FROM (
    SELECT ROW_NUMBER() OVER (ORDER BY [t0].[COLUMNORDER] DESC) AS [ROW_NUMBER], [t0].*
    FROM [dbo].[TABLA] AS [t0]
    WHERE ([t0].[COLUMNS_CONDITIONS] = 1)
    ) AS [t1]
WHERE [t1].[ROW_NUMBER] BETWEEN ((@PAGE*@TAKE) - (@TAKE-1)) AND (@PAGE*@TAKE)
ORDER BY [t1].[ROW_NUMBER]

In t0 sind alle Datensätze. In t1 sind nur diejenigen, die dieser Seite entsprechen

ch2o
quelle
2

Der Ansatz, den ich gebe, ist die schnellste Paginierung, die SQL Server erreichen kann. Ich habe dies an 5 Millionen Datensätzen getestet. Dieser Ansatz ist weitaus besser als der von SQL Server bereitgestellte "OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY".

-- The below given code computes the page numbers and the max row of previous page
-- Replace <<>> with the correct table data.
-- Eg. <<IdentityColumn of Table>> can be EmployeeId and <<Table>> will be dbo.Employees

DECLARE @PageNumber int=1; --1st/2nd/nth page. In stored proc take this as input param.
DECLARE @NoOfRecordsPerPage int=1000;

 DECLARE @PageDetails TABLE
       (
        <<IdentityColumn of Table>> int,
        rownum int,
        [PageNumber] int
       )           
       INSERT INTO @PageDetails values(0, 0, 0)
       ;WITH CTE AS
       (
       SELECT <<IdentityColumn of Table>>, ROW_NUMBER() OVER(ORDER BY <<IdentityColumn of Table>>) rownum FROM <<Table>>
       )
       Insert into @PageDetails 
       SELECT <<IdentityColumn of Table>>, CTE.rownum, ROW_NUMBER() OVER (ORDER BY rownum) as [PageNumber] FROM CTE WHERE CTE.rownum%@NoOfRecordsPerPage=0


--SELECT * FROM @PageDetails 

-- Actual pagination
SELECT TOP (@NoOfRecordsPerPage)
FROM <<Table>> AS <<Table>>
WHERE <<IdentityColumn of Table>> > (SELECT <<IdentityColumn of Table>> FROM 
@PageDetails WHERE PageNumber=@PageNumber)
ORDER BY <<Identity Column of Table>>
srinivas vv
quelle
0

Sie können die Leistung weiter verbessern, überprüfen Sie dies

From CityEntities c
Inner Join dbo.MtCity t0 on c.CodCity = t0.CodCity
Where c.Row Between @p0 + 1 AND @p0 + @p1
Order By c.Row Asc

Wenn Sie das from auf diese Weise verwenden, erhalten Sie ein besseres Ergebnis:

From   dbo.MtCity  t0
   Inner Join  CityEntities c on c.CodCity = t0.CodCity

Grund: Weil Sie die where-Klasse in der CityEntities-Tabelle verwenden, die viele Datensätze eliminiert, bevor Sie sich der MtCity anschließen. 100% sicher, dass dies die Leistung um ein Vielfaches erhöht ...

Auf jeden Fall ist die Antwort von rodrigoelp wirklich hilfreich.

Vielen Dank

Ali Adravi
quelle
Ich bezweifle, dass die Verwendung dieses Hinweises Auswirkungen auf die Leistung haben wird. Es kann keine Referenz dafür gefunden werden, aber die innere Verknüpfungsreihenfolge in der Abfrage kann von der tatsächlichen Verknüpfungsreihenfolge abweichen. Letzteres wird vom Abfrageoptimierer unter Verwendung der Tabellenstatistik und der Betriebskostenschätzungen entschieden.
Imre Pühvel
@ImreP: Dies könnte tatsächlich etwas der Suchmethode entsprechen, die ich beschrieben habe . Obwohl, ich bin nicht sicher , wo @p0und insbesondere @p1kommt aus
Lukas Eder
0

Sie können Paging auf diese einfache Weise implementieren, indem Sie PageIndex übergeben

Declare @PageIndex INT = 1
Declare  @PageSize INT = 20

Select ROW_NUMBER() OVER ( ORDER BY Products.Name ASC )  AS RowNumber,
    Products.ID,
    Products.Name
into #Result 
From Products

SELECT @RecordCount = COUNT(*) FROM #Results 

SELECT * 
FROM #Results
WHERE RowNumber
BETWEEN
    (@PageIndex -1) * @PageSize + 1 
    AND
    (((@PageIndex -1) * @PageSize + 1) + @PageSize) - 1
Rae Lee
quelle
0

Im Jahr 2008 können wir Skip () nicht verwenden. Take ()

Der Weg ist:

var MinPageRank = (PageNumber - 1) * NumInPage + 1
var MaxPageRank = PageNumber * NumInPage

var visit = Visita.FromSql($"SELECT * FROM (SELECT [RANK] = ROW_NUMBER() OVER (ORDER BY Hora DESC),* FROM Visita WHERE ) A WHERE A.[RANK] BETWEEN {MinPageRank} AND {MaxPageRank}").ToList();
Belen Martin
quelle