sql >> Databáze >  >> RDS >> Database

Vícepříkazové TVF v Dynamics CRM

Autor hosta:Andy Mallon (@AMtwo)

Pokud jste obeznámeni s podporou databáze za Microsoft Dynamics CRM, pravděpodobně víte, že nejde o databázi s nejrychlejším výkonem. Upřímně řečeno, to by nemělo být překvapením – není to navrženo tak, aby to byla křiklavě rychlá databáze. Je navržen tak, aby byl flexibilní databáze. Většina systémů CRM (Customer Relationship Management) je navržena tak, aby byla flexibilní, aby mohla vyhovět potřebám mnoha podniků v mnoha odvětvích s výrazně odlišnými obchodními požadavky. Upřednostňují tyto požadavky před výkonem databáze. To je pravděpodobně chytrý byznys, ale nejsem obchodník – jsem databázový člověk. Moje zkušenost s Dynamics CRM je, když za mnou lidé přicházejí a říkají

Andy, databáze je pomalá

Jeden nedávný výskyt byl se zprávou, která selhala kvůli 5minutovému časovému limitu dotazu. Se správnými indexy bychom měli být schopni získat několik set řádků opravdu rychle . Dostal jsem se k dotazu a některým vzorovým parametrům, pustil jsem to do Plan Explorer a několikrát jsem to spustil v našem testovacím prostředí (to vše dělám v Testu – to bude důležité později). Chtěl jsem se ujistit, že to spouštím s teplou cache, abych mohl pro svůj benchmark použít „to nejlepší z nejhoršího“. Dotaz byl velký odporný SELECT s CTE a hromadou spojení. Bohužel nemohu poskytnout přesný dotaz, protože měl nějakou obchodní logiku specifickou pro zákazníka (Omlouvám se!).

7 ​​minut, 37 sekund je tak dobrých, jak jen může být.

Hned na začátku se tady děje hodně špatného. 1,5 milionu přečtení je sakra hodně I/O. 457 sekund na návrat 200 řádků je pomalé. Nástroj Cardinality Estimator očekával 2 řádky místo 200. A došlo k mnoha zápisům – protože tento dotaz je pouze SELECT to znamená, že musíme přelévat do TempDb. Možná budu mít štěstí a budu schopen vytvořit index, který eliminuje skenování tabulek a urychlí tuto věc. Jak vypadá plán?

Vypadá jako apatosaurus nebo možná žirafa.

Nebudou žádné rychlé zásahy

Dovolte mi, abych se na chvíli zastavil, abych vysvětlil něco o Dynamics CRM. Používá pohledy. Používá vnořené pohledy. K vynucení zabezpečení na úrovni řádků používá vnořené pohledy. V jazyce Dynamics se tato vnořená zobrazení vynucující zabezpečení na úrovni řádků nazývají „filtrovaná zobrazení“. Každý dotaz z aplikace prochází těmito filtrovanými pohledy. Jediným „podporovaným“ způsobem, jak provádět přístup k datům, je použití těchto filtrovaných zobrazení.

Připomeňme, že jsem řekl, že tento dotaz odkazuje na spoustu tabulek? No, odkazuje na spoustu filtrovaných pohledů. Takže složitý dotaz, který jsem dostal, je ve skutečnosti o několik vrstev složitější. V tuto chvíli jsem si dal čerstvý šálek kávy a přešel na větší monitor.

Skvělý způsob, jak vyřešit problémy, je začít od začátku. Přiblížil jsem si operátor SELECT a sledoval jsem šipky, abych zjistil, co se děje:

I na mém 34" ultraširokém monitoru jsem si musel pohrát s displejem nastavení pro plán, aby bylo vidět tolik. Plan Explorer dokáže otočit plány o 90 stupňů, aby se „vysoké“ plány vešly na široký monitor.

Podívejte se na všechna ta volání funkcí s hodnotou tabulky! Vzápětí následuje pořádně drahý hash match. Můj Spidey Sense začal brnět. Co je fn_GetMaxPrivilegeDepthMask , a proč se to volá 30krát? Vsadím se, že tohle je problém. Když v plánu vidíte „funkci s tabulkovou hodnotou“ jako operátor, ve skutečnosti to znamená, že se jedná o funkci s tabulkovou hodnotou s více příkazy . Pokud by to byla funkce s inline tabulkou, byla by začleněna do většího plánu a nebyla by to černá skříňka. Vícepříkazové tabulkové funkce jsou zlo. Nepoužívejte je. Nástroj pro odhad mohutnosti není schopen provádět přesné odhady. Optimalizátor dotazů je nedokáže optimalizovat v kontextu většího dotazu. Z hlediska výkonu se neškálují.

I když je tento TVF hotový kus kódu od Dynamics CRM, můj Spidey Sense mi říká, že problém je v tom. Zapomeňte na tento velký ošklivý dotaz s velkým děsivým plánem. Pojďme do této funkce a podívejme se, co se děje:

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

