Database
 sql >> Datenbank >  >> RDS >> Database

TVFs mit mehreren Anweisungen in Dynamics CRM

Gastautor:Andy Mallon (@AMtwo)

Wenn Sie mit der Unterstützung der Datenbank hinter Microsoft Dynamics CRM vertraut sind, wissen Sie wahrscheinlich, dass dies nicht die leistungsstärkste Datenbank ist. Ehrlich gesagt sollte das keine Überraschung sein – es ist nicht als blitzschnelle Datenbank konzipiert. Es ist so konzipiert, dass es flexibel ist Datenbank. Die meisten Customer Relationship Management (CRM)-Systeme sind so konzipiert, dass sie flexibel sind, damit sie die Bedürfnisse vieler Unternehmen in vielen Branchen mit sehr unterschiedlichen Geschäftsanforderungen erfüllen können. Sie stellen diese Anforderungen über die Datenbankleistung. Das ist wahrscheinlich ein kluges Geschäft, aber ich bin kein Geschäftsmann – ich bin ein Datenbankmensch. Meine Erfahrung mit Dynamics CRM ist, wenn Leute zu mir kommen und sagen

Andy, die Datenbank ist langsam

Kürzlich ist ein Bericht aufgrund einer 5-minütigen Abfragezeitüberschreitung fehlgeschlagen. Mit den richtigen Indizes sollten wir in der Lage sein, sehr schnell einige hundert Zeilen zu erhalten . Ich habe die Abfrage und einige Beispielparameter in die Hände bekommen, sie in den Plan-Explorer eingefügt und einige Male in unserer Testumgebung ausgeführt (ich mache das alles in Test – das wird später wichtig). Ich wollte sicherstellen, dass ich es mit einem warmen Cache ausführe, damit ich „das Beste vom Schlimmsten“ für meinen Benchmark verwenden kann. Die Abfrage war ein großes böses SELECT mit einem CTE und einer Reihe von Joins. Leider kann ich die genaue Abfrage nicht bereitstellen, da sie einige kundenspezifische Geschäftslogik enthielt (Entschuldigung!).

7 ​​Minuten, 37 Sekunden reichen nicht aus.

Auf Anhieb ist hier viel Schlimmes los. 1,5 Millionen Lesevorgänge sind verdammt viel I/O. 457 Sekunden für die Rückgabe von 200 Zeilen sind langsam. Der Cardinality Estimator erwartete 2 Zeilen statt 200. Und es gab viele Schreibvorgänge – da diese Abfrage nur ein SELECT ist Anweisung bedeutet dies, dass wir zu TempDb überlaufen müssen. Vielleicht habe ich Glück und kann einen Index erstellen, um einen Tabellenscan zu eliminieren und diese Sache zu beschleunigen. Wie sieht der Plan aus?

Sieht aus wie ein Apatosaurus oder vielleicht eine Giraffe.

Es wird keine schnellen Treffer geben

Lassen Sie mich einen Moment innehalten, um etwas über Dynamics CRM zu erklären. Es verwendet Ansichten. Es verwendet verschachtelte Ansichten. Es verwendet verschachtelte Ansichten, um die Sicherheit auf Zeilenebene zu erzwingen. Im Dynamics-Sprachgebrauch werden diese verschachtelten Ansichten, die die Sicherheit auf Zeilenebene erzwingen, als „gefilterte Ansichten“ bezeichnet. Jede Abfrage der Anwendung durchläuft diese gefilterten Ansichten. Die einzige "unterstützte" Methode für den Datenzugriff ist die Verwendung dieser gefilterten Ansichten.

Erinnern Sie sich, dass ich sagte, dass diese Abfrage auf eine Reihe von Tabellen verweist? Nun, es verweist auf eine Reihe gefilterter Ansichten. Die komplizierte Abfrage, die ich erhalten habe, ist also tatsächlich um mehrere Schichten komplizierter. An diesem Punkt holte ich mir eine frische Tasse Kaffee und wechselte zu einem größeren Monitor.

Eine gute Möglichkeit, Probleme zu lösen, besteht darin, am Anfang zu beginnen. Ich zoomte auf den SELECT-Operator und folgte den Pfeilen, um zu sehen, was los war:

Sogar auf meinem ultrabreiten 34-Zoll-Monitor musste ich mit der Anzeige herumspielen Einstellungen für den Plan, um so viel zu sehen. Plan Explorer kann Pläne um 90 Grad drehen, damit "große" Pläne auf einen breiten Monitor passen.

Sehen Sie sich all diese Tabellenwert-Funktionsaufrufe an! Unmittelbar gefolgt von einem wirklich teuren Hash-Match. Mein Spidey Sense begann zu kribbeln. Was ist fn_GetMaxPrivilegeDepthMask , und warum wird es 30 mal angerufen? Ich wette, das ist ein Problem. Wenn Sie "Tabellenwertfunktion" als Operator in einem Plan sehen, bedeutet das eigentlich, dass es sich um eine Tabellenwertfunktion mit mehreren Anweisungen handelt . Wenn es sich um eine Inline-Tabellenwertfunktion handeln würde, würde sie in den größeren Plan integriert werden und wäre keine Blackbox. Tabellenwertfunktionen mit mehreren Anweisungen sind böse. Verwenden Sie sie nicht. Der Kardinalitätsschätzer kann keine genauen Schätzungen vornehmen. Der Abfrageoptimierer ist nicht in der Lage, sie im Kontext der größeren Abfrage zu optimieren. Aus Leistungssicht lassen sie sich nicht skalieren.

Obwohl dieses TVF ein sofort einsatzbereiter Code von Dynamics CRM ist, sagt mir mein Spidey Sense, dass es das Problem ist. Vergessen Sie diese große böse Abfrage mit einem großen beängstigenden Plan. Lassen Sie uns in diese Funktion einsteigen und sehen, was los ist:

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

