Effizientes Abfragen

Effizientes Abfragen ist ein umfangreiches Thema, das so unterschiedliche Bereiche wie Indizes, Strategien zum Laden verwandter Entitäten und viele andere umfasst. In diesem Abschnitt finden Sie einige allgemeine Themen, mit denen Sie Ihre Abfragen beschleunigen können, sowie typische Fallstricke, auf die Benutzer stoßen können.

Ordnungsgemäße Verwendung von Indizes

Der wichtigste Faktor, der darüber entscheidet, ob eine Abfrage schnell läuft oder nicht, ist, ob sie gegebenenfalls Indizes richtig nutzt: Datenbanken werden in der Regel dazu verwendet, große Datenmengen zu speichern, und Abfragen, die ganze Tabellen durchqueren, sind in der Regel die Ursache für ernsthafte Leistungsprobleme. Indizierungsprobleme sind nicht leicht zu erkennen, da es nicht sofort offensichtlich ist, ob eine bestimmte Abfrage einen Index verwendet oder nicht. Beispiel:

// Matches on start, so uses an index (on SQL Server)
var posts1 = context.Posts.Where(p => p.Title.StartsWith("A")).ToList();
// Matches on end, so does not use the index
var posts2 = context.Posts.Where(p => p.Title.EndsWith("A")).ToList();

Eine gute Möglichkeit zum Erkennen von Indizierungsproblemen besteht darin, zuerst eine langsame Abfrage anzuheften und dann den Abfrageplan über das bevorzugte Tool Ihrer Datenbank zu untersuchen. Weitere Informationen dazu finden Sie auf der Seite Leistungsdiagnose. Der Abfrageplan zeigt an, ob die Abfrage die gesamte Tabelle durchläuft oder einen Index verwendet.

Im Allgemeinen gibt es keine speziellen EF-Kenntnisse, um Indizes zu verwenden oder Leistungsprobleme im Zusammenhang mit ihnen zu diagnostizieren. Allgemeine Datenbankkenntnisse im Zusammenhang mit Indizes sind genauso relevant für EF-Anwendungen wie für Anwendungen, die nicht EF verwenden. Im Folgenden sind einige allgemeine Richtlinien aufgeführt, die bei der Verwendung von Indizes beachtet werden sollten:

  • Während Indizes Abfragen beschleunigen, verlangsamen sie auch Updates, da sie auf dem neuesten Stand gehalten werden müssen. Vermeiden Sie das Definieren von Indizes, die nicht benötigt werden, und erwägen Sie die Verwendung von Indexfiltern, um den Index auf eine Teilmenge der Zeilen zu beschränken, wodurch der Aufwand reduziert wird.
  • Zusammengesetzte Indizes können Abfragen beschleunigen, die nach mehreren Spalten filtern, aber sie können auch Abfragen beschleunigen, die nicht nach allen Spalten des Indexes filtern – je nach Sortierung. Ein Index auf den Spalten A und B beschleunigt zum Beispiel Abfragen, die nach A und B filtern, sowie Abfragen, die nur nach A filtern, aber er beschleunigt keine Abfragen, die nur nach B filtern.
  • Wenn eine Abfrage nach einem Ausdruck über eine Spalte filtert (z. B. price / 2), kann ein einfacher Index nicht verwendet werden. Sie können jedoch eine gespeicherte persistente Spalte für Ihren Ausdruck definieren und einen Index dafür erstellen. Einige Datenbanken unterstützen auch Ausdrucksindizes, die direkt verwendet werden können, um die Filterung von Abfragen nach einem beliebigen Ausdruck zu beschleunigen.
  • Unterschiedliche Datenbanken ermöglichen die Konfiguration von Indizes auf unterschiedliche Weise, und in vielen Fällen machen EF Core-Anbieter diese über die Fluent-API verfügbar. Mit dem SQL Server-Anbieter können Sie beispielsweise konfigurieren, ob ein Index gruppierten ist, oder dessen Füllfaktor festlegen. Informationen dazu können Sie der Dokumentation des Datenanbieters entnehmen.

