Methoden zum Optimieren von LINQ-Abfragen in C # .NET

Einführung


In diesem Artikel haben wir einige Optimierungstechniken für LINQ-Anfragen besprochen .
Hier sind einige weitere Ansätze zur Codeoptimierung im Zusammenhang mit LINQ-Abfragen .

Es ist bekannt, dass LINQ (Language-Integrated Query) eine einfache und bequeme Sprache zum Abfragen einer Datenquelle ist.

Und LINQ to SQL ist eine Datenzugriffstechnologie in einem DBMS. Dies ist ein leistungsstarkes Tool für die Arbeit mit Daten, bei dem Abfragen in einer deklarativen Sprache erstellt werden, die dann von der Plattform in SQL-Abfragen konvertiert und bereits zur Ausführung an den Datenbankserver gesendet werden. In unserem Fall meinen wir mit DBMS MS SQL Server .

Allerdings LINQ - Abfragen sind nicht optimal geschrieben konvertieren SQL - Abfragen , dass ein erfahrener DBA mit allen Nuancen zu optimieren schreiben konnte SQL - Abfragen :

  1. optimale Verbindungen ( JOIN ) und Filterung der Ergebnisse ( WO )
  2. viele Nuancen bei der Verwendung von Verbindungen und Gruppenbedingungen
  3. Viele Variationen beim Ersetzen der IN- Bedingungen durch EXISTS und NOT IN , <> durch EXISTS
  4. Zwischen-Caching von Ergebnissen über temporäre Tabellen, CTE, Tabellenvariablen
  5. Verwenden einer Klausel ( OPTION ) mit Anweisungen und Tabellenhinweisen WITH (...)
  6. die Verwendung von indizierten Ansichten als eines der Mittel, um redundante Messwerte von Daten in Stichproben zu beseitigen

Die wichtigsten Leistungsengpässe der resultierenden SQL-Abfragen beim Kompilieren von LINQ-Abfragen sind:

  1. Konsolidierung des gesamten Datenauswahlmechanismus in einer Anfrage
  2. Duplizieren identischer Codeblöcke, was letztendlich zu mehreren zusätzlichen Datenlesungen führt
  3. Gruppen von Mehrkomponentenbedingungen (logisch "und" und "oder") - UND und ODER , die unter schwierigen Bedingungen kombiniert werden, führen dazu, dass der Optimierer mit geeigneten nicht gruppierten Indizes anhand der erforderlichen Felder schließlich beginnt, nach dem Clusterindex zu scannen ( INDEX SCAN ) nach Bedingungsgruppe
  4. tiefere Verschachtelung von Unterabfragen macht es sehr problematisch zu analysieren SQL - Anweisungen und Parse - Abfragepläne von Entwicklern und DBAs

Optimierungsmethoden


Nun gehen wir direkt zu den Optimierungsmethoden über.

1) Zusätzliche Indizierung


Es ist am besten, Filter in den Hauptstichproben-Tabellen zu berücksichtigen, da die gesamte Abfrage sehr oft um eine oder zwei Haupttabellen (Anwendungen-Personen-Operationen) und mit einem Standardsatz von Bedingungen (IsClosed, Cancelled, Enabled, Status) erstellt wird. Für die identifizierten Stichproben ist es wichtig, die entsprechenden Indizes zu erstellen.

Diese Lösung ist sinnvoll, wenn die Auswahl aus diesen Feldern den zurückgegebenen Satz für die Abfrage erheblich einschränkt.

Zum Beispiel haben wir 500.000 Bewerbungen. Es gibt jedoch nur 2.000 aktive Einträge. Ein korrekt ausgewählter Index speichert uns dann über eine große Tabelle vor INDEX SCAN und ermöglicht uns die schnelle Auswahl von Daten über einen nicht gruppierten Index.

Fehlende Indizes können auch durch Eingabeaufforderungen zum Parsen von Abfrageplänen oder durch das Sammeln von Statistiken für Systemansichten erkannt werdenMS SQL Server :

  1. sys.dm_db_missing_index_groups
  2. sys.dm_db_missing_index_group_stats
  3. sys.dm_db_missing_index_details

Alle Ansichtsdaten enthalten Informationen zu fehlenden Indizes mit Ausnahme von räumlichen Indizes.

Indizes und Caching sind jedoch häufig Methoden, um mit den Auswirkungen schlecht geschriebener LINQ- und SQL-Abfragen umzugehen .

