forma eficiente de implementar la paginación

118

¿Debo usar el método LINQ Skip()y Take()para la paginación, o implementar mi propia paginación con una consulta SQL?

¿Cuál es más eficiente? ¿Por qué elegiría uno sobre el otro?

Estoy usando SQL Server 2008, ASP.NET MVC y LINQ.

Corazón de piedra
fuente
Creo que depende. ¿En qué aplicación estás trabajando? ¿Qué tipo de carga tendrá?
BuddyJoe
Eche un vistazo a esta respuesta también: stackoverflow.com/a/10639172/416996
Õzbek
Eche un vistazo a esto también aspsnippets.com/Articles/…
Frank Myat Jue

Respuestas:

175

Tratando de darle una respuesta breve a su duda, si ejecuta los skip(n).take(m)métodos en linq (con SQL 2005/2008 como servidor de base de datos) su consulta estará usando la Select ROW_NUMBER() Over ...declaración, con una paginación directa de alguna manera en el motor SQL.

Dándote un ejemplo, tengo una tabla db llamada mtcityy escribí la siguiente consulta (también funciona con linq para entidades):

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 consulta resultante será:

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]

Que es un acceso a datos en ventana (muy bien, por cierto, porque devolverá datos desde el principio y accederá a la tabla siempre que se cumplan las condiciones). Esto será muy similar 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 la excepción de que, esta segunda consulta se ejecutará más rápido que el resultado de linq porque usará exclusivamente el índice para crear la ventana de acceso a datos; esto significa que, si necesita algún filtrado, el filtrado debe estar (o debe estar) en la lista de Entidades (donde se crea la fila) y también deben crearse algunos índices para mantener el buen rendimiento.

Ahora, ¿qué es mejor?

Si tiene un flujo de trabajo bastante sólido en su lógica, implementar la forma adecuada de SQL será complicado. En ese caso, LINQ será la solución.

Si puede reducir esa parte de la lógica directamente a SQL (en un procedimiento almacenado), será aún mejor porque puede implementar la segunda consulta que le mostré (usando índices) y permitir que SQL genere y almacene el Plan de ejecución del consulta (mejora del rendimiento).

rodrigoelp
fuente
2
Buena respuesta: la expresión de tabla común es una buena manera de hacer paginación.
Jarrod Dixon
¿Podrías revisar mi pregunta ( stackoverflow.com/questions/11100929/… )? Hice un SP que agregué a mi EDMX y lo usé en una consulta de linq a entidades.
Misi
2
+1, buena respuesta, agradezco que me explique los beneficios de rendimiento del segundo ejemplo
Cohen
@Johan: Existe una alternativa llamada método de búsqueda que supera ampliamente las compensaciones para números de página grandes.
Lukas Eder
50

Intenta usar

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

para obtener las filas 501 a 600 en el servidor SQL, sin cargarlas en la memoria. Tenga en cuenta que esta sintaxis se ha convertido en disponible con SQL Server 2012 sólo se

d.popov
fuente
Creo que esto es incorrecto. El SQL que se muestra muestra filas de 502 a 601 (a menos que tenga una indexación cero)
Smudge202
No, obtiene filas de 501 a 600
Volkan Sen
12

Si bien LINQ-to-SQL generará una OFFSETcláusula (posiblemente emulada usando ROW_NUMBER() OVER() como han mencionado otros ), existe una forma completamente diferente y mucho más rápida de realizar la paginación en SQL. Esto a menudo se denomina "método de búsqueda", como se describe en esta publicación de blog aquí .

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

Los valores @previousScorey @previousPlayerIdson los valores respectivos del último registro de la página anterior. Esto le permite buscar la página "siguiente". Si la ORDER BYdirección es ASC, simplemente use >en su lugar.

Con el método anterior, no puede saltar inmediatamente a la página 4 sin haber obtenido primero los 40 registros anteriores. Pero a menudo, no querrás saltar tan lejos de todos modos. En cambio, obtiene una consulta mucho más rápida que podría obtener datos en tiempo constante, según su indexación. Además, sus páginas permanecen "estables", sin importar si los datos subyacentes cambian (por ejemplo, en la página 1, mientras está en la página 4).

