2013-08-20 9 views
21

Perché forzare la materializzazione utilizzando ToList() rende i miei ordini di query di grandezza più veloci quando, semmai, dovrebbe fare l'esatto opposto?Perché l'aggiunta di una ToList() non necessaria velocizza drasticamente questa query LINQ?

1) Chiamare First() immediatamente

// "Context" is an Entity Framework DB-first model 

    var query = from x in Context.Users 
       where x.Username.ToLower().Equals(User.Identity.Name.ToLower()) 
       select x; 

    var User = query.First(); 

    // ** The above takes 30+ seconds to run ** 

2) Calling First()dopo chiamando ToList():

var query = from x in Context.Users 
       where x.Username.ToLower().Equals(User.Identity.Name.ToLower()) 
       select x; 

    var User = query.ToList().First();  // Added ToList() before First() 

    // ** Now it takes < 1 second to run! ** 

Update e risoluzione

Dopo avere ottenuto l'SQL generato, l'unica differenza è, come previsto, l'aggiunta di TOP (1) nella prima query. Come dice Andyz Smith nella sua risposta di seguito, la causa principale è che l'ottimizzatore di SQL Server, in questo caso particolare, sceglie un piano di esecuzione peggiore quando viene aggiunto TOP (1). Quindi il problema non ha nulla a che fare con LINQ (che ha fatto la cosa giusta aggiungendo TOP (1)) e tutto ciò che ha a che fare con le idiosincrasie di SQL Server.

+2

Poiché il problema ha continuato ad essere indagato, la questione è stata svolta in un'altra domanda, quindi ho pensato che avrei dovuto ripulirlo e contrassegnarlo come risposta, dal momento che la domanda originale (perché LINQ sembrava fare qualcosa di molto strano) è stata effettivamente risposta. Grazie a tutti per l'aiuto su questo. – JoeCool

+0

A meno che non ci sia un buon motivo per mantenere chiusa questa domanda dopo averla drasticamente pulita, riaprire. Grazie. – JoeCool

+0

Se si sta solo per un singolo record, si dovrebbe usare .Single, usando. Per prima cosa si ha la possibilità che ci sia più di una riga corrispondente e non si possono ottenere i dati che ci si aspetta. – Chris

risposta

0

Quindi, l'ottimizzatore sceglie un modo scorretto per eseguire la query.

Poiché non è possibile aggiungere suggerimenti di ottimizzazione all'SQL per forzare l'ottimizzatore a scegliere un piano migliore, vedo due opzioni.

  1. Aggiungere un indice di copertura/vista indicizzata su tutte le colonne che vengono recuperati/inclusi nel selezionare Abbastanza ridicolo, ma penso che funzionerà, perché tale indice renderà più facile peasy per l'ottimizzatore di scegli un piano migliore.

  2. Sempre in modo prematuro materializzare le query che includono Primo o Ultimo o Take. Pericoloso perché man mano che i dati diventano più grandi, il break even point tra il pull di tutti i dati localmente e il primo() e la ricerca con Top sul server sta per cambiare.

http://geekswithblogs.net/Martinez/archive/2013/01/30/why-sql-top-may-slow-down-your-query-and-how.aspx

https://groups.google.com/forum/m/#!topic/microsoft.public.sqlserver.server/L2USxkyV1uw

http://connect.microsoft.com/SQLServer/feedback/details/781990/top-1-is-not-considered-as-a-factor-for-query-optimization

TOP slows down query

Why does TOP or SET ROWCOUNT make my query so slow?

+0

Grazie a tutti coloro che hanno commentato in modo ordinato per risolvere il problema. 'upvote' –

+0

Grazie per l'aiuto, e anche grazie a @ MartinSmith che ha dedicato molto tempo a questa domanda. – JoeCool

3

L'SQL non sarà lo stesso di Linq è lazy loading. Quindi la tua chiamata a .ToList() forzerà. Net per valutare l'espressione, quindi in memoria selezionare l'elemento first().

Dove, come l'altra opzione dovrebbe aggiungere top 1 in SQL

AD ESEMPIO

var query = from x in Context.Users 
       where x.Username.ToLower().Equals(User.Identity.Name.ToLower()) 
       select x; 

//SQL executed here 
var User = query.First(); 

