moyen efficace d'implémenter la pagination

118

Dois-je utiliser la méthode LINQ Skip()et Take()pour la pagination, ou implémenter ma propre pagination avec une requête SQL?

Quel est le plus efficace? Pourquoi choisirais-je l'un plutôt que l'autre?

J'utilise SQL Server 2008, ASP.NET MVC et LINQ.

Cœur de pierre
la source
Je pense que ça dépend. Sur quelle application travaillez-vous? quel genre de charge aura-t-il?
BuddyJoe
Jetez également un œil à cette réponse: stackoverflow.com/a/10639172/416996
Õzbek
Jetez un oeil à ce aussi aspsnippets.com/Articles/…
Frank Myat jeu

Réponses:

175

Si vous essayez de vous donner une brève réponse à votre doute, si vous exécutez les skip(n).take(m)méthodes sur linq (avec SQL 2005/2008 comme serveur de base de données), votre requête utilisera l' Select ROW_NUMBER() Over ...instruction, avec en quelque sorte une pagination directe dans le moteur SQL.

En vous donnant un exemple, j'ai une table db appelée mtcityet j'ai écrit la requête suivante (fonctionne également avec linq aux entités):

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 requête résultante sera:

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]

Ce qui est un accès aux données fenêtré (plutôt cool, btw cuz retournera des données depuis le tout début et accédera à la table tant que les conditions sont remplies). Ce sera très similaire à:

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

Sauf que, cette deuxième requête sera exécutée plus rapidement que le résultat linq car elle utilisera exclusivement l'index pour créer la fenêtre d'accès aux données; cela signifie que si vous avez besoin d'un filtrage, le filtrage doit être (ou doit être) dans la liste des entités (où la ligne est créée) et certains index doivent également être créés pour maintenir les bonnes performances.

Maintenant, quoi de mieux?

Si vous avez un flux de travail assez solide dans votre logique, la mise en œuvre de la méthode SQL appropriée sera compliquée. Dans ce cas, LINQ sera la solution.

Si vous pouvez réduire cette partie de la logique directement en SQL (dans une procédure stockée), ce sera encore mieux car vous pouvez implémenter la deuxième requête que je vous ai montrée (à l'aide d'index) et permettre à SQL de générer et de stocker le plan d'exécution du requête (amélioration des performances).

rodrigoelp
la source
2
Bonne réponse - une expression de table commune est un bon moyen de paginer.
Jarrod Dixon
Pourriez-vous vérifier ma question ( stackoverflow.com/questions/11100929/… )? J'ai créé un SP que j'ai ajouté à mon EDMX et l'ai utilisé dans une requête linq-to-entity.
Misi
2
+1, bonne réponse, j'apprécie que vous expliquiez les avantages en termes de performances du deuxième exemple
Cohen
@Johan: Il existe une alternative appelée méthode de recherche qui surpasse largement les décalages pour les grands nombres de pages.
Lukas Eder
50

Essayez d'utiliser

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

pour obtenir les lignes de 501 à 600 dans le serveur SQL, sans les charger en mémoire. Notez que cette syntaxe est devenue disponible avec SQL Server 2012 uniquement

d.popov
la source
Je pense que c'est incorrect. Le SQL affiché montre les lignes de 502 à 601 (à moins que vous n'indexiez zéro?)
Smudge202
Non, il obtient des lignes de 501 à 600
Volkan Sen
12

Bien que LINQ-to-SQL génère une OFFSETclause (éventuellement émulée en utilisant ROW_NUMBER() OVER() comme d'autres l'ont mentionné ), il existe un moyen entièrement différent et beaucoup plus rapide d'effectuer la pagination en SQL. Ceci est souvent appelé la «méthode de recherche» comme décrit dans cet article de blog ici .

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

Les valeurs @previousScoreet @previousPlayerIdsont les valeurs respectives du dernier enregistrement de la page précédente. Cela vous permet de récupérer la page "suivante". Si la ORDER BYdirection est ASC, utilisez simplement à la >place.

Avec la méthode ci-dessus, vous ne pouvez pas passer immédiatement à la page 4 sans avoir d'abord récupéré les 40 enregistrements précédents. Mais souvent, vous ne voulez pas sauter aussi loin de toute façon. Au lieu de cela, vous obtenez une requête beaucoup plus rapide qui peut être en mesure de récupérer des données en temps constant, en fonction de votre indexation. De plus, vos pages restent «stables», peu importe si les données sous-jacentes changent (par exemple à la page 1, pendant que vous êtes à la page 4).

