LINQ-pÀringute optimeerimise meetodid C#.NET-is

Sissejuhatus

V selles artiklis vaatleme mitmeid optimeerimise meetodeid LINQ-pÀringutele.
Siin toome vÀlja veel mÔned koodi optimeerimise lÀhenemisviisid, mis on seotud LINQ-pÀringutega.

On teada, et LINQ(Language-Integrated Query) — see on lihtne ja mugav pĂ€ringukeel andmeallikate jaoks.

A LINQ to SQL on andmete juurdepÀÀsu tehnoloogia andmebaasisĂŒsteemides. See on vĂ”imas tööriist andmetega töötamiseks, kus deklaratiivse keele kaudu konstrueeritakse pĂ€ringud, mis seejĂ€rel muudetakse SQL-pĂ€ringuteks platvormile ja saadetakse andmebaasiserverile tĂ€itmiseks. Meie puhul mĂ”istame andmebaasisĂŒsteeme MS SQL Server.

Siiski, LINQ-pĂ€ringud ei muutu optimaalselt kirjutatud SQL-pĂ€ringuteks, mille vĂ”iks koostada kogenud DBA, arvestades kĂ”iki optimeerimise nĂŒansse SQL-pĂ€ringutest:

  1. optimaalsetest ĂŒhendustest (JOIN) ja tulemuste filtreerimisest (WHERE)
  2. palju nĂŒansse ĂŒhenduste ja grupitingimuste kasutamisel
  3. palju variatsioone tingimuste asendamisel IN jÀrgnevaga EXISTSja NOT IN, <> asendamine EXISTS
  4. vahepealne tulemuste vahemÀlu ajutiste tabelite, CTE-de, tabelimuutujate kaudu
  5. kasutades lauset (OPTION) suuniste ja tabeli vihjete mÀÀratlemiseks WITH (
)
  6. indekseeritavate vaadete kasutamine, et vĂ€hendada andmete ĂŒlemÀÀraseid lugemisi pĂ€ringute kĂ€igus

Tulevate rakenduste peamised jÔudluskitked SQL-pÀringutest koostamisel LINQ-pÀringutele on:

  1. kogu andmete valimise mehhanismi konsolideerimine ĂŒhes pĂ€ringus
  2. identsete koodiblokkide dubleerimine, mis toob lÔpuks kaasa mitmekordsed tarbetud andmete lugemised
  3. komplekssete tingimuste rĂŒhmad (loogilised 'ja' ja 'vĂ”i') — AND ja OR, ĂŒhendades keerukate tingimuste pĂ”hjal, toob see kaasa, et optimeerija, omades sobivaid klasterdamata indekseid, vajalike vĂ€ljade jĂ€rgi, hakkab lĂ”ppkokkuvĂ”ttes ikkagi klasterindeksi skaneerimist tegema (INDEX SCAN) tingimuste rĂŒhmade kaupa
  4. sĂŒgav allpĂ€ringute pesastamine muudab tĂ”lgendamise vĂ€ga problemaatiliseks SQL-kĂ€sud ja pĂ€ringute plaani tĂ”lgendamise arendajate ja DBA

Optimeerimise meetodid

Liigume nĂŒĂŒd otse optimeerimise meetodite juurde.

1) TĂ€iendav indekseerimine

Parim on vaadata filtreid peamistel valimis tabelitel, kuna sageli pĂ”hineb kogu pĂ€ring ĂŒhel vĂ”i kahel peamisel tabelil (taotlused-inimesed-toimingud) ja tavapĂ€rasel tingimuste kogumil (IsClosed, Canceled, Enabled, Status). Oluline on, et tuvastatud valimitele luuakse vastavad indeksid.

See lahendus on mÔistlik, kui valik nende vÀljade pÔhjal piirdub oluliselt tagastatava kogumi pÀringuga.

NÀiteks, meil on 500000 taotlust. Siiski on aktiivseid taotlusi vaid 2000 kirjet. Siis Ôigesti valitud indeks vabastab meid INDEX SCAN suurest tabelist ja vÔimaldab kiiresti andmeid valida lÀbi klasterdamata indeksi.

Samuti saab indeksite puudust tuvastada pĂ€ringute plaanide analĂŒĂŒsi vĂ”i sĂŒsteemi vaadete statistika kogumise vihjete kaudu. 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

KÔik vaateandmed sisaldavad teavet puuduvate indeksite kohta, vÀlja arvatud ruumiindeksid.

Kuid indeksid ja vahemÀlu on sageli meetodid, et vÔidelda halvasti kirjutatud LINQ-pÀringutele ja SQL-pÀringutest.

ÄÀrmuslikud elupraktikad nĂ€itavad, et Ă€ri jaoks on tihti tĂ€htis Ă€ritegevuse funktsioonide elluviimine kindlate tĂ€htaegadega. SeetĂ”ttu kantakse tihti raskeid pĂ€ringuid taustale koos vahemĂ€llu salvestamisega.

Osaliselt on see pÔhjendatud, kuna kasutaja ei vaja alati kÔige vÀrskemaid andmeid ning kasutajaliidese vastamise tase on aktsepteeritav.

See lĂ€henemine vĂ”imaldab lahendada Ă€ri pĂ€ringuid, kuid lĂ”puks vĂ€hendab see infosĂŒsteemi töövĂ”imet, lihtsalt viibides probleemide lahendamise edasi.

Samuti tuleks meeles pidada, et uute indeksite lisamiseks vajalike otsingute kÀigus vÔivad pakkumised MS SQL optimeerimise kohta olla ebatÀpsed, sealhulgas jÀrgmistes tingimustes:

  1. kui sarnaste vÀljade kombinatsiooniga indeksid juba eksisteerivad
  2. kui tabeli vĂ€ljad ei saa olla indekseeritud indeksimise piirangute tĂ”ttu (sellest on ĂŒksikasjalikumalt kirjeldatud siit).

2) Atribuutide ĂŒhendamine uue atribuudina

MĂ”nikord on mĂ”ningaid vĂ€lju ĂŒhest tabelist, mille alusel toimub tingimuste rĂŒhm, vĂ”imalik asendada ĂŒhe uue vĂ€lja sisseviimisega.

See on eriti oluline olekute vĂ€ljade jaoks, mis on tĂŒĂŒbilt tavaliselt kas bite vĂ”i tĂ€isarv.

NĂ€ide:

IsClosed = 0 JA Canceled = 0 JA Enabled = 0 asendatakse Status = 1.

Siin sisestatakse tÀisarvuline atribuut Status, tÀites need olekud tabelis. SeejÀrel indekseeritakse see uus atribuut.

See on fundamentaalne lahendus jĂ”udlusprobleemile, kuna me kĂŒsime andmeid ilma liigsete arvutusteta.

3) Vaate materialiseerimine

Kahjuks LINQ-i pÀringutes ei saa kasutada ajutisi tabeleid, CTE ja tabelimuutujaid.

Siiski on veel ĂŒks optimeerimise viis — indekseeritavad vaated.

Konditsioneerimiste rĂŒhm (ĂŒlevaltoodud nĂ€ites) IsClosed = 0 JA Canceled = 0 JA Enabled = 0 (vĂ”i muud sarnased tingimused) on hea valik nende kasutamiseks indekseeritavas vaates, mis talletab vĂ€ikese osa andmetest suurest hulgast.

Kuid vaate materialiseerimisel on mitmeid piiranguid:

  1. alamkonstruktsioonide, ettepanekute EXISTS peavad asendama kasutamise JOIN
  2. ei saa kasutada ettepanekuid UNION, UNION ALL, ERAND, INTERSECT
  3. ei saa kasutada tabeli vihjeid ja ettepanekuid OPTION
  4. tsĂŒklitega töötamine ei ole vĂ”imalik
  5. teavet ei saa kuvada ĂŒhelt vaatepunktilt erinevatest tabelitest

Oluline on meeles pidada, et indeksitelt lÀhtuvate vaadete tegelik kasu saab tÔeliselt saavutada ainult nende indekseerimise korral.

Kuid vaate kutsumisel ei pruugi neid indekseid kasutada, ja nende selgeks kasutamiseks tuleb mÀrgata WITH (NOEXPAND).

Kuna LINQ-i pĂ€ringutes tabeli vihjeid mÀÀrata ei saa, tuleb luua veel ĂŒks vaade - 'ĂŒmbris', jĂ€rgmise struktuuriga:

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

4) Tabelifunktsioonide kasutamine

Sageli LINQ-i pÀringutes suured alampÀringud vÔi struktuurselt keerukate vaadetega plokid moodustavad lÔpppÀringu vÀga keerulise ja optimeerimata tÀitmisstruktuuriga.

