Warum verschlechtert der Operator "Contains ()" die Leistung von Entity Framework so dramatisch?

79

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.

Mike
quelle
1
Haben Sie versucht var qry = Main.Where (a => ids.Contains(a.Id)); var rows = qry.ToArray();zu sehen, welcher Teil der Abfrage die Zeit in Anspruch nimmt?
Andrew Cooper
Es ist nicht die EF, die Ihre Abfrage beeinträchtigt, sondern die tatsächliche Abfrage, die Sie ausführen möchten. Können Sie erklären, was Sie versuchen zu tun? Vielleicht gibt es einen besseren Ansatz für Ihre Bedürfnisse
Kris Ivanov
@ AndrewCooper Ich habe es gerade versucht und aufgrund der verzögerten Ausführung wird die erste Anweisung (ohne ToArray) fast sofort ausgeführt. Die Abfrage, einschließlich der Filter "Enthält", wird erst ausgeführt, wenn Sie ToArray () ausführen.
Mike
5
Nur und Update dazu: EF6 Alpha 2 enthält eine Verbesserung, die die Übersetzung von Enumerable.Contains beschleunigt. Siehe die Ankündigung hier: blogs.msdn.com/b/adonet/archive/2012/12/10/… . Meine eigenen Tests zeigen, dass das Übersetzen von list.Contains (x) für eine Liste mit 100.000 int-Elementen jetzt deutlich weniger als eine Sekunde dauert und die Zeit ungefähr linear mit der Anzahl der Elemente in der Liste wächst. Vielen Dank für Ihr Feedback und helfen Sie uns, EF zu verbessern!
Divega
1
Beachten Sie Folgendes: Abfragen mit IEnumerable-Parametern können nicht zwischengespeichert werden. Dies kann zu schwerwiegenden Nebenwirkungen führen, wenn Ihre Abfragepläne kompliziert sind. Wenn Sie die Vorgänge häufig ausführen müssen (z. B. mithilfe von Contains, um Datenblöcke abzurufen), kann es zu unangenehmen Neukompilierungszeiten für Abfragen kommen! Überprüfen Sie die Quelle selbst und Sie können sehen, dass parent._recompileRequired = () => true;dies für alle Abfragen geschieht, die einen IEnumerable <T> -Parameter enthalten. Boo!
Jocull

Antworten:

66

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!

