Optimeringsmetoder för LINQ-frÄgor i C#.NET

Introduktion

I denna artikel NÄgra metoder för optimering övervÀgdes LINQ-frÄgor.
HÀr kommer vi att presentera ytterligare nÄgra metoder för att optimera koden relaterade till LINQ-frÄgor.

Det Ă€r kĂ€nt att LINQ(Language-Integrated Query) — Ă€r ett enkelt och praktiskt frĂ„gesprĂ„k för datakĂ€llor.

A LINQ till SQL Àr en databasÄtkomstteknik. Det Àr ett kraftfullt verktyg för databehandling, dÀr deklarativt sprÄk anvÀnds för att konstruera frÄgor som sedan omvandlas till SQL-frÄgor plattform och skickas till databasservern för utförande. I vÄrt fall kommer vi att förstÄ MS SQL Server.

Men, LINQ-frÄgor inte omvandlas till optimalt skrivna SQL-frÄgor, som en erfaren DBA skulle kunna skriva med alla nyanser av optimering SQL-frÄgor:

  1. optimala föreningar (JOIN) och filtrering av resultat (DÄR)
  2. mÄnga nyanser i anvÀndningen av föreningar och gruppvillkor
  3. mÄnga variationer i ersÀttning av villkor IN till EXISTSoch NOT IN, till EXISTS
  4. intermittent cache av resultat genom temporÀra tabeller, CTE, tabellvariabler
  5. anvÀndning av konstruktionen (OPTION) med anvisningar och tabellhintar WITH (
)
  6. anvÀndning av indexerade vyer som ett sÀtt att bli av med överflödig dataavlÀsning vid val

De frÀmsta flaskhalsarna för prestanda som uppstÄr SQL-frÄgor vid kompilering LINQ-frÄgor Àr:

  1. konsolidering av hela mekanismen för dataselektion i en frÄga
  2. duplicering av identiska kodblock, vilket sÄ smÄningom leder till flera onödiga datalÀsningar
  3. grupper av sammansatta villkor (logiska 'och' och 'eller') — OCH och ELLER, som förenas i komplexa villkor, leder till att optimeraren, med lĂ€mpliga icke-klusterade index pĂ„ nödvĂ€ndiga fĂ€lt, trots allt börjar göra en skanning av det klusterade indexet (INDEX SCAN) baserat pĂ„ villkorsgrupper
  4. djup inbÀddning av underfrÄgor gör tolkningen av SQL-instruktioner och analysen av frÄgeplanen frÄn utvecklarnas och DBA

Optimeringsmetoder

Nu gÄr vi direkt över till optimeringsmetoder.

1) Ytterligare indexering

Det Àr bÀst att betrakta filter pÄ de huvudtabeller som vÀljningar görs frÄn, eftersom en frÄga ofta byggs kring en eller tvÄ huvudtabeller (ansökningar-mÀnniskor-operationer) med en standarduppsÀttning villkor (IsClosed, Canceled, Enabled, Status). Det Àr viktigt att skapa motsvarande index för de identifierade vÀljningarna.

Denna lösning Àr meningsfull nÀr urvalet av dessa fÀlt vÀsentligt begrÀnsar mÀngden resultat frÄn en frÄga.

Till exempel har vi 500000 ansökningar. Men det finns endast 2000 aktiva ansökningar. DÄ kommer ett vÀl valt index att befria oss frÄn INDEX SCAN genom den stora tabellen och möjliggöra snabb hÀmtning av data genom ett icke-klustrat index.

Bristen pÄ index kan ocksÄ identifieras genom tips i analyser av frÄgeplaner eller genom att samla statistik frÄn systemvyer MS SQL Server:

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

Alla data frÄn vyerna innehÄller information om saknade index, med undantag för rumsliga index.

Dock Àr index och cache ofta metoder för att hantera konsekvenserna av dÄligt skrivna LINQ-frÄgor och SQL-frÄgor.

Som hÄrd praktik visar Àr det ofta viktigt för företag att implementera affÀrsfunktioner inom vissa tidsramar. DÀrför konverteras tunga frÄgor ofta till bakgrundslÀge med caching.

Delvis Àr detta motiverat, eftersom anvÀndaren inte alltid behöver de senaste data och den acceptabla nivÄn för anvÀndargrÀnssnittets respons bibehÄlls.

