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.
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.
Risposte:
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 mtcity
e 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).
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
Sebbene LINQ-to-SQL genererà una OFFSET
clausola (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 @previousScore
e @previousPlayerId
sono i rispettivi valori dell'ultimo record della pagina precedente. Ciò ti consente di recuperare la pagina "successiva". Se la ORDER BY
direzione è 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 .
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?
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'
sp_executesql
si 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!
SELECT ROW_NUMBER() OVER (ORDER BY CASE WHEN @CampoId = 1 THEN Id WHEN @CampoId = 2 THEN field2 END)
ROW_NUMBER() OVER()
dell'emulazione offset. Vedi anche: 4guysfromrolla.com/webtech/042606-1.shtml
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
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>>
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
@p0
e più specificamente @p1
vengo
È 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
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();