Gästförfattare:Andy Mallon (@AMtwo)
Om du är bekant med att stödja databasen bakom Microsoft Dynamics CRM, vet du förmodligen att det inte är den snabbast presterande databasen. Ärligt talat, det borde inte vara en överraskning – den är inte utformad för att vara en skriksnabb databas. Den är utformad för att vara en flexibel databas. De flesta CRM-system (Customer Relationship Management) är designade för att vara flexibla så att de kan möta behoven hos många företag i många branscher med väldigt olika affärskrav. De sätter dessa krav före databasprestanda. Det är förmodligen smart affär, men jag är ingen affärsperson – jag är en databasperson. Min erfarenhet av Dynamics CRM är när folk kommer till mig och säger
Andy, databasen är långsam
En nyligen inträffad händelse var att en rapport misslyckades på grund av en 5-minuters tidsgräns för sökfrågan. Med rätt index borde vi kunna få några hundra rader riktigt snabbt . Jag fick tag på frågan och några exempelparametrar, släppte den i Plan Explorer och körde den några gånger i vår testmiljö (jag gör allt detta i Test – det kommer att bli viktigt senare). Jag ville vara säker på att jag körde den med en varm cache, så att jag kunde använda "det bästa av det värsta" för mitt riktmärke. Frågan var en stor otäck SELECT
med en CTE och ett gäng joins. Tyvärr kan jag inte ge den exakta frågan, eftersom den hade någon kundspecifik affärslogik (Tyvärr!).
7 minuter, 37 sekunder är hur bra som helst.
Direkt är det mycket dåligt som händer här. 1,5 miljoner läsningar är jävligt mycket I/O. 457 sekunder att returnera 200 rader är långsamt. Cardinality Estimator förväntade sig 2 rader istället för 200. Och det fanns många skrivningar – eftersom den här frågan bara är en SELECT
uttalande betyder detta att vi måste spilla till TempDb. Jag kanske har tur och kan skapa ett index för att eliminera en tabellskanning och påskynda det här. Hur ser planen ut?
Ser ut som en apatosaurus, eller kanske en giraff.
Det kommer inga snabba träffar
Låt mig pausa en stund för att förklara något om Dynamics CRM. Den använder vyer. Den använder kapslade vyer. Den använder kapslade vyer för att upprätthålla säkerhet på radnivå. På Dynamics-språk kallas dessa säkerhetsupprätthållande kapslade vyer på radnivå för "filtrerade vyer". Varje fråga från applikationen går igenom dessa filtrerade vyer. Det enda "stödda" sättet att utföra dataåtkomst är att använda dessa filtrerade vyer.
Kommer ni ihåg att jag sa att den här frågan hänvisade till ett gäng tabeller? Tja, det hänvisar till ett gäng filtrerade vyer. Så den komplicerade frågan jag fick är faktiskt flera lager mer komplicerad. Vid det här laget fick jag en ny kopp kaffe och bytte till en större bildskärm.
Ett bra sätt att lösa problem är att börja från början. Jag zoomade in på SELECT-operatorn och följde pilarna för att se vad som pågick:
Även på min 34" ultra-wide monitor var jag tvungen att pilla med skärmen inställningar för att planen ska se så mycket. Plan Explorer kan rotera planerna 90 grader för att få "höga" planer att passa på en bred bildskärm.
Titta på alla dessa tabellvärdade funktionsanrop! Följt direkt av en riktigt dyr hashmatch. Mitt Spidey Sense började pirra. Vad är fn_GetMaxPrivilegeDepthMask
, och varför heter det 30 gånger? Jag slår vad om att detta är ett problem. När du ser "Tabellvärderad funktion" som en operator i en plan betyder det faktiskt att det är en funktion med flera påståenden . Om det var en inline-tabellvärderad funktion, skulle den införlivas i den större planen och inte vara en svart låda. Tabellvärdade funktioner med flera påståenden är onda. Använd dem inte. Cardinality Estimator kan inte göra korrekta uppskattningar. Frågeoptimeraren kan inte optimera dem i samband med den större frågan. Ur ett prestationsperspektiv skalas de inte.
Även om denna TVF är en färdig kod från Dynamics CRM, säger min Spidey Sense till mig att det är problemet. Glöm denna stora otäcka fråga med en stor skrämmande plan. Låt oss gå in i den funktionen och se vad som händer:
skapa funktion [dbo].[fn_GetMaxPrivilegeDepthMask](@ObjectTypeCode int) returnerar @d table(PrivilegeDepthMask int)-- Det är designat som vi returnerar en tabell med endast en rad och kolumnabegin deklarerar @UserId unikaidentifierare välj @User dbo.fn_FindUserGuid() deklarerar @t table(depth int) -- från användarroller infoga i @t(depth) välj --privilege depth mask =1(grundläggande) 2(lokal) 4(djup) och 8(global) - - 16(ärvd läsning) 32(ärvd lokal) 64(ärvd djup) och 128(ärvd global) -- gör en AND med 0x0F ( =15) för att få grundläggande/lokal/djup/global max(rp.PrivilegeDepthMask % 0x0F) som PrivilegeDepthMask från PrivilegeBase priv join RolePrivileges rp on (rp.PrivilegeId =priv.PrivilegeId) join Role r on (rp.RoleId =r.ParentRootRoleId) join SystemUserRoles ur on (r.RoleId =User.Iurd =och ) gå med i PrivilegeObjectTypeCodes potc on (potc.PrivilegeId =priv.PrivilegeId) där potc.ObjectTypeCode =@ObjectTypeCode och priv.AccessRight &0x01 =1 -- från användarens team roller infoga i @t(djup) välj --privilegium djupmask =1(grundläggande) 2(lokal) 4(djup) och 8(global) -- 16(ärvd läs) 32(ärvd lokal) 64(ärvd deep) och 128(ärvd global) -- gör en AND med 0x0F ( =15) för att få basic/local/deep/global max(rp.PrivilegeDepthMask % 0x0F) som PrivilegeDepthMask från 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 och sup.SystemUserId) @SystemUserId gå med i PrivilegeObjectTypeCodes potc on (potc.PrivilegeId =priv.PrivilegeId) där potc.ObjectTypeCode =@ObjectTypeCode och priv.AccessRight &0x01 =1 infoga i @d välj max(djup) från @pre>retur slut GODenna funktion följer ett klassiskt mönster i TVF:er med flera påståenden:
- Deklarera en variabel som används som en konstant
- Infoga i en tabellvariabel
- Returnera den tabellvariabeln
Det är inget fint på gång här. Vi skulle kunna skriva om dessa flera påståenden som en enda SELECT
påstående. Om vi kan skriva det som en enda SELECT
uttalande, kan vi skriva om detta som en inline TVF.
Låt oss göra det
Om det inte är uppenbart, är jag på väg att skriva om koden från en mjukvaruleverantör. Jag har aldrig träffat en mjukvaruleverantör som anser att detta är "stödd" beteende. Om du ändrar den färdiga applikationskoden är du på egen hand. Microsoft anser verkligen att detta beteende "inte stöds" för Dynamics. Jag tänker göra det ändå, eftersom jag använder testmiljön och jag inte leker i produktionen. Att skriva om den här funktionen tog bara ett par minuter – så varför inte prova och se vad som händer? Så här ser min version av funktionen ut:
skapa funktion [dbo].[fn_GetMaxPrivilegeDepthMask](@ObjectTypeCode int) returnerar tabell-- Det är designat som vi returnerar en tabell med endast en rad och kolumnerRETURN -- från användarroller välj PrivilegeDepthMask =max(PrivilegeDepthMask) från ( välj --privilege djupmask =1(grundläggande) 2(lokal) 4(djup) och 8(global) -- 16(ärvd läsning) 32(ärvd lokal) 64(ärvd djup) och 128(ärvd global) -- gör en AND med 0x0F ( =15) för att få basic/local/deep/global max(rp.PrivilegeDepthMask % 0x0F) som PrivilegeDepthMask från PrivilegeBase priv join RolePrivileges rp on (rp.PrivilegeId =priv.PrivilegeIrd on (rp.PrivilegeId =priv.Privilege.Privilege. RoleId =r.ParentRootRoleId) gå med i SystemUserRoles ur on (r.RoleId =ur.RoleId och ur.SystemUserId =dbo.fn_FindUserGuid()) gå med i PrivilegeObjectTypeCodes potc on (potc.PrivilegeId =potc.PrivilegeObderi =potc.PrivilegeObderi =potc.PrivilegeObderi =potc.PrivilegeObderi =och priv.AccessRight &0x01 =1 UN ION ALL -- från användarens team roller välj --privilegium djupmask =1(grundläggande) 2(lokal) 4(djup) och 8(global) -- 16(ärvd läs) 32(ärvd lokal) 64(ärvd djup) och 128(inherited global) -- gör en AND med 0x0F ( =15) för att få basic/local/deep/global max(rp.PrivilegeDepthMask % 0x0F) som PrivilegeDepthMask från PrivilegeBase priv join RolePrivileges rp on (rp.PrivilegeDepthMask ) join Roll r on (rp.RoleId =r.ParentRootRoleId) join TeamRoles tr on (r.RoleId =tr.RoleId) join SystemUserPrincipals sup on (sup.PrincipalId =tr.TeamId och sup.SystemUserId =dbo.fn_Find) gå med i PrivilegeObjectTypeCodes potc on (potc.PrivilegeId =priv.PrivilegeId) där potc.ObjectTypeCode =@ObjectTypeCode och priv.AccessRight &0x01 =1 )xGO
Jag gick tillbaka till min ursprungliga testfråga, dumpade cachen och körde den igen några gånger. Här är den långsammaste körtid, när jag använder min version av TVF:
Det ser mycket bättre ut!
Det är fortfarande inte den mest effektiva frågan i världen, men den är tillräckligt snabb – jag behöver inte göra den snabbare. Förutom... jag var tvungen att ändra Microsofts kod för att få det att hända. Det är inte idealiskt. Låt oss ta en titt på den fullständiga planen med nya TVF:
Adjö apatosaurus, hej PEZ-dispenser!
Det är fortfarande en riktigt knäpp plan, men om du tittar på starten så är alla de där svarta lådan TVF-samtal borta. Den superdyra hashmatchen är borta. SQL Server börjar arbeta direkt utan den stora flaskhalsen av TVF-anrop (arbetet bakom TVF är nu inline med resten av SELECT
):
Stor bildeffekt
Var används denna TVF egentligen? Nästan varenda filtrerad vy i Dynamics CRM använder detta funktionsanrop. Det finns 246 filtrerade vyer och 206 av dem refererar till denna funktion. Det är en kritisk funktion som en del av Dynamics-säkerhetsimplementeringen på radnivå. I stort sett varje enskild fråga från applikationen till databaserna anropar den här funktionen minst en gång – vanligtvis några gånger. Det här är ett dubbelsidigt mynt:å ena sidan kommer fixering av den här funktionen sannolikt att fungera som en turboboost för hela applikationen; å andra sidan, det finns inget sätt för mig att göra regressionstest för allt som rör den här funktionen.
Vänta en sekund – om det här funktionsanropet är så centralt för vår prestanda, och så kärnan i Dynamics CRM, så följer det att alla som använder Dynamics träffar denna prestandaflaskhals. Vi öppnade ett ärende med Microsoft, och jag ringde några personer för att få biljetten överlämnad till teknikteamet som ansvarar för den här koden. Med lite tur kommer den här uppdaterade versionen av funktionen att hamna i boxen (och molnet) i en framtida version av Dynamics CRM.
Detta är inte den enda TVF med flera påståenden i Dynamics CRM – jag gjorde samma typ av ändring av fn_UserSharedAttributesAccess
för ett annat prestationsproblem. Och det finns fler TVF som jag inte har rört eftersom de inte har orsakat problem.
En lektion för alla, även om du inte använder Dynamics
Upprepa efter mig:FUNKTIONER VÄRDERADE TABELL MED FLERA STATEMENT ÄR ONDA!
Refaktorera din kod för att undvika att använda TVF:er med flera påståenden. Om du försöker ställa in koden och du ser en TVF med flera påståenden, titta på det kritiskt. Du kan inte alltid ändra koden (eller det kan vara ett brott mot ditt supportavtal om du gör det), men om du kan ändra koden, gör det. Be din programvaruleverantör att sluta använda TVF:er med flera påståenden. Gör världen till en bättre plats genom att ta bort några av dessa otäcka funktioner från din databas.