Wie die harte Lebenspraxis für das Unternehmen zeigt, ist es oft wichtig, Geschäftsfunktionen bis zu einem bestimmten Datum zu implementieren. Daher werden beim Caching häufig umfangreiche Abfragen in den Hintergrund gestellt.

Dies ist teilweise gerechtfertigt, da der Benutzer nicht immer die neuesten Daten benötigt und eine akzeptable Antwort der Benutzeroberfläche auftritt.

Dieser Ansatz ermöglicht es Ihnen, geschäftliche Anforderungen zu lösen, verringert jedoch letztendlich die Effizienz des Informationssystems und verzögert lediglich die Lösung von Problemen.

Es ist auch zu beachten, dass bei dem Suchvorgang, der zum Hinzufügen neuer Indizes erforderlich ist, die Vorschläge zur MS SQL- Optimierung möglicherweise falsch sind, auch unter den folgenden Bedingungen:

  1. wenn bereits Indizes mit einem ähnlichen Satz von Feldern vorhanden sind
  2. wenn die Felder in der Tabelle aufgrund von Indizierungsbeschränkungen nicht indiziert werden können (mehr dazu wird hier beschrieben ).

2) Zusammenführen von Attributen zu einem neuen Attribut


Manchmal können einige Felder aus derselben Tabelle, in denen eine Gruppe von Bedingungen auftritt, durch die Einführung eines neuen Felds ersetzt werden.

Dies gilt insbesondere für Statusfelder, die nach Typ normalerweise entweder bitweise oder ganzzahlig sind.

Beispiel:

IsClosed = 0 AND Cancelled = 0 AND Enabled = 0 wird durch Status = 1 ersetzt .

Hier geben Sie das Integer-Attribut Status ein, das durch Ausfüllen dieser Status in der Tabelle bereitgestellt wird. Der nächste Schritt ist die Indizierung dieses neuen Attributs.

Dies ist eine grundlegende Lösung für das Leistungsproblem, da wir Daten ohne unnötige Berechnungen anfordern.

3) Materialisierung der Einreichung


Leider können LINQ-Abfragen temporäre Tabellen, CTEs und Tabellenvariablen nicht direkt verwenden.

Es gibt jedoch eine andere Möglichkeit zur Optimierung für diesen Fall - dies sind indizierte Ansichten.

Eine Gruppe von Bedingungen (aus dem obigen Beispiel) IsClosed = 0 AND Cancelled = 0 AND Enabled = 0 (oder eine Reihe anderer ähnlicher Bedingungen) ist eine gute Option, um sie in einer indizierten Ansicht zu verwenden und einen kleinen Datenabschnitt aus einer großen Menge zwischenzuspeichern.

Es gibt jedoch eine Reihe von Einschränkungen beim Materialisieren einer Ansicht:

  1. Bei Verwendung von Unterabfragen sollten EXISTS- Klauseln mit JOIN ersetzt werden
  2. Kann die Klauseln UNION , UNION ALL , EXCEPTION , INTERSECT nicht verwenden
  3. Sie können keine Tabellenhinweise und OPTION- Klauseln verwenden
  4. Keine Fähigkeit, mit Zyklen zu arbeiten
  5. Es ist unmöglich, Daten in einer Ansicht aus verschiedenen Tabellen anzuzeigen

Es ist wichtig zu bedenken, dass die tatsächlichen Vorteile der Verwendung einer indizierten Ansicht nur durch Indizierung erzielt werden können.

Beim Aufrufen einer Ansicht werden diese Indizes jedoch möglicherweise nicht verwendet. Um sie explizit zu verwenden, müssen Sie WITH (NOEXPAND) angeben .

Da es unmöglich ist, Tabellenhinweise in LINQ-Abfragen zu definieren, müssen Sie eine andere Darstellung erstellen - einen „Wrapper“ der folgenden Form:

CREATE VIEW _ AS SELECT * FROM MAT_VIEW WITH (NOEXPAND);

4) Verwenden von Tabellenfunktionen


In LINQ-Abfragen bilden häufig große Unterabfrageblöcke oder Blöcke, die Darstellungen mit einer komplexen Struktur verwenden, die endgültige Abfrage mit einer sehr komplexen und nicht optimalen Ausführungsstruktur.

