sql >> Base de Datos >  >> RDS >> Database

TVF de varios extractos en Dynamics CRM

Autor invitado:Andy Mallon (@AMtwo)

Si está familiarizado con el soporte de la base de datos detrás de Microsoft Dynamics CRM, probablemente sepa que no es la base de datos de rendimiento más rápido. Honestamente, eso no debería ser una sorpresa, no está diseñado para ser una base de datos increíblemente rápida. Está diseñado para ser flexible base de datos. La mayoría de los sistemas de gestión de relaciones con los clientes (CRM) están diseñados para ser flexibles, de modo que puedan satisfacer las necesidades de muchas empresas en muchas industrias con requisitos comerciales muy diferentes. Anteponen esos requisitos al rendimiento de la base de datos. Probablemente sea un negocio inteligente, pero no soy una persona de negocios, soy una persona de base de datos. Mi experiencia con Dynamics CRM es cuando la gente viene a mí y dice

Andy, la base de datos es lenta

Una ocurrencia reciente fue un informe que falló debido a un tiempo de espera de consulta de 5 minutos. Con los índices adecuados, deberíamos poder obtener unos cientos de filas muy rápido . Obtuve la consulta y algunos parámetros de ejemplo, los coloqué en Plan Explorer y los ejecuté varias veces en nuestro entorno de Prueba (estoy haciendo todo esto en Prueba, eso será importante más adelante). Quería asegurarme de ejecutarlo con una memoria caché tibia, de modo que pudiera usar "lo mejor de lo peor" para mi punto de referencia. La consulta fue un SELECT grande y desagradable con un CTE y un montón de uniones. Desafortunadamente, no puedo proporcionar la consulta exacta, ya que tenía una lógica comercial específica del cliente (¡lo siento!).

7 ​​minutos, 37 segundos es lo mejor posible.

Desde el principio, hay muchas cosas malas aquí. 1,5 millones de lecturas es una gran cantidad de E/S. 457 segundos para devolver 200 filas es lento. El Estimador de cardinalidad esperaba 2 filas, en lugar de 200. Y hubo muchas escrituras, ya que esta consulta es solo SELECT declaración, esto significa que debemos estar derramando a TempDb. Tal vez tenga suerte y pueda crear un índice para eliminar un escaneo de tabla y acelerar esto. ¿Cómo es el plan?

Parece un apatosaurio, o tal vez una jirafa.

No habrá aciertos rápidos

Permítanme hacer una pausa por un momento para explicar algo sobre Dynamics CRM. Utiliza vistas. Utiliza vistas anidadas. Utiliza vistas anidadas para hacer cumplir la seguridad a nivel de fila. En el lenguaje de Dynamics, estas vistas anidadas que refuerzan la seguridad a nivel de fila se denominan "vistas filtradas". Cada consulta de la aplicación pasa por estas vistas filtradas. La única forma "admitida" de acceder a los datos es usar estas vistas filtradas.

¿Recuerdas que dije que esta consulta hacía referencia a un montón de tablas? Bueno, hace referencia a un montón de vistas filtradas. Entonces, la consulta complicada que me entregaron es en realidad varias capas más complicada. En este punto, tomé una taza de café recién hecho y cambié a un monitor más grande.

Una excelente manera de resolver problemas es comenzar desde el principio. Acerqué el operador SELECCIONAR y seguí las flechas para ver qué estaba pasando:

Incluso en mi monitor ultra ancho de 34", tuve que jugar con la pantalla ajustes para que el plano vea tanto. Plan Explorer puede girar los planos 90 grados para que los planos "altos" quepan en un monitor ancho.

¡Mira todas esas llamadas a funciones con valores de tabla! Seguido inmediatamente por un hash match muy caro. Mi sentido arácnido comenzó a hormiguear. ¿Qué es fn_GetMaxPrivilegeDepthMask? , y ¿por qué se llama 30 veces? Apuesto a que esto es un problema. Cuando ve "Función con valores de tabla" como un operador en un plan, en realidad significa que es una función con valores de tabla de varias instrucciones . Si fuera una función con valores de tabla en línea, se incorporaría al plan más grande y no sería una caja negra. Las funciones con valores de tabla de declaraciones múltiples son malas. No los uses. El Estimador de cardinalidad no puede realizar estimaciones precisas. El Optimizador de consultas no puede optimizarlas en el contexto de una consulta más grande. Desde una perspectiva de rendimiento, no escalan.

