Database
 sql >> Base de Dados >  >> RDS >> Database

TVFs de várias declarações no Dynamics CRM

Autor convidado:Andy Mallon (@AMtwo)


Se você está familiarizado com o suporte ao banco de dados por trás do Microsoft Dynamics CRM, provavelmente sabe que ele não é o banco de dados de desempenho mais rápido. Honestamente, isso não deveria ser uma surpresa – não foi projetado para ser um banco de dados extremamente rápido. Ele foi projetado para ser flexível base de dados. A maioria dos sistemas de CRM (Customer Relationship Management) são projetados para serem flexíveis, para que possam atender às necessidades de muitas empresas em muitos setores com requisitos de negócios muito diferentes. Eles colocam esses requisitos à frente do desempenho do banco de dados. Isso é provavelmente um negócio inteligente, mas eu não sou uma pessoa de negócios – eu sou uma pessoa de banco de dados. Minha experiência com o Dynamics CRM é quando as pessoas vêm até mim e dizem

Andy, o banco de dados está lento


Uma ocorrência recente foi a falha de um relatório devido a um tempo limite de consulta de 5 minutos. Com os índices adequados, devemos conseguir algumas centenas de linhas muito rápido . Coloquei minhas mãos na consulta e em alguns parâmetros de exemplo, coloquei-o no Plan Explorer e o executei algumas vezes em nosso ambiente de teste (estou fazendo tudo isso no teste – isso será importante mais tarde). Eu queria ter certeza de que estava executando-o com um cache quente, para que eu pudesse usar "o melhor do pior" para meu benchmark. A consulta foi um grande e desagradável SELECT com um CTE, e um monte de junções. Infelizmente, não posso fornecer a consulta exata, pois ela tinha alguma lógica de negócios específica do cliente (desculpe!).

7 ​​minutos e 37 segundos é o máximo.

Logo de cara, há muita coisa ruim acontecendo aqui. 1,5 milhão de leituras é muito I/O. 457 segundos para retornar 200 linhas é lento. O Estimador de Cardinalidade esperava 2 linhas, em vez de 200. E havia muitas gravações, já que essa consulta é apenas um SELECT declaração, isso significa que devemos estar derramando para TempDb. Talvez eu tenha sorte e consiga criar um índice para eliminar uma varredura de tabela e acelerar isso. Como é o plano?

Parece um apatosaurus, ou talvez uma girafa.

Não haverá acertos rápidos


Deixe-me fazer uma pausa por um momento para explicar algo sobre o Dynamics CRM. Ele usa visualizações. Ele usa visualizações aninhadas. Ele usa exibições aninhadas para impor a segurança em nível de linha. Na linguagem do Dynamics, essas exibições aninhadas que reforçam a segurança em nível de linha são chamadas de "exibições filtradas". Cada consulta do aplicativo passa por essas visualizações filtradas. A única maneira "suportada" de realizar o acesso a dados é usar essas exibições filtradas.

Lembre-se que eu disse que esta consulta estava referenciando um monte de tabelas? Bem, está fazendo referência a várias visualizações filtradas. Portanto, a consulta complicada que recebi é, na verdade, várias camadas mais complicadas. Nesse ponto, peguei uma xícara de café fresca e mudei para um monitor maior.

Uma ótima maneira de resolver problemas é começar do início. Dei zoom no operador SELECT e segui as setas para ver o que estava acontecendo:

Mesmo no meu monitor ultra-amplo de 34", tive que mexer na tela configurações do plano para ver isso. O Plan Explorer pode girar os planos em 90 graus para fazer com que os planos "altos" caibam em um monitor amplo.

Veja todas aquelas chamadas de função com valor de tabela! Seguido imediatamente por uma partida de hash muito cara. Meu Sentido Aranha começou a formigar. O que é fn_GetMaxPrivilegeDepthMask , e por que está sendo chamado 30 vezes? Aposto que isso é um problema. Quando você vê "Função com valor de tabela" como um operador em um plano, isso significa que é uma função com valor de tabela de várias instruções . Se fosse uma função com valor de tabela embutido, ela seria incorporada ao plano maior e não seria uma caixa preta. Funções com valor de tabela com várias instruções são más. Não os use. O Estimador de Cardinalidade não é capaz de fazer estimativas precisas. O Query Optimizer não consegue otimizá-los no contexto da consulta maior. Do ponto de vista do desempenho, eles não escalam.

