Gastauteur:Andy Mallon (@AMtwo)
Als u bekend bent met het ondersteunen van de database achter Microsoft Dynamics CRM, weet u waarschijnlijk dat dit niet de snelst presterende database is. Eerlijk gezegd zou dat geen verrassing moeten zijn - het is niet ontworpen als een razendsnelle database. Het is ontworpen om een flexibele . te zijn databank. De meeste CRM-systemen (Customer Relationship Management) zijn ontworpen om flexibel te zijn, zodat ze kunnen voldoen aan de behoeften van veel bedrijven in veel sectoren met enorm verschillende zakelijke vereisten. Ze stellen die vereisten boven databaseprestaties. Dat is waarschijnlijk een slimme zaak, maar ik ben geen zakenman - ik ben een database-persoon. Mijn ervaring met Dynamics CRM is wanneer mensen naar me toe komen en zeggen
Andy, de database is traag
Een recent voorval was dat een rapport mislukte vanwege een querytime-out van 5 minuten. Met de juiste indexen zouden we een paar honderd rijen heel snel moeten kunnen krijgen . Ik kreeg de query en enkele voorbeeldparameters in handen, liet het in Plan Explorer vallen en voerde het een paar keer uit in onze testomgeving (ik doe dit allemaal in Test - dat wordt later belangrijk). Ik wilde er zeker van zijn dat ik het draaide met een warme cache, zodat ik "het beste van het slechtste" voor mijn benchmark kon gebruiken. De vraag was een grote vervelende SELECT
met een CTE en een heleboel joins. Helaas kan ik de exacte zoekopdracht niet geven, omdat deze klantspecifieke bedrijfslogica bevatte (Sorry!).
7 minuten, 37 seconden is zo goed als mogelijk.
Meteen, er is hier veel ergs aan de hand. 1,5 miljoen reads is een hoop I/O. 457 seconden om 200 rijen te retourneren is traag. De Cardinality Estimator verwachtte 2 rijen in plaats van 200. En er waren veel schrijfbewerkingen, aangezien deze query slechts een SELECT
is statement, betekent dit dat we moeten overlopen naar TempDb. Misschien heb ik geluk en kan ik een index maken om een tabelscan te elimineren en dit ding te versnellen. Hoe ziet het plan eruit?
Lijkt op een apatosaurus, of misschien een giraf.
Er zullen geen snelle treffers zijn
Laat me even pauzeren om iets uit te leggen over Dynamics CRM. Het maakt gebruik van weergaven. Het maakt gebruik van geneste weergaven. Het gebruikt geneste weergaven om beveiliging op rijniveau af te dwingen. In het spraakgebruik van Dynamics worden deze geneste weergaven die de beveiliging op rijniveau afdwingen 'gefilterde weergaven' genoemd. Elke query van de applicatie gaat door deze gefilterde views. De enige "ondersteunde" manier om gegevenstoegang uit te voeren, is door deze gefilterde weergaven te gebruiken.
Weet je nog dat ik zei dat deze query verwees naar een aantal tabellen? Nou, het verwijst naar een aantal gefilterde weergaven. Dus de gecompliceerde vraag die ik kreeg, is eigenlijk meerdere lagen gecompliceerder. Op dit punt kreeg ik een verse kop koffie en schakelde ik over op een grotere monitor.
Een geweldige manier om problemen op te lossen is om bij het begin te beginnen. Ik zoomde in op de SELECT-operator en volgde de pijlen om te zien wat er aan de hand was:
Zelfs op mijn 34" ultrabrede monitor moest ik aan het scherm prutsen instellingen voor het plan om zoveel te zien. Planverkenner kan plannen 90 graden draaien om "hoge" plannen op een brede monitor te laten passen.
Kijk naar al die functieaanroepen met tabelwaarde! Onmiddellijk gevolgd door een echt dure hash-match. Mijn Spidey Sense begon te tintelen. Wat is fn_GetMaxPrivilegeDepthMask
, en waarom wordt het 30 keer gebeld? Ik wed dat dit een probleem is. Als u 'functie met tabelwaarde' als operator in een plan ziet, betekent dit in feite dat het een functie met tabelwaarde met meerdere instructies is . Als het een inline functie met tabelwaarde zou zijn, zou het worden opgenomen in het grotere plan en geen zwarte doos zijn. Tabelwaardefuncties met meerdere instructies zijn slecht. Gebruik ze niet. De kardinaliteitschatter kan geen nauwkeurige schattingen maken. De Query Optimizer kan ze niet optimaliseren in de context van de grotere query. Vanuit prestatieperspectief schalen ze niet.
Ook al is deze TVF een kant-en-klaar stukje code van Dynamics CRM, mijn Spidey Sense vertelt me dat dit het probleem is. Vergeet deze grote vervelende vraag met een groot eng plan. Laten we in die functie stappen en kijken wat er aan de hand is:
create function [dbo].[fn_GetMaxPrivilegeDepthMask](@ObjectTypeCode int) returns @d table(PrivilegeDepthMask int) -- It is by design that we return a table with only one row and column as begin declare @UserId uniqueidentifier select @UserId = dbo.fn_FindUserGuid() declare @t table(depth int) -- from user roles insert into @t(depth) select --privilege depth mask = 1(basic) 2(local) 4(deep) and 8(global) -- 16(inherited read) 32(inherited local) 64(inherited deep) and 128(inherited global) -- do an AND with 0x0F ( =15) to get basic/local/deep/global max(rp.PrivilegeDepthMask % 0x0F) as PrivilegeDepthMask from PrivilegeBase priv join RolePrivileges rp on (rp.PrivilegeId = priv.PrivilegeId) join Role r on (rp.RoleId = r.ParentRootRoleId) join SystemUserRoles ur on (r.RoleId = ur.RoleId and ur.SystemUserId = @UserId) join PrivilegeObjectTypeCodes potc on (potc.PrivilegeId = priv.PrivilegeId) where potc.ObjectTypeCode = @ObjectTypeCode and priv.AccessRight & 0x01 = 1 -- from user's teams roles insert into @t(depth) select --privilege depth mask = 1(basic) 2(local) 4(deep) and 8(global) -- 16(inherited read) 32(inherited local) 64(inherited deep) and 128(inherited global) -- do an AND with 0x0F ( =15) to get basic/local/deep/global max(rp.PrivilegeDepthMask % 0x0F) as PrivilegeDepthMask from PrivilegeBase priv join RolePrivileges rp on (rp.PrivilegeId = 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 and sup.SystemUserId = @UserId) join PrivilegeObjectTypeCodes potc on (potc.PrivilegeId = priv.PrivilegeId) where potc.ObjectTypeCode = @ObjectTypeCode and priv.AccessRight & 0x01 = 1 insert into @d select max(depth) from @t return end GO
Deze functie volgt een klassiek patroon in TVF's met meerdere statements:
- Declareer een variabele die als constante wordt gebruikt
- Invoegen in een tabelvariabele
- Retourneer die tabelvariabele
Er is hier niets bijzonders aan de hand. We zouden deze meerdere instructies kunnen herschrijven als een enkele SELECT
uitspraak. Als we het kunnen schrijven als een enkele SELECT
statement, kunnen we dit herschrijven als een inline TVF.
Laten we het doen
Als het niet duidelijk is, sta ik op het punt de code van een softwareleverancier te herschrijven. Ik heb nog nooit een softwareleverancier ontmoet die dit als "ondersteund" gedrag beschouwt. Als u de kant-en-klare applicatiecode wijzigt, staat u er alleen voor. Microsoft beschouwt dit zeker als "niet-ondersteund" gedrag voor Dynamics. Ik ga het toch doen, aangezien ik de testomgeving gebruik en niet aan het spelen ben in de productie. Het herschrijven van deze functie duurde slechts een paar minuten, dus waarom zou u het niet eens proberen en kijken wat er gebeurt? Zo ziet mijn versie van de functie eruit:
create function [dbo].[fn_GetMaxPrivilegeDepthMask](@ObjectTypeCode int) returns table -- It is by design that we return a table with only one row and column as RETURN -- from user roles select PrivilegeDepthMask = max(PrivilegeDepthMask) from ( select --privilege depth mask = 1(basic) 2(local) 4(deep) and 8(global) -- 16(inherited read) 32(inherited local) 64(inherited deep) and 128(inherited global) -- do an AND with 0x0F ( =15) to get basic/local/deep/global max(rp.PrivilegeDepthMask % 0x0F) as PrivilegeDepthMask from PrivilegeBase priv join RolePrivileges rp on (rp.PrivilegeId = priv.PrivilegeId) join Role r on (rp.RoleId = r.ParentRootRoleId) join SystemUserRoles ur on (r.RoleId = ur.RoleId and ur.SystemUserId = dbo.fn_FindUserGuid()) join PrivilegeObjectTypeCodes potc on (potc.PrivilegeId = priv.PrivilegeId) where potc.ObjectTypeCode = @ObjectTypeCode and priv.AccessRight & 0x01 = 1 UNION ALL -- from user's teams roles select --privilege depth mask = 1(basic) 2(local) 4(deep) and 8(global) -- 16(inherited read) 32(inherited local) 64(inherited deep) and 128(inherited global) -- do an AND with 0x0F ( =15) to get basic/local/deep/global max(rp.PrivilegeDepthMask % 0x0F) as PrivilegeDepthMask from PrivilegeBase priv join RolePrivileges rp on (rp.PrivilegeId = 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 and sup.SystemUserId = dbo.fn_FindUserGuid()) join PrivilegeObjectTypeCodes potc on (potc.PrivilegeId = priv.PrivilegeId) where potc.ObjectTypeCode = @ObjectTypeCode and priv.AccessRight & 0x01 = 1 )x GO
Ik ging terug naar mijn oorspronkelijke testquery, dumpte de cache en voerde hem een paar keer opnieuw uit. Dit is de langzaamste looptijd, bij gebruik van mijn versie van de TVF:
Dat ziet er veel beter uit!
Het is nog steeds niet de meest efficiënte query ter wereld, maar het is snel genoeg - ik hoef het niet sneller te maken. Behalve... ik moest de code van Microsoft aanpassen om het mogelijk te maken. Dat is niet ideaal. Laten we eens kijken naar het volledige plan met de nieuwe TVF:
Tot ziens apatosaurus, hallo PEZ-dispenser!
Het is nog steeds een heel lastig plan, maar als je naar het begin kijkt, zijn al die black box TVF-oproepen verdwenen. De superdure hash-match is verdwenen. SQL Server gaat meteen aan de slag zonder dat grote knelpunt van TVF-aanroepen (het werk achter de TVF is nu in lijn met de rest van de SELECT
):
Grote foto-impact
Waar wordt deze TVF eigenlijk gebruikt? Bijna elke afzonderlijke gefilterde weergave in Dynamics CRM gebruikt deze functieaanroep. Er zijn 246 gefilterde weergaven en 206 daarvan verwijzen naar deze functie. Het is een kritieke functie als onderdeel van de beveiligingsimplementatie op rijniveau van Dynamics. Bijna elke afzonderlijke query van de toepassing naar de databases roept deze functie minstens één keer aan, meestal een paar keer. Dit is een medaille van twee kanten:aan de ene kant zal het repareren van deze functie waarschijnlijk werken als een turboboost voor de hele applicatie; aan de andere kant kan ik op geen enkele manier regressietests doen voor alles wat met deze functie te maken heeft.
Wacht even - als deze functieaanroep zo essentieel is voor onze prestaties, en zo essentieel voor Dynamics CRM, dan volgt hieruit dat iedereen die Dynamics gebruikt, dit prestatieknelpunt raakt. We hebben een zaak geopend met Microsoft en ik heb een paar mensen gebeld om het ticket door te sturen naar het technische team dat verantwoordelijk is voor deze code. Met een beetje geluk zal deze bijgewerkte versie van de functie in de box (en de cloud) terechtkomen in een toekomstige release van Dynamics CRM.
Dit is niet de enige TVF met meerdere verklaringen in Dynamics CRM. Ik heb hetzelfde type wijziging aangebracht in fn_UserSharedAttributesAccess
voor een ander prestatieprobleem. En er zijn meer TVF's die ik niet heb aangeraakt omdat ze geen problemen hebben veroorzaakt.
Een les voor iedereen, zelfs als je Dynamics niet gebruikt
Herhaal na mij:MULTI-STATEMENT TABEL GEWAARDEERDE FUNCTIES ZIJN KWADE!
Re-factor uw code om te voorkomen dat u TVF's met meerdere instructies gebruikt. Als u code probeert af te stemmen en u ziet een TVF met meerdere verklaringen, bekijk deze dan kritisch. Je kunt de code niet altijd wijzigen (of het kan een schending zijn van je ondersteuningscontract als je dat doet), maar als je de code kunt wijzigen, doe het dan. Vertel uw softwareleverancier om te stoppen met het gebruik van multi-statement TVF's. Maak de wereld een betere plek door enkele van deze vervelende functies uit je database te verwijderen.