UPDATE 3: Laut dieser Ankündigung wurde dies vom EF-Team in EF6 alpha 2 behoben.
UPDATE 2: Ich habe einen Vorschlag zur Behebung dieses Problems erstellt. Um dafür zu stimmen, klicken Sie hier .
Stellen Sie sich eine SQL-Datenbank mit einer sehr einfachen Tabelle vor.
CREATE TABLE Main (Id INT PRIMARY KEY)
Ich fülle die Tabelle mit 10.000 Datensätzen.
WITH Numbers AS
(
SELECT 1 AS Id
UNION ALL
SELECT Id + 1 AS Id FROM Numbers WHERE Id <= 10000
)
INSERT Main (Id)
SELECT Id FROM Numbers
OPTION (MAXRECURSION 0)
Ich erstelle ein EF-Modell für die Tabelle und führe die folgende Abfrage in LINQPad aus (ich verwende den Modus "C # -Anweisungen", damit LINQPad nicht automatisch einen Speicherauszug erstellt).
var rows =
Main
.ToArray();
Die Ausführungszeit beträgt ~ 0,07 Sekunden. Jetzt füge ich den Operator Enthält hinzu und führe die Abfrage erneut aus.
var ids = Main.Select(a => a.Id).ToArray();
var rows =
Main
.Where (a => ids.Contains(a.Id))
.ToArray();
Die Ausführungszeit für diesen Fall beträgt 20,14 Sekunden (288-mal langsamer)!
Zuerst vermutete ich, dass die Ausführung des für die Abfrage ausgegebenen T-SQL länger dauerte, und versuchte, es aus dem SQL-Bereich von LINQPad auszuschneiden und in SQL Server Management Studio einzufügen.
SET NOCOUNT ON
SET STATISTICS TIME ON
SELECT
[Extent1].[Id] AS [Id]
FROM [dbo].[Primary] AS [Extent1]
WHERE [Extent1].[Id] IN (1,2,3,4,5,6,7,8,...
Und das Ergebnis war
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 88 ms.
Als nächstes vermutete ich, dass LINQPad das Problem verursacht, aber die Leistung ist gleich, unabhängig davon, ob ich es in LINQPad oder in einer Konsolenanwendung ausführe.
Es scheint also, dass das Problem irgendwo im Entity Framework liegt.
Mache ich hier etwas falsch? Dies ist ein zeitkritischer Teil meines Codes. Kann ich also etwas tun, um die Leistung zu beschleunigen?
Ich verwende Entity Framework 4.1 und SQL Server 2008 R2.
UPDATE 1:
In der folgenden Diskussion gab es einige Fragen dazu, ob die Verzögerung aufgetreten ist, während EF die erste Abfrage erstellt oder die zurückerhaltenen Daten analysiert hat. Um dies zu testen, habe ich den folgenden Code ausgeführt:
var ids = Main.Select(a => a.Id).ToArray();
var rows =
(ObjectQuery<MainRow>)
Main
.Where (a => ids.Contains(a.Id));
var sql = rows.ToTraceString();
Dadurch wird EF gezwungen, die Abfrage zu generieren, ohne sie für die Datenbank auszuführen. Das Ergebnis war, dass für die Ausführung dieses Codes ~ 20 Sekunden erforderlich waren. Es scheint also, dass fast die gesamte Zeit für die Erstellung der ersten Abfrage benötigt wird.
CompiledQuery zur Rettung dann? Nicht so schnell ... CompiledQuery erfordert, dass die an die Abfrage übergebenen Parameter grundlegende Typen sind (int, string, float usw.). Es werden keine Arrays oder IEnumerable akzeptiert, daher kann ich es nicht für eine Liste von IDs verwenden.
var qry = Main.Where (a => ids.Contains(a.Id)); var rows = qry.ToArray();
zu sehen, welcher Teil der Abfrage die Zeit in Anspruch nimmt?parent._recompileRequired = () => true;
dies für alle Abfragen geschieht, die einen IEnumerable <T> -Parameter enthalten. Boo!Antworten:
UPDATE: Mit dem Hinzufügen von InExpression in EF6 wurde die Leistung der Verarbeitung von Enumerable.Contains erheblich verbessert. Der in dieser Antwort beschriebene Ansatz ist nicht mehr erforderlich.
Sie haben Recht, dass die meiste Zeit für die Verarbeitung der Übersetzung der Abfrage aufgewendet wird. Das Anbietermodell von EF enthält derzeit keinen Ausdruck, der eine IN-Klausel darstellt. Daher können ADO.NET-Anbieter IN nicht nativ unterstützen. Stattdessen übersetzt die Implementierung von Enumerable.Contains es in einen Baum von ODER-Ausdrücken, dh für etwas, das in C # so aussieht:
new []{1, 2, 3, 4}.Contains(i)
... wir werden einen DbExpression-Baum generieren, der folgendermaßen dargestellt werden könnte:
((1 = @i) OR (2 = @i)) OR ((3 = @i) OR (4 = @i))
(Die Ausdrucksbäume müssen ausgeglichen sein, denn wenn wir alle OPs über einem einzigen langen Rücken hätten, wäre die Wahrscheinlichkeit größer, dass der Ausdrucksbesucher einen Stapelüberlauf erleidet (ja, das haben wir in unseren Tests tatsächlich erreicht).)
Wir senden später einen solchen Baum an den ADO.NET-Anbieter, der dieses Muster erkennen und während der SQL-Generierung auf die IN-Klausel reduzieren kann.
Als wir die Unterstützung für Enumerable.Contains in EF4 hinzufügten, hielten wir es für wünschenswert, dies zu tun, ohne die Unterstützung für IN-Ausdrücke im Anbietermodell einführen zu müssen, und ehrlich gesagt sind 10.000 viel mehr als die Anzahl der Elemente, an die Kunden voraussichtlich weitergeben würden Aufzählbar. Enthält. Ich verstehe jedoch, dass dies ärgerlich ist und dass die Manipulation von Ausdrucksbäumen die Dinge in Ihrem speziellen Szenario zu teuer macht.
Ich habe dies mit einem unserer Entwickler besprochen und wir glauben, dass wir in Zukunft die Implementierung ändern können, indem wir erstklassigen Support für IN hinzufügen. Ich werde sicherstellen, dass dies zu unserem Rückstand hinzugefügt wird, aber ich kann nicht versprechen, wann es dazu kommt, da wir noch viele andere Verbesserungen vornehmen möchten.
Zu den bereits im Thread vorgeschlagenen Problemumgehungen möchte ich Folgendes hinzufügen:
Erstellen Sie eine Methode, die die Anzahl der Datenbank-Roundtrips mit der Anzahl der Elemente, die Sie an Contains übergeben, in Einklang bringt. Bei meinen eigenen Tests habe ich beispielsweise festgestellt, dass das Berechnen und Ausführen einer Abfrage mit 100 Elementen für eine lokale Instanz von SQL Server 1/60 Sekunde dauert. Wenn Sie Ihre Abfrage so schreiben können, dass die Ausführung von 100 Abfragen mit 100 verschiedenen ID-Sätzen zu einem Ergebnis führt, das der Abfrage mit 10.000 Elementen entspricht, können Sie die Ergebnisse in ungefähr 1,67 Sekunden anstelle von 18 Sekunden erhalten.
Unterschiedliche Blockgrößen sollten je nach Abfrage und Latenz der Datenbankverbindung besser funktionieren. Bei bestimmten Abfragen, dh wenn die übergebene Sequenz Duplikate enthält oder wenn Enumerable.Contains in einem verschachtelten Zustand verwendet wird, erhalten Sie möglicherweise doppelte Elemente in den Ergebnissen.
Hier ist ein Code-Snippet (Entschuldigung, wenn der Code, der zum Aufteilen der Eingabe in Blöcke verwendet wird, etwas zu komplex aussieht. Es gibt einfachere Möglichkeiten, dasselbe zu erreichen, aber ich habe versucht, ein Muster zu entwickeln, das das Streaming für die Sequenz und beibehält Ich konnte so etwas in LINQ nicht finden, also habe ich diesen Teil wahrscheinlich übertrieben :)):
Verwendung:
var list = context.GetMainItems(ids).ToList();
Methode für Kontext oder Repository:
public partial class ContainsTestEntities { public IEnumerable<Main> GetMainItems(IEnumerable<int> ids, int chunkSize = 100) { foreach (var chunk in ids.Chunk(chunkSize)) { var q = this.MainItems.Where(a => chunk.Contains(a.Id)); foreach (var item in q) { yield return item; } } } }
Erweiterungsmethoden zum Schneiden von unzähligen Sequenzen:
public static class EnumerableSlicing { private class Status { public bool EndOfSequence; } private static IEnumerable<T> TakeOnEnumerator<T>(IEnumerator<T> enumerator, int count, Status status) { while (--count > 0 && (enumerator.MoveNext() || !(status.EndOfSequence = true))) { yield return enumerator.Current; } } public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> items, int chunkSize) { if (chunkSize < 1) { throw new ArgumentException("Chunks should not be smaller than 1 element"); } var status = new Status { EndOfSequence = false }; using (var enumerator = items.GetEnumerator()) { while (!status.EndOfSequence) { yield return TakeOnEnumerator(enumerator, chunkSize, status); } } } }
Hoffe das hilft!
quelle
!(status.EndOfSequence = true)
in der TakeOnEnumerator <T> -Methode: Der Nebeneffekt dieser Ausdruckszuweisung ist also immer! True, wodurch der Gesamtausdruck nicht beeinflusst wird. Es markiert im Wesentlichen dasstats.EndOfSequence
alstrue
nur, wenn noch Elemente abgerufen werden müssen, aber Sie das Ende der Aufzählung erreicht haben.Enumerable.Contains
in EF 6 im Vergleich zu den vorherigen Versionen von EF dramatisch verbessert. Leider ist es in unseren Anwendungsfällen noch lange nicht zufriedenstellend / produktionsbereit.Wenn Sie ein Leistungsproblem finden, das für Sie blockiert, versuchen Sie nicht, ewig damit zu verbringen, es zu lösen, da Sie höchstwahrscheinlich keinen Erfolg haben und es direkt mit MS kommunizieren müssen (wenn Sie Premium-Support haben), und es dauert Alter.
Verwenden Sie Workaround und Workaround bei Leistungsproblemen, und EF bedeutet Direct SQL. Es ist nichts Schlimmes daran. Globale Idee, dass die Verwendung von EF = keine Verwendung von SQL mehr eine Lüge ist. Sie haben SQL Server 2008 R2 also:
Include
Logik optimal zu emulierenSqlDataReader
diese Option , um Ergebnisse zu erhalten und Ihre Entitäten zu erstellenWenn die Leistung für Sie kritisch ist, werden Sie keine bessere Lösung finden. Diese Prozedur kann von EF nicht zugeordnet und ausgeführt werden, da die aktuelle Version weder Tabellenwertparameter noch mehrere Ergebnismengen unterstützt.
quelle
Wir konnten das EF Contains-Problem lösen, indem wir eine Zwischentabelle hinzufügten und diese Tabelle aus einer LINQ-Abfrage zusammenfügten, die die Contains-Klausel verwenden musste. Mit diesem Ansatz konnten wir erstaunliche Ergebnisse erzielen. Wir haben ein großes EF-Modell und da "Enthält" beim Vorkompilieren von EF-Abfragen nicht zulässig ist, wurde bei Abfragen, die die Klausel "Enthält" verwenden, eine sehr schlechte Leistung erzielt.
Ein Überblick:
Erstellen Sie eine Tabelle in SQL Server - zum Beispiel
HelperForContainsOfIntType
mit SpaltenHelperID
vomGuid
Datentyp undReferenceID
vomint
Datentyp. Erstellen Sie nach Bedarf verschiedene Tabellen mit der Referenz-ID unterschiedlicher Datentypen.Erstellen Sie ein Entity / EntitySet für
HelperForContainsOfIntType
und andere solche Tabellen im EF-Modell. Erstellen Sie nach Bedarf unterschiedliche Entity / EntitySet für unterschiedliche Datentypen.Erstellen Sie eine Hilfsmethode in .NET-Code, die die Eingabe von a übernimmt
IEnumerable<int>
und eine zurückgibtGuid
. Diese Methode erzeugt eine neueGuid
und fügt die Werte vonIEnumerable<int>
in dieHelperForContainsOfIntType
zusammen mit dem erzeugtenGuid
. Als nächstes gibt die Methode diese neu generierteGuid
an den Aufrufer zurück.HelperForContainsOfIntType
Erstellen Sie zum schnellen Einfügen in eine Tabelle eine gespeicherte Prozedur, die eine Werteliste eingibt und einfügt . Siehe Tabellenwertparameter in SQL Server 2008 (ADO.NET) . Erstellen Sie verschiedene Helfer für verschiedene Datentypen oder erstellen Sie eine generische Hilfsmethode, um verschiedene Datentypen zu verarbeiten.Erstellen Sie eine EF-kompilierte Abfrage, die der folgenden ähnelt:
static Func<MyEntities, Guid, IEnumerable<Customer>> _selectCustomers = CompiledQuery.Compile( (MyEntities db, Guid containsHelperID) => from cust in db.Customers join x in db.HelperForContainsOfIntType on cust.CustomerID equals x.ReferenceID where x.HelperID == containsHelperID select cust );
Rufen Sie die Hilfsmethode mit Werten auf, die in der
Contains
Klausel verwendet werden sollen, und rufen Sie dieGuid
in der Abfrage zu verwendenden Werte ab . Zum Beispiel:var containsHelperID = dbHelper.InsertIntoHelperForContainsOfIntType(new int[] { 1, 2, 3 }); var result = _selectCustomers(_dbContext, containsHelperID).ToList();
quelle
Bearbeiten meiner ursprünglichen Antwort - Abhängig von der Komplexität Ihrer Entitäten gibt es eine mögliche Problemumgehung. Wenn Sie die SQL kennen, die EF zum Auffüllen Ihrer Entitäten generiert, können Sie sie direkt mit DbContext.Database.SqlQuery ausführen . In EF 4 könnten Sie ObjectContext.ExecuteStoreQuery verwenden , aber ich habe es nicht ausprobiert.
Wenn ich beispielsweise den Code aus meiner ursprünglichen Antwort unten verwende, um die SQL-Anweisung mit a zu generieren
StringBuilder
, konnte ich Folgendes tunvar rows = db.Database.SqlQuery<Main>(sql).ToArray();
und die Gesamtzeit ging von ungefähr 26 Sekunden auf 0,5 Sekunden.
Ich werde als erster sagen, dass es hässlich ist, und hoffentlich bietet sich eine bessere Lösung an.
aktualisieren
Nach einigem Überlegen wurde mir klar, dass EF nicht diese lange Liste von IDs erstellen muss, wenn Sie einen Join zum Filtern Ihrer Ergebnisse verwenden. Dies kann abhängig von der Anzahl der gleichzeitigen Abfragen komplex sein, aber ich glaube, Sie können Benutzer- oder Sitzungs-IDs verwenden, um sie zu isolieren.
Um dies zu testen, habe ich eine
Target
Tabelle mit demselben Schema wie erstelltMain
. Ich habe dann a verwendetStringBuilder
, umINSERT
Befehle zum Auffüllen derTarget
Tabelle in Stapeln von 1.000 zu erstellen , da dies der höchste SQL Server ist, der in einem einzigen akzeptiert wirdINSERT
. Das direkte Ausführen der SQL-Anweisungen war viel schneller als das Durchlaufen von EF (ca. 0,3 Sekunden gegenüber 2,5 Sekunden), und ich glaube, dies wäre in Ordnung, da sich das Tabellenschema nicht ändern sollte.Schließlich führte die Auswahl mit a
join
zu einer viel einfacheren Abfrage, die in weniger als 0,5 Sekunden ausgeführt wurde.ExecuteStoreCommand("DELETE Target"); var ids = Main.Select(a => a.Id).ToArray(); var sb = new StringBuilder(); for (int i = 0; i < 10; i++) { sb.Append("INSERT INTO Target(Id) VALUES ("); for (int j = 1; j <= 1000; j++) { if (j > 1) { sb.Append(",("); } sb.Append(i * 1000 + j); sb.Append(")"); } ExecuteStoreCommand(sb.ToString()); sb.Clear(); } var rows = (from m in Main join t in Target on m.Id equals t.Id select m).ToArray(); rows.Length.Dump();
Und die von EF für den Join generierte SQL:
SELECT [Extent1].[Id] AS [Id] FROM [dbo].[Main] AS [Extent1] INNER JOIN [dbo].[Target] AS [Extent2] ON [Extent1].[Id] = [Extent2].[Id]
(ursprüngliche Antwort)
Dies ist keine Antwort, aber ich wollte einige zusätzliche Informationen teilen und es ist viel zu lang, um in einen Kommentar zu passen. Ich konnte Ihre Ergebnisse reproduzieren und noch einige weitere Dinge hinzufügen:
SQL Profiler zeigt an, dass die Verzögerung zwischen der Ausführung der ersten Abfrage (
Main.Select
) und der zweitenMain.Where
Abfrage liegt. Daher vermutete ich, dass das Problem beim Generieren und Senden einer Abfrage dieser Größe (48.980 Byte) lag.Das dynamische Erstellen derselben SQL-Anweisung in T-SQL dauert jedoch weniger als 1 Sekunde. Das Erstellen derselben SQL-Anweisung
ids
aus IhrerMain.Select
Anweisung, das Erstellen derselben SQL-Anweisung und das Ausführen mit einer Anweisung dauertSqlCommand
0,112 Sekunden. Dazu gehört auch die Zeit, um den Inhalt in die Konsole zu schreiben .An diesem Punkt vermute ich, dass EF beim
ids
Erstellen der Abfrage eine Analyse / Verarbeitung für jede der 10.000 durchführt . Ich wünschte, ich könnte eine endgültige Antwort und Lösung geben :(.Hier ist der Code, den ich in SSMS und LINQPad ausprobiert habe (bitte nicht zu scharf kritisieren, ich bin in Eile und versuche, die Arbeit zu verlassen):
declare @sql nvarchar(max) set @sql = 'SELECT [Extent1].[Id] AS [Id] FROM [dbo].[Main] AS [Extent1] WHERE [Extent1].[Id] IN (' declare @count int = 0 while @count < 10000 begin if @count > 0 set @sql = @sql + ',' set @count = @count + 1 set @sql = @sql + cast(@count as nvarchar) end set @sql = @sql + ')' exec(@sql)
var ids = Mains.Select(a => a.Id).ToArray(); var sb = new StringBuilder(); sb.Append("SELECT [Extent1].[Id] AS [Id] FROM [dbo].[Main] AS [Extent1] WHERE [Extent1].[Id] IN ("); for(int i = 0; i < ids.Length; i++) { if (i > 0) sb.Append(","); sb.Append(ids[i].ToString()); } sb.Append(")"); using (SqlConnection connection = new SqlConnection("server = localhost;database = Test;integrated security = true")) using (SqlCommand command = connection.CreateCommand()) { command.CommandText = sb.ToString(); connection.Open(); using(SqlDataReader reader = command.ExecuteReader()) { while(reader.Read()) { Console.WriteLine(reader.GetInt32(0)); } } }
quelle
Ich bin mit Entity Framework nicht vertraut, aber ist die Leistung besser, wenn Sie Folgendes tun?
An Stelle von:
var ids = Main.Select(a => a.Id).ToArray(); var rows = Main.Where (a => ids.Contains(a.Id)).ToArray();
Wie wäre es damit (vorausgesetzt, die ID ist ein Int):
var ids = new HashSet<int>(Main.Select(a => a.Id)); var rows = Main.Where (a => ids.Contains(a.Id)).ToArray();
quelle
Es wurde in Entity Framework 6 Alpha 2 behoben: http://entityframework.codeplex.com/SourceControl/changeset/a7b70f69e551
http://blogs.msdn.com/b/adonet/archive/2012/12/10/ef6-alpha-2-available-on-nuget.aspx
quelle
Eine zwischenspeicherbare Alternative zu Contains?
Das hat mich nur gebissen, also habe ich meine zwei Pence zum Link "Vorschläge für Entity Framework-Funktionen" hinzugefügt.
Das Problem ist definitiv beim Generieren der SQL. Ich habe einen Client, auf dessen Daten die Abfragegenerierung 4 Sekunden betrug, die Ausführung jedoch 0,1 Sekunden.
Mir ist aufgefallen, dass bei Verwendung von dynamischen LINQs und ORs die SQL-Generierung genauso lange dauerte, aber etwas erzeugte, das zwischengespeichert werden konnte . Bei erneuter Ausführung waren es also nur noch 0,2 Sekunden.
Beachten Sie, dass noch ein SQL-In generiert wurde.
Nur etwas anderes zu beachten, wenn Sie den ersten Treffer ertragen können, ändert sich Ihre Array-Anzahl nicht viel und führen Sie die Abfrage häufig aus. (Getestet im LINQ Pad)
quelle
Das Problem liegt in der SQL-Generierung von Entity Framework. Die Abfrage kann nicht zwischengespeichert werden, wenn einer der Parameter eine Liste ist.
Damit EF Ihre Abfrage zwischenspeichert, können Sie Ihre Liste in eine Zeichenfolge konvertieren und eine .Contains für die Zeichenfolge ausführen.
So würde dieser Code beispielsweise viel schneller ausgeführt, da EF die Abfrage zwischenspeichern könnte:
var ids = Main.Select(a => a.Id).ToArray(); var idsString = "|" + String.Join("|", ids) + "|"; var rows = Main .Where (a => idsString.Contains("|" + a.Id + "|")) .ToArray();
Wenn diese Abfrage generiert wird, wird sie wahrscheinlich mit einem Like anstelle eines In generiert, sodass Ihr C # beschleunigt wird, Ihr SQL jedoch möglicherweise verlangsamt wird. In meinem Fall habe ich keinen Leistungsabfall bei meiner SQL-Ausführung festgestellt, und das C # lief erheblich schneller.
quelle