Projektieren nur der Eigenschaften, die benötigt werden

EF Core macht es sehr einfach, Entitätsinstanzen abzufragen und diese Instanzen dann im Code zu verwenden. Das Abfragen von Entitätsinstanzen kann jedoch häufig mehr Daten als erforderlich aus der Datenbank abrufen. Beachten Sie Folgendes:

foreach (var blog in context.Blogs)
{
    Console.WriteLine("Blog: " + blog.Url);
}

Obwohl dieser Code nur die Url-Eigenschaft jedes Blogs benötigt, wird die gesamte Blogentität abgerufen, und nicht benötigte Spalten werden aus der Datenbank übertragen:

SELECT [b].[BlogId], [b].[CreationDate], [b].[Name], [b].[Rating], [b].[Url]
FROM [Blogs] AS [b]

Dies kann mithilfe von Select optimiert werden, um EF mitzuteilen, welche Spalten projiziert werden sollen:

foreach (var blogName in context.Blogs.Select(b => b.Url))
{
    Console.WriteLine("Blog: " + blogName);
}

Die resultierende SQL-Datei ruft nur die erforderlichen Spalten zurück:

SELECT [b].[Url]
FROM [Blogs] AS [b]

Wenn Sie mehr als eine Spalte projizieren müssen, projizieren Sie einen anonymen C#-Typ mit den gewünschten Eigenschaften.

Beachten Sie, dass diese Technik für reine Leseabfragen sehr nützlich ist, aber die Dinge werden komplizierter, wenn Sie die abgerufenen Blogs aktualisieren müssen, da die Änderungsverfolgung von EF nur mit Entitätsinstanzen funktioniert. Es ist möglich, Aktualisierungen durchzuführen, ohne ganze Entitäten zu laden, indem Sie eine geänderte Bloginstanz anhängen und EF mitteilen, welche Eigenschaften sich geändert haben, aber das ist eine fortgeschrittene Technik, die sich möglicherweise nicht lohnt.

Beschränken der Resultset-Größe

Standardmäßig gibt eine Abfrage alle Zeilen zurück, die ihren Filtern entsprechen:

var blogsAll = context.Posts
    .Where(p => p.Title.StartsWith("A"))
    .ToList();

Da die Anzahl der zurückgegebenen Zeilen von den tatsächlichen Daten in Ihrer Datenbank abhängt, ist es unmöglich zu wissen, wie viele Daten aus der Datenbank geladen werden, wie viel Speicherplatz die Ergebnisse beanspruchen und wie viel zusätzliche Last bei der Verarbeitung dieser Ergebnisse (z. B. durch das Senden an einen Benutzer-Browser über das Netzwerk) erzeugt wird. Entscheidend ist, dass Testdatenbanken häufig nur wenige Daten enthalten, so dass beim Testen alles gut funktioniert, aber plötzlich Leistungsprobleme auftreten, wenn die Abfrage auf realen Daten läuft und viele Zeilen zurückgegeben werden.

Daher lohnt es sich in der Regel, die Anzahl der Ergebnisse zu begrenzen:

var blogs25 = context.Posts
    .Where(p => p.Title.StartsWith("A"))
    .Take(25)
    .ToList();

Zumindest könnte Ihre Benutzeroberfläche eine Meldung anzeigen, die darauf hinweist, dass möglicherweise weitere Zeilen in der Datenbank vorhanden sind (und die Abfrage dieser Zeilen auf andere Weise ermöglichen). Eine vollwertige Lösung würde eine Paginierung implementieren, bei der Ihre Benutzeroberfläche jeweils nur eine bestimmte Anzahl von Zeilen anzeigt und es den Benutzern ermöglicht, bei Bedarf zur nächsten Seite zu wechseln. Im nächsten Abschnitt erfahren Sie mehr darüber, wie Sie dies effizient umsetzen können.

Effiziente Paginierung

Die Paginierung bezieht sich darauf, dass die Ergebnisse seitenweise abgerufen werden und nicht alle auf einmal. Dies geschieht in der Regel bei großen Ergebnismengen, bei denen eine Benutzeroberfläche angezeigt wird, über die der Benutzer zur nächsten oder vorherigen Seite der Ergebnisse navigieren kann. Eine gängige Methode zur Implementierung der Paginierung in Datenbanken ist die Verwendung der Operatoren Skip und Take (OFFSET und LIMIT in SQL). Dies ist zwar eine intuitive Implementierung, aber auch ziemlich ineffizient. Wenn Sie eine Paginierung wünschen, bei der Sie sich seitenweise bewegen können (im Gegensatz zum Springen auf beliebige Seiten), sollten Sie stattdessen die Paginierung über Tastenkombinationen verwenden.

Weitere Informationen finden Sie auf der Dokumentationsseite zur Paginierung.

In relationalen Datenbanken werden alle zugehörigen Entitäten standardmäßig durch Einführung von JOIN-Vorgängen in eine Einzelabfrage geladen.

SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title]
FROM [Blogs] AS [b]
LEFT JOIN [Post] AS [p] ON [b].[BlogId] = [p].[BlogId]
ORDER BY [b].[BlogId], [p].[PostId]

Wenn ein typischer Blog mehrere zugehörige Beiträge enthält, werden in den Zeilen für diese Beiträge die Informationen des Blogs dupliziert. Diese Duplizierung führt zum so genannten Problem der „kartesischen Explosion“. Wenn weitere 1:n-Beziehungen geladen werden, wächst die Menge an duplizierten Daten weiter und beeinträchtigt die Leistung Ihrer Anwendung.

EF ermöglicht es, diesen Effekt durch die Verwendung von „geteilten Abfragen“ zu vermeiden, die die verbundenen Entitäten über separate Abfragen laden. Weitere Informationen finden Sie in der Dokumentation zu geteilten und einzelnen Abfragen.

Hinweis

Die aktuelle Implementierung von geteilten Abfragen führt für jede Abfrage einen Roundtrip aus. Wir planen, dies in Zukunft zu verbessern und alle Abfragen in einem einzigen Roundtrip auszuführen.

Es wird empfohlen, die dedizierte Seite zu verwandten Entitäten zu lesen, bevor Sie mit diesem Abschnitt fortfahren.

Wenn wir mit zusammenhängenden Entitäten arbeiten, wissen wir in der Regel im Voraus, was wir laden müssen: Ein typisches Beispiel wäre das Laden einer bestimmten Gruppe von Blogs zusammen mit all ihren Beiträgen. In diesen Szenarien ist es immer besser, Eager Loading zu verwenden, damit EF alle erforderlichen Daten in einem Roundtrip abrufen kann. Mit der Funktion für gefilterte Includes können Sie auch die zugehörigen Entitäten einschränken, die Sie laden möchten, wobei Eager Loading verwendet wird und es daher in einem einzigen Roundtrip durchführbar ist:

using (var context = new BloggingContext())
{
    var filteredBlogs = context.Blogs
        .Include(
            blog => blog.Posts
                .Where(post => post.BlogId == 1)
                .OrderByDescending(post => post.Title)
                .Take(5))
        .ToList();
}

