Database
 sql >> Base de données >  >> RDS >> Database

TVF multi-instructions dans Dynamics CRM

Auteur invité :Andy Mallon (@AMtwo)

Si vous avez l'habitude de prendre en charge la base de données derrière Microsoft Dynamics CRM, vous savez probablement qu'il ne s'agit pas de la base de données la plus performante. Honnêtement, cela ne devrait pas être une surprise - ce n'est pas conçu pour être une base de données ultra-rapide. Il est conçu pour être flexible base de données. La plupart des systèmes de gestion de la relation client (CRM) sont conçus pour être flexibles afin de pouvoir répondre aux besoins de nombreuses entreprises dans de nombreux secteurs avec des exigences commerciales très différentes. Ils placent ces exigences avant les performances de la base de données. C'est probablement une entreprise intelligente, mais je ne suis pas un homme d'affaires - je suis un spécialiste des bases de données. Mon expérience avec Dynamics CRM, c'est quand les gens viennent me voir et me disent

Andy, la base de données est lente

Un événement récent a été l'échec d'un rapport en raison d'un délai d'attente de requête de 5 minutes. Avec les index appropriés, nous devrions être en mesure d'obtenir quelques centaines de lignes très rapidement . J'ai mis la main sur la requête et quelques exemples de paramètres, je l'ai déposé dans Plan Explorer et je l'ai exécuté plusieurs fois dans notre environnement de test (je fais tout cela dans Test - cela sera important plus tard). Je voulais m'assurer que je l'exécutais avec un cache chaud, afin de pouvoir utiliser "le meilleur du pire" pour mon benchmark. La requête était un gros méchant SELECT avec un CTE, et un tas de jointures. Malheureusement, je ne peux pas fournir la requête exacte, car elle avait une logique métier spécifique au client (désolé !).

7 ​​minutes, 37 secondes, c'est mieux.

Dès le départ, il se passe beaucoup de mal ici. 1,5 million de lectures, c'est beaucoup d'E/S. 457 secondes pour renvoyer 200 lignes est lent. L'estimateur de cardinalité attendait 2 lignes, au lieu de 200. Et il y a eu beaucoup d'écritures, car cette requête n'est qu'un SELECT déclaration, cela signifie que nous devons renverser vers TempDb. Peut-être que j'aurai de la chance et que je pourrai créer un index pour éliminer une analyse de table et accélérer cette chose. À quoi ressemble le plan ?

On dirait un apatosaurus, ou peut-être une girafe.

Il n'y aura pas de résultats rapides

Permettez-moi de m'arrêter un instant pour expliquer quelque chose à propos de Dynamics CRM. Il utilise des vues. Il utilise des vues imbriquées. Il utilise des vues imbriquées pour appliquer la sécurité au niveau des lignes. Dans le langage Dynamics, ces vues imbriquées renforçant la sécurité au niveau des lignes sont appelées « vues filtrées ». Chaque requête de l'application passe par ces vues filtrées. La seule façon "prise en charge" d'accéder aux données consiste à utiliser ces vues filtrées.

Rappelez-vous que j'ai dit que cette requête faisait référence à un tas de tables ? Eh bien, il fait référence à un tas de vues filtrées. Ainsi, la requête compliquée qui m'a été remise est en fait plusieurs couches plus compliquées. À ce stade, j'ai pris une tasse de café fraîche et je suis passé à un moniteur plus grand.

Une excellente façon de résoudre les problèmes est de commencer par le début. J'ai zoomé sur l'opérateur SELECT et suivi les flèches pour voir ce qui se passait :

Même sur mon moniteur ultra-large 34", j'ai dû jouer avec l'affichage paramètres pour que le plan puisse en voir autant. Plan Explorer peut faire pivoter les plans de 90 degrés pour que les plans "grands" tiennent sur un écran large.

Regardez tous ces appels de fonction table ! Suivi immédiatement par une correspondance de hachage très coûteuse. Mon Spidey Sense a commencé à picoter. Qu'est-ce que fn_GetMaxPrivilegeDepthMask , et pourquoi est-il appelé 30 fois ? Je parie que c'est un problème. Lorsque vous voyez "Fonction de table" comme opérateur dans un plan, cela signifie en fait qu'il s'agit d'une fonction de table à plusieurs instructions . S'il s'agissait d'une fonction de table en ligne, elle serait intégrée dans le plan plus large et ne serait pas une boîte noire. Les fonctions table multi-instructions sont mauvaises. Ne les utilisez pas. L'estimateur de cardinalité n'est pas en mesure de faire des estimations précises. L'optimiseur de requête n'est pas en mesure de les optimiser dans le contexte d'une requête plus large. Du point de vue des performances, ils ne sont pas évolutifs.

Même si ce TVF est un morceau de code prêt à l'emploi de Dynamics CRM, mon Spidey Sense me dit que c'est le problème. Oubliez cette grosse requête désagréable avec un gros plan effrayant. Entrons dans cette fonction et voyons ce qui se passe :

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

