Perché è .Contains slow? Il modo più efficiente per ottenere più quadro tramite chiave primaria?

Qual è il modo più efficace per selezionare più quadro per chiave primaria?

public IEnumerable GetImagesById(IEnumerable ids) { //return ids.Select(id => Images.Find(id)); //is this cool? return Images.Where( im => ids.Contains(im.Id)); //is this better, worse or the same? //is there a (better) third way? } 

Mi rendo conto che potrei fare alcuni test di prestazioni da confrontare, ma mi chiedo se ci sia in effetti un modo migliore di entrambi, e sto cercando qualche chiarimento su quale sia la differenza tra queste due domande, se ce ne sono, una volta che sono state ‘tradotto’.

AGGIORNAMENTO: con l’aggiunta di InExpression in EF6, le prestazioni dell’elaborazione di Enumerable.Contains sono migliorate notevolmente. L’analisi di questa risposta è ottima ma in gran parte obsoleta dal 2013.

L’utilizzo di Contains in Entity Framework è in realtà molto lento. È vero che si traduce in una clausola IN in SQL e che la query SQL stessa viene eseguita velocemente. Ma il problema e il collo di bottiglia delle prestazioni è nella traduzione dalla query LINQ in SQL. L’albero delle espressioni che verrà creato viene espanso in una lunga catena di concatenazioni OR perché non esiste un’espressione nativa che rappresenta un IN . Quando l’SQL viene creato, questa espressione di molte OR viene riconosciuta e compressa nuovamente nella clausola SQL IN .

Ciò non significa che l’utilizzo di Contains sia peggio dell’emissione di una query per elemento nella raccolta ids (la prima opzione). Probabilmente è ancora meglio, almeno per le collezioni non troppo grandi. Ma per le grandi collezioni è davvero pessimo. Ricordo che avevo provato qualche tempo fa una query Contains con circa 12.000 elementi che funzionava ma impiegava circa un minuto anche se la query in SQL veniva eseguita in meno di un secondo.

Potrebbe valere la pena di testare le prestazioni di una combinazione di più roundtrip al database con un numero inferiore di elementi in un’espressione Contains per ogni roundtrip.

Questo approccio e anche i limiti dell’utilizzo di Contains with Entity Framework sono illustrati e spiegati qui:

Perché l’operatore Contains () riduce le prestazioni di Entity Framework in modo così drammatico?

È ansible che un comando SQL grezzo funzioni meglio in questa situazione, il che significherebbe chiamare dbContext.Database.SqlQuery(sqlString) o dbContext.Images.SqlQuery(sqlString) dove sqlString è l’SQL mostrato nella risposta di sqlString .

modificare

Ecco alcune misure:

Ho fatto questo su una tabella con 550000 record e 11 colonne (gli ID iniziano da 1 senza spazi vuoti) e ho scelto in modo casuale 20000 ID:

 using (var context = new MyDbContext()) { Random rand = new Random(); var ids = new List(); for (int i = 0; i < 20000; i++) ids.Add(rand.Next(550000)); Stopwatch watch = new Stopwatch(); watch.Start(); // here are the code snippets from below watch.Stop(); var msec = watch.ElapsedMilliseconds; } 

Test 1

 var result = context.Set() .Where(e => ids.Contains(e.ID)) .ToList(); 

Risultato -> msec = 85,5 sec

Test 2

 var result = context.Set().AsNoTracking() .Where(e => ids.Contains(e.ID)) .ToList(); 

Risultato -> msec = 84,5 sec

Questo piccolo effetto di AsNoTracking è molto insolito. Indica che il collo di bottiglia non è la materializzazione dell'object (e non SQL come mostrato di seguito).

Per entrambi i test è ansible vedere in SQL Profiler che la query SQL arriva al database molto tardi. (Non ho misurato esattamente ma è stato dopo 70 secondi.) Ovviamente la traduzione di questa query LINQ in SQL è molto costosa.

Test 3

 var values = new StringBuilder(); values.AppendFormat("{0}", ids[0]); for (int i = 1; i < ids.Count; i++) values.AppendFormat(", {0}", ids[i]); var sql = string.Format( "SELECT * FROM [MyDb].[dbo].[MyEntities] WHERE [ID] IN ({0})", values); var result = context.Set().SqlQuery(sql).ToList(); 

Risultato -> msec = 5.1 sec

Test 4

 // same as Test 3 but this time including AsNoTracking var result = context.Set().SqlQuery(sql).AsNoTracking().ToList(); 

Risultato -> msec = 3,8 sec

Questa volta l'effetto della distriggerszione del tracciamento è più evidente.

Test 5

 // same as Test 3 but this time using Database.SqlQuery var result = context.Database.SqlQuery(sql).ToList(); 

Risultato -> msec = 3,7 sec

La mia comprensione è che context.Database.SqlQuery(sql) è la stessa di context.Set().SqlQuery(sql).AsNoTracking() , quindi non ci sono differenze tra il Test 4 e il Test 5.

(La lunghezza dei set di risultati non era sempre la stessa a causa di possibili duplicati dopo la selezione di ID casuale ma era sempre tra 19600 e 19640 elementi.)

Modifica 2

Test 6

Anche 20000 roundtrip al database sono più veloci dell'utilizzo di Contains :

 var result = new List(); foreach (var id in ids) result.Add(context.Set().SingleOrDefault(e => e.ID == id)); 

Risultato -> msec = 73,6 sec

Si noti che ho usato SingleOrDefault invece di Find . L'utilizzo dello stesso codice con Find è molto lento (ho annullato il test dopo alcuni minuti) perché Find chiama DetectChanges internamente. Disabilitare il rilevamento del cambio automatico ( context.Configuration.AutoDetectChangesEnabled = false ) porta all'incirca le stesse prestazioni di SingleOrDefault . L'utilizzo di AsNoTracking riduce il tempo di uno o due secondi.

I test sono stati effettuati con client di database (app per console) e server di database sulla stessa macchina. L'ultimo risultato potrebbe peggiorare significativamente con un database "remoto" a causa dei numerosi roundtrip.

La seconda opzione è decisamente migliore della prima. La prima opzione risulterà nelle query ids.Length nel database, mentre la seconda opzione può utilizzare un operatore 'IN' nella query SQL. In pratica trasformsrà la tua query LINQ in qualcosa di simile al seguente SQL:

 SELECT * FROM ImagesTable WHERE id IN (value1,value2,...) 

dove valore1, valore2 ecc. sono i valori della variabile ids. Siate consapevoli, tuttavia, che penso che ci possa essere un limite superiore al numero di valori che possono essere serializzati in una query in questo modo. Vedrò se riesco a trovare qualche documentazione …

Sto usando Entity Framework 6.1 e ho scoperto usando il tuo codice che, è meglio usare:

 return db.PERSON.Find(id); 

piuttosto che:

 return db.PERSONA.FirstOrDefault(x => x.ID == id); 

Le prestazioni di Find () rispetto a FirstOrDefault sono alcune considerazioni su questo.

Weel, recentemente ha un problema simile e il modo migliore che ho trovato è stato inserire l’elenco di contiene in una tabella temporanea e dopo aver fatto un join.

 private List GetFoos(IEnumerable ids) { var sb = new StringBuilder(); sb.Append("DECLARE @Temp TABLE (Id bitint PRIMARY KEY)\n"); foreach (var id in ids) { sb.Append("INSERT INTO @Temp VALUES ('"); sb.Append(id); sb.Append("')\n"); } sb.Append("SELECT f.* FROM [dbo].[Foo] f inner join @Temp t on f.Id = t.Id"); return this.context.Database.SqlQuery(sb.ToString()).ToList(); } 

Non è un bel modo, ma per le liste di grandi dimensioni è molto performante.

La trasformazione dell’elenco in una matrice con toArray () aumenta le prestazioni. Puoi farlo in questo modo:

 ids.Select(id => Images.Find(id)); return Images.toArray().Where( im => ids.Contains(im.Id));