Hauptvorteile der Verwendung von Tabellenfunktionen in LINQ-Abfragen :

  1. Die Möglichkeit, wie im Fall von Ansichten, als Objekt zu verwenden und anzugeben, aber Sie können eine Reihe von Eingabeparametern übergeben:
    FROM FUNCTION (@ param1, @ param2 ...)
    am Ende können Sie eine flexible Datenabtastung erzielen
  2. Bei Verwendung einer Tabellenfunktion gibt es keine so starken Einschränkungen wie bei den oben beschriebenen indizierten Ansichten:

    1. :
      LINQ .
      .
      ,
    2. , , :

      • ( )
      • UNION EXISTS

  3. OPTION , OPTION(MAXDOP N), . :

    • OPTION (RECOMPILE)
    • , OPTION (FORCE ORDER)

    OPTION .
  4. :
    ( ), .
    , , WHERE (a, b, c).

    a = 0 and b = 0.

    , c .

    a = 0 and b = 0 , .

    Hier kann die Tabellenfunktion eine bessere Option sein.

    Außerdem ist die Tabellenfunktion vorhersehbarer und in der Ausführungszeit konstanter.

Beispiele


Betrachten wir eine Beispielimplementierung am Beispiel der Fragendatenbank.

Es gibt eine SELECT- Abfrage , die mehrere Tabellen kombiniert und eine Ansicht (OperativeQuestions) verwendet, die per E-Mail die Zugehörigkeit (über EXISTS ) von "Active Queries" ([OperativeQuestions]) überprüft :

Anfrage Nr. 1
(@p__linq__0 nvarchar(4000))SELECT
1 AS [C1],
[Extent1].[Id] AS [Id],
[Join2].[Object_Id] AS [Object_Id],
[Join2].[ObjectType_Id] AS [ObjectType_Id],
[Join2].[Name] AS [Name],
[Join2].[ExternalId] AS [ExternalId]
FROM [dbo].[Questions] AS [Extent1]
INNER JOIN (SELECT [Extent2].[Object_Id] AS [Object_Id],
[Extent2].[Question_Id] AS [Question_Id], [Extent3].[ExternalId] AS [ExternalId],
[Extent3].[ObjectType_Id] AS [ObjectType_Id], [Extent4].[Name] AS [Name]
FROM [dbo].[ObjectQuestions] AS [Extent2]
INNER JOIN [dbo].[Objects] AS [Extent3] ON [Extent2].[Object_Id] = [Extent3].[Id]
LEFT OUTER JOIN [dbo].[ObjectTypes] AS [Extent4] 
ON [Extent3].[ObjectType_Id] = [Extent4].[Id] ) AS [Join2] 
ON [Extent1].[Id] = [Join2].[Question_Id]
WHERE ([Extent1].[AnswerId] IS NULL) AND (0 = [Extent1].[Exp]) AND ( EXISTS (SELECT
1 AS [C1]
FROM [dbo].[OperativeQuestions] AS [Extent5]
WHERE (([Extent5].[Email] = @p__linq__0) OR (([Extent5].[Email] IS NULL) 
AND (@p__linq__0 IS NULL))) AND ([Extent5].[Id] = [Extent1].[Id])
));


Die Ansicht hat eine ziemlich komplizierte Struktur: Sie hat Unterabfrage-Verknüpfungen und die Verwendung der DISTINCT- Sortierung , was im allgemeinen Fall eine ziemlich ressourcenintensive Operation ist.

Eine Auswahl von ungefähr zehntausend Datensätzen aus OperativeQuestions.

Das Hauptproblem dieser Abfrage besteht darin, dass für Datensätze aus einer externen Abfrage eine interne Unterabfrage in der Ansicht [OperativeQuestions] ausgeführt wird, die das Ausgabebeispiel (über EXISTS ) auf Hunderte von Datensätzen für [Email] = @ p__linq__0 beschränken sollte .

Und es könnte den Anschein haben, dass die Unterabfrage die Datensätze einmal mit [Email] = @ p__linq__0 berechnen sollte, und dann sollten diese paar hundert Datensätze mit ID c-Fragen verbunden werden, und die Abfrage wird schnell sein.

Tatsächlich sind alle Tabellen in Reihe geschaltet: ID-Fragen und ID von OperativeQuestions werden auf Konformität überprüft und E-Mail wird gefiltert.

Tatsächlich funktioniert die Anforderung mit allen Zehntausenden von OperativeQuestions-Datensätzen, und Sie benötigen nur Daten von Interesse per E-Mail.

OperativeQuestions Text anzeigen:

Anfrage Nr. 2
 