Aunque este TVF es un código listo para usar de Dynamics CRM, mi Spidey Sense me dice que ese es el problema. Olvídese de esta gran consulta desagradable con un gran plan aterrador. Entremos en esa función y veamos qué está pasando:

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

Esta función sigue un patrón clásico en los TVF de varias instrucciones:

  • Declarar una variable que se utiliza como constante
  • Insertar en una variable de tabla
  • Devolver esa variable de tabla

Aquí no pasa nada lujoso. Podríamos reescribir estas declaraciones múltiples como un solo SELECT declaración. Si podemos escribirlo como un solo SELECT declaración, podemos reescribir esto como un TVF en línea.

Hagámoslo

Si no es obvio, estoy a punto de volver a escribir el código proporcionado por un proveedor de software. Nunca he conocido a un proveedor de software que considere que este es un comportamiento "compatible". Si cambia el código de aplicación listo para usar, está solo. Microsoft ciertamente considera este comportamiento "no compatible" para Dynamics. Lo haré de todos modos, ya que estoy usando el entorno de prueba y no estoy jugando en producción. Reescribir esta función tomó solo un par de minutos, entonces, ¿por qué no intentarlo y ver qué sucede? Así es como se ve mi versión de la función:

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

Volví a mi consulta de prueba original, descargué el caché y lo volví a ejecutar varias veces. Aquí está el más lento tiempo de ejecución, cuando uso mi versión de TVF:

¡Eso se ve mucho mejor!

Todavía no es la consulta más eficiente del mundo, pero es lo suficientemente rápida, no necesito que sea más rápida. Excepto... Tuve que modificar el código de Microsoft para que esto sucediera. Eso no es ideal. Echemos un vistazo al plan completo con el nuevo TVF:

¡Adiós apatosaurio, hola dispensador PEZ!

Todavía es un plan realmente retorcido, pero si miras el comienzo, todas esas llamadas TVF de caja negra se han ido. La coincidencia de hash súper costosa se ha ido. SQL Server se pone manos a la obra sin ese gran cuello de botella de llamadas TVF (el trabajo detrás de TVF ahora está en línea con el resto de SELECT ):

Impacto general

¿Dónde se usa realmente este TVF? Casi todas las vistas filtradas en Dynamics CRM utilizan esta llamada de función. Hay 246 vistas filtradas y 206 de ellas hacen referencia a esta función. Es una función crítica como parte de la implementación de seguridad de nivel de fila de Dynamics. Prácticamente todas las consultas desde la aplicación a las bases de datos llaman a esta función al menos una vez, generalmente varias veces. Esta es una moneda de dos caras:por un lado, arreglar esta función probablemente actuará como un impulso turbo para toda la aplicación; por otro lado, no tengo forma de hacer pruebas de regresión para todo lo que toca esta función.

Espere un segundo:si esta llamada de función es tan fundamental para nuestro rendimiento y tan fundamental para Dynamics CRM, entonces se deduce que todos los que usan Dynamics se enfrentan a este cuello de botella de rendimiento. Abrimos un caso con Microsoft y llamé a algunas personas para que enviaran el ticket al equipo de ingeniería responsable de este código. Con un poco de suerte, esta versión actualizada de la función llegará a la caja (y a la nube) en una versión futura de Dynamics CRM.

Este no es el único TVF de declaración múltiple en Dynamics CRM:realicé el mismo tipo de cambio en fn_UserSharedAttributesAccess por otro problema de rendimiento. Y hay más TVF que no he tocado porque no han dado problemas.

Una lección para todos, incluso si no usa Dynamics

Repite conmigo:¡LAS FUNCIONES VALORADAS EN LA TABLA DE MÚLTIPLES DECLARACIONES SON MALAS!

Vuelva a factorizar su código para evitar el uso de TVF de múltiples declaraciones. Si está tratando de ajustar el código y ve un TVF de varias declaraciones, mírelo con ojo crítico. No siempre puede cambiar el código (o puede ser una violación de su contrato de soporte si lo hace), pero si puede cambiar el código, hágalo. Dígale a su proveedor de software que deje de usar TVF de declaraciones múltiples. Haz del mundo un lugar mejor eliminando algunas de estas desagradables funciones de tu base de datos.

Sobre el autor

Andy Mallon es DBA de SQL Server y MVP de Microsoft Data Platform que ha administrado bases de datos en los - Comercio y sectores sin fines de lucro. Desde 2003, Andy ha brindado soporte a entornos OLTP de alto volumen y alta disponibilidad con necesidades de rendimiento exigentes. Andy es el fundador de BostonSQL, coorganizador de SQLSaturday Boston y bloguea en am2.co.