Cette fonction suit un schéma classique dans les TVF multi-instructions :

  • Déclarer une variable utilisée comme constante
  • Insérer dans une variable de tableau
  • Renvoyer cette variable de table

Il n'y a rien d'extraordinaire ici. Nous pourrions réécrire ces multiples déclarations en un seul SELECT déclaration. Si nous pouvons l'écrire comme un seul SELECT déclaration, nous pouvons la réécrire en tant que TVF en ligne.

Faisons-le

Si ce n'est pas évident, je suis sur le point de réécrire le code fourni par un fournisseur de logiciels. Je n'ai jamais rencontré un fournisseur de logiciels qui considère cela comme un comportement "supporté". Si vous modifiez le code d'application prêt à l'emploi, vous êtes seul. Microsoft considère certainement ce comportement "non pris en charge" pour Dynamics. Je vais le faire quand même, puisque j'utilise l'environnement de test et que je ne joue pas en production. La réécriture de cette fonction n'a pris que quelques minutes, alors pourquoi ne pas essayer et voir ce qui se passe ? Voici à quoi ressemble ma version de la fonction :

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

Je suis revenu à ma requête de test d'origine, j'ai vidé le cache et je l'ai réexécuté plusieurs fois. Voici le plus lent temps d'exécution, lors de l'utilisation de ma version du TVF :

Ça a l'air beaucoup mieux !

Ce n'est toujours pas la requête la plus efficace au monde, mais elle est assez rapide - je n'ai pas besoin de la rendre plus rapide. Sauf que… j'ai dû modifier le code de Microsoft pour que cela se produise. Ce n'est pas idéal. Jetons un coup d'œil au plan complet avec le nouveau TVF :

Au revoir apatosaurus, bonjour distributeur PEZ !

C'est toujours un plan vraiment époustouflant, mais si vous regardez au début, tous ces appels TVF de boîte noire ont disparu. La correspondance de hachage super chère a disparu. SQL Server se met immédiatement au travail sans ce gros goulot d'étranglement d'appels TVF (le travail derrière le TVF est maintenant en ligne avec le reste du SELECT ):

Impact global

Où ce TVF est-il réellement utilisé ? Presque chaque vue filtrée dans Dynamics CRM utilise cet appel de fonction. Il existe 246 vues filtrées et 206 d'entre elles font référence à cette fonction. Il s'agit d'une fonction essentielle dans le cadre de l'implémentation de la sécurité au niveau des lignes de Dynamics. Virtuellement, chaque requête de l'application aux bases de données appelle cette fonction au moins une fois, généralement plusieurs fois. Il s'agit d'une pièce à double face :d'une part, la correction de cette fonction agira probablement comme un turbo boost pour l'ensemble de l'application; par contre, il n'y a aucun moyen pour moi de faire des tests de régression pour tout ce qui touche à cette fonction.

Attendez une seconde - si cet appel de fonction est si essentiel à nos performances, et si essentiel à Dynamics CRM, il s'ensuit que tous ceux qui utilisent Dynamics rencontrent ce goulot d'étranglement de performances. Nous avons ouvert un dossier avec Microsoft et j'ai appelé quelques personnes pour que le ticket soit transmis à l'équipe d'ingénierie responsable de ce code. Avec un peu de chance, cette version mise à jour de la fonction sera intégrée à la boîte (et au cloud) dans une future version de Dynamics CRM.

Ce n'est pas le seul TVF multi-instructions dans Dynamics CRM - j'ai apporté le même type de modification à fn_UserSharedAttributesAccess pour un autre problème de performances. Et il y a plus de TVF auxquels je n'ai pas touché car ils n'ont pas causé de problèmes.

Une leçon pour tout le monde, même si vous n'utilisez pas Dynamics

Répétez après moi :LES FONCTIONS VALORISÉES DE LA TABLE MULTI-DÉCLARATIONS SONT MAL !

Refactorisez votre code pour éviter d'utiliser des TVF à plusieurs instructions. Si vous essayez de régler le code et que vous voyez un TVF à plusieurs instructions, examinez-le d'un œil critique. Vous ne pouvez pas toujours changer le code (ou cela peut être une violation de votre contrat de support si vous le faites), mais si vous pouvez changer le code, faites-le. Dites à votre fournisseur de logiciels d'arrêter d'utiliser les TVF multi-instructions. Rendez le monde meilleur en éliminant certaines de ces fonctions désagréables de votre base de données.

À propos de l'auteur

Andy Mallon est un administrateur de base de données SQL Server et un MVP de la plate-forme de données Microsoft qui a géré des bases de données dans les domaines de la santé, de la finance, de l'e -secteurs du commerce et à but non lucratif. Depuis 2003, Andy prend en charge des environnements OLTP à haut volume et hautement disponibles avec des besoins de performances exigeants. Andy est le fondateur de BostonSQL, co-organisateur de SQLSaturday Boston et blogue sur am2.co.