Divega
quelle
So erklären Sie das !(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 das stats.EndOfSequenceals truenur, wenn noch Elemente abgerufen werden müssen, aber Sie das Ende der Aufzählung erreicht haben.
Arviman
Möglicherweise hat sich die Verarbeitungsleistung Enumerable.Containsin EF 6 im Vergleich zu den vorherigen Versionen von EF dramatisch verbessert. Leider ist es in unseren Anwendungsfällen noch lange nicht zufriedenstellend / produktionsbereit.
Nik
24

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:

  • Erstellen Sie eine gespeicherte Prozedur, die einen Parameter mit Tabellenwert akzeptiert, um Ihre IDs zu übergeben
  • Lassen Sie Ihre gespeicherte Prozedur mehrere Ergebnismengen zurückgeben, um die IncludeLogik optimal zu emulieren
  • Wenn Sie eine komplexe Abfrageerstellung benötigen, verwenden Sie dynamisches SQL in der gespeicherten Prozedur
  • Verwenden Sie SqlDataReaderdiese Option , um Ergebnisse zu erhalten und Ihre Entitäten zu erstellen
  • Hängen Sie sie an den Kontext an und arbeiten Sie mit ihnen, als wären sie von EF geladen worden

Wenn 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.

Ladislav Mrnka
quelle
@Laddislav Mrnka Aufgrund von list.Contains () sind wir auf ein ähnliches Leistungsproblem gestoßen. Wir werden versuchen, Prozeduren zu erstellen, indem wir IDs übergeben. Sollten wir einen Leistungseinbruch erleben, wenn wir dieses Verfahren über EF ausführen?
Kurubaran
9

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 HelperForContainsOfIntTypemit Spalten HelperIDvom GuidDatentyp und ReferenceIDvom intDatentyp. Erstellen Sie nach Bedarf verschiedene Tabellen mit der Referenz-ID unterschiedlicher Datentypen.

  • Erstellen Sie ein Entity / EntitySet für HelperForContainsOfIntTypeund 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ückgibt Guid. Diese Methode erzeugt eine neue Guidund fügt die Werte von IEnumerable<int>in die HelperForContainsOfIntTypezusammen mit dem erzeugten Guid. Als nächstes gibt die Methode diese neu generierte Guidan den Aufrufer zurück. HelperForContainsOfIntTypeErstellen 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 ContainsKlausel verwendet werden sollen, und rufen Sie die Guidin der Abfrage zu verwendenden Werte ab . Zum Beispiel:

    var containsHelperID = dbHelper.InsertIntoHelperForContainsOfIntType(new int[] { 1, 2, 3 });
    var result = _selectCustomers(_dbContext, containsHelperID).ToList();
    
Dhwanil Shah
quelle
Danke dafür! Ich habe eine Variation Ihrer Lösung verwendet, um mein Problem zu lösen.
Mike
5

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 tun

var 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 TargetTabelle mit demselben Schema wie erstellt Main. Ich habe dann a verwendet StringBuilder, um INSERTBefehle zum Auffüllen der TargetTabelle in Stapeln von 1.000 zu erstellen , da dies der höchste SQL Server ist, der in einem einzigen akzeptiert wird INSERT. 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 joinzu 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 zweiten Main.WhereAbfrage 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 idsaus Ihrer Main.SelectAnweisung, das Erstellen derselben SQL-Anweisung und das Ausführen mit einer Anweisung dauert SqlCommand0,112 Sekunden. Dazu gehört auch die Zeit, um den Inhalt in die Konsole zu schreiben .

An diesem Punkt vermute ich, dass EF beim idsErstellen 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));
        }
    }
}
Jeff Ogata
quelle
Vielen Dank für Ihre Arbeit daran. Wenn ich weiß, dass du es reproduzieren kannst, fühle ich mich besser - zumindest bin ich nicht verrückt! Leider hilft Ihre Problemumgehung in meinem Fall nicht wirklich, da das hier gegebene Beispiel, wie Sie vielleicht erraten haben, so weit wie möglich vereinfacht wurde, um das Problem einzugrenzen. Meine eigentliche Abfrage beinhaltet ein ziemlich kompliziertes Schema, .Include () in mehreren anderen Tabellen und einige andere LINQ-Operatoren.
Mike
@ Mike, ich habe eine weitere Idee hinzugefügt, die für komplexe Entitäten funktionieren würde. Hoffentlich ist die Implementierung nicht allzu schwierig, wenn Sie keine andere Wahl haben.
Jeff Ogata
Ich habe einige Tests durchgeführt und ich denke, Sie haben Recht, dass die Verzögerung beim Erstellen des SQL liegt, bevor es ausgeführt wird. Ich habe meine Frage mit den Details aktualisiert.
Mike
@ Mike, konnten Sie versuchen, sich den IDs anzuschließen (siehe das Update in meiner Antwort)?
Jeff Ogata
Ich habe eine Variation Ihres Ansatzes verwendet, um das Leistungsproblem zu lösen. Es war ziemlich hässlich, aber wahrscheinlich die beste Option, bis (und wenn) Microsoft dieses Problem behebt.
Mike
5

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();
Shiv
quelle
Ich weiß nicht warum und wie, aber es hat wie ein Zauber funktioniert :) Vielen Dank :)
Wahid Bitar
1
Die Erklärung, warum die Leistung besser ist, ist int []. Enthält den Aufruf im ersten Aufruf ist O (n) - möglicherweise ein vollständiger Array-Scan -, während der HashSet <int> .Contains-Aufruf O (1) ist. Siehe stackoverflow.com/questions/9812020/... für Hashset Leistung.
Shiv
3
@Shiv Ich glaube nicht, dass das richtig ist. EF nimmt jede Sammlung und übersetzt sie in SQL. Die Art der Sammlung sollte kein Problem sein.
Rob
@ Rob Ich bin skeptisch - ratlos, um den Leistungsunterschied zu erklären, wenn das dann der Fall ist. Möglicherweise muss die Binärdatei analysiert werden, um zu sehen, was sie getan hat.
Shiv
1
HashSet ist nicht IEnumerable. IEnumerables, die .Contains in LINQ aufrufen, weisen eine schlechte Leistung auf (zumindest vor EF6.)
Jason Beck,
2

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)

Dave
quelle
Stimmen Sie auch auf der Codeplex-Website < entityframework.codeplex.com/workitem/245 > dafür ab
Dave
2

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.

user2704238
quelle
1
Gute Idee, aber hierfür wird kein Index für die betreffende Spalte verwendet.
Spender
Ja, das stimmt, weshalb ich erwähnt habe, dass dies die SQL-Ausführung verlangsamen könnte. Ich denke, dies ist nur eine mögliche Alternative, wenn Sie den Prädikaten-Builder nicht verwenden können und mit einem ausreichend kleinen Datensatz arbeiten, sodass Sie es sich leisten können, keinen Index zu verwenden. Ich nehme auch an, dass ich hätte erwähnen sollen, dass der Prädikaten-Builder die bevorzugte Option ist
user2704238
1
Was für eine erstaunliche Lösung. Wir haben es geschafft, die Laufzeit unserer Produktionsabfragen von ~ 12.600 Millisekunden auf nur ~ 18 Millisekunden zu erhöhen. Das ist eine RIESIGE Verbesserung. Vielen Dank !!!
Jacob