Tato funkce se řídí klasickým vzorem ve vícepříkazových TVF:

  • Deklarujte proměnnou, která se používá jako konstanta
  • Vložit do proměnné tabulky
  • Vraťte proměnnou tabulky

Tady se neděje nic převratného. Tyto vícenásobné příkazy bychom mohli přepsat jako jeden SELECT prohlášení. Pokud to můžeme napsat jako jeden SELECT prohlášení, můžeme to přepsat jako inline TVF.

Pojďme na to

Pokud to není zřejmé, chystám se přepsat kód poskytnutý dodavatelem softwaru. Nikdy jsem se nesetkal s dodavatelem softwaru, který by toto považoval za „podporované“ chování. Pokud změníte přednastavený kód aplikace, budete na to sami. Microsoft toto chování Dynamics rozhodně považuje za „nepodporované“. Stejně to udělám, protože používám testovací prostředí a nehraju si na produkci. Přepsání této funkce trvalo jen pár minut – tak proč to nezkusit a neuvidíte, co se stane? Moje verze funkce vypadá takto:

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

Vrátil jsem se ke svému původnímu testovacímu dotazu, vyprázdnil mezipaměť a několikrát jej znovu spustil. Zde je nejpomalejší doba běhu při použití mé verze TVF:

To vypadá mnohem lépe!

Stále to není nejefektivnější dotaz na světě, ale je dostatečně rychlý – nepotřebuji ho zrychlovat. Kromě... Musel jsem upravit kód Microsoftu, aby se to stalo. To není ideální. Pojďme se podívat na celý plán s novým TVF:

Sbohem apatosaure, ahoj dávkovač PEZ!

Je to stále opravdu drsný plán, ale když se podíváte na začátek, všechny ty černé skříňky TVF jsou pryč. Super-drahá hash match je pryč. SQL Server začne pracovat přímo bez velkého úzkého hrdla volání TVF (práce za TVF je nyní v souladu se zbytkem SELECT ):

Dopad velkého obrazu

Kde se vlastně tato TVF používá? Téměř každý jednotlivý filtrovaný pohled v Dynamics CRM používá toto volání funkce. Existuje 246 filtrovaných pohledů a 206 z nich odkazuje na tuto funkci. Jde o kritickou funkci jako součást implementace zabezpečení na úrovni řádků Dynamics. Prakticky každý jednotlivý dotaz z aplikace do databází volá tuto funkci alespoň jednou – obvykle několikrát. Toto je oboustranná mince:na jedné straně bude oprava této funkce pravděpodobně fungovat jako turbo boost pro celou aplikaci; na druhou stranu pro mě neexistuje způsob, jak provést regresní testy pro vše, co se týká této funkce.

Počkejte chvíli – pokud je toto volání funkce tak klíčové pro náš výkon a tak klíčové pro Dynamics CRM, pak z toho vyplývá, že každý, kdo používá Dynamics, naráží na toto úzké hrdlo výkonu. Otevřeli jsme případ se společností Microsoft a zavolal jsem několika lidem, aby poslali lístek technickému týmu odpovědnému za tento kód. S trochou štěstí se tato aktualizovaná verze funkce dostane do krabice (a cloudu) v budoucí verzi Dynamics CRM.

Toto není jediný vícepříkazový TVF v Dynamics CRM – provedl jsem stejný typ změny u fn_UserSharedAttributesAccess pro jiný problém s výkonem. A existuje více TVF, kterých jsem se nedotkl, protože nezpůsobily problémy.

Lekce pro každého, i když nepoužíváte Dynamics

Opakujte po mně:FUNKCE HODNOTY TABULKY VÍCE VÝKAZŮ JSOU ZLO!

Změňte svůj kód, abyste se vyhnuli používání vícepříkazových TVF. Pokud se pokoušíte vyladit kód a vidíte vícepříkazový TVF, podívejte se na něj kriticky. Kód nemůžete vždy změnit (nebo to může být porušení vaší smlouvy o podpoře, pokud to uděláte), ale pokud můžete kód změnit, udělejte to. Řekněte svému dodavateli softwaru, aby přestal používat vícepříkazové TVF. Udělejte ze světa lepší místo tím, že ze své databáze odstraníte některé z těchto nepříjemných funkcí.

O autorovi

Andy Mallon je SQL Server DBA a Microsoft Data Platform MVP, který spravuje databáze v oblasti zdravotnictví, financí, e -obchod a neziskový sektor. Od roku 2003 Andy podporuje velkoobjemová a vysoce dostupná prostředí OLTP s náročnými požadavky na výkon. Andy je zakladatelem BostonSQL, spoluorganizátorem SQLSaturday Boston a bloguje na am2.co.
  1. Jak mohu odstranit duplicitní řádky?

  2. ORA-01882:Oblast časového pásma nenalezena

  3. sloupec zde není povolen chyba v příkazu INSERT

  4. Neuspořádané výsledky v SQL