CREATE VIEW [dbo].[OperativeQuestions]
AS
SELECT DISTINCT Q.Id, USR.email AS Email
FROM            [dbo].Questions AS Q INNER JOIN
                         [dbo].ProcessUserAccesses AS BPU ON BPU.ProcessId = CQ.Process_Id 
OUTER APPLY
                     (SELECT   1 AS HasNoObjects
                      WHERE   NOT EXISTS
                                    (SELECT   1
                                     FROM     [dbo].ObjectUserAccesses AS BOU
                                     WHERE   BOU.ProcessUserAccessId = BPU.[Id] AND BOU.[To] IS NULL)
) AS BO INNER JOIN
                         [dbo].Users AS USR ON USR.Id = BPU.UserId
WHERE        CQ.[Exp] = 0 AND CQ.AnswerId IS NULL AND BPU.[To] IS NULL 
AND (BO.HasNoObjects = 1 OR
              EXISTS (SELECT   1
                           FROM   [dbo].ObjectUserAccesses AS BOU INNER JOIN
                                      [dbo].ObjectQuestions AS QBO 
                                                  ON QBO.[Object_Id] =BOU.ObjectId
                               WHERE  BOU.ProcessUserAccessId = BPU.Id 
                               AND BOU.[To] IS NULL AND QBO.Question_Id = CQ.Id));


Ursprüngliche Mapping-Darstellung in DbContext (EF Core 2)
public class QuestionsDbContext : DbContext
{
    //...
    public DbQuery<OperativeQuestion> OperativeQuestions { get; set; }
    //...
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Query<OperativeQuestion>().ToView("OperativeQuestions");
    }
}


Ursprüngliche LINQ-Abfrage
var businessObjectsData = await context
    .OperativeQuestions
    .Where(x => x.Email == Email)
    .Include(x => x.Question)
    .Select(x => x.Question)
    .SelectMany(x => x.ObjectQuestions,
                (x, bo) => new
                {
                    Id = x.Id,
                    ObjectId = bo.Object.Id,
                    ObjectTypeId = bo.Object.ObjectType.Id,
                    ObjectTypeName = bo.Object.ObjectType.Name,
                    ObjectExternalId = bo.Object.ExternalId
                })
    .ToListAsync();


In diesem speziellen Fall wird eine Lösung für dieses Problem ohne infrastrukturelle Änderungen in Betracht gezogen, ohne dass eine separate Tabelle mit vorgefertigten Ergebnissen („aktive Abfragen“) eingeführt wird, unter der ein Mechanismus erforderlich wäre, um die Daten auszufüllen und auf dem neuesten Stand zu halten.

Obwohl dies eine gute Lösung ist, gibt es eine andere Möglichkeit, diese Aufgabe zu optimieren.

Das Hauptziel besteht darin, Einträge in der Ansicht OperativeQuestions mit [Email] = @ p__linq__0 zwischenzuspeichern.

Wir geben die Tabellenfunktion [dbo]. [OperativeQuestionsUserMail] in die Datenbank ein.

Wenn Sie E-Mail als Eingabeparameter senden, erhalten Sie die Wertetabelle zurück:

Anfrage Nr. 3

CREATE FUNCTION [dbo].[OperativeQuestionsUserMail]
(
    @Email  nvarchar(4000)
)
RETURNS
@tbl TABLE
(
    [Id]           uniqueidentifier,
    [Email]      nvarchar(4000)
)
AS
BEGIN
        INSERT INTO @tbl ([Id], [Email])
        SELECT Id, @Email
        FROM [OperativeQuestions]  AS [x] WHERE [x].[Email] = @Email;
     
    RETURN;
END


Dies gibt eine Wertetabelle mit einer vordefinierten Datenstruktur zurück.

Damit die Abfragen an OperativeQuestionsUserMail optimal sind und optimale Abfragepläne haben, ist eine strikte Struktur erforderlich und nicht RETURNS TABLE AS RETURN ...

In diesem Fall wird die gewünschte Anforderung 1 in Anforderung 4 konvertiert:

