2012-04-09 10 views
16

Ich versuche, Code neu zu gestalten, der in letzter Zeit sehr langsam geworden ist, und ich stieß auf einen Codeblock, der mehr als 5 Sekunden für die Ausführung benötigt.Entity Framework + LINQ + "Enthält" == Super Slow?

Der Code besteht aus zwei Aussagen:

IEnumerable<int> StudentIds = _entities.Filters 
        .Where(x => x.TeacherId == Profile.TeacherId.Value && x.StudentId != null) 
        .Select(x => x.StudentId) 
        .Distinct<int>(); 

und

_entities.StudentClassrooms 
        .Include("ClassroomTerm.Classroom.School.District") 
        .Include("ClassroomTerm.Teacher.Profile") 
        .Include("Student") 
        .Where(x => StudentIds.Contains(x.StudentId) 
        && x.ClassroomTerm.IsActive 
        && x.ClassroomTerm.Classroom.IsActive 
        && x.ClassroomTerm.Classroom.School.IsActive 
        && x.ClassroomTerm.Classroom.School.District.IsActive).AsQueryable<StudentClassroom>(); 

So ist es ein bisschen chaotisch, aber zuerst bekomme ich eine Distinct Liste von IDs aus einer Tabelle (Filter), dann ich abfragen eine andere Tabelle, die es benutzt.

Dies sind relativ kleine Tabellen, aber es ist immer noch 5+ Sekunden Abfragezeit.

Ich legte dies in LINQPad und es zeigte, dass es zuerst die untere Abfrage und dann 1000 "distinct" Abfragen später ausgeführt wurde.

Aus einer Laune heraus änderte ich den "StudentIds" -Code, indem ich am Ende nur .ToArray() hinzufügte. Dies hat die Geschwindigkeit 1000x verbessert ... es dauert jetzt 100ms, um die gleiche Abfrage zu vervollständigen.

Was ist das Geschäft? Was mache ich falsch?

+2

'Was mache ich falsch?' Ähm ... macht StudentID nicht zu einem Array? :) –

+1

In anderen Nachrichten, ist Linqpad nicht ein cooles Werkzeug? –

+0

Dies ist einer der Fälle, in denen "var" den Code magisch schneller gemacht hätte, indem festgestellt wurde, dass StudentIds IQueryable sein könnten. –

Antwort

24

Dies ist eine der Fallstricke der verzögerten Ausführung in Linq: In Ihrem ersten Ansatz StudentIds ist wirklich eine IQueryable, keine In-Memory-Sammlung. Das heißt, wenn Sie es in der zweiten Abfrage verwenden, wird die Abfrage erneut in der Datenbank ausgeführt - jedes Mal.

Erzwingen der Ausführung der ersten Abfrage von ToArray() Verwendung macht StudentIds eine speicherinterne Sammlung und den Contains Teil in Ihre zweite Abfrage wird diese Sammlung überfahren, die eine festgelegte Abfolge von Elementen enthält - Dies wird äquivalent zu einer SQL etwas kartiert where StudentId in (1,2,3,4) Abfrage.

Diese Abfrage wird natürlich viel viel schneller sein, da Sie diese Sequenz einmal im Voraus bestimmt haben, und nicht jedes Mal, wenn die Where-Klausel ausgeführt wird. Ihre zweite Abfrage ohne Verwendung von ToArray() (würde ich denken) würde einer SQL-Abfrage mit einer where exists (...) Unterabfrage zugeordnet werden, die für jede Zeile ausgewertet wird.

+0

Danke, ich habe nicht lange genug nachgedacht, warum es überhaupt keine In-Memory-Sammlung machen möchte. Ich habe nur angenommen, und Sie wissen, was passiert, wenn Sie annehmen: - P –

+2

Ich verstehe es nicht :(Ich würde zustimmen über die verzögerte Ausführung Argument * wenn * die zweite Abfrage wäre LINQ zu Objekten. Aber anscheinend ist es LINQ zu Entities Die zweite Abfrage wird in SQL übersetzt (nur einmal) und dann wird die SQL ausgeführt.Auch für den Ausdruck ist der * deklarierte * Typ von 'StudentIds' wichtig, nicht der Laufzeittyp.Es ist ein Unterschied, wenn Sie' IEnumerable 'verwenden oder wenn Sie 'var' verwenden (=' IQueryable '). Im ersten Fall wird die erste Abfrage einmal ausgeführt und die zweite Abfrage wird in' IN' übersetzt, der zweite Fall ist 'exist (subquery)' weiß, wo 1000 Abfragen herkommen – Slauma

+0

@Slauma: Wenn ich es heute mit frischen Augen betrachte, denke ich, dass du recht hast - es sollte nur zwei Fälle geben - entweder die 'where in..' Abfrage oder die' wo existiert (Unterabfrage) '. Der spätere könnte weniger effizient sein (dh einige Indizes fehlen) Dies erklärt, warum die Verwendung von IQ Queryable in diesem Fall schlechter als IEnumerable ist. Es erklärt jedoch nicht den Unterschied, der bei Verwendung von ToArray() zu sehen ist. Die Timing-Ergebnisse von OP deuten auch darauf hin, dass jede der drei Versionen (IEnumerable, IQueryable, IEnumerable + ToArray) sehr unterschiedliche Leistungsmerkmale aufweist. Vielleicht kann ein Experte einspringen. – BrokenGlass

4

ToArray() Verarbeitet die anfängliche Abfrage zum Serverspeicher.

Meine Vermutung wäre, dass der Abfrageanbieter den Ausdruck StudentIds.Contains(x.StudentId) nicht parsen kann. Daher denkt es wahrscheinlich, dass das studentIds ein Array ist, das bereits in den Speicher geladen wurde. Es wird also wahrscheinlich während der Analysephase die Datenbank immer wieder abgefragt. Der einzige Weg, um sicher zu wissen, ist, den Profiler einzurichten.

Wenn Sie dies auf dem Datenbankserver tun müssen, verwenden Sie einen Join statt "enthält". Wenn Sie contains verwenden müssen, um etwas zu tun, das wie ein Join-Problem aussieht, fehlt Ihnen wahrscheinlich irgendwo ein Ersatz-Primärschlüssel oder ein Fremdschlüssel.

Sie könnten auch studentIds als IQueryable anstelle von IEnumerable deklarieren. Dies kann dem Abfrageanbieter den Hinweis geben, der benötigt wird, um die studentIds als Ausdruck aka zu interpretieren. Daten nicht bereits in den Speicher geladen. Ich bezweifle das irgendwie, aber einen Versuch wert.

Wenn alles andere fehlschlägt, verwenden Sie ToArray(). Dies lädt die ursprüngliche studentIds in den Speicher.

+0

+1 für die Empfehlung, dies zu einem Join zu restrukturieren. Je mehr Student-IDs sie haben, desto weniger skalierbar ist ein contains/in. – Devin