Database
 sql >> Baza danych >  >> RDS >> Database

Wielowyrazowe TVF w Dynamics CRM

Autor gościnny:Andy Mallon (@AMtwo)

Jeśli znasz obsługę bazy danych w programie Microsoft Dynamics CRM, prawdopodobnie wiesz, że nie jest to najszybsza baza danych. Szczerze mówiąc, nie powinno to być niespodzianką — nie została zaprojektowana jako błyskawiczna baza danych. Został zaprojektowany jako elastyczny Baza danych. Większość systemów zarządzania relacjami z klientami (CRM) zaprojektowano tak, aby były elastyczne, tak aby mogły spełniać potrzeby wielu firm z wielu branż o bardzo różnych wymaganiach biznesowych. Stawiają te wymagania przed wydajnością bazy danych. To prawdopodobnie sprytny biznes, ale nie jestem osobą biznesową – jestem osobą zajmującą się bazami danych. Moje doświadczenie z Dynamics CRM polega na tym, że ludzie przychodzą do mnie i mówią

Andy, baza danych jest wolna

Jedno z ostatnich zdarzeń dotyczyło niepowodzenia raportu z powodu przekroczenia 5-minutowego limitu czasu zapytania. Przy odpowiednich indeksach powinniśmy być w stanie uzyskać kilkaset wierszy bardzo szybko . Dostałem w swoje ręce zapytanie i kilka przykładowych parametrów, wrzuciłem je do Eksploratora planów i uruchomiłem je kilka razy w naszym środowisku testowym (robię to wszystko w Test – to będzie ważne później). Chciałem się upewnić, że uruchamiam go z ciepłą pamięcią podręczną, aby móc użyć „najlepszego z najgorszych” w moim benchmarku. Zapytanie było dużym, paskudnym SELECT z CTE i kilkoma połączeniami. Niestety nie mogę podać dokładnego zapytania, ponieważ zawierało ono pewną logikę biznesową specyficzną dla klienta (przepraszam!).

7 ​​minut, 37 sekund jest tak dobre, jak to tylko możliwe.

Od samego początku dzieje się tu wiele zła. 1,5 miliona odczytów to cholernie dużo I/O. 457 sekund na zwrócenie 200 wierszy jest wolne. Estymator kardynalizacji oczekiwał 2 wierszy zamiast 200. I było dużo zapisów — ponieważ to zapytanie jest tylko SELECT oświadczenie, oznacza to, że musimy przechodzić do TempDb. Może będę miał szczęście i będę w stanie stworzyć indeks, aby wyeliminować skanowanie tabeli i przyspieszyć to. Jak wygląda plan?

Wygląda jak apatozaur, a może żyrafa.

Nie będzie szybkich trafień

Zatrzymam się na chwilę, aby wyjaśnić coś o Dynamics CRM. Wykorzystuje widoki. Wykorzystuje widoki zagnieżdżone. Używa widoków zagnieżdżonych, aby wymusić zabezpieczenia na poziomie wiersza. W żargonie Dynamics te zagnieżdżone widoki na poziomie wiersza są nazywane „widokami filtrowanymi”. Każde zapytanie z aplikacji przechodzi przez te filtrowane widoki. Jedynym „obsługiwanym” sposobem uzyskiwania dostępu do danych jest użycie tych filtrowanych widoków.

Przypomnij sobie, powiedziałem, że to zapytanie odwołuje się do kilku tabel? Cóż, odwołuje się do wielu przefiltrowanych widoków. Tak więc złożone zapytanie, które otrzymałem, jest w rzeczywistości o kilka warstw bardziej skomplikowane. W tym momencie dostałem filiżankę świeżej kawy i przestawiłem się na większy monitor.

Świetnym sposobem na rozwiązywanie problemów jest rozpoczęcie od początku. Powiększyłem operator SELECT i podążałem za strzałkami, aby zobaczyć, co się dzieje:

Nawet na moim 34-calowym ultraszerokim monitorze musiałem bawić się wyświetlaczem ustawienia planu, aby zobaczyć tyle. Eksplorator planów może obracać plany o 90 stopni, aby „wysokie” plany zmieściły się na szerokim monitorze.

Spójrz na te wszystkie wywołania funkcji z wartościami tabeli! Zaraz po nim nastąpił naprawdę drogi hash match. Mój zmysł pająka zaczął drżeć. Co to jest fn_GetMaxPrivilegeDepthMask i dlaczego jest wywoływany 30 razy? Założę się, że to jest problem. Gdy widzisz „Funkcja z wartościami tabelarycznymi” jako operator w planie, w rzeczywistości oznacza to, że jest to funkcja z wartościami tabelarycznymi z wieloma instrukcjami . Gdyby była to funkcja o wartości wbudowanej w tabeli, zostałaby włączona do większego planu, a nie byłaby czarną skrzynką. Wieloinstrukcyjne funkcje z wartościami tabelarycznymi są złe. Nie używaj ich. Estymator kardynacji nie jest w stanie dokonać dokładnych szacunków. Optymalizator zapytań nie może ich zoptymalizować w kontekście większego zapytania. Z perspektywy wydajności nie skalują się.

Mimo że ten TVF jest gotowym fragmentem kodu z Dynamics CRM, mój Spidey Sense mówi mi, że to jest problem. Zapomnij o tym wielkim paskudnym zapytaniu z wielkim, przerażającym planem. Przejdźmy do tej funkcji i zobaczmy, co się dzieje:

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