Denna metod möjliggör hantering av affÀrsfrÄgor, men sÀnker i slutÀndan prestandan för informationssystemet, eftersom problem bara skjuts upp.

Det Àr ocksÄ vÀrt att komma ihÄg att det i processen att hitta nödvÀndiga förslag för att lÀgga till nya index kan framstÀllas MS SQL felaktiga optimeringsförslag, sÀrskilt under följande förhÄllanden:

  1. om det redan finns index med en liknande uppsÀttning fÀlt
  2. om fÀlten i tabellen inte kan indexeras pÄ grund av indexeringsbegrÀnsningar (detta beskrivs mer i detalj hÀr).

2) Sammanfoga attribut till ett nytt attribut

Ibland kan vissa fÀlt frÄn en tabell, dÀr ett gruppvillkor förekommer, ersÀttas med ett nytt enskilt fÀlt.

Detta Àr sÀrskilt relevant för tillstÄndsfÀlt som vanligtvis Àr antingen bitar eller heltal.

Exempel:

IsClosed = 0 OCH Canceled = 0 OCH Enabled = 0 ersÀtts med Status = 1.

HÀr anges en heltalsattribut som Status, vilket sÀkerstÀlls genom att fylla i dessa statuser i tabellen. Sedan görs indexering av denna nya attribut.

Detta Àr en grundlÀggande lösning pÄ prestandaproblemet, eftersom vi hÀmtar data utan onödiga berÀkningar.

3) Materialisering av vyer

TyvÀrr kan LINQ-frÄgor inte direkt anvÀnda temporÀra tabeller, CTE och tabellvariabler.

Men det finns ett annat sĂ€tt att optimera i detta fall — indexerade vyer.

Gruppen av villkor (frÄn exemplet ovan) IsClosed = 0 OCH Canceled = 0 OCH Enabled = 0 (eller en uppsÀttning andra liknande villkor) blir ett bra alternativ att anvÀnda i en indexerad vy, vilket caching av en liten del av data frÄn en stor mÀngd.

Men det finns ett antal begrÀnsningar vid materialisering av vyer:

  1. anvÀndning av underfrÄgor, satser EXISTS ska ersÀttas med anvÀndning av JOIN
  2. kan inte anvÀnda satser UNION, UNION ALL, EXCEPTION, INTERSECT
  3. kan inte anvÀnda tabellhintar och satser OPTION
  4. det finns ingen möjlighet att arbeta med loopar
  5. det Àr omöjligt att exportera data i en vy frÄn olika tabeller

Det Àr viktigt att komma ihÄg att den verkliga nyttan av att anvÀnda indexerade vyer faktiskt bara kan uppnÄs nÀr de indexeras.

Men vid anrop av vyn kanske dessa index inte anvÀnds, och för att uttryckligen anvÀnda dem mÄste man ange WITH (NOEXPAND).

Eftersom det i LINQ-frĂ„gor inte gĂ„r att definiera tabellhintar, mĂ„ste man skapa en annan vy — en "wrapper" av följande typ:

CREATE VIEW NAMN_PÅ_VY AS SELECT * FROM MAT_VIEW WITH (NOEXPAND);

4) AnvÀndning av tabellfunktioner

Ofta i LINQ-frÄgor skapar stora block av underfrÄgor eller block som anvÀnder vyer med komplex struktur ett slutligt frÄgesvar med en mycket komplex och icke-optimal exekveringsstruktur.

