Sissejuhatus
V 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:
- optimaalsetest ĂŒhendustest (JOIN) ja tulemuste filtreerimisest (WHERE)
- palju nĂŒansse ĂŒhenduste ja grupitingimuste kasutamisel
- palju variatsioone tingimuste asendamisel IN jÀrgnevaga EXISTSja NOT IN, <> asendamine EXISTS
- vahepealne tulemuste vahemÀlu ajutiste tabelite, CTE-de, tabelimuutujate kaudu
- kasutades lauset (OPTION) suuniste ja tabeli vihjete mÀÀratlemiseks WITH (âŠ)
- 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:
- kogu andmete valimise mehhanismi konsolideerimine ĂŒhes pĂ€ringus
- identsete koodiblokkide dubleerimine, mis toob lÔpuks kaasa mitmekordsed tarbetud andmete lugemised
- 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
- 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:
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:
- kui sarnaste vÀljade kombinatsiooniga indeksid juba eksisteerivad
- kui tabeli vĂ€ljad ei saa olla indekseeritud indeksimise piirangute tĂ”ttu (sellest on ĂŒksikasjalikumalt kirjeldatud ).
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:
- alamkonstruktsioonide, ettepanekute EXISTS peavad asendama kasutamise JOIN
- ei saa kasutada ettepanekuid UNION, UNION ALL, ERAND, INTERSECT
- ei saa kasutada tabeli vihjeid ja ettepanekuid OPTION
- tsĂŒklitega töötamine ei ole vĂ”imalik
- 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:
- 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 - Tabeli funktsiooni kasutamisel ei ole nii tugevaid piiranguid kui ĂŒlaltoodud indekseeritud vaadete puhul:
- 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. - 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
- Tabeli vihjed:
- 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 .
- 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:
- ĂŒldine koormuse vĂ€henemine lugemisel,
- tunduvalt vÀiksem tÔenÀosus lukustuste tekkeks
- 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:
- on vajalik kontrollida andmeid, millega pĂ€ring töötab (vÀÀrtused, valitud andmetĂŒĂŒbid)
- teha nende andmete Ôige indekseerimine
- kontrollida ĂŒhendustingimuste Ă”igsust tabelite vahel
JÀrgmise optimeerimise iteratsiooni kÀigus tuvastatakse:
- pÀringu alus ja mÀÀratakse pÀringu pÔhifilter
- korratud sarnased pĂ€ringu osad ja analĂŒĂŒsitakse tingimuste kattuvust
- 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))
- 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 ja ettevÔttest Fortis abi eest selle materjali ettevalmistamisel.
Allikas: habr.com
