2012-05-14 5 views
10

J'ai une API de pagination qui renvoie les lignes demandées par un utilisateur, mais seulement en même temps, et non pas la totalité de la collection. L'API fonctionne comme prévu, mais je dois calculer le nombre total d'enregistrements disponibles (pour les calculs de page appropriés). Dans l'API, j'utilise Linq2Sql et je travaille beaucoup avec le IQueryable avant de faire mes demandes. Quand je vais obtenir le compte, j'appelle quelque chose comme: totalRecordCount = queryable.Count();Supprimer OrderBy à partir d'un IQueryable <T>

Le SQL qui en résulte n'en est pas moins intéressant, mais il ajoute également un ordre inutile qui rend la requête très coûteuse.

exec sp_executesql N'SELECT COUNT(*) AS [value] 
FROM (
    SELECT TOP (1) NULL AS [EMPTY] 
    FROM [dbo].[JournalEventsView] AS [t0] 
    WHERE [t0].[DataOwnerID] = @p0 
    ORDER BY [t0].[DataTimeStamp] DESC 
    ) AS [t1]',N'@p0 int',@p0=1 

Parce que je suis en utilisant le IQueryable, je peux manipuler le IQueryable avant qu'il fait sur le serveur SQL.

Ma question est, si j'ai déjà un IQueryable avec un OrderBy, est-il possible de supprimer ce OrderBy avant d'appeler le Count()?

comme: totalRecordCount = interrogeable. NoOrder .Count();

Sinon, pas de biggie. Je vois beaucoup de questions sur OrderBy, mais pas sur la suppression d'une OrderBy de l'expression Linq.

Merci!

+2

Pouvez-vous poster plus de votre code? En particulier, je suis intéressé par le code de la requête que vous avez assigné à 'queryable'. –

+0

vous pouvez toujours analyser l'arbre d'expression, puis supprimer le orderby à partir de là – dbarnes

Répondre

6

Il n'y a pas qu'un ORDER BY inutile, il y a aussi un TOP parasite (1).

SELECT TOP (1) NULL AS [EMPTY] ... 

Cette sous-sélection ne retournera que 0 ou 1 lignes. En fait, sans le TOP, il ne serait pas légal d'avoir un ORDER BY dans une sous-sélection.

La clause ORDER BY est invalide dans les vues, fonctions en ligne, les tables dérivées, les sous-requêtes et expressions de table commune, à moins que TOP ou FOR XML est également spécifié .: SELECT COUNT (*) FROM (SELECT * FROM ORDER Tableau 1 foo)

sqlfiddle

Je pense que vous avez probablement fait quelque chose de mal dans votre LINQ. Etes-vous sûr de ne pas avoir écrit .Take(1) ou similaire quelque part dans votre requête, avant d'appeler .Count()?

Ceci est faux:

IQueryable<Foo> foo = (...).OrderBy(x => x.Foo).Take(1); 
int count = foo.Count(); 

Vous devriez faire à la place:

IQueryable<Foo> foo = (...); 
Iqueryable<Foo> topOne = foo.OrderBy(x => x.Foo).Take(1); 
int count = foo.Count(); 
+0

Comprendre totalement ce que vous dites .. Je sais qu'il génère un étrange sql. Mais vraiment plus à la recherche de la réponse à la question, pas vraiment à quoi ressemble le SQL. Mais oui ... c'est assez fugace. – TravisWhidden

+0

Mark a déclaré que cette requête ne compte rien. Êtes-vous sûr que c'est la bonne question? Est-ce qu'il renvoie un nombre plus grand 1? – usr

+0

Juste pour clarifier, Normalement, il n'y a pas de Top (1), mais dans cet exemple, j'ai demandé un seul enregistrement de notre API et il appellerait toujours le .Count() même si le compte pouvait être 1 ou 0. est un top 100000. le Count() semble envelopper les résultats de la requête réelle, mais l'ordre par n'est pas vraiment nécessaire et coûteux pour la requête. Supprimer OrderBy avant le nombre réduirait considérablement le coût SQL tout en restant précis dans le nombre total d'enregistrements à renvoyer. Espérons que cela efface, car oui à première vue je serais comme "wtf est ce gars faire" – TravisWhidden

2

Si vous ne pouvez pas éliminer la cause racine, voici une solution:

totalRecordCount = queryable.OrderBy(x => 0).Count(); 

L'optimiseur de requêtes de SQL Server supprimera cette commande inutile. Il n'aura pas de coût d'exécution.

+0

J'ai donné un tourbillon, mais sans chance. J'espérais que cela supprimerait l'expression OrderBy et OrderByDescending pour l'IQueryable. count = queryResults.OrderBy (x => 0) .OrderByDescending (x => 0).Compter(); mais le résultat contenait toujours la clause Order By. En passant un OrderBy, il devrait remplacer le droit existant OrderBy, ne pas les stagner? – TravisWhidden

+1

Je ne pense pas que l'ajout d'une nouvelle clause 'OrderBy' fera disparaître l'ancien. '.OrderBy (x => x.Foo) .OrderBy (x => x.Bar)' donne les mêmes résultats que '.OrderBy (x => x.Bar) .ThenBy (y => y.Foo)'. De plus, vous n'avez pas cela, vous avez '.OrderBy (x => x.Foo) .Take (1) .OrderBy (x => 0)'. –

+0