Huvudfördelarna med att anvÀnda tabellfunktioner i LINQ-frÄgor:

  1. Möjligheten, precis som med vyer, att ange och anvÀnda som objekt, men man kan ocksÄ skicka en uppsÀttning inparametrar:
    FROM FUNCTION(@param1, @param2 
)
    i slutÀndan kan man uppnÄ flexibel dataval
  2. Vid anvÀndning av tabellfunktioner finns det inga sÄ strikta begrÀnsningar som vid indexerade vyer, som beskrivits ovan:
    1. Tabellhintar:
      via LINQ Det gÄr inte att specificera vilka index som ska anvÀndas och bestÀmma dataskyddsnivÄn vid en förfrÄgan.
      Men i funktionen finns dessa möjligheter.
      Med funktionen kan man uppnÄ en tillrÀckligt stabil exekveringsplan dÀr reglerna för arbete med index och dataskyddsnivÄer Àr definierade.
    2. AnvÀndningen av funktionen gör det möjligt, jÀmfört med indexerade vyer, att fÄ:
      • komplex logik för dataval (inklusive anvĂ€ndning av cykler)
      • dataval frĂ„n flera olika tabeller.
      • anvĂ€ndningen av UNION och EXISTS

  3. Förslaget OPTION Àr mycket anvÀndbart nÀr vi behöver sÀkerstÀlla hantering av parallellism. OPTION(MAXDOP N), ordningen för exekveringsplanen. Till exempel:
    • man kan ange att exekveringsplanen ska tvingas Ă„terskapas. OPTION (RECOMPILE)
    • man kan ange behovet av att sĂ€kerstĂ€lla tvingad anvĂ€ndning av den angivna join-ordningen i förfrĂ„gan. OPTION (FORCE ORDER)

    Mer detaljerat om OPTION beskrivet hÀr.

  4. AnvÀndningen av det smalaste och mest nödvÀndiga urvalet av data:
    Det Àr inte nödvÀndigt att hÄlla stora datamÀngder i cache (som i fallet med indexerade vyer), frÄn vilka man fortfarande behöver filtrera data baserat pÄ parameter.
    Till exempel finns det en tabell dĂ€r filtreringen DÄR anvĂ€nder tre fĂ€lt. (a, b, c).

    Villkoret gÀller för alla förfrÄgningar. a = 0 och b = 0..

    Men förfrÄgan till fÀltet c Àr mer variabel.

    LÄt villkoret a = 0 och b = 0. verkligen hjÀlpa oss att begrÀnsa den uppnÄdda uppsÀttningen till tusen poster, men villkoret angÄende med begrÀnsar urvalet till hundra poster.

    HÀr kan en tabellfunktion vara ett mer fördelaktigt alternativ.

    Dessutom Àr tabellfunktionen mer förutsÀgbar och stabil nÀr det gÀller exekveringstid.

Exempel

LÄt oss övervÀga ett exempel pÄ implementering i databasen Questions.

Det finns en förfrĂ„gan VÄLJ, som förenar flera tabeller och anvĂ€nder en vy (OperativeQuestions), dĂ€r man kontrollerar via e-post tillhörigheten (genom EXISTS) till "Aktiva förfrĂ„gningar" ([OperativeQuestions]):

FörfrĂ„gan № 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])
));

Representation has a fairly complex structure: it includes subquery joins and sorting usage. DISTINCT, which is generally a resource-intensive operation.

The selection from OperativeQuestions amounts to about ten thousand records.

The main issue with this query is that for records from the outer query, an inner subquery on the view [OperativeQuestions] is executed, which should limit the output selection for [Email] = @p__linq__0 (through EXISTS) to hundreds of records.

It may seem that the subquery should calculate the records for [Email] = @p__linq__0 once, and then these few hundred records should be joined by Id with Questions, making the query fast.

In reality, there is a sequential join of all tables: checking the matching Id of Questions with Id from OperativeQuestions, and filtering by Email.

Essentially, the query operates on all tens of thousands of records from OperativeQuestions, while only the relevant data by Email is needed.

The text of the view OperativeQuestions:

Query No. 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));

The original mapping of the view in DbContext (EF Core 2)

offentlig klass QuestionsDbContext : DbContext
{
    
    offentlig DbQuery OperativeQuestions { fÄ; set; }
    
    skyddad override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Query().ToView("OperativeQuestions");
    }
}

Ursprunglig LINQ-frÄga

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

I detta specifika fall övervÀgs en lösning pÄ detta problem utan infrastrukturella förÀndringar, utan att införa en separat tabell med fÀrdiga resultat ("Aktiva frÄgor"), för vilken det skulle behövas en mekanism för att fylla den med data och hÄlla den aktuell.

Även om detta Ă€r en bra lösning, finns det ett annat sĂ€tt att optimera denna uppgift.

Huvudsyftet Àr att cachelagra poster baserat pÄ [Email] = @p__linq__0 frÄn vyer OperativeQuestions.

Vi introducerar tabellfunktionen [dbo].[OperativeQuestionsUserMail] i databasen.

Genom att skicka Email som inparameter, fÄr vi tillbaka en vÀrdestabell:

FrÄga nr 3