In anderen Szenarien wissen wir möglicherweise nicht, welche verwandte Entität wir benötigen, bevor wir die Hauptentität erhalten. Wenn Sie beispielsweise einen Blog laden, müssen wir möglicherweise eine andere Datenquelle – eventuell einen Webservice – konsultieren, um zu wissen, ob wir an den Beiträgen dieses Blogs interessiert sind. In diesen Fällen kann explizites oder verzögertes Laden verwendet werden, um verwandte Entitäten separat abzurufen und die Navigation der Beiträge des Blogs aufzufüllen. Beachten Sie, dass diese Methoden nicht „eager“ sind und zusätzliche Roundtrips zur Datenbank erfordern, was zu einer Verlangsamung führt. Je nach Ihrem spezifischen Szenario kann es effizienter sein, einfach immer alle Beiträge zu laden, anstatt die zusätzlichen Roundtrips auszuführen und nur die Beiträge abzurufen, die Sie benötigen.

Vorsicht mit verzögertem Laden

Verzögertes Laden (auch Lazy Loading) scheint oft eine sehr nützliche Methode zum Schreiben von Datenbanklogik zu sein, da EF Core automatisch verwandte Entitäten aus der Datenbank lädt, wenn Ihr Code darauf zugreift. Dies vermeidet das Laden von verwandten Entitäten, die nicht benötigt werden (wie das explizite Laden), und befreit den Programmierer scheinbar davon, sich überhaupt mit verwandten Entitäten befassen zu müssen. Allerdings ist das verzögerte Laden besonders anfällig für unnötige zusätzliche Roundtrips, die die Anwendung verlangsamen können.

Beachten Sie Folgendes:

foreach (var blog in context.Blogs.ToList())
{
    foreach (var post in blog.Posts)
    {
        Console.WriteLine($"Blog {blog.Url}, Post: {post.Title}");
    }
}

Dieser scheinbar unschuldige Code durchläuft alle Blogs und deren Beiträge und druckt sie aus. Das Aktivieren der Anweisungsprotokollierung von EF Core zeigt Folgendes:

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT [b].[BlogId], [b].[Rating], [b].[Url]
      FROM [Blogs] AS [b]
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (5ms) [Parameters=[@__p_0='1'], CommandType='Text', CommandTimeout='30']
      SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[Title]
      FROM [Post] AS [p]
      WHERE [p].[BlogId] = @__p_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1ms) [Parameters=[@__p_0='2'], CommandType='Text', CommandTimeout='30']
      SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[Title]
      FROM [Post] AS [p]
      WHERE [p].[BlogId] = @__p_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1ms) [Parameters=[@__p_0='3'], CommandType='Text', CommandTimeout='30']
      SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[Title]
      FROM [Post] AS [p]
      WHERE [p].[BlogId] = @__p_0

... and so on

Was geht da vor? Warum werden alle diese Abfragen für die oben genannten einfachen Schleifen gesendet? Beim Lazy Loading werden die Posts eines Blogs nur dann (verzögert) geladen, wenn auf die Eigenschaft des Beitrags zugegriffen wird. Das hat zur Folge, dass jede Iteration in der inneren foreach eine zusätzliche Datenbankabfrage auslöst, und zwar in einem eigenen Roundtrip. Dies führt dazu, dass nach der ersten Abfrage, mit der alle Blogs geladen werden, eine weitere Abfrage pro Blog erfolgt, mit der alle Beiträge geladen werden. Dies wird manchmal als N+1-Problem bezeichnet und kann zu erheblichen Leistungsproblemen führen.

Wenn wir davon ausgehen, dass wir alle Beiträge der Blogs benötigen, ist es sinnvoll, stattdessen Eager Loading zu verwenden. Wir können den Include-Operator verwenden, um das Laden durchzuführen, aber da wir nur die URLs der Blogs benötigen (und wir sollten nur das laden, was benötigt wird), verwenden wir stattdessen eine Projektion:

foreach (var blog in context.Blogs.Select(b => new { b.Url, b.Posts }).ToList())
{
    foreach (var post in blog.Posts)
    {
        Console.WriteLine($"Blog {blog.Url}, Post: {post.Title}");
    }
}