Ta funkcja jest zgodna z klasycznym wzorcem w wielowyrazowych plikach TVF:

  • Zadeklaruj zmienną, która jest używana jako stała
  • Wstaw do zmiennej tabeli
  • Zwróć tę zmienną tabeli

Nie dzieje się tu nic wymyślnego. Moglibyśmy przepisać te wiele instrukcji jako pojedynczy SELECT oświadczenie. Jeśli możemy napisać to jako pojedynczy SELECT oświadczenie, możemy przepisać to jako wbudowany TVF.

Zróbmy to

Jeśli nie jest to oczywiste, mam zamiar przepisać kod dostarczony przez dostawcę oprogramowania. Nigdy nie spotkałem dostawcy oprogramowania, który uważałby to za zachowanie „obsługiwane”. Jeśli zmienisz gotowy kod aplikacji, jesteś zdany na siebie. Microsoft z pewnością uważa to "nieobsługiwane" zachowanie dla Dynamics. I tak zamierzam to zrobić, ponieważ korzystam ze środowiska testowego i nie bawię się w środowisku produkcyjnym. Ponowne napisanie tej funkcji zajęło tylko kilka minut – dlaczego więc nie spróbować i zobaczyć, co się stanie? Oto jak wygląda moja wersja funkcji:

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

Wróciłem do pierwotnego zapytania testowego, zrzuciłem pamięć podręczną i kilka razy uruchomiłem ją ponownie. Oto najwolniejszy czas działania, gdy używam mojej wersji TVF:

Wygląda znacznie lepiej!

To wciąż nie jest najbardziej wydajne zapytanie na świecie, ale jest wystarczająco szybkie – nie muszę go przyspieszać. Poza tym… musiałem zmodyfikować kod Microsoftu, żeby tak się stało. To nie jest idealne. Rzućmy okiem na pełny plan z nowym TVF:

Żegnaj apatozaur, witaj dystrybutorze PEZ!

To wciąż bardzo kiepski plan, ale jeśli spojrzysz na początek, wszystkie te czarne skrzynki TVF zniknęły. Super drogi hash match zniknął. SQL Server przystępuje do pracy bez dużego wąskiego gardła wywołań TVF (praca za TVF jest teraz zintegrowana z resztą SELECT ):

Wpływ na duży obraz

Gdzie jest faktycznie używany ten TVF? Prawie każdy filtrowany widok w Dynamics CRM korzysta z tego wywołania funkcji. Istnieje 246 przefiltrowanych widoków, a 206 z nich odwołuje się do tej funkcji. Jest to krytyczna funkcja w ramach implementacji zabezpieczeń Dynamics na poziomie wiersza. Prawie każde zapytanie z aplikacji do baz danych wywołuje tę funkcję co najmniej raz – zwykle kilka razy. Jest to moneta dwustronna:z jednej strony naprawienie tej funkcji prawdopodobnie zadziała jak turbodoładowanie dla całej aplikacji; z drugiej strony nie ma możliwości przeprowadzenia testów regresji dla wszystkiego, co dotyczy tej funkcji.

Poczekaj chwilę — jeśli to wywołanie funkcji jest tak istotne dla naszej wydajności i tak kluczowe dla Dynamics CRM, wynika z tego, że każdy, kto korzysta z Dynamics, napotyka na wąskie gardło wydajności. Otworzyliśmy sprawę z Microsoftem i zadzwoniłem do kilku osób, aby dostać bilet do zespołu inżynierów odpowiedzialnego za ten kod. Przy odrobinie szczęścia ta zaktualizowana wersja funkcji pojawi się w pudełku (i chmurze) w przyszłej wersji Dynamics CRM.

To nie jedyny wielowyrazowy TVF w Dynamics CRM — wprowadziłem ten sam typ zmiany w fn_UserSharedAttributesAccess dla innego problemu z wydajnością. I jest więcej programów TVF, których nie dotknąłem, ponieważ nie spowodowały problemów.

Lekcja dla wszystkich, nawet jeśli nie używasz Dynamics

Powtarzaj za mną:WIELOKROTNY TABELA CENNE FUNKCJE SĄ ZŁE!

Zmień czynniki w swoim kodzie, aby uniknąć używania wielowyrazowych plików TVF. Jeśli próbujesz dostroić kod i widzisz wielowyrazowy TVF, spójrz na to krytycznie. Nie zawsze możesz zmienić kod (lub może to stanowić naruszenie umowy o pomoc techniczną, jeśli to zrobisz), ale jeśli możesz zmienić kod, zrób to. Poinformuj swojego dostawcę oprogramowania, aby przestał korzystać z wielowyrazowych plików TVF. Uczyń świat lepszym miejscem, eliminując niektóre z tych paskudnych funkcji ze swojej bazy danych.

O autorze

Andy Mallon jest administratorem baz danych SQL Server i Microsoft Data Platform MVP, który zarządza bazami danych w służbie zdrowia, finansach, e - handel i sektory non-profit. Od 2003 roku Andy wspiera środowiska OLTP o dużej objętości i wysokiej dostępności o wysokich wymaganiach w zakresie wydajności. Andy jest założycielem BostonSQL, współorganizatorem SQLSaturday Boston i blogów w am2.co.
  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Model bazy danych dla systemu rezerwacji szkoły nauki jazdy. Część 1

  2. SQL DROP TABLE dla początkujących

  3. Jak sztuczna inteligencja zmieni tworzenie i testowanie oprogramowania

  4. Prosty przypadek użycia indeksów w kluczach podstawowych

  5. Jak dezaktywować wtyczki z bazy danych WordPress