SKAPA FUNKTION [dbo].[OperativeQuestionsUserMail]
(
    @Email  nvarchar(4000)
)
RETURNS
@tbl TABLE
(
    [Id]           uniqueidentifier,
    [Email]      nvarchar(4000)
)
AS
BEGIN
        INSERT INTO @tbl ([Id], [Email])
        VÄLJ Id, @Email
        FRÅN [OperativeQuestions]  SOM [x] DÄR [x].[Email] = @Email;
     
    RETURN;
SLUT

HÀr returneras en vÀrdestabell med en förutbestÀmd datastruktur.

För att förfrĂ„gningar till OperativeQuestionsUserMail ska vara optimala, med optimala förfrĂ„gningsplaner, behövs en strikt struktur, inte RETURNS TABLE AS RETURN


I detta fall omvandlas den önskade FrÄga 1 till FrÄga 4:

FrÄga nr 4

(@p__linq__0 nvarchar(4000))VÄLJ
1 SOM [C1],
[Extent1].[Id] SOM [Id],
[Join2].[Object_Id] SOM [Object_Id],
[Join2].[ObjectType_Id] SOM [ObjectType_Id],
[Join2].[Name] SOM [Name],
[Join2].[ExternalId] SOM [ExternalId]
FRÅN (
    VÄLJ Id, Email FRÅN [dbo].[OperativeQuestionsUserMail] (@p__linq__0)
) SOM [Extent0]
INNER JOIN [dbo].[Questions] SOM [Extent1] PÅ([Extent0].Id=[Extent1].Id)
INNER JOIN (VÄLJ [Extent2].[Object_Id] SOM [Object_Id], [Extent2].[Question_Id] SOM [Question_Id], [Extent3].[ExternalId] SOM [ExternalId], [Extent3].[ObjectType_Id] SOM [ObjectType_Id], [Extent4].[Name] SOM [Name]
FRÅN [dbo].[ObjectQuestions] SOM [Extent2]
INNER JOIN [dbo].[Objects] SOM [Extent3] PÅ [Extent2].[Object_Id] = [Extent3].[Id]
LEFT OUTER JOIN [dbo].[ObjectTypes] SOM [Extent4] 
PÅ [Extent3].[ObjectType_Id] = [Extent4].[Id] ) SOM [Join2] 
PÅ [Extent1].[Id] = [Join2].[Question_Id]
DÄR ([Extent1].[AnswerId] ÄR NULL) OCH (0 = [Extent1].[Exp]);

Mapping av vy och funktion i DbContext (EF Core 2)

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

Slutgiltig LINQ-frÄga

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();

Körningstiden har minskat frÄn 200-800 ms till 2-20 ms, dvs. flera gÄnger snabbare.

Om vi ser mer genomsnittligt sÄ fick vi istÀllet för 350 ms, 8 ms.

NÄgra uppenbara fördelar inkluderar:

  1. ett allmÀnt minskande av lÀsbelastningen,
  2. en betydande minskning av blockeringar
  3. minskning av den genomsnittliga blockeringstiden till acceptabla nivÄer

Utdata

Optimering och finjustering av databasfrÄgor MS SQL via LINQ Àr en uppgift som kan lösas.

I detta arbete Àr noggrannhet och konsekvens mycket viktiga.

I början av processen:

  1. mÄste vi kontrollera de data som frÄgan arbetar med (vÀrden, valda datatyper)
  2. utföra korrekt indexering av dessa data
  3. kontrollera korrektheten i anslutningsvillkoren mellan tabellerna

Vid nÀsta optimeringsiteration identifieras:

  1. grunden för frÄgan och den huvudsakliga filtreringen av frÄgan faststÀlls
  2. Äterkommande liknande delar av frÄgan och analys av överlappande villkor
  3. i SSMS eller annan GUI för SQL Server sjÀlva optimeras SQL-frÄga (extrahering av mellanlagring av data, bygga en resultatfrÄga med hjÀlp av denna lagring (kan vara flera))
  4. i det sista steget, med den resulterande SQL-frÄga, ombyggs strukturen av LINQ-frÄgan

I slutÀnde ska den resulterande LINQ-frÄgan vara strukturellt identisk med den optimala SQL-frÄgan frÄn punkt 3.

Tack.

Stort tack till kollegorna jobgemws och alex_ozr frÄn företaget Fortis för hjÀlpen med att förbereda detta material.

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster