sql >> Database teknologi >  >> RDS >> Database

Multi-Statement TVF'er i Dynamics CRM

Gæsteforfatter:Andy Mallon (@AMtwo)

Hvis du er fortrolig med at understøtte databasen bag Microsoft Dynamics CRM, ved du sikkert, at det ikke er den hurtigst ydende database. Helt ærligt, det burde ikke være en overraskelse – det er ikke designet til at være en skrigende hurtig database. Den er designet til at være en fleksibel database. De fleste Customer Relationship Management (CRM)-systemer er designet til at være fleksible, så de kan opfylde behovene hos mange virksomheder i mange brancher med vidt forskellige forretningskrav. De sætter disse krav forud for databasens ydeevne. Det er nok en smart forretning, men jeg er ikke en forretningsmand – jeg er en databaseperson. Min erfaring med Dynamics CRM er, når folk kommer til mig og siger

Andy, databasen er langsom

En nylig hændelse var med en rapport, der mislykkedes på grund af en 5-minutters forespørgselstimeout. Med de rigtige indekser burde vi være i stand til at få et par hundrede rækker rigtigt hurtigt . Jeg fik fingrene i forespørgslen og nogle eksempelparametre, droppede den i Plan Explorer og kørte den et par gange i vores testmiljø (jeg gør alt dette i Test – det bliver vigtigt senere). Jeg ville sikre mig, at jeg kørte den med en varm cache, så jeg kunne bruge "det bedste af det værste" til mit benchmark. Forespørgslen var en stor grim SELECT med en CTE og en masse joins. Desværre kan jeg ikke give den nøjagtige forespørgsel, da den havde en eller anden kundespecifik forretningslogik (Beklager!).

7 ​​minutter, 37 sekunder er så godt, som det kan blive.

Umiddelbart sker der en masse dårligt her. 1,5 millioner aflæsninger er en pokkers masse I/O. 457 sekunder at returnere 200 rækker er langsomt. Cardinality Estimator forventede 2 rækker i stedet for 200. Og der var mange skrivninger – da denne forespørgsel kun er en SELECT erklæring, betyder det, at vi skal spilde til TempDb. Måske vil jeg være heldig og være i stand til at oprette et indeks for at eliminere en tabelscanning og fremskynde denne ting. Hvordan ser planen ud?

Ligner en apatosaurus eller måske en giraf.

Der vil ikke være nogen hurtige hits

Lad mig stoppe et øjeblik for at forklare noget om Dynamics CRM. Det bruger synspunkter. Den bruger indlejrede visninger. Den bruger indlejrede visninger til at håndhæve sikkerhed på rækkeniveau. I Dynamics-sprog kaldes disse sikkerhedshåndhævende indlejrede visninger på rækkeniveau "filtrerede visninger". Hver forespørgsel fra applikationen går gennem disse filtrerede visninger. Den eneste "understøttede" måde at udføre dataadgang på er at bruge disse filtrerede visninger.

Kan du huske, at jeg sagde, at denne forespørgsel refererede til en masse tabeller? Nå, det refererer til en masse filtrerede visninger. Så den komplicerede forespørgsel, jeg fik, er faktisk flere lag mere kompliceret. På dette tidspunkt fik jeg en frisk kop kaffe og skiftede til en større skærm.

En god måde at løse problemer på er at starte fra starten. Jeg zoomede ind på SELECT-operatoren og fulgte pilene for at se, hvad der foregik:

Selv på min 34" ultra-brede skærm var jeg nødt til at rode med skærmen indstillinger for at planen kan se så meget. Plan Explorer kan rotere planer 90 grader for at få "høje" planer til at passe på en bred skærm.

Se på alle de funktionskald med tabelværdi! Efterfulgt med det samme af en rigtig dyr hash-match. Min Spidey Sense begyndte at krible. Hvad er fn_GetMaxPrivilegeDepthMask , og hvorfor bliver det kaldt 30 gange? Jeg vil vædde på, at dette er et problem. Når du ser "Tabelvurderet funktion" som en operator i en plan, betyder det faktisk, at det er en tabelvurderet funktion med flere sætninger . Hvis det var en inline-tabelvurderet funktion, ville den blive inkorporeret i den større plan og ikke være en sort boks. Funktioner med flere sætninger i tabelværdier er onde. Brug dem ikke. Cardinality Estimator er ikke i stand til at lave nøjagtige estimater. Query Optimizer er ikke i stand til at optimere dem i sammenhæng med den større forespørgsel. Fra et præstationsperspektiv skalerer de ikke.

Selvom denne TVF er en klar kode fra Dynamics CRM, fortæller min Spidey Sense mig, at det er problemet. Glem denne store grimme forespørgsel med en stor skræmmende plan. Lad os træde ind i den funktion og se, hvad der sker:

