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.