Tabelifunktsioonide kasutamise peamised eelised LINQ-i pÀringutes:

  1. VÔimalus, nagu ka vaadete puhul, kasutada ja mÀÀrata objektina, kuid saab edastada rikka sisendi parameetrite komplekti:
    FROM FUNCTION(@param1, @param2 
)
    lÔpptulemuseks on paindlik andmevalik
  2. Tabeli funktsiooni kasutamisel ei ole nii tugevaid piiranguid kui ĂŒlaltoodud indekseeritud vaadete puhul:
    1. Tabeli vihjed:
      kaudu LINQ ei saa mÀÀrata, milliseid indekse tuleb kasutada ja andmete isolatsiooni taset pÀringus.
      Kuid funktsioonis on need vÔimalused olemas.
      Funktsiooniga saab saavutada piisavalt stabiilse pÀringuplaani, kus on kehtestatud reeglid indeksitega töötamiseks ja andmete isolatsiooni tasemed.
    2. Funktsiooni kasutamine vÔimaldab, vÔrreldes indekseeritud vaadetega, saada:
      • keerulist andmete valikuloogikat (kuni tsĂŒklite kasutamiseni)
      • andmete valikuid paljusid erinevaid tabeleid.
      • kasutamine UNION ja EXISTS

  3. Pakkumine OPTION on vÀga kasulik, kui peame tagama paralleelsuse juhtimise. OPTION(MAXDOP N), paarilise pÀringu plaani korras. NÀiteks:
    • saab mÀÀrata pĂ€ringu plaani sundreteerimise. OPTION (RECOMPILE)
    • saab mÀÀrata vajaduse tagada pĂ€ringu plaani sundkasutamine ĂŒhendamise jĂ€rjekorras, mis on pĂ€ringus mĂ€rgitud. OPTION (FORCE ORDER)

    TĂ€psemini teema kohta OPTION detailsemalt siit.

  4. Kasutades kÔige kitsamat ja vajalikku andmepiiri:
    Ei ole vajalik hoida suuri andmekogusid vahemÀ caches (nÀiteks indeksiseeritud vaadete korral), millest tuleb veel andmeid filtrite kaudu eraldada.
    NÀiteks on tabel, mille puhul WHERE kasutatakse kolme vÀljakutset (a, b, c).

    Konditsioneerimise kohaselt on kÔigil pÀringutel pidev tingimus a = 0 ja b = 0.

    Kuid pÀring c on mitmekesisem.

    Oletame, et tingimus a = 0 ja b = 0 aitab meil tÔeliselt piirata vajalikku saadud komplekti tuhandete teadeteni, kuid tingimus koos piirab meie valikut sajani.

    Siin vÔib tabelifunktsioon osutuda kasulikuks valikuks.

    Samuti on tabelifunktsioon ennustatav ja ĂŒhtlane tĂ€itmise ajal.

NĂ€ited

Vaatame rakenduse nĂ€idet kĂŒsimuste andmebaasis.

On pĂ€ring SELECT, mis ĂŒhendab mitu tabelit ja kasutab ĂŒhte vaadet (OperativeQuestions), kus kontrollitakse e-posti kaudu kuuluvust (kaudu EXISTS) "Aktiivsed pĂ€ringud"([OperativeQuestions]):

PĂ€ring 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])
));

Kujundus on ĂŒsna keeruline: see sisaldab alavalduste ĂŒhendusi ja sorteerimist. DISTINCT, mis on ĂŒldiselt piisavalt ressursimahukas operatsioon.

Andmete valimine OperativeQuestions'ist, umbes kĂŒmme tuhat kirjet.

Selle pĂ€ringu peamine probleem on see, et vĂ€lise pĂ€ringu kirjed kĂ€ivitavad sisemise alavaldusselektiivi [OperativeQuestions] ĂŒle, mis peaks [Email] = @p__linq__0 kaudu vĂ€ljundi valikut piirama (kaudu EXISTS) sadade kirjeteni.

Ja vĂ”ib tunduda, et alampĂ€ring peaks ĂŒks kord arvutama kirjed, mille [Email] = @p__linq__0, ja seejĂ€rel peaks need paar sada kirjet Id jĂ€rgi kĂŒsimustega ĂŒhendama, muutes pĂ€ringu kiireks.

Tegelikult toimub kĂ”igi tabelite jĂ€rjestikune ĂŒhendamine: ja Id Questions-i vastavuse kontrollimine OperativeQuestions-i Id-dega ning filtreerimine Email'i jĂ€rgi.

