Гост автор:Анди Малън (@AMtwo)
Ако сте запознати с поддръжката на базата данни зад Microsoft Dynamics CRM, вероятно знаете, че това не е най-бързо работещата база данни. Честно казано, това не трябва да е изненада – не е проектирана да бъде крещяща бърза база данни. Той е проектиран да бъде гъвкав база данни. Повечето системи за управление на взаимоотношенията с клиенти (CRM) са проектирани да бъдат гъвкави, така че да могат да отговорят на нуждите на много фирми в много индустрии с много различни бизнес изисквания. Те поставят тези изисквания пред производителността на базата данни. Това вероятно е умен бизнес, но аз не съм бизнесмен – аз съм човек с база данни. Моят опит с Dynamics CRM е, когато хората идват при мен и казват
Анди, базата данни е бавна
Едно скорошно събитие беше с неуспешен отчет поради 5-минутно изчакване на заявката. С правилните индекси би трябвало да можем да получим няколкостотин реда много бързо . Взех заявката и някои примерни параметри, пуснах я в Plan Explorer и я стартирах няколко пъти в нашата тестова среда (правя всичко това в Test – това ще бъде важно по-късно). Исках да се уверя, че го изпълнявам с топъл кеш, за да мога да използвам "най-доброто от най-лошото" за моя еталон. Заявката беше голяма гадна SELECT
с CTE и куп съединения. За съжаление, не мога да дам точната заявка, тъй като имаше някаква специфична за клиента бизнес логика (Съжалявам!).
7 минути, 37 секунди са толкова добри, колкото са.
Веднага, тук се случват много лошо. 1,5 милиона четения са адски много I/O. 457 секунди за връщане на 200 реда е бавно. Cardinality Estimator очакваше 2 реда, вместо 200. И имаше много записи – тъй като тази заявка е само SELECT
изявление, това означава, че трябва да преливаме към TempDb. Може би ще имам късмет и ще мога да създам индекс, за да премахна сканирането на таблицата и да ускоря това нещо. Как изглежда планът?
Прилича на апатозавър или може би на жираф.
Няма да има бързи попадения
Позволете ми да направя пауза за момент, за да обясня нещо за Dynamics CRM. Използва изгледи. Той използва вложени изгледи. Той използва вложени изгледи, за да наложи сигурност на ниво ред. На езика на Dynamics тези вложени изгледи, налагащи сигурност на ниво ред, се наричат „филтрирани изгледи“. Всяка заявка от приложението преминава през тези филтрирани изгледи. Единственият "поддържан" начин за осъществяване на достъп до данни е използването на тези филтрирани изгледи.
Спомнете си, че казах, че тази заявка препраща към куп таблици? Е, препраща към куп филтрирани изгледи. Така че сложната заявка, която получих, всъщност е няколко слоя по-сложна. В този момент си взех чаша прясно кафе и преминах към по-голям монитор.
Чудесен начин за решаване на проблеми е да започнете от самото начало. Увеличих мащаба на оператора SELECT и последвах стрелките, за да видя какво се случва:
Дори на моя 34-инчов ултраширок монитор трябваше да се занимавам с дисплея настройките за плана, за да видите толкова много. Plan Explorer може да завърта плановете на 90 градуса, за да направи "високите" планове подходящи на широк монитор.
Вижте всички тези извиквания на функции с таблица! Следва веднага един наистина скъп хеш мач. Spidey Sense започна да изтръпва. Какво е fn_GetMaxPrivilegeDepthMask
, и защо се обажда 30 пъти? Обзалагам се, че това е проблем. Когато видите „функция с таблична стойност“ като оператор в план, това всъщност означава, че това е функция с таблично стойности с множество изрази . Ако беше вградена функция с таблица, тя щеше да бъде включена в по-големия план, а не да бъде черна кутия. Функциите с таблично стойности с множество изрази са зли. Не ги използвайте. Оценителят на кардиналитета не може да направи точни оценки. Оптимизаторът на заявки не може да ги оптимизира в контекста на по-голямата заявка. От гледна точка на производителността, те не се мащабират.
Въпреки че този TVF е готов част от кода от Dynamics CRM, моят Spidey Sense ми казва, че това е проблемът. Забравете тази голяма гадна заявка с голям страшен план. Нека влезем в тази функция и да видим какво се случва:
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
Тази функция следва класически модел в TVF с множество изявления:
- Декларирайте променлива, която се използва като константа
- Вмъкване в променлива на таблица
- Върнете тази променлива в таблицата
Тук не се случва нищо фантастично. Бихме могли да пренапишем тези множество изрази като един SELECT
изявление. Ако можем да го запишем като единичен SELECT
изявление, можем да го пренапишем като вграден TVF.
Нека го направим
Ако не е очевидно, ще пренапиша код, предоставен от доставчик на софтуер. Никога не съм срещал доставчик на софтуер, който смята това за "поддържано" поведение. Ако промените готовия код на приложението, вие сте сами. Microsoft със сигурност смята това "неподдържано" поведение за Dynamics. Все пак ще го направя, тъй като използвам тестовата среда и не си играя в производството. Пренаписването на тази функция отне само няколко минути – така че защо да не опитате и да видите какво ще се случи? Ето как изглежда моята версия на функцията:
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
Върнах се към първоначалната си тестова заявка, изхвърлих кеша и го стартирах отново няколко пъти. Ето най-бавния време на изпълнение, когато използвам моята версия на TVF:
Това изглежда много по-добре!
Това все още не е най-ефективната заявка в света, но е достатъчно бърза – няма нужда да я правя по-бърза. Освен… трябваше да променя кода на Microsoft, за да го направя. Това не е идеално. Нека да разгледаме пълния план с новия TVF:
Сбогом апатозавър, здравей PEZ дозатор!
Това все още е наистина грозен план, но ако погледнете началото, всички тези обаждания в черна кутия TVF са изчезнали. Изчезна супер скъпият хеш мач. SQL Server започва да работи без това голямо препятствие на TVF обажданията (работата зад TVF вече е в съответствие с останалата част от SELECT
):
Влияние на голяма картина
Къде всъщност се използва този TVF? Почти всеки един филтриран изглед в Dynamics CRM използва това извикване на функция. Има 246 филтрирани изгледа и 206 от тях препращат към тази функция. Това е критична функция като част от реализацията на сигурността на ниво ред на Dynamics. На практика всяка една заявка от приложението към базите данни извиква тази функция поне веднъж – обикновено няколко пъти. Това е двустранна монета:от една страна, фиксирането на тази функция вероятно ще действа като турбо тласък за цялото приложение; от друга страна, няма начин да направя регресионни тестове за всичко, което се докосва до тази функция.
Изчакайте малко – ако това извикване на функция е толкова основно за нашата производителност и толкова ядро за Dynamics CRM, тогава следва, че всеки, който използва Dynamics, удря това затруднение в производителността. Открихме дело с Microsoft и се обадих на няколко души, за да предадат билета на инженерния екип, отговорен за този код. С малко късмет тази актуализирана версия на функцията ще влезе в кутията (и в облака) в бъдеща версия на Dynamics CRM.
Това не е единственият TVF с няколко изявления в Dynamics CRM – направих същия тип промяна на fn_UserSharedAttributesAccess
за друг проблем с производителността. И има още TVF, които не съм пипал, защото не са създавали проблеми.
Урок за всички, дори ако не използвате Dynamics
Повторете след мен:ТАБЛИЦИТЕ С МНОГО ИЗЛОЖЕНИЯ, ОЦЕНЕНИТЕ ФУНКЦИИ СА ЗЛИ!
Префакторирайте кода си, за да избегнете използването на TVF с множество изявления. Ако се опитвате да настроите кода и видите TVF с множество изявления, погледнете го критично. Не винаги можете да промените кода (или може да е нарушение на договора ви за поддръжка, ако го направите), но ако можете да промените кода, направете го. Кажете на вашия доставчик на софтуер да спре да използва TVF с няколко изявления. Направете света по-добро място, като премахнете някои от тези неприятни функции от вашата база данни.