Mesmo que este TVF seja um pedaço de código pronto para uso do Dynamics CRM, meu Spidey Sense me diz que é o problema. Esqueça essa grande consulta desagradável com um grande plano assustador. Vamos entrar nessa função e ver o que está acontecendo:
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

Essa função segue um padrão clássico em TVFs de várias instruções:
  • Declare uma variável que é usada como uma constante
  • Inserir em uma variável de tabela
  • Retorne essa variável de tabela

Não há nada extravagante acontecendo aqui. Poderíamos reescrever essas várias instruções como um único SELECT demonstração. Se pudermos escrevê-lo como um único SELECT declaração, podemos reescrever isso como um TVF embutido.

Vamos fazer isso


Se não for óbvio, estou prestes a reescrever o código fornecido por um fornecedor de software. Eu nunca conheci um fornecedor de software que considera esse comportamento "suportado". Se você alterar o código do aplicativo pronto para uso, estará por conta própria. A Microsoft certamente considera esse comportamento "sem suporte" para o Dynamics. Vou fazer assim mesmo, já que estou usando o ambiente de teste e não estou brincando na produção. Reescrever essa função levou apenas alguns minutos, então por que não tentar e ver o que acontece? Veja como é minha versão da função:
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

Voltei para minha consulta de teste original, descartei o cache e executei novamente algumas vezes. Aqui está o mais lento tempo de execução, ao usar minha versão do TVF:

Isso parece muito melhor!

Ainda não é a consulta mais eficiente do mundo, mas é rápida o suficiente – não preciso torná-la mais rápida. Exceto… eu tive que modificar o código da Microsoft para que isso acontecesse. Isso não é o ideal. Vamos dar uma olhada no plano completo com o novo TVF:

Adeus apatosaurus, olá PEZ dispenser!

Ainda é um plano muito complicado, mas se você olhar para o início, todas aquelas chamadas de TVF de caixa preta sumiram. A partida de hash super cara se foi. O SQL Server começa a trabalhar sem aquele grande gargalo de chamadas TVF (o trabalho por trás do TVF agora está alinhado com o restante do SELECT ):


Impacto geral


Onde este TVF é realmente usado? Quase todas as exibições filtradas no Dynamics CRM usam essa chamada de função. Existem 246 visualizações filtradas e 206 delas fazem referência a essa função. É uma função crítica como parte da implementação de segurança em nível de linha do Dynamics. Praticamente todas as consultas do aplicativo aos bancos de dados chamam essa função pelo menos uma vez, geralmente algumas vezes. Esta é uma moeda de dois lados:por um lado, corrigir essa função provavelmente funcionará como um turbo boost para todo o aplicativo; por outro lado, não tenho como fazer testes de regressão para tudo que toca nessa função.

Espere um segundo – se essa chamada de função é tão essencial para nosso desempenho e tão essencial para o Dynamics CRM, então todos que usam o Dynamics estão atingindo esse gargalo de desempenho. Abrimos um caso com a Microsoft e liguei para algumas pessoas para enviar o ticket para a equipe de engenharia responsável por esse código. Com um pouco de sorte, esta versão atualizada da função chegará à caixa (e à nuvem) em uma versão futura do Dynamics CRM.

Este não é o único TVF de várias instruções no Dynamics CRM – fiz o mesmo tipo de alteração em fn_UserSharedAttributesAccess para outro problema de desempenho. E há mais TVFs que não toquei porque não causaram problemas.

Uma lição para todos, mesmo se você não estiver usando o Dynamics


Repita comigo:FUNÇÕES DE VALOR DE MULTI-STATEMENT TABLE SÃO MAL!

Refatore seu código para evitar o uso de TVFs de várias instruções. Se você estiver tentando ajustar o código e vir um TVF com várias instruções, observe-o criticamente. Você nem sempre pode alterar o código (ou pode ser uma violação do seu contrato de suporte se o fizer), mas se puder alterar o código, faça-o. Diga ao seu fornecedor de software para parar de usar TVFs de várias instruções. Torne o mundo um lugar melhor eliminando algumas dessas funções desagradáveis ​​do seu banco de dados.

Sobre o autor

Andy Mallon é um DBA SQL Server e Microsoft Data Platform MVP que gerenciou bancos de dados nas áreas de saúde, finanças, e -comércio e setores sem fins lucrativos. Desde 2003, Andy oferece suporte a ambientes OLTP de alto volume e alta disponibilidade com necessidades de desempenho exigentes. Andy é o fundador do BostonSQL, co-organizador do SQLSaturday Boston, e bloga em am2.co.