Sisuliselt töötab pĂ€ring kĂ”igi kĂŒtte kĂŒmnete tuhandete OperativeQuestions kirjadega, kuigi tegelikult on vajalikud ainult huvipakkuvad andmed Email'i jĂ€rgi.

OperativeQuestions-i esituse tekst:

PĂ€ring nr 2

 
LOOMINE VAATE [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));

Algne esitus kaardistamine DbContextis (EF Core 2)

public class QuestionsDbContext : DbContext
{
    //...
    public DbQuery<OperativeQuestion> OperativeQuestions { get; set; }
    //...
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Query<OperativeQuestion>().ToView("OperativeQuestions");
    }
}

Algne LINQ-pÀring

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

KĂ€esolevas konkreetses olukorras kĂ€sitletakse probleemilahendust ilma infrastruktuuri muudatusteta, ilma eraldi tabeli loomata valmis tulemustega („Aktiivsed pĂ€ringud“), mille jaoks oleks vajalik selle andmete tĂ€iendamise mehhanism ning selle ajakohasena hoidmine.

Kuigi see on hea lahendus, on ka teine vĂ”imalus selle ĂŒlesande optimeerimiseks.

Peamine eesmÀrk on salvestada kirjed, mille puhul [Email] = @p__linq__0 vaates OperativeQuestions.

Sisestame tabelifunktsiooni [dbo].[OperativeQuestionsUserMail] andmebaasi.

Sisendparameetrina Emaili saades saame tagasi vÀÀrtustetabeli:

PĂ€ring 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

Siin tagastatakse vÀÀrtuste tabel, millel on eelnevalt mÀÀratletud andmestruktuur.

Et OperativeQuestionsUserMail'i pÀringud oleksid optimaalsed ja neil oleksid optimaalsed pÀringukavad, on vajalik rangem struktuur, mitte TAGASTAB TABELINA KUI TAGASTUS


Antud juhul muudetakse otsitav PĂ€ring 1 PĂ€ringuks 4:

PĂ€ring 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]);

Vaate ja funktsiooni kaardistamine DbContextis (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})");
}

LÔplik LINQ pÀring

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

TĂ€ideviimise aeg vĂ€henes 200-800 ms, kuni 2-20 ms, jne, see tĂ€hendab, et see on kĂŒmneid kordi kiirem.

Keskelt arvestades, saime 350 ms asemel 8 ms.

Ilmselt saame ka jÀrgmised eelised:

  1. ĂŒldine koormuse vĂ€henemine lugemisel,
  2. tunduvalt vÀiksem tÔenÀosus lukustuste tekkeks
  3. keskmise lukustuse aja vÀhendamine vastuvÔetavatesse vÀÀrtustesse

KokkuvÔte

Andmebaasi töötlemise optimeerimine ja hÀÀlestamine MS SQL kaudu LINQ on ĂŒlesanne, mille saab lahendada.

Selles töös on ÀÀrmiselt oluline tÀhelepanu ja jÀrjepidevus.

Protsessi alguses:

  1. on vajalik kontrollida andmeid, millega pĂ€ring töötab (vÀÀrtused, valitud andmetĂŒĂŒbid)
  2. teha nende andmete Ôige indekseerimine
  3. kontrollida ĂŒhendustingimuste Ă”igsust tabelite vahel

JÀrgmise optimeerimise iteratsiooni kÀigus tuvastatakse:

  1. pÀringu alus ja mÀÀratakse pÀringu pÔhifilter
  2. korratud sarnased pĂ€ringu osad ja analĂŒĂŒsitakse tingimuste kattuvust
  3. SSMS-is vÔi muus GUI-s SQL Server optimeeritakse ise SQL-pÀring (vaheandmete salvestamise esitlemine, lÔpppÀringu koostamine selle salvestuse abil (vÔivad olla mitu))
  4. viimasel etapil, vÔttes aluseks lÔpppÀringu SQL-pÀring, muudetakse struktuuri LINQ-pÀring

LÔppude lÔpuks peab saadud LINQ-pÀring struktuurilt olema identne kindlaks tehtud optimaalsele SQL-pÀringule punktist 3.

TĂ€nud

Suur tÀnu kolleegidele jobgemws ja alex_ozr ettevÔttest Fortis abi eest selle materjali ettevalmistamisel.

Allikas: habr.com

Osta usaldusvÀÀrne veebihosting DDoS kaitsega, VPS VDS serverid đŸ”„ Osta usaldusvÀÀrne veebihosting DDoS kaitsega, VPS VDS serverid | ProHoster