Questo è uno di quei dibattiti religiosi/politici che imperversano da anni:dovrei usare stored procedure o dovrei inserire query ad hoc nella mia applicazione? Sono sempre stato un sostenitore delle stored procedure, per alcuni motivi:
Vedo anche che molte persone stanno abbandonando le stored procedure a favore degli ORM. Per le applicazioni semplici probabilmente andrà bene, ma man mano che la tua applicazione diventa più complessa, è probabile che il tuo ORM preferito sia semplicemente incapace di eseguire determinati modelli di query, *forzandoti* a utilizzare una procedura memorizzata. Se supporta le procedure memorizzate, cioè.
Anche se trovo ancora tutti questi argomenti piuttosto convincenti, non sono ciò di cui voglio parlare oggi; Voglio parlare di prestazioni.
Molti argomenti là fuori diranno semplicemente "le stored procedure funzionano meglio!" Ciò potrebbe essere stato marginalmente vero a un certo punto, ma poiché SQL Server ha aggiunto la possibilità di compilare a livello di istruzioni anziché a livello di oggetto e ha acquisito potenti funzionalità come optimize for ad hoc workloads
, questo non è più un argomento molto forte. L'ottimizzazione dell'indice e modelli di query ragionevoli hanno un impatto molto maggiore sulle prestazioni rispetto alla scelta di utilizzare una stored procedure; nelle versioni moderne, dubito che troverai molti casi in cui la stessa identica query mostra notevoli differenze di prestazioni, a meno che tu non stia introducendo anche altre variabili (come l'esecuzione di una procedura localmente rispetto a un'applicazione in un data center diverso in un continente diverso).
Detto questo, c'è un aspetto delle prestazioni che viene spesso trascurato quando si tratta di query ad hoc:la cache del piano. Possiamo utilizzare optimize for ad hoc workloads
per evitare che i piani monouso riempiano la nostra cache (Kimberly Tripp (@KimberlyLTripp) di SQLskills.com ha alcune ottime informazioni al riguardo qui) e ciò influisce sui piani monouso indipendentemente dal fatto che le query vengano eseguite da una stored procedure oppure vengono eseguiti ad hoc. Un impatto diverso che potresti non notare, indipendentemente da questa impostazione, è quando identico i piani occupano più slot nella cache a causa delle differenze in SET
opzioni o delta minori nel testo della query effettivo. L'intero fenomeno "lento nell'applicazione, veloce negli SSMS" ha aiutato molte persone a risolvere problemi relativi a impostazioni come SET ARITHABORT
. Oggi volevo parlare delle differenze tra i testi delle query e dimostrare qualcosa che sorprende le persone ogni volta che ne parlo.
Cache da masterizzare
Diciamo che abbiamo un sistema molto semplice che esegue AdventureWorks2012. E solo per dimostrare che non aiuta, abbiamo abilitato optimize for ad hoc workloads
:
EXEC sp_configure 'show advanced options', 1; GO RECONFIGURE WITH OVERRIDE; GO EXEC sp_configure 'optimize for ad hoc workloads', 1; GO RECONFIGURE WITH OVERRIDE;
E poi libera la cache del piano:
DBCC FREEPROCCACHE;
Ora generiamo alcune semplici variazioni a una query altrimenti identica. Queste variazioni possono potenzialmente rappresentare stili di codifica per due diversi sviluppatori:lievi differenze negli spazi bianchi, maiuscole/minuscole, ecc.
SELECT TOP (1) SalesOrderID, OrderDate, SubTotal FROM Sales.SalesOrderHeader WHERE SalesOrderID >= 75120 ORDER BY OrderDate DESC; GO -- change >= 75120 to > 75119 (same logic since it's an INT) GO SELECT TOP (1) SalesOrderID, OrderDate, SubTotal FROM Sales.SalesOrderHeader WHERE SalesOrderID > 75119 ORDER BY OrderDate DESC; GO -- change the query to all lower case GO select top (1) salesorderid, orderdate, subtotal from sales.salesorderheader where salesorderid > 75119 order by orderdate desc; GO -- remove the parentheses around the argument for top GO select top 1 salesorderid, orderdate, subtotal from sales.salesorderheader where salesorderid > 75119 order by orderdate desc; GO -- add a space after top 1 GO select top 1 salesorderid, orderdate, subtotal from sales.salesorderheader where salesorderid > 75119 order by orderdate desc; GO -- remove the spaces between the commas GO select top 1 salesorderid,orderdate,subtotal from sales.salesorderheader where salesorderid > 75119 order by orderdate desc; GO
Se eseguiamo quel batch una volta e poi controlliamo la cache del piano, vediamo che abbiamo 6 copie, essenzialmente, dello stesso identico piano di esecuzione. Questo perché il testo della query è un hash binario, il che significa che maiuscole e minuscole e spazi bianchi fanno la differenza e possono rendere le query altrimenti identiche univoche per SQL Server.
SELECT [text], size_in_bytes, usecounts, cacheobjtype FROM sys.dm_exec_cached_plans AS p CROSS APPLY sys.dm_exec_sql_text(p.plan_handle) AS t WHERE LOWER(t.[text]) LIKE '%ales.sales'+'orderheader%';
Risultati:
testo | dimensione_in_byte | conteggi di utilizzo | tipocacheobj |
---|---|---|---|
seleziona il primo ID ordine di vendita,o... | 272 | 1 | Stubo del piano compilato |
seleziona il primo ID ordine di vendita, … | 272 | 1 | Stubo del piano compilato |
seleziona il primo ID ordine di vendita, o... | 272 | 1 | Stubo del piano compilato |
seleziona top (1) salesorderid,... | 272 | 1 | Stubo del piano compilato |
SELEZIONA TOP (1) SalesOrderID,... | 272 | 1 | Stubo del piano compilato |
SELEZIONA TOP (1) SalesOrderID,... | 272 | 1 | Stubo del piano compilato |
Risultati dopo la prima esecuzione di query "identiche"
Quindi, questo non è del tutto dispendioso, poiché l'impostazione ad hoc ha consentito a SQL Server di archiviare solo piccoli stub alla prima esecuzione. Se eseguiamo nuovamente il batch (senza liberare la cache delle procedure), vediamo un risultato leggermente più allarmante:
testo | dimensione_in_byte | conteggi di utilizzo | tipocacheobj |
---|---|---|---|
seleziona il primo ID ordine di vendita,o... | 49.152 | 1 | Piano compilato |
seleziona il primo ID ordine di vendita, … | 49.152 | 1 | Piano compilato |
seleziona il primo ID ordine di vendita, o... | 49.152 | 1 | Piano compilato |
seleziona top (1) salesorderid,... | 49.152 | 1 | Piano compilato |
SELEZIONA TOP (1) SalesOrderID,... | 49.152 | 1 | Piano compilato |
SELEZIONA TOP (1) SalesOrderID,... | 49.152 | 1 | Piano compilato |
Risultati dopo la seconda esecuzione di query "identiche"
La stessa cosa accade per le query parametrizzate, indipendentemente dal fatto che la parametrizzazione sia semplice o forzata. E la stessa cosa accade quando l'impostazione ad hoc non è abilitata, tranne per il fatto che accade prima.
Il risultato netto è che questo può produrre un sacco di rigonfiamento della cache del piano, anche per query che sembrano identiche, fino a due query in cui uno sviluppatore rientra con una scheda e l'altro con 4 spazi. Non devo dirti che cercare di imporre questo tipo di coerenza in una squadra può essere da noioso a impossibile. Quindi nella mia mente questo fa un forte cenno alla modularizzazione, cedendo a DRY e centralizzando questo tipo di query in un'unica stored procedure.
Un avvertimento
Ovviamente, se si inserisce questa query in una procedura memorizzata, ne avrai solo una copia, quindi eviti completamente la possibilità di avere più versioni della query con testo della query leggermente diverso. Si potrebbe anche sostenere che utenti diversi potrebbero creare la stessa stored procedure con nomi diversi e che in ciascuna stored procedure è presente una leggera variazione del testo della query. Sebbene possibile, penso che rappresenti un problema completamente diverso. :-)