opret funktion [dbo].[fn_GetMaxPrivilegeDepthMask](@ObjectTypeCode int) returnerer @d table(PrivilegeDepthMask int)-- Det er designmæssigt, at vi returnerer en tabel med kun én række og spalteabegin erklærer @UserId unikkeidentifikator vælg @User dbo.fn_FindUserGuid() erklærer @t table(depth int) -- fra brugerroller indsæt i @t(depth) vælg --privilege dybdemaske =1(grundlæggende) 2(lokal) 4(dyb) og 8(global) - - 16(inherited read) 32(inherited local) 64(inherited deep) og 128(inherited global) -- lav en AND med 0x0F ( =15) for at få basic/local/deep/global max(rp.PrivilegeDepthMask % 0x0F) som PrivilegeDepthMask fra PrivilegeBase priv join RolePrivileges rp on (rp.PrivilegeId =priv.PrivilegeId) join Rolle r on (rp.RoleId =r.ParentRootRoleId) join SystemUserRoles ur on (r.RoleId =r.RoleId =r.RoleId =og ) join PrivilegeObjectTypeCodes potc on (potc.PrivilegeId =priv.PrivilegeId) hvor potc.ObjectTypeCode =@ObjectTypeCode og priv.AccessRight &0x01 =1 -- fra brugerens teams roller indsæt i @t(dybde) vælg --privilege dybdemaske =1(grundlæggende) 2(lokal) 4(dyb) og 8(global) -- 16(nedarvet læst) 32(nedarvet lokal) 64(nedarvet) deep) og 128(inherited global) -- lav en OG med 0x0F ( =15) for at få basic/local/deep/global max(rp.PrivilegeDepthMask % 0x0F) som PrivilegeDepthMask fra PrivilegeBase priv join RolePrivileges rp on (rp.PrivilegeDepthMask. priv.PrivilegeId) join Role r on (rp.RoleId =r.ParentRootRoleId) join TeamRoles tr on (r.RoleId =tr.RoleId) join SystemUserPrincipals sup on (sup.PrincipalId =tr.TeamId og sup.SystemUserId) @SystemUserId join PrivilegeObjectTypeCodes potc on (potc.PrivilegeId =priv.PrivilegeId) hvor potc.ObjectTypeCode =@ObjectTypeCode og priv.AccessRight &0x01 =1 indsæt i @d vælg max(depth) fra @t return end GO 

Denne funktion følger et klassisk mønster i multi-statement TVF'er:

  • Deklarer en variabel, der bruges som en konstant
  • Indsæt i en tabelvariabel
  • Returner den tabelvariabel

Der sker ikke noget fancy her. Vi kunne omskrive disse flere udsagn som en enkelt SELECT udmelding. Hvis vi kan skrive det som en enkelt SELECT erklæring, kan vi omskrive dette som en inline TVF.

Lad os gøre det

Hvis det ikke er indlysende, er jeg ved at omskrive kode fra en softwareleverandør. Jeg har aldrig mødt en softwareleverandør, der anser dette for at være "understøttet" adfærd. Hvis du ændrer den out-of-the-box applikationskode, er du på egen hånd. Microsoft anser bestemt denne "ikke-understøttede" adfærd for Dynamics. Jeg har tænkt mig at gøre det alligevel, da jeg bruger testmiljøet, og jeg ikke leger rundt i produktionen. Genskrivning af denne funktion tog kun et par minutter - så hvorfor ikke prøve det og se, hvad der sker? Sådan ser min version af funktionen ud:

