C'est un de ces débats religieux/politiques qui font rage depuis des années :dois-je utiliser des procédures stockées, ou dois-je mettre des requêtes ad hoc dans mon application ? J'ai toujours été partisan des procédures stockées, pour plusieurs raisons :
- Je ne peux pas implémenter les protections d'injection SQL si la requête est construite dans le code de l'application. Les développeurs connaissent peut-être les requêtes paramétrées, mais rien ne les oblige à les utiliser correctement.
- Je ne peux pas régler une requête intégrée dans le code source de l'application, ni appliquer les meilleures pratiques.
- Si je trouve une opportunité de réglage des requêtes, pour la déployer, je dois recompiler et redéployer le code de l'application, au lieu de simplement modifier la procédure stockée.
- Si la requête est utilisée à plusieurs endroits dans l'application, ou dans plusieurs applications, et qu'elle nécessite une modification, je dois la modifier à plusieurs endroits, alors qu'avec une procédure stockée, je ne dois la modifier qu'une seule fois (problèmes de déploiement de côté).
Je vois aussi que beaucoup de gens abandonnent les procédures stockées au profit des ORM. Pour les applications simples, cela ira probablement bien, mais à mesure que votre application devient plus complexe, vous constaterez probablement que l'ORM de votre choix est tout simplement incapable d'exécuter certains modèles de requête, vous *forçant* à utiliser une procédure stockée. S'il prend en charge les procédures stockées, c'est-à-dire.
Bien que je trouve toujours tous ces arguments assez convaincants, ce n'est pas ce dont je veux parler aujourd'hui ; Je veux parler de performances.
Beaucoup d'arguments là-bas diront simplement, "les procédures stockées fonctionnent mieux!" Cela a peut-être été légèrement vrai à un moment donné, mais depuis que SQL Server a ajouté la possibilité de compiler au niveau de l'instruction plutôt qu'au niveau de l'objet, et a acquis des fonctionnalités puissantes telles que optimize for ad hoc workloads
, ce n'est plus un argument très fort. Le réglage d'index et les modèles de requêtes sensibles ont un impact beaucoup plus important sur les performances que le choix d'utiliser une procédure stockée; sur les versions modernes, je doute que vous trouviez de nombreux cas où la même requête présente des différences de performances notables, à moins que vous n'introduisiez également d'autres variables (comme l'exécution d'une procédure localement par rapport à une application dans un centre de données différent sur un autre continent).
Cela dit, il y a un aspect de performance qui est souvent négligé lorsqu'il s'agit de requêtes ad hoc :le cache du plan. Nous pouvons utiliser optimize for ad hoc workloads
pour empêcher les plans à usage unique de remplir notre cache (Kimberly Tripp (@KimberlyLTripp) de SQLskills.com a d'excellentes informations à ce sujet ici), et cela affecte les plans à usage unique, que les requêtes soient exécutées à partir d'une procédure stockée ou sont gérés ad hoc. Un impact différent que vous ne remarquerez peut-être pas, quel que soit ce paramètre, est lorsqu'il est identique les plans occupent plusieurs emplacements dans le cache en raison des différences dans SET
options ou deltas mineurs dans le texte réel de la requête. L'ensemble du phénomène "lent dans l'application, rapide dans SSMS" a aidé de nombreuses personnes à résoudre des problèmes impliquant des paramètres tels que SET ARITHABORT
. Aujourd'hui, je voulais parler des différences de texte de requête et démontrer quelque chose qui surprend les gens à chaque fois que j'en parle.
Cache à graver
Disons que nous avons un système très simple exécutant AdventureWorks2012. Et juste pour prouver que cela n'aide pas, nous avons activé optimize for ad hoc workloads
:
EXEC sp_configure 'afficher les options avancées', 1;GORECONFIGURE WITH OVERRIDE;GOEXEC sp_configure 'optimiser pour les charges de travail ad hoc', 1;GORECONFIGURE WITH OVERRIDE;
Et puis libérez le cache du plan :
DBCC FREEPROCCACHE ;
Maintenant, nous générons quelques variantes simples à une requête qui est par ailleurs identique. Ces variations peuvent potentiellement représenter des styles de codage pour deux développeurs différents - de légères différences dans les espaces blancs, les majuscules/minuscules, etc.
SELECT TOP (1) SalesOrderID, OrderDate, SubTotalFROM Sales.SalesOrderHeaderWHERE SalesOrderID>=75120ORDER BY OrderDate DESC;GO -- change>=75120 to> 75119 (même logique puisqu'il s'agit d'un INT)GO SELECT TOP (1) SalesOrderID, OrderDate, SubTotalFROM Sales.SalesOrderHeaderWHERE SalesOrderID> 75119ORDER BY OrderDate DESC;GO -- changez la requête en minusculesGO select top (1) salesorderid, orderdate, subtotalfrom sales.salesorderheaderwhere salesorderid> 75119order by orderdate desc;GO -- supprimez les parenthèses autour l'argument pour topGO select top 1 salesorderid, orderdate, subtotalfrom sales.salesorderheaderwhere salesorderid> 75119order by orderdate desc;GO -- ajouter un espace après top 1GO select top 1 salesorderid, orderdate, subtotalfrom sales.salesorderheaderwhere salesorderid> 75119order by orderdate desc;GO -- supprimer les espaces entre les virgulesGO select top 1 salesorderid,orderdate,subtotalfrom sales.salesorderheaderwhere salesorderid> 75119order by orderdate desc;GOSi nous exécutons ce lot une fois, puis vérifions le cache du plan, nous constatons que nous avons 6 copies, essentiellement, du même plan d'exécution. En effet, le texte de la requête est haché binaire, ce qui signifie que la casse et les espaces blancs font une différence et peuvent donner à SQL Server des requêtes par ailleurs identiques.
SELECT [text], size_in_bytes, usecounts, cacheobjtypeFROM sys.dm_exec_cached_plans AS pCROSS APPLY sys.dm_exec_sql_text(p.plan_handle) AS tWHERE LOWER(t.[text]) LIKE '%ales.sales'+'orderheader%' ;Résultats :
texte | size_in_bytes | comptes d'utilisation | cacheobjtype |
---|---|---|---|
sélectionnez le numéro de commande numéro un des ventes,o… | 272 | 1 | Stub de plan compilé |
sélectionnez le premier numéro de commande de vente, … | 272 | 1 | Stub de plan compilé |
sélectionnez l'identifiant de commande le plus important, o… | 272 | 1 | Stub de plan compilé |
sélectionnez le premier (1) ID de commande de vente,… | 272 | 1 | Stub de plan compilé |
SÉLECTIONNER LE TOP (1) SalesOrderID,… | 272 | 1 | Stub de plan compilé |
SÉLECTIONNER LE TOP (1) SalesOrderID,… | 272 | 1 | Stub de plan compilé |
Résultats après la première exécution de requêtes "identiques"
Ce n'est donc pas tout à fait inutile, car le paramètre ad hoc a permis à SQL Server de ne stocker que de petits stubs lors de la première exécution. Si nous exécutons à nouveau le lot (sans libérer le cache de la procédure), nous voyons un résultat légèrement plus alarmant :
texte | size_in_bytes | comptes d'utilisation | cacheobjtype |
---|---|---|---|
sélectionnez le numéro de commande numéro un des ventes,o… | 49 152 | 1 | Plan compilé |
sélectionnez le premier numéro de commande de vente, … | 49 152 | 1 | Plan compilé |
sélectionnez l'identifiant de commande le plus important, o… | 49 152 | 1 | Plan compilé |
sélectionnez le premier (1) ID de commande de vente,… | 49 152 | 1 | Plan compilé |
SÉLECTIONNER LE TOP (1) SalesOrderID,… | 49 152 | 1 | Plan compilé |
SÉLECTIONNER LE TOP (1) SalesOrderID,… | 49 152 | 1 | Plan compilé |
Résultats après deuxième exécution de requêtes "identiques"
La même chose se produit pour les requêtes paramétrées, que le paramétrage soit simple ou forcé. Et la même chose se produit lorsque le paramètre ad hoc n'est pas activé, sauf que cela se produit plus tôt.
Le résultat net est que cela peut produire beaucoup de gonflement du cache de plan, même pour des requêtes qui semblent identiques - jusqu'à deux requêtes où un développeur indente avec une tabulation et l'autre avec 4 espaces. Je n'ai pas à vous dire qu'essayer d'imposer ce type de cohérence au sein d'une équipe peut être fastidieux ou impossible. Donc, dans mon esprit, cela donne un clin d'œil fort à la modularisation, à céder à DRY et à centraliser ce type de requête dans une seule procédure stockée.
Une mise en garde
Bien sûr, si vous placez cette requête dans une procédure stockée, vous n'en aurez qu'une seule copie, vous évitez donc entièrement la possibilité d'avoir plusieurs versions de la requête avec un texte de requête légèrement différent. Maintenant, vous pouvez également faire valoir que différents utilisateurs peuvent créer la même procédure stockée avec des noms différents, et dans chaque procédure stockée, il existe une légère variation du texte de la requête. Bien que possible, je pense que cela représente un problème entièrement différent. :-)