Oui, Linq2Sql a omis le x => 0 mais a conservé l'ordre original. Je pense que l'ajout de OrderBy supplémentaires à l'expression doit le rendre similaire à ThenBy. Aussi élégant/hacky que cette solution sonnait, cela n'a pas fonctionné :( – TravisWhidden

2

Je crains qu'il n'y ait aucun moyen facile de supprimer l'opérateur OrderBy de queryable.

Ce que vous pouvez faire, cependant, est de recréer le IQueryable en fonction de la nouvelle expression obtenue à partir de réécriture queryable.Expression (see here) en omettant l'appel OrderBy.

0

Je pense que vous avez mal implémenté votre code de pagination. Vous devez en fait interroger la base de données deux fois, une fois pour la source de données paginée et une fois pour le nombre total de lignes. C'est ainsi que l'installation devrait ressembler.

public IList<MyObj> GetPagedData(string filter, string sort, int skip, int take) 
{ 
    using(var db = new DataContext()) 
    { 
     var q = GetDataInternal(db); 
     if(!String.IsNullOrEmpty(filter)) 
     q = q.Where(filter); //Using Dynamic linq 

     if(!String.IsNullOrEmpty(sort)) 
     q = q.OrderBy(sort); //And here 

     return q.Skip(skip).Take(take).ToList(); 
    } 
} 

public int GetTotalCount(string filter) 
{ 
    using(var db = new DataContext()) 
    { 
     var q = GetDataInternal(db); 
     if(!String.IsNullOrEmpty(filter)) 
     q = q.Where(filter); //Using Dynamic linq 

     return q.Count(); //Without ordering and paging. 
    } 
} 

private static IQuerable<MyObj> GetDataInternal(DataContext db) 
{ 
    return 
     from x in db.JournalEventsView 
     where ... 
     select new ...; 
} 

Le filtrage et le tri est effectué en utilisant la Dynamic linq library

+0

Merci pour l'entrée. En fait, je l'interroge deux fois. Le Count() est pour le nombre de pages, mais j'utilise la même requête pour le jeu de résultats. que j'utilise plus tard quand je fais un saut, prenez: var queryResultsList = orderdResults.Skip ((result.ReturnValue.CurrentPage) * result.ReturnValue.RecordsPerPage) .Take (result.ReturnValue.RecordsPerPage) .ToList(); – TravisWhidden

+0

à partir du code sql généré dans votre question, il semble que vous faites 'Count()' sur l'objet 'IQuerable' _after_' OrderBy' et 'skip/take' est appliqué – Magnus

+0

Nah, le compte était en cours d'exécution après la commande, mais avant le saut/prendre. C'était la principale raison pour laquelle je voulais supprimer la commande, mais j'ai juste créé deux IQueryable comme suggéré ci-dessus, un avec, un sans le genre. – TravisWhidden

5

Ainsi, le code ci-dessous est une pointe contre une matrice en mémoire. Il peut y avoir quelques obstacles pour que cela fonctionne avec Entity Framework (ou une autre implémentation arbitraire IQueryProvider). Fondamentalement, ce que nous allons faire est de visiter l'arbre d'expression et de chercher n'importe quel appel de méthode d'ordre et simplement le retirer de l'arbre. J'espère que cela vous indique la bonne direction.

class Program 
{ 
    static void Main(string[] args) 
    { 
     var seq = new[] { 1, 3, 5, 7, 9, 2, 4, 6, 8 }; 

     var query = seq.OrderBy(x => x); 

     Console.WriteLine("Print out in reverse order."); 
     foreach (var item in query) 
     { 
      Console.WriteLine(item); 
     } 

     Console.WriteLine("Prints out in original order"); 
     var queryExpression = seq.AsQueryable().OrderBy(x => x).ThenByDescending(x => x).Expression; 

     var queryDelegate = Expression.Lambda<Func<IEnumerable<int>>>(new OrderByRemover().Visit(queryExpression)).Compile(); 

     foreach (var item in queryDelegate()) 
     { 
      Console.WriteLine(item); 
     } 


     Console.ReadLine(); 
    } 
} 

public class OrderByRemover : ExpressionVisitor 
{ 
    protected override Expression VisitMethodCall(MethodCallExpression node) 
    { 
     if (node.Method.DeclaringType != typeof(Enumerable) && node.Method.DeclaringType != typeof(Queryable)) 
      return base.VisitMethodCall(node); 

     if (node.Method.Name != "OrderBy" && node.Method.Name != "OrderByDescending" && node.Method.Name != "ThenBy" && node.Method.Name != "ThenByDescending") 
      return base.VisitMethodCall(node); 

     //eliminate the method call from the expression tree by returning the object of the call. 
     return base.Visit(node.Arguments[0]); 
    } 
} 
+0

Juste pour l'exemple, cela mérite certainement une mise à jour. Techniquement pourrait créer une belle méthode d'extension pour Linq. J'ai déjà fait mon re-facteur, mais cela pourrait aussi être une réponse possible à cela. Techniquement, cela répond à la question plus que le travail que nous avons fait ci-dessus. Quelqu'un d'autre est d'accord? Je ne l'ai pas testé. – TravisWhidden

0

Je sais que ce n'est pas tout à fait ce que vous recherchez, mais index sur [DataOwnerID] avec l'inclusion de DataTimeStamp pourrait rendre votre requête moins cher.

Questions connexes