Freigeben über


Effiziente Abfrage

Die effiziente Abfrage ist ein großes Thema, das Themen so breit wie Indizes, verwandte Entitätsladestrategien und viele andere umfasst. In diesem Abschnitt werden einige gemeinsame Themen beschrieben, mit denen Ihre Abfragen schneller ausgeführt werden können, sowie häufige Fehler, auf die Benutzer normalerweise stoßen.

Ordnungsgemäße Verwendung von Indizes

Der wichtigste Faktor, ob eine Abfrage schnell ausgeführt wird oder nicht, ist, ob sie Indizes bei Bedarf ordnungsgemäß verwendet: Datenbanken werden in der Regel verwendet, um große Datenmengen zu speichern, und Abfragen, die ganze Tabellen durchlaufen, sind in der Regel Quellen schwerwiegender 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 = await context.Posts.Where(p => p.Title.StartsWith("A")).ToListAsync();
// Matches on end, so does not use the index
var posts2 = await context.Posts.Where(p => p.Title.EndsWith("A")).ToListAsync();

Eine gute Möglichkeit, Indizierungsprobleme zu erkennen, besteht darin, zuerst eine langsame Abfrage zu identifizieren und dann ihren Abfrageplan mithilfe des bevorzugten Tools in Ihrer Datenbank zu analysieren. Weitere Informationen dazu finden Sie auf der Seite zur 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 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 up-to-date beibehalten 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 dieser 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. Beispielsweise beschleunigt ein Index für die Spalten A und B die Abfragen, die nach A und B filtern, sowie diejenigen, die nur nach A filtern. Er beschleunigt jedoch nicht Abfragen, die nur nach B filtern.
  • Wenn eine Abfrage nach einem Ausdruck über eine Spalte (z. B. price / 2) filtert, kann kein einfacher Index verwendet werden. Sie können jedoch eine gespeicherte persistierte 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 gruppiert ist, oder den Füllfaktor festlegen. Weitere Informationen finden Sie in der Dokumentation Ihres Anbieters.

Projizieren Sie nur die Eigenschaften, die Sie benötigen.

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. Beachte Folgendes:

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

Obwohl dieser Code nur jede Blogeigenschaft Url 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 optimiert werden, indem Select verwendet wird, um EF mitzuteilen, welche Spalten projiziert werden sollen.