Dadurch ruft EF Core alle Blogs zusammen mit ihren Beiträgen in einer einzigen Abfrage ab. In einigen Fällen kann es auch sinnvoll sein, kartesische Explosionseffekte zu vermeiden, indem geteilte Abfragenverwendet werden.

Warnung

Da das verzögerte Laden das N+1-Problem versehentlich auslöst, wird empfohlen, es zu vermeiden. Eager Loading oder explizites Laden macht es im Quellcode sehr deutlich, wenn ein Datenbank-Roundtrip auftritt.

Puffern und Streaming

Puffern bedeutet, dass alle Abfrageergebnisse in den Speicher geladen werden, während Streaming bedeutet, dass EF der Anwendung jedes Mal ein einzelnes Ergebnis aushändigt und nie die gesamte Ergebnismenge im Speicher enthält. Im Prinzip ist der Speicherbedarf einer Streaming-Abfrage fest – er ist derselbe, egal ob die Abfrage 1 Zeile oder 1000 Zeilen zurückgibt; eine Pufferabfrage hingegen benötigt mehr Speicher, je mehr Zeilen zurückgegeben werden. Bei Abfragen, die große Resultsets ergeben, kann dies ein wichtiger Leistungsfaktor sein.

Ob eine Abfrage puffert oder streamt, hängt davon ab, wie sie ausgewertet wird:

// ToList and ToArray cause the entire resultset to be buffered:
var blogsList = context.Posts.Where(p => p.Title.StartsWith("A")).ToList();
var blogsArray = context.Posts.Where(p => p.Title.StartsWith("A")).ToArray();

// Foreach streams, processing one row at a time:
foreach (var blog in context.Posts.Where(p => p.Title.StartsWith("A")))
{
    // ...
}

// AsEnumerable also streams, allowing you to execute LINQ operators on the client-side:
var doubleFilteredBlogs = context.Posts
    .Where(p => p.Title.StartsWith("A")) // Translated to SQL and executed in the database
    .AsEnumerable()
    .Where(p => SomeDotNetMethod(p)); // Executed at the client on all database results

Wenn Ihre Abfragen nur wenige Ergebnisse zurückgeben, müssen Sie sich wahrscheinlich keine Gedanken darüber machen. Wenn Ihre Abfrage jedoch möglicherweise eine große Anzahl von Zeilen zurückgibt, ist es eine Überlegung wert, Streaming statt Pufferung zu verwenden.

Hinweis

Vermeiden Sie die Verwendung von ToList oder ToArray, wenn Sie einen anderen LINQ-Operator für das Ergebnis verwenden möchten. Dadurch werden alle Ergebnisse unnötigerweise in den Arbeitsspeicher gepuffert. Verwenden Sie stattdessen AsEnumerable.

Interne Pufferung durch EF

In bestimmten Situationen puffert EF das Resultset intern, unabhängig davon, wie Sie Ihre Abfrage auswerten. Das geschieht in den beiden folgenden Fällen:

  • Wenn eine Strategie für Wiederholungsausführung vorhanden ist. Dadurch wird sichergestellt, dass dieselben Ergebnisse zurückgegeben werden, wenn die Abfrage später erneut versucht wird.
  • Wenn eine geteilte Abfrage verwendet wird, werden die Ergebnissätze aller Abfragen bis auf die letzte gepuffert – es sei denn, MARS (Multiple Active Result Sets) ist auf SQL Server aktiviert. Dies liegt daran, dass es in der Regel unmöglich ist, dass mehrere Abfrage-Resultsets gleichzeitig aktiv sind.

Beachten Sie, dass diese interne Pufferung zusätzlich zu der Pufferung erfolgt, die Sie über LINQ-Operatoren verursachen. Wenn Sie z. B. ToList für eine Abfrage verwenden und eine Strategie für Wiederholungsausführung vorhanden ist, wird das Resultset zweimal in den Arbeitsspeicher geladen: einmal intern von EF und einmal von ToList.

