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.
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.
Respostas:
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 mtcity
e 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).
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
Embora o LINQ-to-SQL gere uma OFFSET
clá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 @previousScore
e @previousPlayerId
são os respectivos valores do último registro da página anterior. Isso permite que você busque a "próxima" página. Se a ORDER BY
direçã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 .
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?
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'
sp_executesql
você 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á!
SELECT ROW_NUMBER() OVER (ORDER BY CASE WHEN @CampoId = 1 THEN Id WHEN @CampoId = 2 THEN field2 END)
ROW_NUMBER() OVER()
a emulação de deslocamento. Veja também: 4guysfromrolla.com/webtech/042606-1.shtml
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
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>>
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
@p0
e mais especificamente @p1
vêm
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
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();