opret funktion [dbo].[fn_GetMaxPrivilegeDepthMask](@ObjectTypeCode int) returnerer tabel-- Det er designmæssigt, at vi returnerer en tabel med kun én række og kolonnerRETURN -- fra brugerroller skal du vælge PrivilegeDepthMask =max(PrivilegeDepthMask) fra ( vælg --privilege dybdemaske =1(grundlæggende) 2(lokal) 4(dyb) og 8(global) -- 16(nedarvet læst) 32(nedarvet lokal) 64(nedarvet dyb) og 128(nedarvet global) -- do en OG med 0x0F ( =15) for at få basic/local/deep/global max(rp.PrivilegeDepthMask % 0x0F) som PrivilegeDepthMask fra PrivilegeBase priv join RolePrivileges rp on (rp.PrivilegeId =priv.PrivilegeId on (rp.PrivilegeId =priv.PrivilegeIrd on) RoleId =r.ParentRootRoleId) join SystemUserRoles ur on (r.RoleId =ur.RoleId og ur.SystemUserId =dbo.fn_FindUserGuid()) join PrivilegeObjectTypeCodes potc on (potc.PrivilegeId =potc.PrivilegeObderi =potc.PrivilegeObderi =potc.PrivilegeObderi =potc.PrivilegeObderi =og priv.AccessRight &0x01 =1 UN ION ALL -- fra brugerens teams roller vælg --privilege dybdemaske =1(grundlæggende) 2(lokal) 4(dyb) og 8(global) -- 16(arvet læst) 32(nedarvet lokal) 64(nedarvet dyb) og 128(inherited global) -- lav en OG med 0x0F ( =15) for at få basic/local/deep/global max(rp.PrivilegeDepthMask % 0x0F) som PrivilegeDepthMask fra PrivilegeBase priv join RolePrivileges rp on (rp.PrivilegeDepthMask ) join Role r on (rp.RoleId =r.ParentRootRoleId) join TeamRoles tr on (r.RoleId =tr.RoleId) join SystemUserPrincipals sup on (sup.PrincipalId =tr.TeamId og sup.SystemUserId =dbo.Fn_Find) join PrivilegeObjectTypeCodes potc on (potc.PrivilegeId =priv.PrivilegeId) hvor potc.ObjectTypeCode =@ObjectTypeCode og priv.AccessRight &0x01 =1 )xGO

Jeg gik tilbage til min oprindelige testforespørgsel, dumpede cachen og kørte den igen et par gange. Her er den langsommeste køretid, når jeg bruger min version af TVF:

Det ser meget bedre ud!

Det er stadig ikke den mest effektive forespørgsel i verden, men det er hurtigt nok – jeg behøver ikke gøre det hurtigere. Bortset fra... jeg var nødt til at ændre Microsofts kode for at få det til at ske. Det er ikke ideelt. Lad os tage et kig på den fulde plan med den nye TVF:

Farvel apatosaurus, hej PEZ-dispenser!

Det er stadig en virkelig knudret plan, men hvis du ser på starten, er alle de sorte boks TVF-opkald væk. Det superdyre hash-match er væk. SQL Server går lige i gang uden den store flaskehals af TVF-kald (arbejdet bag TVF er nu inline med resten af ​​SELECT ):

Stor billede indvirkning

Hvor bruges denne TVF egentlig? Næsten hver enkelt filtreret visning i Dynamics CRM bruger dette funktionskald. Der er 246 filtrerede visninger, og 206 af dem refererer til denne funktion. Det er en kritisk funktion som en del af Dynamics-sikkerhedsimplementeringen på rækkeniveau. Stort set hver eneste forespørgsel fra applikationen til databaserne kalder denne funktion mindst én gang – normalt et par gange. Dette er en tosidet mønt:På den ene side vil fixering af denne funktion sandsynligvis fungere som et turboboost for hele applikationen; på den anden side er der ingen måde for mig at lave regressionstest for alt, der berører denne funktion.

Vent et øjeblik – hvis dette funktionskald er så kerne i vores ydeevne, og så kerne til Dynamics CRM, så følger det, at alle, der bruger Dynamics, rammer denne præstationsflaskehals. Vi åbnede en sag med Microsoft, og jeg ringede til et par folk for at få billetten sendt til ingeniørteamet, der er ansvarligt for denne kode. Med lidt held vil denne opdaterede version af funktionen komme ind i boksen (og skyen) i en fremtidig udgivelse af Dynamics CRM.

Dette er ikke det eneste TVF med flere sætninger i Dynamics CRM – jeg lavede den samme type ændring til fn_UserSharedAttributesAccess til et andet præstationsproblem. Og der er flere TVF'ere, som jeg ikke har rørt, fordi de ikke har givet problemer.

En lektion til alle, også selvom du ikke bruger Dynamics

Gentag efter mig:FUNKTIONER VÆRDIDE TABLER MED FLERE STATEMENTER ER ONDE!

Re-faktor din kode for at undgå at bruge multi-statement TVF'er. Hvis du forsøger at tune kode, og du ser en TVF med flere sætninger, så se kritisk på det. Du kan ikke altid ændre koden (eller det kan være en overtrædelse af din supportkontrakt, hvis du gør det), men hvis du kan ændre koden, så gør det. Fortæl din softwareleverandør om at stoppe med at bruge multi-statement TVF'er. Gør verden til et bedre sted ved at fjerne nogle af disse grimme funktioner fra din database.

Om forfatteren

Andy Mallon er en SQL Server DBA og Microsoft Data Platform MVP, der har administreret databaser inden for sundhedssektoren, finans, e. -handel og non-profit sektorer. Siden 2003 har Andy understøttet højvolumen, meget tilgængelige OLTP-miljøer med krævende ydeevnebehov. Andy er grundlæggeren af ​​BostonSQL, medarrangør af SQLSaturday Boston, og blogger på am2.co.
  1. Sådan konverteres tidsstempel til datetime i MySQL?

  2. MySql-fejl:1364 Feltet 'display_name' har ikke standardværdi

  3. Spørgsmål og svar fra vores Parameter Sniffing-webinarserie

  4. MySQL streng udskiftning