Nachverfolgung, keine Nachverfolgung und Identitätsauflösung

Es wird empfohlen, die dedizierte Seite zu Nachverfolgung und keine Nachverfolgung zu lesen, bevor Sie mit diesem Abschnitt fortfahren.

EF verfolgt Entitätsinstanzen standardmäßig nach, sodass Änderungen erkannt und beibehalten werden, wenn SaveChanges aufgerufen wird. Ein weiterer Effekt der Nachverfolgung von Abfragen ist, dass EF erkennt, ob bereits eine Instanz für Ihre Daten geladen wurde, und automatisch diese verfolgte Instanz zurückgibt, anstatt eine neue zurückzugeben; dies wird als Identitätsauflösung bezeichnet. Aus Leistungsperspektive bedeutet die Änderungsnachverfolgung Folgendes:

  • EF verwaltet intern ein Wörterbuch mit nachverfolgten Instanzen. Wenn neue Daten geladen werden, überprüft EF das Wörterbuch, um festzustellen, ob eine Instanz bereits für den Schlüssel dieser Entität (Identitätsauflösung) nachverfolgt wird. Die Wörterbuchwartung und Nachschlagevorgänge dauern einige Zeit, wenn die Ergebnisse der Abfrage geladen werden.
  • Bevor eine geladene Instanz an die Anwendung übergeben wird, erstellt EF eine Momentaufnahme dieser Instanz und speichert diese intern. Wenn SaveChanges aufgerufen wird, wird die Instanz der Anwendung mit der Momentaufnahme verglichen, um die zu speichernden Änderungen zu ermitteln. Die Momentaufnahme benötigt mehr Speicherplatz und der Prozess selbst nimmt Zeit in Anspruch. Manchmal ist es möglich, ein anderes, möglicherweise effizienteres Momentaufnahmeverhalten über Wertevergleiche zu spezifizieren oder Änderungsnachverfolgungsproxys zu verwenden, um das Erstellen der Momentaufnahme ganz zu umgehen (was allerdings eine Reihe von Nachteilen mit sich bringt).

In schreibgeschützten Szenarien, in denen Änderungen nicht in der Datenbank gespeichert werden, können der oben erwähnte Mehraufwand vermieden werden, indem Sie Abfragen ohne Nachverfolgung verwenden. Da Abfragen ohne Verfolgung jedoch keine Identitätsauflösung durchführen, wird eine Datenbankzeile, auf die mehrere andere geladene Zeilen verweisen, als verschiedene Instanzen materialisiert.

Zur Veranschaulichung nehmen wir an, dass wir eine große Anzahl von Beiträgen aus der Datenbank laden, sowie den Blog, auf den jeder Beitrag verweist. Wenn 100 Beiträge auf denselben Blog verweisen, erkennt eine Nachverfolgungsabfrage dies über die Identitätsauflösung, und alle Beitragsinstanzen verweisen auf dieselbe deduplizierte Bloginstanz. Bei einer Abfrage ohne Nachverfolgung wird derselbe Blog dagegen 100 Mal dupliziert – und der Anwendungscode muss entsprechend geschrieben werden.

Hier sind die Ergebnisse für einen Benchmark, bei dem das Nachverfolgungsverhalten mit dem Verhalten ohne Nachverfolgung für eine Abfrage verglichen wird, bei der 10 Blogs mit jeweils 20 Beiträgen geladen werden. Der Quellcode ist hier verfügbar, und Sie können ihn gerne als Grundlage für Ihre eigenen Messungen verwenden.

Methode NumBlogs NumPostsPerBlog Mittelwert Fehler StdDev Median Gewinnanteil RatioSD Gen 0 Gen1 Gen2 Zugeordnet
AsTracking 10 20 1.414,7 us 27,20 us 45,44 us 1.405,5 us 1.00 0,00 60.5469 13.6719 - 380,11 KB
AsNoTracking 10 20 993,3 us 24,04 us 65,40 us 966,2 us 0.71 0.05 37.1094 6.8359 - 232,89 KB.