Anfrage Nr. 4
(@p__linq__0 nvarchar(4000))SELECT
1 AS [C1],
[Extent1].[Id] AS [Id],
[Join2].[Object_Id] AS [Object_Id],
[Join2].[ObjectType_Id] AS [ObjectType_Id],
[Join2].[Name] AS [Name],
[Join2].[ExternalId] AS [ExternalId]
FROM (
    SELECT Id, Email FROM [dbo].[OperativeQuestionsUserMail] (@p__linq__0)
) AS [Extent0]
INNER JOIN [dbo].[Questions] AS [Extent1] ON([Extent0].Id=[Extent1].Id)
INNER JOIN (SELECT [Extent2].[Object_Id] AS [Object_Id], [Extent2].[Question_Id] AS [Question_Id], [Extent3].[ExternalId] AS [ExternalId], [Extent3].[ObjectType_Id] AS [ObjectType_Id], [Extent4].[Name] AS [Name]
FROM [dbo].[ObjectQuestions] AS [Extent2]
INNER JOIN [dbo].[Objects] AS [Extent3] ON [Extent2].[Object_Id] = [Extent3].[Id]
LEFT OUTER JOIN [dbo].[ObjectTypes] AS [Extent4] 
ON [Extent3].[ObjectType_Id] = [Extent4].[Id] ) AS [Join2] 
ON [Extent1].[Id] = [Join2].[Question_Id]
WHERE ([Extent1].[AnswerId] IS NULL) AND (0 = [Extent1].[Exp]);


Zuordnen von Ansichten und Funktionen in DbContext (EF Core 2)
public class QuestionsDbContext : DbContext
{
    //...
    public DbQuery<OperativeQuestion> OperativeQuestions { get; set; }
    //...
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Query<OperativeQuestion>().ToView("OperativeQuestions");
    }
}
 
public static class FromSqlQueries
{
    public static IQueryable<OperativeQuestion> GetByUserEmail(this DbQuery<OperativeQuestion> source, string Email)
        => source.FromSql($"SELECT Id, Email FROM [dbo].[OperativeQuestionsUserMail] ({Email})");
}


Letzte LINQ-Abfrage
var businessObjectsData = await context
    .OperativeQuestions
    .GetByUserEmail(Email)
    .Include(x => x.Question)
    .Select(x => x.Question)
    .SelectMany(x => x.ObjectQuestions,
                (x, bo) => new
                {
                    Id = x.Id,
                    ObjectId = bo.Object.Id,
                    ObjectTypeId = bo.Object.ObjectType.Id,
                    ObjectTypeName = bo.Object.ObjectType.Name,
                    ObjectExternalId = bo.Object.ExternalId
                })
    .ToListAsync();


Die Reihenfolge der Ausführungszeit verringerte sich von 200 bis 800 ms auf 2 bis 20 ms usw., dh zehnmal schneller.

Wenn wir mehr gemittelt nehmen, haben wir statt 350 ms 8 ms.

Aus den offensichtlichen Pluspunkten erhalten wir auch:

  1. allgemeine Reduzierung der Leselast,
  2. signifikante Verringerung der Wahrscheinlichkeit einer Blockierung
  3. Reduzierung der durchschnittlichen Blockierzeit auf akzeptable Werte

Fazit


Die Optimierung und Feinabstimmung von Aufrufen der MS SQL-Datenbank über LINQ ist ein Problem, das gelöst werden kann.

Bei dieser Arbeit sind Sorgfalt und Beständigkeit sehr wichtig.

Zu Beginn des Prozesses:

  1. Es ist notwendig, die Daten zu überprüfen, mit denen die Abfrage arbeitet (Werte, ausgewählte Datentypen).
  2. Indizieren Sie diese Daten ordnungsgemäß
  3. Überprüfen Sie die Richtigkeit der Verbindungsbedingungen zwischen den Tabellen

Bei der nächsten Iteration zeigt die Optimierung:

  1. die Basis der Anfrage und den Hauptanforderungsfilter
  2. Wiederholen ähnlicher Abfrageblöcke und sich überschneidender Bedingungen
  3. In SSMS oder einer anderen GUI für SQL Server wird die SQL-Abfrage selbst optimiert (Zuweisung eines Zwischendatenspeichers, Erstellen der resultierenden Abfrage mithilfe dieses Speichers (möglicherweise mehrere).
  4. In der letzten Phase wird die LINQ-Abfragestruktur auf der Grundlage der resultierenden SQL-Abfrage neu erstellt

Infolgedessen sollte die resultierende LINQ-Abfrage in ihrer Struktur mit der identifizierten optimalen SQL-Abfrage aus Absatz 3 identisch sein .

Danksagung


Vielen Dank an die Kollegen jobgemws und alex_ozrvon Fortis für die Hilfe bei diesem Artikel.

All Articles