e

var query = from x in Context.Users 
       where x.Username.ToLower().Equals(User.Identity.Name.ToLower()) 
       select x; 

//SQL executed here! 
var list = query.ToList(); 
var User = query.First(); 

Come sotto, la prima query dovrebbe essere più veloce! Suggerirei di fare un SQL profiler per vedere cosa sta succedendo. La velocità delle query dipenderà dalla struttura dei dati, dal numero di record, indici, ecc.

I tempi del test modificheranno anche i risultati. Come un paio di persone hanno menzionato nei commenti, la prima volta che si preme EF è necessario inizializzare e caricare i metadati. quindi se li esegui insieme, il primo dovrebbe essere sempre lento.

Ecco qualche informazione in più su EF performance considerations

avviso la linea:

modello e la mappatura dei metadati usati per Entity Framework viene caricato in un MetadataWorkspace. Questi metadati sono memorizzati nella cache globalmente ed è disponibile in altre istanze di ObjectContext nello stesso dominio dell'applicazione.

&

Perché una connessione aperta al database consuma una risorsa preziosa , Entity Framework apre e chiude la connessione al database solo se necessario.È anche possibile aprire esplicitamente la connessione . Per ulteriori informazioni, vedere Gestione delle connessioni e Transazioni in Entity Framework.

+7

Ciò renderebbe la seconda query più lunga, non meno tempo. –

+0

^Esattamente. Sapevo che era un SQL diverso, quindi avrebbe dovuto impiegare meno tempo se non altro! – JoeCool

+1

'.ToList() imporrà .Net per valutare l'espressione' ma' First() 'fa anche lo stesso (forzarlo). –

11

posso solo pensare a una ragione ... Per provarlo, si può rimuovere la clausola Where e ri-eseguire il test? Commenta qui se il risultato è che la prima affermazione è più veloce e spiegherò perché.

Modifica
Nella dichiarazione LINQ clausola Where, si utilizza il metodo .ToLower() della stringa. La mia ipotesi è che LINQ non hanno costruito nella conversione di SQL per questo metodo, in modo che lo SQL risultante è qualcosa di linea

SELECT * 
FROM Users 

Ora, sappiamo che LINQ carichi pigri, ma sa anche che, dato che non ha valutato la clausola WHERE, ha bisogno di caricare gli elementi per fare il confronto.

Ipotesi
La prima interrogazione è lazy loading ogni elemento nel set di risultati. Quindi esegue il confronto .ToLower() e restituisce il primo risultato. Ciò si traduce in n richieste al server e un enorme overhead delle prestazioni. Non posso essere sicuro senza vedere il Tracelog SQL.

La seconda affermazione chiama ToList, che richiede un batch SQL prima di fare il confronto ToLower, con conseguente solo una richiesta al server

ipotesi alternativa
Se il profiler mostra solo un'esecuzione server, provare l'esecuzione la stessa query con la clausola Top 1 e vedere se ci vuole tanto tempo. In base a questo post (Why is doing a top(1) on an indexed column in SQL Server slow?), la clausola TOP può talvolta compromettere l'ottimizzatore del server SQL e interromperlo utilizzando gli indici corretti.

Curiosità Modifica
provare a cambiare il LINQ to

var query = from x in Context.Users 
      where x.Username.Equals(User.Identity.Name, StringComparison.OrdinalIgnoreCase) 
      select x; 

credito a @ Scott per trovare il modo di fare confronto case insensitive in LINQ. Provalo e vedi se è più veloce.

+1

Hai ragione! Ho eliminato completamente la clausola where e qui ci sono i miei risultati: con ToList() = 0.74s, senza ToList() = 0,33 secondi. Quei numeri hanno molto più senso! – JoeCool

+0

@JoeCool, puoi provare la modifica della curiosità? Sarei davvero curioso di vedere se è il tolower o il '.Equals()' che causa il problema –

+0

Come sidenote il modo "corretto" per fare un case insensibile confrontare usando EF (secondo [questo SO post] (http : //stackoverflow.com/questions/18337103/why-does-adding-an-unnecessary-tolist-drastically-speed-this-linq-query-up)) è 'x.Username.Equals (User.Identity.Name, StringComparison.OrdinalIgnoreCase) '. –

Problemi correlati