Autore ospite:Andy Mallon (@AMtwo)
Se hai familiarità con il supporto del database dietro Microsoft Dynamics CRM, probabilmente saprai che non è il database con le prestazioni più veloci. Onestamente, non dovrebbe essere una sorpresa:non è progettato per essere un database velocissimo. È progettato per essere un flessibile Banca dati. La maggior parte dei sistemi di gestione delle relazioni con i clienti (CRM) sono progettati per essere flessibili in modo da poter soddisfare le esigenze di molte aziende in molti settori con requisiti aziendali molto diversi. Mettono questi requisiti prima delle prestazioni del database. Probabilmente è un affare intelligente, ma non sono un uomo d'affari, sono una persona di database. La mia esperienza con Dynamics CRM è quando le persone vengono da me e dicono
Andy, il database è lento
Un'occorrenza recente si è verificata con un rapporto non riuscito a causa di un timeout della query di 5 minuti. Con gli indici appropriati, dovremmo essere in grado di ottenere alcune centinaia di righe molto velocemente . Ho messo le mani sulla query e su alcuni parametri di esempio, l'ho rilasciata in Plan Explorer e l'ho eseguita alcune volte nel nostro ambiente di test (sto facendo tutto questo in Test, sarà importante in seguito). Volevo assicurarmi di eseguirlo con una cache calda, in modo da poter utilizzare "il meglio del peggio" per il mio benchmark. La domanda era un grosso brutto SELECT
con un CTE e un sacco di join. Sfortunatamente, non posso fornire la query esatta, poiché aveva una logica aziendale specifica del cliente (scusate!).
7 minuti e 37 secondi sono il massimo.
Fin dall'inizio, qui stanno succedendo un sacco di cose brutte. 1,5 milioni di letture sono un sacco di I/O. 457 secondi per restituire 200 righe sono lenti. Lo stimatore di cardinalità prevedeva 2 righe, invece di 200. E ci sono state molte scritture, poiché questa query è solo un SELECT
dichiarazione, questo significa che dobbiamo riversare su TempDb. Forse sarò fortunato e sarò in grado di creare un indice per eliminare una scansione della tabella e accelerare questa cosa. Com'è il piano?
Sembra un apatosauro, o forse una giraffa.
Non ci saranno colpi veloci
Mi fermo un momento per spiegare qualcosa su Dynamics CRM. Utilizza le visualizzazioni. Utilizza viste nidificate. Utilizza viste nidificate per rafforzare la sicurezza a livello di riga. Nel linguaggio di Dynamics, queste viste nidificate che impongono la sicurezza a livello di riga sono chiamate "viste filtrate". Ogni query dall'applicazione passa attraverso queste viste filtrate. L'unico modo "supportato" per eseguire l'accesso ai dati è utilizzare queste viste filtrate.
Ricordiamo che ho detto che questa query faceva riferimento a un gruppo di tabelle? Bene, fa riferimento a un gruppo di visualizzazioni filtrate. Quindi la domanda complicata che mi è stata data è in realtà di diversi livelli più complicata. A questo punto, ho preso una tazza di caffè fresca e sono passato a un monitor più grande.
Un ottimo modo per risolvere i problemi è iniziare dall'inizio. Ho ingrandito l'operatore SELECT e ho seguito le frecce per vedere cosa stava succedendo:
Anche sul mio monitor ultra-wide da 34" ho dovuto giocherellare con il display impostazioni in modo che il piano possa vedere così tanto. Plan Explorer può ruotare i piani di 90 gradi per adattarli a piani "alti" su un monitor ampio.
Guarda tutte quelle chiamate di funzione con valori di tabella! Seguito immediatamente da un hash match davvero costoso. Il mio senso di Spidey iniziò a formicolare. Che cos'è fn_GetMaxPrivilegeDepthMask
, e perché viene chiamato 30 volte? Scommetto che questo è un problema. Quando vedi "Funzione con valori di tabella" come operatore in un piano, significa in realtà che si tratta di una funzione con valori di tabella a più istruzioni . Se fosse una funzione inline con valori di tabella, verrebbe incorporata nel piano più ampio e non sarebbe una scatola nera. Le funzioni con valori di tabella multi-istruzione sono malvagie. Non usarli. Lo stimatore di cardinalità non è in grado di effettuare stime accurate. Query Optimizer non è in grado di ottimizzarli nel contesto della query più ampia. Dal punto di vista delle prestazioni, non sono scalabili.
Anche se questo TVF è un pezzo di codice pronto all'uso di Dynamics CRM, il mio Spidey Sense mi dice che è il problema. Dimentica questa grande brutta domanda con un grande piano spaventoso. Entriamo in quella funzione e vediamo cosa sta succedendo:
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
Questa funzione segue uno schema classico nei TVF con più istruzioni:
- Dichiara una variabile usata come costante
- Inserisci in una variabile di tabella
- Restituisci quella variabile di tabella
Non c'è niente di speciale qui. Potremmo riscrivere queste istruzioni multiple come un unico SELECT
dichiarazione. Se possiamo scriverlo come un singolo SELECT
dichiarazione, possiamo riscriverlo come TVF in linea.
Facciamolo
Se non è ovvio, sto per riscrivere il codice fornito da un fornitore di software. Non ho mai incontrato un fornitore di software che consideri questo comportamento "supportato". Se modifichi il codice dell'applicazione pronto all'uso, sei da solo. Microsoft considera certamente questo comportamento "non supportato" per Dynamics. Lo farò comunque, dal momento che sto usando l'ambiente di test e non sto giocando in produzione. La riscrittura di questa funzione ha richiesto solo un paio di minuti, quindi perché non provarla e vedere cosa succede? Ecco come appare la mia versione della funzione:
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
Sono tornato alla mia query di test originale, ho scaricato la cache e l'ho eseguito nuovamente alcune volte. Ecco il più lento runtime, quando si utilizza la mia versione di TVF:
Sembra molto meglio!
Non è ancora la query più efficiente al mondo, ma è abbastanza veloce, non è necessario che sia più veloce. Tranne... ho dovuto modificare il codice di Microsoft per farlo accadere. Non è l'ideale. Diamo un'occhiata al piano completo con il nuovo TVF:
Addio apatosauro, ciao dispenser PEZ!
È ancora un piano davvero nodoso, ma se guardi all'inizio, tutte quelle chiamate TVF black box sono sparite. L'hash match super costoso è sparito. SQL Server si mette subito al lavoro senza il grosso collo di bottiglia delle chiamate TVF (il lavoro dietro la TVF è ora in linea con il resto di SELECT
):
Grande impatto sull'immagine
Dove viene effettivamente utilizzato questo TVF? Quasi ogni singola visualizzazione filtrata in Dynamics CRM utilizza questa chiamata di funzione. Ci sono 246 viste filtrate e 206 di esse fanno riferimento a questa funzione. È una funzione fondamentale nell'ambito dell'implementazione della sicurezza a livello di riga di Dynamics. Praticamente ogni singola query dall'applicazione ai database chiama questa funzione almeno una volta, di solito alcune volte. Questa è una medaglia a due facce:da un lato, correggere questa funzione agirà probabilmente come un turbo boost per l'intera applicazione; d'altra parte, non c'è modo per me di eseguire test di regressione per tutto ciò che tocca questa funzione.
Aspetta un secondo:se questa chiamata di funzione è così fondamentale per le nostre prestazioni e così fondamentale per Dynamics CRM, ne consegue che tutti coloro che utilizzano Dynamics stanno colpendo questo collo di bottiglia delle prestazioni. Abbiamo aperto un caso con Microsoft e ho chiamato alcune persone per far passare il biglietto al team di ingegneri responsabile di questo codice. Con un po' di fortuna, questa versione aggiornata della funzione sarà inclusa nella confezione (e nel cloud) in una versione futura di Dynamics CRM.
Questa non è l'unica TVF con più istruzioni in Dynamics CRM:ho apportato lo stesso tipo di modifica a fn_UserSharedAttributesAccess
per un altro problema di prestazioni. E ci sono più TVF che non ho toccato perché non hanno causato problemi.
Una lezione per tutti, anche se non utilizzi Dynamics
Ripeti dopo di me:LE FUNZIONI CON VALORE DELLA TABELLA MULTI-DICHIARAZIONE SONO MALE!
Rifattorizzare il codice per evitare di utilizzare TVF con più istruzioni. Se stai cercando di sintonizzare il codice e vedi un TVF con più istruzioni, guardalo in modo critico. Non puoi sempre cambiare il codice (o potrebbe essere una violazione del tuo contratto di supporto se lo fai), ma se puoi cambiare il codice, fallo. Di' al tuo fornitore di software di smettere di usare TVF con più dichiarazioni. Rendi il mondo un posto migliore eliminando alcune di queste brutte funzioni dal tuo database.