Diese Funktion folgt einem klassischen Muster in TVFs mit mehreren Anweisungen:

  • Deklarieren Sie eine Variable, die als Konstante verwendet wird
  • In eine Tabellenvariable einfügen
  • Gib diese Tabellenvariable zurück

Hier ist nichts Besonderes los. Wir könnten diese mehreren Anweisungen als ein einziges SELECT umschreiben Erklärung. Wenn wir es als einzelnes SELECT schreiben können -Anweisung können wir dies als Inline-TVF umschreiben.

Lass es uns tun

Wenn es nicht offensichtlich ist, bin ich dabei, den von einem Softwareanbieter bereitgestellten Code neu zu schreiben. Ich habe noch nie einen Softwareanbieter getroffen, der dies als "unterstütztes" Verhalten betrachtet. Wenn Sie den vorkonfigurierten Anwendungscode ändern, sind Sie auf sich allein gestellt. Microsoft betrachtet dieses Verhalten sicherlich als "nicht unterstützt" für Dynamics. Ich werde es trotzdem tun, da ich die Testumgebung verwende und nicht in der Produktion herumspiele. Das Umschreiben dieser Funktion hat nur ein paar Minuten gedauert – warum probieren Sie es also nicht aus und sehen, was passiert? So sieht meine Version der Funktion aus:

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

Ich kehrte zu meiner ursprünglichen Testabfrage zurück, leerte den Cache und führte ihn ein paar Mal erneut aus. Hier ist die langsamste Laufzeit, wenn ich meine Version des TVF verwende:

Das sieht viel besser aus!

Es ist immer noch nicht die effizienteste Abfrage der Welt, aber sie ist schnell genug – ich muss sie nicht noch schneller machen. Außer… ich musste den Code von Microsoft ändern, um es zu ermöglichen. Das ist nicht ideal. Werfen wir einen Blick auf den vollständigen Plan mit dem neuen TVF:

Auf Wiedersehen Apatosaurus, hallo PEZ-Spender!

Es ist immer noch ein wirklich knorriger Plan, aber wenn Sie sich den Start ansehen, sind all diese Black-Box-TVF-Anrufe weg. Das superteure Hash-Match ist weg. SQL Server macht sich sofort an die Arbeit, ohne diesen großen Engpass von TVF-Aufrufen (die Arbeit hinter dem TVF ist jetzt inline mit dem Rest von SELECT ):

Große Wirkung

Wo wird dieser TVF eigentlich eingesetzt? Nahezu jede einzelne gefilterte Ansicht in Dynamics CRM verwendet diesen Funktionsaufruf. Es gibt 246 gefilterte Ansichten und 206 davon verweisen auf diese Funktion. Dies ist eine kritische Funktion als Teil der Dynamics-Sicherheitsimplementierung auf Zeilenebene. Praktisch jede einzelne Anfrage von der Anwendung an die Datenbanken ruft diese Funktion mindestens einmal auf – normalerweise einige Male. Dies ist eine zweiseitige Medaille:Einerseits wird die Behebung dieser Funktion wahrscheinlich als Turbo-Boost für die gesamte Anwendung wirken; Andererseits gibt es für mich keine Möglichkeit, Regressionstests für alles durchzuführen, was diese Funktion berührt.

Warten Sie eine Sekunde – wenn dieser Funktionsaufruf so wichtig für unsere Leistung und so wichtig für Dynamics CRM ist, dann folgt daraus, dass jeder, der Dynamics verwendet, diesen Leistungsengpass trifft. Wir haben einen Fall bei Microsoft eröffnet, und ich habe ein paar Leute angerufen, um das Ticket an das für diesen Code verantwortliche Engineering-Team weiterzuleiten. Mit etwas Glück wird es diese aktualisierte Version der Funktion in einer zukünftigen Version von Dynamics CRM in die Box (und die Cloud) schaffen.

Dies ist nicht das einzige TVF mit mehreren Anweisungen in Dynamics CRM – ich habe dieselbe Art von Änderung an fn_UserSharedAttributesAccess vorgenommen für ein anderes Leistungsproblem. Und es gibt noch mehr TVFs, die ich nicht angerührt habe, weil sie keine Probleme verursacht haben.

Eine Lektion für alle, auch wenn Sie Dynamics nicht verwenden

Wiederholen Sie nach mir:MULTI-STATEMENT TABLE VALUED FUNCTIONS ARE EVIL!

Refaktorisieren Sie Ihren Code, um die Verwendung von TVFs mit mehreren Anweisungen zu vermeiden. Wenn Sie versuchen, Code zu tunen, und Sie ein TVF mit mehreren Anweisungen sehen, sehen Sie es sich kritisch an. Sie können den Code nicht immer ändern (oder es kann ein Verstoß gegen Ihren Supportvertrag sein, wenn Sie dies tun), aber wenn Sie den Code ändern können, tun Sie es. Weisen Sie Ihren Softwareanbieter an, die Verwendung von TVFs mit mehreren Anweisungen einzustellen. Machen Sie die Welt zu einem besseren Ort, indem Sie einige dieser unangenehmen Funktionen aus Ihrer Datenbank entfernen.

Über den Autor

Andy Mallon ist ein SQL Server DBA und Microsoft Data Platform MVP, der Datenbanken in den Bereichen Gesundheitswesen, Finanzen, z -Handel und Non-Profit-Sektoren. Seit 2003 unterstützt Andy hochvolumige, hochverfügbare OLTP-Umgebungen mit anspruchsvollen Leistungsanforderungen. Andy ist der Gründer von BostonSQL, Mitorganisator von SQLSaturday Boston und bloggt auf am2.co.