await foreach (var blogName in context.Blogs.Select(b => b.Url).AsAsyncEnumerable())
{
    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 schreibgeschützte Abfragen sehr nützlich ist, aber die Dinge werden komplizierter, wenn Sie die abgerufenen Blogs aktualisieren müssen, da die Änderungsnachverfolgung von EF nur mit Entitätsinstanzen funktioniert. Es ist möglich, Aktualisierungen durchzuführen, ohne ganze Entitäten zu laden, indem eine geänderte Bloginstanz angefügt und EF mitgeteilt wird, welche Eigenschaften geändert wurden, aber das ist eine komplexere Technik, die es nicht wert ist.

Resultsetgröße begrenzen

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

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

Da die Anzahl der zurückgegebenen Zeilen von tatsächlichen Daten in Ihrer Datenbank abhängt, ist es unmöglich zu wissen, wie viele Daten aus der Datenbank geladen werden, wie viel Arbeitsspeicher von den Ergebnissen beansprucht wird und wie viel zusätzliche Last beim Verarbeiten dieser Ergebnisse generiert wird (z. B. durch Senden an einen Benutzerbrowser über das Netzwerk). Entscheidend ist, dass Testdatenbanken häufig kleine Daten enthalten, damit alles beim Testen gut funktioniert, aber Leistungsprobleme treten plötzlich auf, wenn die Abfrage mit realen Daten beginnt und viele Zeilen zurückgegeben werden.

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

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

Auf der Benutzeroberfläche kann mindestens eine Meldung angezeigt werden, die angibt, dass mehr Zeilen in der Datenbank vorhanden sein können (und das Abrufen auf andere Weise zulassen). Eine vollgeblasene Lösung würde Paginierung implementieren, bei der die Benutzeroberfläche nur jeweils eine bestimmte Anzahl von Zeilen anzeigt und Benutzern die Möglichkeit gibt, nach Bedarf zur nächsten Seite zu wechseln; Weitere Informationen zur effizienten Implementierung finden Sie im nächsten Abschnitt.

Effiziente Paginierung

Paginierung bezieht sich auf das Abrufen von Ergebnissen in Seiten und nicht auf einmal; Dies erfolgt in der Regel für große Resultsets, bei denen eine Benutzeroberfläche angezeigt wird, mit der der Benutzer zur nächsten oder vorherigen Seite der Ergebnisse navigieren kann. Eine gängige Möglichkeit zum Implementieren der Paginierung mit Datenbanken besteht darin, die Skip Operatoren Take (OFFSET und LIMIT in SQL) zu verwenden. Dies ist zwar eine intuitive Implementierung, ist aber auch ziemlich ineffizient. Bei Paginierung, die das Gleichzeitige Verschieben einer Seite (im Gegensatz zum Springen zu beliebigen Seiten) ermöglicht, sollten Sie stattdessen die Verwendung der Keyset-Paginierung in Betracht ziehen.

Weitere Informationen finden Sie auf der Dokumentationsseite zur Paginierung.

In relationalen Datenbanken werden alle verwandten Entitäten geladen, indem JOINs in einer einzigen Abfrage eingeführt werden.

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 verwandte Beiträge enthält, duplizieren Zeilen für diese Beiträge die Informationen des Blogs. Diese Duplizierung führt zu dem sogenannten "kartesischen Explosionsproblem". Wenn mehr 1:n-Beziehungen in das System geladen werden, kann die Menge der duplizierten Daten zunehmen und die Performance Ihrer Anwendung negativ beeinflussen.

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

Hinweis

Die derzeitige Umsetzung von Split-Queries führt für jede Abfrage einen Roundtrip durch. 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.

Beim Umgang mit verwandten Entitäten 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, eifriges Laden zu verwenden, damit EF alle erforderlichen Daten in einem Roundtrip abrufen kann. Mit der gefilterten Include-Funktion können Sie auch einschränken, welche zugehörigen Entitäten Sie laden möchten, während Sie den Ladevorgang auf Eager-Loading einstellen und ihn daher in einem einzigen Roundtrip erledigen können.

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

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 – möglicherweise einen Webservice – konsultieren, um zu wissen, ob wir an den Beiträgen dieses Blogs interessiert sind. In diesen Fällen können Explizites oder Lazy Loading verwendet werden, um verwandte Entitäten separat abzurufen und die Beitragsnavigation des Blogs zu füllen. Beachten Sie, dass diese Methoden nicht unmittelbar ausgeführt werden und zusätzliche Roundtrips zur Datenbank erfordern, was eine Quelle der Verlangsamung darstellt; Je nach Ihrem spezifischen Szenario kann es effizienter sein, einfach immer alle Beiträge zu laden, anstatt die zusätzlichen Roundtrips auszuführen und selektiv nur die benötigten Beiträge abzurufen.

Vorsicht beim Lazy Loading

Lazy loading scheint oft eine sehr nützliche Möglichkeit zum Schreiben von Datenbanklogik zu sein, da EF Core automatisch verwandte Entitäten aus der Datenbank lädt, da sie von Ihrem Code aufgerufen werden. Dadurch wird verhindert, dass verwandte Entitäten geladen werden, die nicht benötigt werden (z. B. explizites Laden), und der Programmierer wird scheinbar davon befreit, sich vollständig mit verwandten Entitäten zu befassen. Das faule Laden ist jedoch besonders anfällig für die Herstellung nicht benötigter zusätzlicher Roundtrips, die die Anwendung verlangsamen können.

Beachte Folgendes:

foreach (var blog in await context.Blogs.ToListAsync())
{
    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. Wenn Sie die Anweisungsprotokollierung von EF Core aktivieren, wird Folgendes angezeigt:

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 Beiträge eines Blogs nur geladen, wenn auf deren Posts-Eigenschaft zugegriffen wird. Daher löst jede Iteration in der inneren foreach-Schleife eine zusätzliche Datenbankabfrage in einem eigenen Roundtrip aus. Daher erfolgt nach dem Laden der anfänglichen Abfrage, die alle Blogs umfasst, eine weitere Abfrage pro Blog, um alle Beiträge zu laden. Dies wird manchmal als N+1-Problem bezeichnet, und es kann sehr erhebliche Leistungsprobleme verursachen.

Angenommen, wir werden alle Blogbeiträge benötigen, macht es Sinn, 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 laden, was erforderlich ist). Daher verwenden wir stattdessen eine Projektion:

await foreach (var blog in context.Blogs.Select(b => new { b.Url, b.Posts }).AsAsyncEnumerable())
{
    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 hilfreich sein, kartesische Explosionseffekte mithilfe von geteilten Abfragen zu vermeiden.

Warnung

Da beim Lazy Loading das N+1-Problem sehr leicht unbeabsichtigt ausgelöst werden kann, wird empfohlen, es zu vermeiden. Das vorsorgliche oder explizite Laden macht es im Quellcode sehr deutlich, wenn eine Datenbank-Abfrage auftritt.

Puffern und Streaming

Die Pufferung bezieht sich auf das Laden aller Abfrageergebnisse in den Arbeitsspeicher, während Streaming bedeutet, dass EF die Anwendung jedes Mal ein einzelnes Ergebnis übergibt und nie das gesamte Resultset im Arbeitsspeicher enthält. Grundsätzlich sind die Speicheranforderungen einer Streamingabfrage fest – sie sind gleich, ob die Abfrage 1 Zeile oder 1000 zurückgibt; Eine Pufferabfrage erfordert andererseits mehr Arbeitsspeicher, wenn 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 = await context.Posts.Where(p => p.Title.StartsWith("A")).ToListAsync();
var blogsArray = await context.Posts.Where(p => p.Title.StartsWith("A")).ToArrayAsync();

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

// AsAsyncEnumerable 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
    .AsAsyncEnumerable()
    .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, lohnt es sich, das Streaming zu überwägen, anstatt puffern zu müssen.

Hinweis

Vermeiden Sie die Verwendung 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 die Ergebnismenge intern, unabhängig davon, wie Sie Ihre Abfrage auswerten. Die beiden Fälle, in denen dies geschieht, sind:

  • Wenn eine Wiederholungsausführungsstrategie vorhanden ist. Dadurch wird sichergestellt, dass dieselben Ergebnisse zurückgegeben werden, wenn die Abfrage später erneut versucht wird.
  • Wenn die Funktion 'geteilte Abfrage' verwendet wird, werden die Resultsets aller Abfragen, außer der letzten Abfrage, gepuffert – es sei denn, MARS (Mehrere aktive Resultsets) ist auf dem SQL Server aktiviert. Dies liegt daran, dass es in der Regel unmöglich ist, mehrere Abfrage-Resultsets gleichzeitig aktiv zu haben.

Beachten Sie, dass diese interne Pufferung zusätzlich zu allen Puffern erfolgt, die Sie über LINQ-Operatoren verursachen. Verwenden Sie beispielsweise ToList bei einer Abfrage und es ist eine Strategie zur wiederholten Ausführung implementiert, wird das Resultset zweimal in den Arbeitsspeicher geladen: einmal intern von EF und einmal von ToList.

Nachverfolgen, Nichtverfolgung und Identitätsauflösung

Es wird empfohlen, die spezielle Seite zum Tracking und ohne Tracking zu lesen, bevor Sie mit diesem Abschnitt fortfahren.

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

  • EF verwaltet intern ein Verzeichnis mit überwachten 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 Wartung und das Nachschlagen im Wörterbuch nehmen etwas Zeit in Anspruch, wenn die Abfrageergebnisse geladen werden.
  • Bevor Sie eine geladene Instanz an die Anwendung übergeben, erstellt EF Momentaufnahmen dieser Instanz und behält die Momentaufnahme intern bei. Wenn SaveChanges aufgerufen wird, wird die Instanz der Anwendung mit der Momentaufnahme verglichen, um die Änderungen zu ermitteln, die gespeichert werden müssen. Die Momentaufnahme benötigt mehr Arbeitsspeicher, und der Momentaufnahmeprozess selbst benötigt Zeit; Es ist manchmal möglich, ein anderes, möglicherweise effizienteres Snapshottingverhalten über Wertabgleiche anzugeben oder Änderungsnachverfolgungsproxys zu verwenden, um den Snapshottingprozess vollständig zu umgehen (obwohl das mit einem eigenen Satz von Nachteilen kommt).

In schreibgeschützten Szenarien, in denen Änderungen nicht in der Datenbank gespeichert werden, können die oben genannten Mehrkosten mithilfe von No-Tracking-Abfragen vermieden werden. Da Abfragen ohne Nachverfolgung jedoch keine Identitätsauflösung durchführen, wird eine Datenbankzeile, auf die von mehreren anderen geladenen Zeilen verwiesen wird, als unterschiedliche Instanzen dargestellt.

Gehen Sie zur Veranschaulichung davon aus, dass wir eine große Anzahl von Beiträgen aus der Datenbank laden, sowie den Blog, auf den von jedem Beitrag verwiesen wird. Wenn 100 Beiträge auf denselben Blog verweisen, erkennt eine Nachverfolgungsabfrage dies über die Identitätsauflösung, und alle Postinstanzen verweisen auf dieselbe deduplizierte Bloginstanz. Eine No-Tracking-Abfrage dupliziert im Gegensatz dazu denselben Blog 100 Mal – und der Anwendungscode muss entsprechend geschrieben werden.

Hier sind die Ergebnisse für einen Benchmark, der die Nachverfolgung im Vergleich zum No-Tracking-Verhalten für eine Abfrage vergleicht, die 10 Blogs mit jeweils 20 Beiträgen lädt. Der Quellcode ist hier verfügbar, und Sie können ihn gerne als Grundlage für Ihre eigenen Messungen verwenden.

Methode NumBlogs AnzahlBeiträgeProBlog Mittelwert Fehler StdDev Median Verhältnis RatioSD Gen 0 Gen1 Gen2 Zugeordnet
AsTracking 10 20 1.414,7 us 27.20 uns 45.44 uns 1.405.5 uns 1.00 0,00 60,5469 13.6719 - 380.11 KB
AsNoTracking (als keine Nachverfolgung) 10 20 993.3 uns 24.04 uns 65.40 uns 966.2 uns 0.71 0.05 37.1094 6.8359 - 232,89 KB

Schließlich ist es möglich, Updates ohne den Aufwand der Änderungsnachverfolgung durchzuführen, indem eine No-Tracking-Abfrage verwendet und dann die zurückgegebene Instanz an den Kontext angefügt wird, wobei angegeben wird, welche Änderungen vorgenommen werden sollen. Dies überträgt die Belastung der Änderungsnachverfolgung von EF an den Benutzer und sollte nur versucht werden, wenn der Aufwand für die Änderungsnachverfolgung durch Profilerstellung oder Benchmarking unannehmbar ist.

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 es Ihnen sogar, reguläre LINQ-Abfragen mit SQL zu kombinieren, sodass Sie nur einen Teil der Anfrage 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 sie dann aus Ihren Abfragen auf. Beachten Sie, dass EF UDFs ermöglicht, vollständige Resultsets zurückzugeben – diese werden als Tabellenwertfunktionen (TVFs) bezeichnet und ermöglichen auch die Zuordnung einer DbSet Funktion, sodass sie wie nur eine andere 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

Raw 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 sie zu rechtfertigen. Die Verwendung von raw SQL bringt erhebliche Wartungsnachteile.

Asynchrone Programmierung

In der Regel ist es wichtig, asynchrone APIs anstelle synchroner APIs (z. B. SaveChangesAsync statt SaveChangessynchron) immer zu verwenden, damit Ihre Anwendung skalierbar ist. Synchrone APIs blockieren Threads während der Datenbank-E/A, wodurch die Notwendigkeit für Threads und die Anzahl der Thread-Kontextwechsel, die auftreten müssen, erhöht wird.

Weitere Informationen finden Sie auf der Seite zur asynchronen Programmierung.

Warnung

Vermeiden Sie das Mischen von synchronem und asynchronem Code in derselben Anwendung – es ist sehr leicht, versehentlich subtile Threadpool-Auslastungsprobleme auszulösen.

Warnung

Die asynchrone Implementierung von Microsoft.Data.SqlClient hat leider einige bekannte Probleme (z. B. #593, #601und andere). Wenn unerwartete Leistungsprobleme auftreten, versuchen Sie stattdessen, die Ausführung von Synchronisierungsbefehlen zu verwenden, insbesondere bei großen Text- oder Binärwerten.

Weitere Ressourcen