Innledning
I noen metoder for optimalisering ble vurdert LINQ-spÞrsmÄl.
Her vil vi ogsÄ presentere noen tilnÊrminger for Ä optimalisere koden relatert til LINQ-spÞrsmÄlene.
Det er kjent at LINQ(Language-Integrated Query) er et enkelt og praktisk spÞrresprÄk for datakilder.
A LINQ to SQL er en datatilgangsteknologi i RDBMS. Dette er et kraftig verktÞy for databehandling, der forespÞrslene bygges gjennom et deklarativt sprÄk, som deretter blir transformert til SQL-spÞrsmÄl plattform og sendt til databasen for behandling. I vÄrt tilfelle vil vi forstÄ RDBMS som MS SQL Server.
Imidlertid, LINQ-spÞrsmÄl blir ikke konvertert til optimalt skrevne SQL-spÞrsmÄl, som en erfaren DBA ville skrevet med alle optimaliseringsnyansene for SQL-spÞrsmÄl:
- optimale koblinger (JOIN) og filtrering av resultater (Den maksimale/minimale hurtig oppfriskningen etter sletting eller blandede DML har ikke samme oppfÞrsel som innsetting-tilfelle. Den sletter og beregner pÄ nytt maks/min verdiene for de berÞrte gruppene. Du mÄ vÊre oppmerksom pÄ dens ytelseseffekt.)
- mange nyanser i bruken av koblinger og gruppere betingelser
- mange variasjoner i Ă„ erstatte betingelser IN En funksjon har blitt EXISTSog NOT IN, til EXISTS
- midlertidig caching av resultater gjennom midlertidige tabeller, CTE, tabellvariabler
- bruk av setning (OPTION) med instruksjoner og tabellhint WITH (âŠ)
- bruk av indekserte visninger som et av hjelpemidlene for Ă„ bli kvitt overflĂždige datalesninger ved spĂžrringer
De viktigste flaskhalser i ytelsen for de resulterende SQL-spÞrsmÄl ved kompileringsprosessen LINQ-spÞrsmÄl er:
- konsolidering av hele datavalgmekanismen i en spĂžrring
- duplisering av identiske kodeblokker, noe som til slutt fĂžrer til unĂždvendige datalesninger
- grupper av sammensatte betingelser (logiske «og» og «eller») â OG og Eller, som kombineres i komplekse betingelser, fĂžrer til at optimalisatoren, med passende ikke-klyngede indekser for de nĂždvendige feltene, til slutt begynner Ă„ skanne gjennom klyngeindeksen (INDEX SCAN) etter grupper av betingelser
- dyp nesting av underforespĂžrsel gjĂžr analysen av SQL-setningene og analysen av forespĂžrselplaner fra utviklernes og DBA
Optimaliseringsmetoder
La oss nÄ gÄ direkte videre til optimaliseringsmetodene.
1) Ytterligere indeksering
Det er best Ä se pÄ filtre pÄ de viktigste utvalgstabellene, ettersom hele spÞrringen ofte bygges rundt en eller to hovedtabeller (applikasjoner-personer-operasjoner) og med et standard sett av betingelser (IsClosed, Canceled, Enabled, Status). Det er viktig Ä opprette passende indekser for de valgte utvalgene.
Denne lÞsningen gir mening nÄr utvalget etter disse feltene betydelig begrenser det returnerte mangfoldet av forespÞrselen.
For eksempel har vi 500000 sĂžknader. Men det er kun 2000 aktive sĂžknader. Da vil en riktig valgt indeks spare oss for INDEX SCAN motorveien og tillate oss Ă„ raskt hente data gjennom en ikke-klaseindeks.
Mangelen pÄ indekser kan ogsÄ identifiseres gjennom analysering av spÞrringsplaner eller innsamling av statistikk fra systemvisninger MS SQL Server:
Alle dataene i visningene inneholder informasjon om manglende indekser, unntatt for romlige indekser.
Men indekser og caching er ofte metoder for Ä takle konsekvensene av dÄrlig skrivne LINQ-spÞrsmÄl og SQL-spÞrsmÄl.
Som hard praksis viser, er det ofte viktig for virksomheten Ă„ implementere forretningsfunksjoner innen bestemte frister. Og derfor blir ofte tunge forespĂžrseler satt til bakgrunnsmodus med caching.
Delvis er dette berettiget, ettersom brukeren ikke alltid trenger de nyeste dataene, og det oppstÄr et akseptabelt nivÄ av respons fra brukergrensesnittet.
Denne tilnÊrmingen lar oss mÞte forretningsforespÞrselene, men reduserer til slutt funksjonaliteten til informasjonssystemet ved bare Ä utsette lÞsningene pÄ problemene.
Det er ogsÄ viktig Ä huske at i prosessen med Ä finne nÞdvendige nye indekser Ä legge til, kan forslagene MS SQL for optimalisering vÊre uriktige, spesielt under fÞlgende forhold:
- hvis det allerede finnes indekser med et lignende sett av felt
- hvis felt i tabellen ikke kan indekseres pÄ grunn av indeksbegrensninger (mer detaljert om dette er beskrevet ).
2) SammenfĂžring av attributter til et nytt attributt
Noen ganger kan enkelte felt fra én tabell, som er gruppert, erstattes med innfÞringen av et nytt felt.
Dette er spesielt aktuelt for tilstandsfelt, som vanligvis er enten binĂŠre eller heltall.
Eksempel:
IsClosed = 0 AND Canceled = 0 AND Enabled = 0 erstattes med Status = 1.
Her skrivast ein heilag heiltal-attributt om Status, som vert sikra ved Ă„ fylle ut desse statusane i tabellen. Deretter vert det gjennomfĂžrt indeksering av dette nye attributtet.
Dette er ei grunnleggjande lÞysing pÄ ytelsesproblemet, sidan vi henter data utan unÞdvendige berekningar.
3) Materialisering av visninga
Dessverre kan i LINQ-spÞrsmÄl ein ikkje bruke midlertidige tabeller, CTE eller tabellvariablar direkte.
Men det finst ei anna mĂ„te Ă„ optimalisere i dette tilfellet â indekserte visningar.
Gruppene av vilkÄr (frÄ dÞmet ovanfor) IsClosed = 0 AND Canceled = 0 AND Enabled = 0 (eller eit sett med andre liknande vilkÄr) blir eit godt alternativ for Ä bruke dei i den indekserte visninga, og lagre eit lite utsnitt av data frÄ eit stort mangfald.
Men det er ein rekkje avgrensingar ved materialisering av visninga:
- bruken av underspÞrsmÄl, forslag EXISTS mÄ verte erstatta med bruk JOIN
- ein kan ikkje bruke forslag Den definerende spÞrringen mÄ ha, UNION ALL, UNNTAK, INTERSECT
- ein kan ikkje bruke tabellhint og forslag OPTION
- det er ikkje mogleg Ă„ jobbe med lĂžkker
- det er ikkje mogleg Ä hente data i éin visning frÄ ulike tabeller
Det er viktig Ä hugse at den verkelege nytta med Ä bruke indekserte visningar kan oppnÄs faktisk berre ved indeksering av dei.
Men ved Ä kalle visninga kan desse indeksane kanskje ikkje bli brukt, og for Ä bruke dei eksplisitt mÄ ein angi WITH (NOEXPAND).
Sidan ein kan ikkje definere tabellhint, mĂ„ ein lage ei ny visning â «innpakning» av fĂžlgjande slag: i LINQ-spĂžrsmĂ„l CREATE VIEW NAMN_presentation AS SELECT * FROM MAT_VIEW WITH (NOEXPAND);
4) Bruk av tabellfunksjonar
Ofte i
store blokkane med underspÞrsmÄl eller blokker som bruker visningar med komplisert struktur, dannar ein endeleg forespÞrsel med ei svÊrt kompleks og ikkje optimal utfÞrelsesstruktur. i LINQ-spÞrsmÄl Dei viktigaste fordelane med Ä bruke tabellfunksjonar i
Moglegheita, akkurat som med visningar, Ä bruke og indikere som objekt, men ein kan overfÞre eit sett med inndata-parametrar: i LINQ-spÞrsmÄl:
- FRĂ
FUNKSJON(@param1, @param2 âŠ)
til slutt kan ein oppnÄ ei fleksibel dataval
I tilfelle av Ă„ bruke tabellfunksjonen er det ikkje slike strenge avgrensingar som i tilfellet med indekserte visningar, som nevnt ovanfor: - Tabellhint:
- Tabellhint:
gjennom LINQ Det er ikke mulig Ä spesifisere hvilke indekser som skal brukes og bestemme isolasjonsnivÄet for data under forespÞrselen.
Men i funksjonen er disse mulighetene tilgjengelige.
Med funksjonen kan man oppnÄ en ganske konstant forespÞrselplan, hvor reglene for arbeidet med indekser og nivÄene for datainndeling er definert. - Bruken av funksjonen gir, sammenlignet med indekserte visninger, mulighet for:
- kompleks logikk for datauthenting (til og med med bruk av lĂžkker)
- uthenting av data fra mange forskjellige tabeller
- bruk av Den definerende spÞrringen mÄ ha og EXISTS
- Tabellhint:
- Forslag OPTION er veldig nyttig nÄr vi mÄ sÞrge for styring av parallellisme. OPTION(MAXDOP N), rekkefÞlgen pÄ forespÞrselplanen. For eksempel:
- man kan spesifisere tvungen rekreasjon av forespĂžrselplanen. OPTION (RECOMPILE)
- man kan spesifisere behovet for Ă„ sikre at forespĂžrselplanen bruker rekkefĂžlgen for sammenkoblingen som er angitt i forespĂžrselen. OPTION (FORCE ORDER)
Mer detaljert om OPTION beskrevet .
- Bruken av det smaleste og mest nĂždvendige dataskjermbildet:
Det er ingen behov for Ä holde store datasett i hurtigbufferen (som i tilfelle med indekserte visninger), hvor man fortsatt mÄ filtrere dataene etter parameter.
For eksempel, det er en tabell, som for filteret Den maksimale/minimale hurtig oppfriskningen etter sletting eller blandede DML har ikke samme oppfÞrsel som innsetting-tilfelle. Den sletter og beregner pÄ nytt maks/min verdiene for de berÞrte gruppene. Du mÄ vÊre oppmerksom pÄ dens ytelseseffekt. bruken av tre felt (a, b, c).Betingelsen for alle forespÞrslene er en konstant betingelse a = 0 og b = 0..
Imidlertid er forespĂžrselen til feltet med mer variert.
La oss si at betingelsen a = 0 og b = 0. virkelig hjelper oss med Ä begrense det nÞdvendige oppnÄdde settet til tusen poster, men betingelsen for med innskrenker utvalget til hundre poster.
Her kan en tabellfunksjon vise seg Ă„ vĂŠre et bedre alternativ.
OgsÄ er tabellfunksjonen mer forutsigbar og konstant i utfÞrelsestid.
Eksempler
La oss se pÄ et eksempel pÄ implementering med databasen Questions.
Det er en forespĂžrsel SELECT, som setter sammen flere tabeller og bruker en visning (OperativeQuestions), der det sjekkes gjennom email om tilhĂžrighet (gjennom EXISTS) til "Aktive forespĂžrselene"([OperativeQuestions]):
ForespĂžrsel 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])
));
Visningen har en ganske komplisert struktur: den har underforespĂžrsel-tilkoblinger og bruk av sortering DISTINCT, som generelt er en ganske ressurskrevende operasjon.
Utvalg fra OperativeQuestions inkluderer omtrent ti tusen oppfĂžringer.
Hovedproblemet med denne forespÞrselen er at for postene fra den ytre forespÞrselen utfÞres en intern underforespÞrsel pÄ visningen [OperativeQuestions], som for [Email] = @p__linq__0 skal begrense utvalget (gjennom EXISTS) til hundrevis av oppfÞringer.
Og det kan virke som om underforespÞrselen bare skal beregne postene én gang for [Email] = @p__linq__0, og deretter skal disse par hundre postene kobles sammen etter Id med Questions, og forespÞrselen vil vÊre rask.
I virkeligheten skjer det imidlertid en sekvensiell sammenkobling av alle tabellene: bÄde kontroll av samsvar mellom Id i Questions og Id fra OperativeQuestions, samt filtrering etter Email.
I bunn og grunn jobber forespĂžrselen med alle titusener av oppfĂžringer i OperativeQuestions, mens vi bare trenger de relevante dataene etter Email.
Teksten i visningen OperativeQuestions:
ForespĂžrsel 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));
Den opprinnelige mappingen av visningen i DbContext (EF Core 2)
offentlig klasse QuestionsDbContext : DbContext
{
//...
offentlig DbQuery<OperativeQuestion> OperativeQuestions { get; set; }
//...
beskyttet override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Query<OperativeQuestion>().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) => new
{
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 lösningen av detta problem utan infrastrukturella förÀndringar, utan att införa en separat tabell med fÀrdiga resultat ("Aktiva frÄgor"), för vilken en mekanism skulle vara nödvÀndig för att fylla den med data och hÄlla den uppdaterad.
Ăven om det hĂ€r Ă€r en bra lösning finns det ett annat alternativ för att optimera denna uppgift.
Huvudsyftet Àr att cachelagra poster med [Email] = @p__linq__0 frÄn vyen OperativeQuestions.
Vi introducerar tabellfunktionen [dbo].[OperativeQuestionsUserMail] i databasen.
Genom att skicka in som parameter Email fÄr vi tillbaka en vÀrdetabell:
FrÄga 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
HÀr returneras en vÀrdetabell med en fördefinierad databasstruktur.
För att förfrĂ„gningar till OperativeQuestionsUserMail ska vara optimala och ha optimala frĂ„geplaner krĂ€vs en strikt struktur, och inte RETURNS TABLE AS RETURNâŠ
I det hÀr fallet omvandlas den sökta FrÄga 1 till FrÄga 4:
FrÄga 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]);
Mappning 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})");
}
Den endelige LINQ-forespĂžrselen
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();
KjÞrets tid har blitt redusert fra 200-800 ms til 2-20 ms, og sÄ videre, det vil si flere tiÄr raskere.
Ser man mer i snitt, fikk vi i stedet for 350 ms, 8 ms.
Noen Äpenbare fordeler er ogsÄ:
- generell reduksjon i lese belastning,
- betydelig reduksjon i sannsynligheten for lÄsing
- reduksjon av gjennomsnittlig lÄsetid til akseptable verdier
Utdata
Optimalisering og finjustering av forespĂžrslene til databasen MS SQL gjennom LINQ er en oppgave som kan lĂžses.
I dette arbeidet er oppmerksomhet og konsistens veldig viktig.
I starten av prosessen:
- mÄ dataene som forespÞrselen arbeider med sjekkes (verdier, valgte datatyper)
- gjennomfĂžre riktig indeksering av disse dataene
- sjekke korrektheten av join-betingelsene mellom tabellene
I neste iterasjon av optimaliseringen identifiseres:
- grunnlaget for forespĂžrselen og den viktigste filtreringen blir bestemt
- gjentakende like blokker av forespĂžrselen og kryssanalysere betingelsene
- i SSMS eller annen GUI for SQL Server optimaliseres selve SQL-forespĂžrselen (opprettelse av mellomlagringslagring, bygd opp resultatforespĂžrselen ved hjelp av dette lageret (kan vĂŠre flere))
- I det siste steget, basert pÄ resultatet SQL-forespÞrselen, omstrukturert struktur av LINQ-forespÞrselen
Som et resultat skal den resulterende LINQ-forespĂžrselen bli strukturmessig identisk med den optimale SQL-forespĂžrselen fra punkt 3.
Takk
Stor takk til kollegene og fra selskapet Fortis for hjelpen med forberedelsen av dette materialet.
Kilde: habr.com
