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.