2012-05-14 14 views
10

Ho un'API di paging che restituisce le righe richieste dell'utente, ma solo tante in una volta, non l'intera raccolta. L'API funziona come progettato, ma devo calcolare il numero totale di record disponibili (per calcoli di pagina corretti). All'interno dell'API, io uso Linq2Sql e lavoro molto con IQueryable prima di poter finalmente effettuare le mie richieste. Quando vado a prendere il conteggio, chiamo qualcosa come: totalRecordCount = queryable.Count();Rimuovi ordine da un IQueryable <T>

L'SQL risultante è comunque interessante, ma aggiunge anche un ordine non necessario che rende la query molto costosa.

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 

perché sto usando l'IQueryable, posso manipolare l'IQueryable prima di esso che lo rende al server SQL.

La mia domanda è, se ho già un IQueryable con un OrderBy in esso, è possibile rimuovere quel OrderBy prima che chiamo il conteggio()?

come: totalRecordCount = queryable. NoOrder .Count();

In caso contrario, nessun problema. Vedo molte domande su OrderBy, ma non tutte le operazioni che comportano la rimozione di un OrderBy dall'espressione Linq.

Grazie!

+2

Puoi pubblicare altro codice? In particolare, mi interessa il codice per la query che hai assegnato a 'queryable'. –

+0

si può sempre analizzare l'albero delle espressioni e quindi rimuovere l'ordine da lì – dbarnes

risposta

6

Non c'è solo un ordine non necessario, c'è anche un TOP spuri (1).

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

Quella sottoselezione restituirà solo 0 o 1 righe. In effetti senza il TOP non sarebbe legale avere un ORDER BY in una sottoselezione.

La clausola ORDER BY non è valida in viste, funzioni inline, tabelle derivate, subquery, e le espressioni di tabella comuni, a meno che TOP o FOR XML si specifica anche .: SELECT COUNT (*) FROM (SELECT * FROM Tabella 1 ORDINE BY foo)

sqlfiddle

Penso che probabilmente avete fatto qualcosa di sbagliato nel vostro LINQ. Sei sicuro di non aver scritto .Take(1) o simile da qualche parte nella tua query, prima di chiamare .Count()?

Questo è sbagliato:

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

Si dovrebbe fare questo, invece:

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

Capire completamente quello che stai dicendo .. So che sta generando qualche strano sql. Ma davvero più alla ricerca della risposta alla domanda, non proprio su come si presenta l'SQL. Ma sì ... è abbastanza fugace. – TravisWhidden

+0

Mark ha dichiarato che questa query non conta nulla. Sei sicuro che questa sia la domanda giusta? Restituisce un conteggio maggiore 1? – usr

+0

Giusto per chiarire, Normalmente non c'è un Top (1), ma in questo esempio ho richiesto solo un record dalla nostra API e chiamerebbe comunque il .Count() anche se il conteggio potrebbe essere 1 o 0. Di solito c'è è il primo 100000. Il conteggio() sembra racchiudere i risultati della query effettiva, ma l'ordine per non è realmente necessario e costoso per la query. La rimozione dell'Ordine prima del conteggio ridurrebbe drasticamente il costo SQL pur rimanendo accurato nel numero totale di record da restituire. Spero che questo chiarisca tutto, perché sì a prima vista vorrei dire "wtf is that guy doing" – TravisWhidden

2

Se non è possibile eliminare la causa principale, qui è una soluzione:

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

Query Optimizer di SQL Server rimuoverà questo ordinamento inutile. Non avrà costi di runtime.

+0

Ho fatto un vortice, ma senza fortuna. Speravo che avrebbe rimosso l'espressione OrderBy e OrderByDescending per IQueryable. count = queryResults.OrderBy (x => 0) .OrderByDescending (x => 0).Contare(); ma il risultato conteneva ancora la clausola Order By. Passando un OrderBy, dovrebbe sostituire l'OrderBy esistente a destra, non barcollare loro giusto? – TravisWhidden

+1

Non penso che l'aggiunta di una nuova clausola 'OrderBy' farà sparire il vecchio. '.OrderBy (x => x.Foo) .OrderBy (x => x.Bar)' dà gli stessi risultati di '. OrdderBy (x => x.Bar) .ThenBy (y => y.Foo)'. Inoltre, non ce l'hai, hai '.OrderBy (x => x.Foo) .Take (1) .OrderBy (x => 0)'. –

+0

Sì, Linq2Sql ha omesso x => 0 ma ha mantenuto l'ordine originale. Penso che aggiungere ulteriori OrderBy all'espressione deve renderlo simile a ThenBy. Come elegante/hacky come questa soluzione ha funzionato, non ha funzionato :( – TravisWhidden

2

Ho paura che non vi è un modo semplice per rimuovere l'operatore OrderBy da queryable.

Cosa si può fare, però, è quello di ricreare il IQueryable basato sulla nuova espressione ottenuta da riscrivere queryable.Expression (see here) omettendo la chiamata OrderBy.

0

Penso che tu abbia implementato il codice di impaginazione erroneamente. In realtà è necessario interrogare il database due volte, una volta per l'origine dati paginata e una volta per il conteggio totale delle righe. Ecco come dovrebbe apparire l'installazione.

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 ...; 
} 

Il filtraggio e l'ordinamento viene fatto usando il Dynamic linq library

+0

Grazie per l'input. In realtà lo interrogo due volte. Il conteggio() è per il conteggio delle pagine, ma io uso la stessa queryable per il set di risultati. che uso più tardi quando eseguo uno Skip, Take: var queryResultsList = orderdResults.Skip ((result.ReturnValue.CurrentPage) * result.ReturnValue.RecordsPerPage) .Take (result.ReturnValue.RecordsPerPage) .ToList(); – TravisWhidden

+0

dal codice sql generato nella tua domanda sembra che tu stia facendo 'Count()' sull'oggetto 'IQuerable' _after_' OrderBy' e 'skip/take' è applicato – Magnus

+0

Nah, il conteggio è stato eseguito dopo il orderby, ma prima di saltare/prendere. Questa era la ragione principale per cui volevo rimuovere l'orderby, ma ho appena creato due IQueryable come suggerito sopra, uno con, uno senza l'ordinamento. – TravisWhidden

5

Così, il codice sotto è un picco contro una matrice in memoria. Potrebbero esserci alcuni ostacoli per farlo funzionare con Entity Framework (o qualche altra implementazione IQueryProvider arbitraria). Fondamentalmente, quello che faremo è visitare l'albero delle espressioni e cercare qualsiasi chiamata al metodo di ordinamento e semplicemente rimuoverla dall'albero. Spero che questo ti guidi nella giusta direzione.

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

Solo per l'esempio, questo merita sicuramente un voto in rialzo. Tecnicamente potrebbe creare un buon metodo di estensione per Linq. Ho già fatto il mio re-factor, ma anche questa potrebbe essere una possibile risposta a questo. Tecnicamente questo risponde alla domanda più del lavoro che abbiamo fatto sopra. Qualcun altro è d'accordo? Non l'ho provato – TravisWhidden

0

So che non è proprio quello che state cercando, ma indice su [DataOwnerID] con inclusione di DataTimeStamp potuto fare la query meno costoso.

Problemi correlati