C'est le meilleur moyen d'implémenter la pagination lors du chargement différé de plus de données dans des applications Web, par exemple.

Notez que la "méthode de recherche" est également appelée pagination de jeu de clés .

Lukas Eder
la source
5

LinqToSql convertira automatiquement un .Skip (N1) .Take (N2) dans la syntaxe TSQL pour vous. En fait, chaque "requête" que vous faites dans Linq, est en fait juste la création d'une requête SQL pour vous en arrière-plan. Pour tester cela, exécutez simplement SQL Profiler pendant que votre application est en cours d'exécution.

La méthodologie skip / take a très bien fonctionné pour moi et pour d'autres d'après ce que j'ai lu.

Par curiosité, quel type de requête d'auto-pagination avez-vous, que vous croyez plus efficace que le saut / prise de Linq?

Mandreko
la source
4

Nous utilisons un CTE enveloppé dans Dynamic SQL (car notre application nécessite un tri dynamique du côté serveur de données) au sein d'une procédure stockée. Je peux vous donner un exemple de base si vous le souhaitez.

Je n'ai pas eu l'occasion d'examiner le T / SQL produit par LINQ. Quelqu'un peut-il publier un échantillon?

Nous n'utilisons pas LINQ ou un accès direct aux tables car nous avons besoin d'une couche de sécurité supplémentaire (étant donné que le SQL dynamique casse quelque peu cela).

Quelque chose comme ça devrait faire l'affaire. Vous pouvez ajouter des valeurs paramétrées pour les paramètres, 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
la source
2
@mrdenny - Un indice pour l'exemple que vous avez fourni: Avec sp_executesqlvous avez la possibilité de passer des paramètres de manière sécurisée, par exemple: EXECUTE sp_executesql 'WITH myCTE AS ... WHERE Col4=@p1) ...', '@p1 nvarchar(max)', @ValueForCol4. Sécurisé dans ce contexte signifie qu'il est robuste contre l'injection SQL - vous pouvez passer toutes les valeurs possibles à l'intérieur de la variable @ValueForCol4- même '--', et la requête fonctionnera toujours!
Matt
1
@mrdenny Bonjour, au lieu de concaténer la requête, nous utilisons quelque chose comme ceci: SELECT ROW_NUMBER() OVER (ORDER BY CASE WHEN @CampoId = 1 THEN Id WHEN @CampoId = 2 THEN field2 END)
Ezequiel
Cela peut produire de terribles plans d'exécution SQL.
mrdenny
@mrdenny: Pour les grands numéros de page, la méthode de recherche peut être beaucoup plus rapide que ROW_NUMBER() OVER()l'émulation de décalage. Voir aussi: 4guysfromrolla.com/webtech/042606-1.shtml
Lukas Eder
2

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

Dans t0 sont tous les enregistrements dans t1 sont seulement ceux correspondant à cette page

ch2o
la source
2

L'approche que je donne est la pagination la plus rapide que le serveur SQL puisse réaliser. J'ai testé cela sur 5 millions de disques. Cette approche est bien meilleure que «OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY» fourni par 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
la source
0

vous pouvez encore améliorer les performances, vérifiez ceci

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 vous utilisez from de cette manière, cela donnera un meilleur résultat:

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

raison: parce que vous utilisez la classe where sur la table CityEntities qui éliminera de nombreux enregistrements avant de rejoindre le MtCity, donc 100% sûr que cela augmentera les performances de plusieurs fois ...

Quoi qu'il en soit, la réponse de rodrigoelp est vraiment utile.

Merci

Ali Adravi
la source
Je doute que l'utilisation de ces conseils ait un impact sur les performances. Impossible de trouver une référence pour cela, mais l'ordre de jointure interne dans la requête peut différer de l'ordre de jointure réel. Ce dernier est décidé par l'optimiseur de requêtes à l'aide des statistiques de la table et des estimations des coûts d'exploitation.
Imre Pühvel
@ImreP: Cela pourrait en fait correspondre quelque peu à la méthode de recherche, que j'ai décrite . Bien que, je ne sais pas où , @p0et plus particulièrement @p1proviennent de
Lukas Eder
0

Vous pouvez implémenter la pagination de cette manière simple en transmettant 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
la source
0

En 2008, nous ne pouvons pas utiliser Skip (). Take ()

Le chemin est:

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