maneira eficiente de implementar paging

118

Devo usar LINQ Skip()e Take()método para paginação ou implementar minha própria paginação com uma consulta SQL?

Qual é o mais eficiente? Por que eu escolheria um em vez do outro?

Estou usando o SQL Server 2008, ASP.NET MVC e LINQ.

Coração de pedra
fonte
Eu acho que depende. Em que aplicativo você está trabalhando? que tipo de carga vai ter?
BuddyJoe
Dê uma olhada nesta resposta também: stackoverflow.com/a/10639172/416996
Õzbek
Dê uma olhada neste também aspsnippets.com/Articles/…
Frank Myat Qui

Respostas:

175

Tentando dar uma resposta breve à sua dúvida, se você executar os skip(n).take(m)métodos no linq (com SQL 2005/2008 como servidor de banco de dados) sua consulta estará usando oSelect ROW_NUMBER() Over ... comando, com alguma forma de paginação direta no motor SQL.

Dando um exemplo, eu tenho uma tabela db chamada mtcitye escrevi a seguinte consulta (funciona bem com 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.
}

A 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 é um acesso de dados em janela (muito legal, btw porque estará retornando dados desde o início e acessará a tabela desde que as condições sejam atendidas). Isso será muito semelhante 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

Com a exceção de que, esta segunda consulta será executada mais rapidamente do que o resultado do linq porque estará usando exclusivamente o índice para criar a janela de acesso aos dados; isso significa que, se você precisar de alguma filtragem, a filtragem deve estar (ou deve estar) na lista de entidades (onde a linha é criada) e alguns índices também devem ser criados para manter o bom desempenho.

Agora, o que é melhor?

Se você tiver um fluxo de trabalho bastante sólido em sua lógica, implementar a maneira SQL adequada será complicado. Nesse caso, o LINQ será a solução.

Se você puder reduzir essa parte da lógica diretamente para SQL (em um procedimento armazenado), será ainda melhor porque você pode implementar a segunda consulta que mostrei a você (usando índices) e permitir que o SQL gere e armazene o Plano de Execução do consulta (melhorando o desempenho).

rodrigoelp
fonte
2
Boa resposta - a expressão de tabela comum é uma boa maneira de fazer paginação.
Jarrod Dixon
Você poderia verificar minha pergunta ( stackoverflow.com/questions/11100929/… )? Fiz um SP que adicionei ao meu EDMX e o usei em uma consulta linq-to-entity.
Misi de
2
+1, boa resposta, agradeço que você explique os benefícios de desempenho do segundo exemplo
Cohen,
@Johan: Existe uma alternativa chamada método de busca que supera fortemente os deslocamentos para grandes números de página.
Lukas Eder
50

Tente usar

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

para obter as linhas de 501 a 600 no servidor SQL, sem carregá-las na memória. Note-se que esta sintaxe tornou-se disponível com SQL Server 2012 única

d.popov
fonte
Eu acho que isso está incorreto. O SQL exibido mostra linhas de 502-601 (a menos que você esteja indexando zero?)
Smudge202
Não, ele obtém linhas de 501 a 600
Volkan Sen
12

Embora o LINQ-to-SQL gere uma OFFSETcláusula (possivelmente emulada usando ROW_NUMBER() OVER() como outros mencionaram ), há uma maneira totalmente diferente e muito mais rápida de executar a paginação em SQL. Isso geralmente é chamado de "método de busca", conforme descrito nesta postagem do blog aqui .

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

Os valores @previousScoree @previousPlayerIdsão os respectivos valores do último registro da página anterior. Isso permite que você busque a "próxima" página. Se a ORDER BYdireção for ASC, basta usar> .

Com o método acima, você não pode pular imediatamente para a página 4 sem primeiro ter buscado os 40 registros anteriores. Mas frequentemente, você não quer ir tão longe de qualquer maneira. Em vez disso, você obtém uma consulta muito mais rápida que pode buscar dados em tempo constante, dependendo de sua indexação. Além disso, suas páginas permanecem "estáveis", não importa se os dados subjacentes mudam (por exemplo, na página 1, enquanto você está na página 4).