Schließlich ist es möglich, Aktualisierungen ohne den Mehraufwand der Änderungsverfolgung durchzuführen, indem Sie eine Abfrage ohne Änderungsverfolgung verwenden und dann die zurückgegebene Instanz an den Kontext anhängen, wobei Sie angeben, welche Änderungen vorgenommen werden sollen. Dies überträgt die Last der Änderungsverfolgung von EF auf den Benutzer und sollte nur dann versucht werden, wenn der Mehraufwand der Änderungsverfolgung durch Profiling oder Benchmarking als inakzeptabel erwiesen wurde.

Verwenden von SQL-Abfragen

In einigen Fällen ist für Ihre Abfrage eine optimierte SQL-Datei vorhanden, die von EF nicht generiert wird. Dies kann passieren, wenn das SQL-Konstrukt eine für Ihre Datenbank spezifische Erweiterung ist, die nicht unterstützt wird, oder einfach, weil EF noch nicht in die Datenbank übersetzt wird. In diesen Fällen kann das Schreiben von SQL von Hand eine erhebliche Leistungssteigerung bieten, und EF unterstützt mehrere Möglichkeiten, dies zu tun.

  • Verwenden Sie SQL-Abfragen direkt in Ihrer Abfrage, z. B. über FromSqlRaw. EF ermöglicht sogar das Verfassen über SQL mit regulären LINQ-Abfragen, sodass Sie auch nur einen Teil der Abfrage in SQL ausdrücken können. Dies ist eine gute Technik, wenn SQL nur in einer einzigen Abfrage in Ihrer Codebasis verwendet werden muss.
  • Definieren Sie eine benutzerdefinierte Funktion (UDF), und rufen Sie diese dann aus Ihren Abfragen auf. Beachten Sie, dass EF UDFs erlaubt, vollständige Ergebnissätze zurückzugeben – diese sind als Tabellenwertfunktionen (TVFs) bekannt – und auch die Zuordnung eines DbSet zu einer Funktion erlaubt, so dass es wie eine weitere Tabelle aussieht.
  • Definieren Sie eine Datenbankansicht und -abfrage in Ihren Abfragen. Beachten Sie, dass Ansichten im Gegensatz zu Funktionen keine Parameter akzeptieren können.

Hinweis

Unformatiertes SQL sollte in der Regel als letztes Mittel verwendet werden, nachdem sichergestellt wurde, dass EF die gewünschte SQL-Datei nicht generieren kann, und wenn die Leistung für die angegebene Abfrage wichtig genug ist, um dies zu rechtfertigen. Die Verwendung von unformatiertem SQL bringt erhebliche Wartungsnachteile mit sich.

Asynchrone Programmierung

Generell gilt: Damit Ihre Anwendung skalierbar ist, sollten Sie immer asynchrone APIs statt synchroner APIs verwenden (z. B. SaveChangesAsync statt SaveChanges). Synchrone APIs blockieren den Thread für die Dauer der Datenbank-E/A, was den Bedarf an Threads und die Anzahl der erforderlichen Thread-Kontextwechsel erhöht.

Weitere Informationen finden Sie auf der Seite zur asynchronen Programmierung.

Warnung

Vermeiden Sie die Vermischung von synchronem und asynchronem Code in derselben Anwendung – es ist sehr leicht, unbeabsichtigt subtile Probleme mit dem Thread-Pool auszulösen.

Warnung

Die asynchrone Implementierung von Microsoft.Data.SqlClient weist einige bekannte Probleme auf (z. B. 593 und 601). Wenn unerwartete Leistungsprobleme auftreten, versuchen Sie stattdessen, die synchrone Befehlsausführung zu verwenden. Dies gilt insbesondere bei großen Text- oder Binärwerten.

Zusätzliche Ressourcen