Esta es la mejor manera de implementar la paginación cuando se carga de forma diferida más datos en aplicaciones web, por ejemplo.

Tenga en cuenta que el "método de búsqueda" también se denomina paginación de conjuntos de claves .

Lukas Eder
fuente
5

LinqToSql convertirá automáticamente un .Skip (N1) .Take (N2) en la sintaxis TSQL por usted. De hecho, cada "consulta" que hace en Linq, en realidad es simplemente crear una consulta SQL para usted en segundo plano. Para probar esto, simplemente ejecute SQL Profiler mientras se ejecuta su aplicación.

La metodología de saltar / tomar ha funcionado muy bien para mí y para otros por lo que leí.

Por curiosidad, ¿qué tipo de consulta de autopaginación tiene, que cree que es más eficiente que la omisión / toma de Linq?

mandreko
fuente
4

Usamos un CTE envuelto en SQL dinámico (porque nuestra aplicación requiere una clasificación dinámica del lado del servidor de datos) dentro de un procedimiento almacenado. Puedo proporcionar un ejemplo básico si lo desea.

No he tenido la oportunidad de ver el T / SQL que produce LINQ. ¿Alguien puede publicar una muestra?

No usamos LINQ o acceso directo a las tablas ya que requerimos la capa adicional de seguridad (dado que el SQL dinámico rompe esto de alguna manera).

Algo como esto debería funcionar. Puede agregar valores parametrizados para parámetros, etc.

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
fuente
2
@mrdenny - Una sugerencia para el ejemplo que has proporcionado: Con sp_executesqlusted tiene la posibilidad de pasar parámetros de una manera segura, por ejemplo: EXECUTE sp_executesql 'WITH myCTE AS ... WHERE Col4=@p1) ...', '@p1 nvarchar(max)', @ValueForCol4. Seguro en este contexto significa que es robusto contra la inyección de SQL; puede pasar todos los valores posibles dentro de la variable @ValueForCol4, incluso '--', ¡y la consulta seguirá funcionando!
Matt
1
@mrdenny Hola, en lugar de concatenar la consulta usamos algo como esto: SELECT ROW_NUMBER() OVER (ORDER BY CASE WHEN @CampoId = 1 THEN Id WHEN @CampoId = 2 THEN field2 END)
Ezequiel
Eso puede producir planes de ejecución de SQL horribles.
mrdenny
@mrdenny: para números de página grandes, el método de búsqueda puede ser mucho más rápido que la ROW_NUMBER() OVER()emulación de desplazamiento. Ver también: 4guysfromrolla.com/webtech/042606-1.shtml
Lukas Eder
2

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

En t0 están todos los registros En t1 son solo los correspondientes a esa página

ch2o
fuente
2

El enfoque que estoy dando es la paginación más rápida que puede lograr el servidor SQL. He probado esto en 5 millones de registros. Este enfoque es mucho mejor que "DESPLAZAR 10 FILAS, BUSCAR SIGUIENTES 10 FILAS SOLAMENTE" proporcionado por 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>>
srinivas vv
fuente
0

puede mejorar aún más el rendimiento, compruebe esto

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

si usa el from de esta manera, dará un mejor resultado:

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

motivo: porque está utilizando la clase where en la tabla CityEntities que eliminará muchos registros antes de unirse a MtCity, así que 100% seguro de que aumentará el rendimiento muchas veces ...

De todos modos, la respuesta de rodrigoelp es realmente útil.

Gracias

Ali Adravi
fuente
Dudo que haya algún impacto en el rendimiento al usar este consejo. No se puede encontrar una referencia para esto, pero el orden de unión interno en la consulta puede diferir del orden de unión real. Esto último se decide mediante el optimizador de consultas utilizando las estadísticas de la tabla y las estimaciones de costos de operación.
Imre Pühvel
@ImreP: Esto en realidad podría corresponder de alguna manera al método de búsqueda, que he descrito . Aunque, no estoy seguro de dónde @p0y más específicamente de dónde @p1vengo
Lukas Eder
0

Puede implementar la paginación de esta manera simple pasando 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
Rae Lee
fuente
0

En 2008 no podemos usar Skip (). Take ()

El camino es:

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();
Belén Martín
fuente