Esta é a melhor maneira de implementar paginação ao carregar lentamente mais dados em aplicativos da web, por exemplo.

Observe que o "método de busca" também é chamado de paginação do conjunto de chaves .

Lukas Eder
fonte
5

LinqToSql irá converter automaticamente um .Skip (N1) .Take (N2) na sintaxe TSQL para você. Na verdade, cada "consulta" que você faz no Linq, na verdade, está apenas criando uma consulta SQL para você em segundo plano. Para testar isso, basta executar o SQL Profiler enquanto seu aplicativo está em execução.

A metodologia pular / pegar funcionou muito bem para mim e para outros pelo que li.

Por curiosidade, que tipo de consulta de auto-paginação você tem e que acredita ser mais eficiente do que pular / pegar do Linq?

Mandreko
fonte
4

Usamos um CTE empacotado em SQL dinâmico (porque nosso aplicativo requer classificação dinâmica do lado do servidor de dados) dentro de um procedimento armazenado. Posso fornecer um exemplo básico, se desejar.

Não tive a chance de olhar o T / SQL que o LINQ produz. Alguém pode postar uma amostra?

Não usamos LINQ ou acesso direto às tabelas, pois exigimos a camada extra de segurança (dado que o SQL dinâmico quebra isso um pouco).

Algo assim deve resolver o problema. Você pode adicionar 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
fonte
2
@mrdenny - Uma dica para o exemplo que você forneceu: Com sp_executesqlvocê tem a possibilidade de passar parâmetros de forma segura, por exemplo: EXECUTE sp_executesql 'WITH myCTE AS ... WHERE Col4=@p1) ...', '@p1 nvarchar(max)', @ValueForCol4. Seguro neste contexto significa que é robusto contra injeção de SQL - você pode passar todos os valores possíveis dentro da variável @ValueForCol4- mesmo '--', e a consulta ainda funcionará!
Matt
1
@mrdenny Olá, em vez de concatenar a consulta, usamos algo assim: SELECT ROW_NUMBER() OVER (ORDER BY CASE WHEN @CampoId = 1 THEN Id WHEN @CampoId = 2 THEN field2 END)
Ezequiel
Isso pode produzir alguns planos de execução SQL terríveis.
mrdenny
@mrdenny: Para grandes números de página, o método de busca pode ser muito mais rápido do que ROW_NUMBER() OVER()a emulação de deslocamento. Veja também: 4guysfromrolla.com/webtech/042606-1.shtml
Lukas Eder
2

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

Em t0 são todos os registros Em t1 são apenas aqueles que correspondem a essa página

ch2o
fonte
2

A abordagem que estou dando é a paginação mais rápida que o servidor SQL pode alcançar. Eu testei isso em 5 milhões de registros. Essa abordagem é muito melhor do que "OFFSET 10 ROWS FETCH NEXT 10 ROWS SOMENTE" fornecido pelo 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
fonte
0

você pode melhorar ainda mais o desempenho, verifique este

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 você usar o de desta forma, terá um resultado melhor:

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

motivo: porque você está usando a classe where na tabela CityEntities que eliminará muitos registros antes de ingressar na MtCity, então, 100% de certeza, aumentará o desempenho em muitas vezes ...

De qualquer forma, a resposta do rodrigoelp é realmente útil.

obrigado

Ali Adravi
fonte
Duvido que haja qualquer impacto no desempenho ao usar este conselho. Não é possível encontrar uma referência para isso, mas a ordem de junção interna na consulta pode ser diferente da ordem de junção real. O último é decidido pelo otimizador de consulta usando estatísticas da tabela e estimativas de custo operacional.
Imre Pühvel
@ImreP: Isso pode realmente corresponder de alguma forma ao método de busca, que descrevi . Embora eu não tenha certeza de onde @p0e mais especificamente @p1vêm
Lukas Eder
0

Você pode implementar a paginação dessa maneira simples, 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
Rae Lee
fonte
0

Em 2008, não podemos usar Skip (). Take ()

O jeito é:

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
fonte