modo efficiente per implementare il paging


118

Devo usare LINQ Skip()e il Take()metodo per il paging o implementare il mio paging con una query SQL?

Qual è il più efficiente? Perché dovrei sceglierne uno piuttosto che l'altro?

Utilizzo SQL Server 2008, ASP.NET MVC e LINQ.


Penso che dipenda. Su quale app stai lavorando? che tipo di carico avrà?
BuddyJoe

Date un'occhiata su questa risposta così: stackoverflow.com/a/10639172/416996
Özbek

Dai un'occhiata anche a questo aspsnippets.com/Articles/…
Frank Myat Thu

Risposte:


175

Cercando di darti una breve risposta al tuo dubbio, se esegui i skip(n).take(m)metodi su linq (con SQL 2005/2008 come database server) la tua query utilizzerà l' Select ROW_NUMBER() Over ...istruzione, con in qualche modo il paging diretto nel motore SQL.

Facendoti un esempio, ho chiamato una tabella db mtcitye ho scritto la seguente query (funziona anche con 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.
}

La query risultante sarà:

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]

Che è un accesso ai dati in finestra (piuttosto interessante, btw cuz restituirà dati sin dall'inizio e accederà alla tabella finché le condizioni saranno soddisfatte). Questo sarà molto simile a:

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

Con l'eccezione che, questa seconda query verrà eseguita più velocemente del risultato di linq perché utilizzerà esclusivamente l'indice per creare la finestra di accesso ai dati; questo significa che, se hai bisogno di un filtro, il filtro dovrebbe essere (o deve essere) nell'elenco delle entità (dove viene creata la riga) e dovrebbero essere creati anche alcuni indici per mantenere le buone prestazioni.

Ora, cosa c'è di meglio?

Se hai un flusso di lavoro abbastanza solido nella tua logica, l'implementazione del modo SQL corretto sarà complicato. In quel caso LINQ sarà la soluzione.

Se puoi abbassare quella parte della logica direttamente a SQL (in una stored procedure), sarà ancora meglio perché puoi implementare la seconda query che ti ho mostrato (utilizzando gli indici) e consentire a SQL di generare e memorizzare il piano di esecuzione del query (miglioramento delle prestazioni).


2
Bella risposta: un'espressione di tabella comune è un buon modo per eseguire il paging.
Jarrod Dixon

Potresti controllare la mia domanda ( stackoverflow.com/questions/11100929/… )? Ho creato un SP che ho aggiunto al mio EDMX e l'ho usato in una query linq-to-entity.
Misi

2
+1, buona risposta, apprezzo che tu spieghi i vantaggi in termini di prestazioni del secondo esempio
Cohen

@ Johan: esiste un'alternativa chiamata metodo seek che supera di gran lunga gli offset per numeri di pagina elevati.
Lukas Eder

50

Prova a usare

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

per ottenere le righe da 501 a 600 nel server SQL, senza caricarle in memoria. Si noti che questa sintassi è diventato disponibile con SQL Server 2012 solo


Penso che questo non sia corretto. L'SQL visualizzato mostra le righe da 502-601 (a meno che tu non stia indicizzando zero?)
Smudge202

No, riceve righe da 501 a 600
Volkan Sen

12

Sebbene LINQ-to-SQL genererà una OFFSETclausola (possibilmente emulata utilizzando ROW_NUMBER() OVER() come altri hanno menzionato ), esiste un modo completamente diverso e molto più veloce per eseguire il paging in SQL. Questo è spesso chiamato "metodo seek", come descritto in questo post del blog qui .

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

I valori @previousScoree @previousPlayerIdsono i rispettivi valori dell'ultimo record della pagina precedente. Ciò ti consente di recuperare la pagina "successiva". Se la ORDER BYdirezione è ASC, usa semplicemente >invece.

Con il metodo precedente, non è possibile passare immediatamente alla pagina 4 senza aver prima recuperato i 40 record precedenti. Ma spesso, comunque, non vuoi saltare così lontano. Invece, ottieni una query molto più veloce che potrebbe essere in grado di recuperare i dati in tempo costante, a seconda della tua indicizzazione. Inoltre, le tue pagine rimangono "stabili", non importa se i dati sottostanti cambiano (ad esempio a pagina 1, mentre tu sei a pagina 4).

Questo è il modo migliore per implementare il paging, ad esempio, durante il caricamento lento di più dati nelle applicazioni web.

Nota, il "metodo di ricerca" è anche chiamato paginazione keyset .


5

LinqToSql convertirà automaticamente un .Skip (N1) .Take (N2) nella sintassi TSQL per te. In effetti, ogni "query" che fai in Linq, in realtà crea solo una query SQL per te in background. Per verificarlo, è sufficiente eseguire SQL Profiler mentre l'applicazione è in esecuzione.

La metodologia salta / prendi ha funzionato molto bene per me e per altri da quello che ho letto.

Per curiosità, che tipo di query di auto-paginazione hai, che ritieni sia più efficiente dello skip / take di Linq?


4

Utilizziamo un CTE avvolto in Dynamic SQL (poiché la nostra applicazione richiede l'ordinamento dinamico dei dati lato server) all'interno di una procedura memorizzata. Posso fornire un esempio di base se lo desideri.

Non ho avuto la possibilità di esaminare il T / SQL prodotto da LINQ. Qualcuno può pubblicare un campione?

Non usiamo LINQ o l'accesso diretto alle tabelle poiché richiediamo un ulteriore livello di sicurezza (dato che l'SQL dinamico lo interrompe in qualche modo).

Qualcosa di simile dovrebbe fare il trucco. È possibile aggiungere valori parametrizzati per i parametri, ecc.

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'

2
@mrdenny - Un suggerimento per l'esempio che ci hai fornito: Con sp_executesqlsi ha la possibilità di passare i parametri in modo sicuro, per esempio: EXECUTE sp_executesql 'WITH myCTE AS ... WHERE Col4=@p1) ...', '@p1 nvarchar(max)', @ValueForCol4. Sicuro in questo contesto significa che è robusto contro SQL injection: puoi passare ogni valore possibile all'interno della variabile @ValueForCol4- anche '--'e la query funzionerà ancora!
Matt

