Introduktion
I 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:
- optimala föreningar (JOIN) och filtrering av resultat (DĂR)
- mÄnga nyanser i anvÀndningen av föreningar och gruppvillkor
- mÄnga variationer i ersÀttning av villkor IN till EXISTSoch NOT IN, till EXISTS
- intermittent cache av resultat genom temporÀra tabeller, CTE, tabellvariabler
- anvĂ€ndning av konstruktionen (OPTION) med anvisningar och tabellhintar WITH (âŠ)
- 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:
- konsolidering av hela mekanismen för dataselektion i en frÄga
- duplicering av identiska kodblock, vilket sÄ smÄningom leder till flera onödiga datalÀsningar
- 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
- 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:
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:
- om det redan finns index med en liknande uppsÀttning fÀlt
- om fÀlten i tabellen inte kan indexeras pÄ grund av indexeringsbegrÀnsningar (detta beskrivs mer i detalj ).
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:
- anvÀndning av underfrÄgor, satser EXISTS ska ersÀttas med anvÀndning av JOIN
- kan inte anvÀnda satser UNION, UNION ALL, EXCEPTION, INTERSECT
- kan inte anvÀnda tabellhintar och satser OPTION
- det finns ingen möjlighet att arbeta med loopar
- 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:
- 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 - Vid anvÀndning av tabellfunktioner finns det inga sÄ strikta begrÀnsningar som vid indexerade vyer, som beskrivits ovan:
- 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. - 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
- Tabellhintar:
- 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 .
- 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:
- ett allmÀnt minskande av lÀsbelastningen,
- en betydande minskning av blockeringar
- 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:
- mÄste vi kontrollera de data som frÄgan arbetar med (vÀrden, valda datatyper)
- utföra korrekt indexering av dessa data
- kontrollera korrektheten i anslutningsvillkoren mellan tabellerna
Vid nÀsta optimeringsiteration identifieras:
- grunden för frÄgan och den huvudsakliga filtreringen av frÄgan faststÀlls
- Äterkommande liknande delar av frÄgan och analys av överlappande villkor
- 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))
- 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 och frÄn företaget Fortis för hjÀlpen med att förbereda detta material.
KĂ€lla: habr.com
