it-swarm.com.de

Warum ist .Contains langsam? Der effizienteste Weg, um mehrere Entitäten nach Primärschlüssel zu erhalten?

Was ist der effizienteste Weg, um mehrere Entitäten nach Primärschlüssel auszuwählen?

public IEnumerable<Models.Image> GetImagesById(IEnumerable<int> 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?

}

Ich erkenne, dass ich einige Leistungstests durchführen könnte, um zu vergleichen, aber ich frage mich, ob es tatsächlich einen besseren Weg als beide gibt, und suche nach Aufklärung darüber, was der Unterschied zwischen diesen beiden Abfragen ist, wenn überhaupt 'übersetzt'.

54
Tom

UPDATE: Mit dem Hinzufügen von InExpression in EF6 wurde die Verarbeitungsleistung von Enumerable.Contains erheblich verbessert. Die Analyse in dieser Antwort ist großartig, aber seit 2013 weitgehend veraltet.

Die Verwendung von Contains in Entity Framework ist tatsächlich sehr langsam. Es ist wahr, dass es in eine IN-Klausel in SQL übersetzt wird und dass die SQL-Abfrage selbst schnell ausgeführt wird. Das Problem und der Leistungsengpass liegt jedoch in der Übersetzung Ihrer LINQ-Abfrage in SQL. Der Ausdrucksbaum, der erstellt wird, wird zu einer langen Kette von OR-Verkettungen erweitert, da es keinen nativen Ausdruck gibt, der eine IN darstellt. Wenn das SQL erstellt wird, wird dieser Ausdruck von vielen ORs erkannt und in die SQL-Klausel IN zurückgefallen.

Dies bedeutet nicht, dass die Verwendung von Contains schlechter ist als das Ausgeben einer Abfrage pro Element in Ihrer ids-Auflistung (Ihre erste Option). Es ist wahrscheinlich immer noch besser - zumindest für nicht zu große Sammlungen. Aber für große Sammlungen ist es wirklich schlecht. Ich erinnere mich, dass ich vor einiger Zeit eine Contains-Abfrage mit etwa 12.000 Elementen getestet hatte, die funktionierte, aber etwa eine Minute dauerte, obwohl die Abfrage in SQL in weniger als einer Sekunde ausgeführt wurde.

Es kann sich lohnen, die Leistung einer Kombination mehrerer Roundtrips in die Datenbank mit einer geringeren Anzahl von Elementen in einem Contains-Ausdruck für jeden Roundtrip zu testen.

Dieser Ansatz sowie die Einschränkungen bei der Verwendung von Contains mit Entity Framework werden hier gezeigt und erläutert:

Warum verringert der Contains () - Operator die Leistung von Entity Framework so drastisch?

Es ist möglich, dass ein unbearbeiteter SQL-Befehl in dieser Situation die beste Leistung bringt. Dies bedeutet, dass Sie dbContext.Database.SqlQuery<Image>(sqlString) oder dbContext.Images.SqlQuery(sqlString) aufrufen, wobei sqlString die in @ Rune's Antwort angegebene SQL ist.

Bearbeiten

Hier sind einige Messungen:

Ich habe dies für eine Tabelle mit 550000 Datensätzen und 11 Spalten getan (IDs beginnen mit 1 ohne Lücken) und habe zufällig 20000 IDs ausgewählt:

using (var context = new MyDbContext())
{
    Random Rand = new Random();
    var ids = new List<int>();
    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<MyEntity>()
    .Where(e => ids.Contains(e.ID))
    .ToList();

Ergebnis -> ms = 85,5 s

Test 2

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

Ergebnis -> ms = 84,5 s

Dieser winzige Effekt von AsNoTracking ist sehr ungewöhnlich. Es zeigt an, dass der Engpass keine Objektmaterialisierung ist (und nicht SQL (siehe unten)).

In beiden Tests ist in SQL Profiler zu erkennen, dass die SQL-Abfrage sehr spät in der Datenbank ankommt. (Ich habe nicht genau gemessen, aber es war später als 70 Sekunden.) Offensichtlich ist die Übersetzung dieser LINQ-Abfrage in SQL sehr teuer.

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<MyEntity>().SqlQuery(sql).ToList();

Ergebnis -> ms = 5,1 s

Test 4

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

Ergebnis -> ms = 3,8 s

Diesmal ist der Effekt der Deaktivierung der Nachverfolgung auffälliger.

Test 5

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

Ergebnis -> ms = 3,7 s

Ich verstehe, dass context.Database.SqlQuery<MyEntity>(sql) dasselbe ist wie context.Set<MyEntity>().SqlQuery(sql).AsNoTracking(), sodass zwischen Test 4 und Test 5 kein Unterschied erwartet wird.

(Die Länge der Ergebnismengen war aufgrund von möglichen Duplikaten nach der Zufalls-ID-Auswahl nicht immer gleich, betrug jedoch immer zwischen 19600 und 19640 Elementen.)

Edit 2

Test 6

Selbst 20000 Roundtrips zur Datenbank sind schneller als die Verwendung von Contains:

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

Ergebnis -> ms = 73,6 s

Beachten Sie, dass ich SingleOrDefault anstelle von Find verwendet habe. Die Verwendung desselben Codes mit Find ist sehr langsam (ich habe den Test nach einigen Minuten abgebrochen), da Find intern DetectChanges aufruft. Das Deaktivieren der automatischen Änderungserkennung (context.Configuration.AutoDetectChangesEnabled = false) führt zu ungefähr derselben Leistung wie SingleOrDefault. Die Verwendung von AsNoTracking reduziert die Zeit um ein oder zwei Sekunden.

Die Tests wurden mit Datenbankclient (Konsolen-App) und Datenbankserver auf demselben Computer durchgeführt. Das letzte Ergebnis kann bei einer "entfernten" Datenbank aufgrund der vielen Rundreisen erheblich schlechter werden.

127
Slauma

Die zweite Option ist definitiv besser als die erste. Die erste Option führt zu ids.Length-Abfragen an die Datenbank, während die zweite Option einen 'IN'-Operator in der SQL-Abfrage verwenden kann. Im Wesentlichen wird Ihre LINQ-Abfrage in etwas wie das folgende SQL umgewandelt:

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

dabei sind Wert1, Wert2 usw. die Werte Ihrer ids-Variablen. Beachten Sie jedoch, dass es meiner Meinung nach eine Obergrenze für die Anzahl der Werte gibt, die auf diese Weise in eine Abfrage serialisiert werden können. Ich werde sehen, ob ich Dokumentation finden kann ...

4
Rune

Weel, kürzlich ein ähnliches Problem und die beste Methode, die ich gefunden habe, war, die Liste der enthaltenen Inhalte in eine temporäre Tabelle einzufügen und danach einen Join zu machen.

private List<Foo> GetFoos(IEnumerable<long> 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<Foo>(sb.ToString()).ToList();
}

Es ist kein schöner Weg, aber für große Listen ist es sehr performant.

0
nelson eldoro

Das Transformieren der Liste in ein Array mit toArray () erhöht die Leistung. Sie können es so machen:

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

Ich verwende Entity Framework 6.1 und habe herausgefunden, dass Ihr code das besser ist:

return db.PERSON.Find(id);

eher, als:

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

Performance von Find () vs. FirstOrDefault sind einige Gedanken dazu.

0
Juanito