1
@mrdenny Ciao, invece di concatenare la query usiamo qualcosa del genere: SELECT ROW_NUMBER() OVER (ORDER BY CASE WHEN @CampoId = 1 THEN Id WHEN @CampoId = 2 THEN field2 END)
Ezequiel

Ciò può produrre alcuni piani di esecuzione SQL orribili.
mrdenny

@mrdenny: per grandi numeri di pagina, il metodo seek può essere molto più veloce ROW_NUMBER() OVER()dell'emulazione offset. Vedi anche: 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 ci sono tutti i record In t1 sono solo quelli corrispondenti a quella pagina


2

L'approccio che sto fornendo è l'impaginazione più veloce che il server SQL può ottenere. L'ho testato su 5 milioni di dischi. Questo approccio è di gran lunga migliore di "OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY" fornito da SQL Server.

-- 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>>

0

puoi migliorare ulteriormente le prestazioni, controlla questo

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

se userete il from in questo modo darà risultati migliori:

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

motivo: perché stai usando la classe where sulla tabella CityEntities che eliminerà molti record prima di entrare a far parte del MtCity, quindi sicuro al 100% che aumenterà le prestazioni molte volte ...

Comunque la risposta di rodrigoelp è davvero utile.

Grazie


Dubito che ci sarà un impatto sulle prestazioni utilizzando questo consiglio. Non è possibile trovare un riferimento per questo, ma l'ordine di join interno nella query può differire dall'ordine di join effettivo. Quest'ultimo viene deciso da Query Optimizer utilizzando le statistiche della tabella e le stime dei costi operativi.
Imre Pühvel

@ ImreP: questo potrebbe effettivamente corrispondere in qualche modo al metodo di ricerca, che ho descritto . Anche se, non sono sicuro da dove @p0e più specificamente @p1vengo
Lukas Eder

0

È possibile implementare il paging in questo modo semplice passando PageIndex

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

0

Nel 2008 non possiamo usare Skip (). Take ()

Il modo è:

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();
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.