Sql server : requêtes sql texte (adhoc) contre procédures stockées

Les procédures stockées

Les procédures stockées sont ce qu’on appelle un « batch » nommé, c’est ni plus ni moins qu’un ensemble d’instruction SQL (Transact-SQL dans le cas de SQL Server) qui est stocké sur le serveur et qui porte un nom et peut avoir un ou plusieurs arguments en entrée et en sortie.

Les avantages des procédures stockées.

Réduction du trafic réseau en entrée du serveur SQL

En effet on ne passe pas la requête, mais uniquement son nom et ses paramètres. Pour un grand ou très grand nombre de requêtes exécutées cela est très significatif.

Prenez un lot SQL de 20ko qui sera transmis en intégralité au serveur par rapport au nom de la procédure stockée qui fera au plus 128 caractères. En réalité ce sera le nombre de paquets réseau qui changera, ceux-ci étant de 4ko par défaut le changement sera visible uniquement pour de très grosses requêtes (plus de la taille du paquet réseau).

Sécurité accrue

Le fait de passer par une requête déjà stockées sur le serveur, dont seuls les paramètres sont passés par le réseau réduit les risques de SQL Injection, augmente la confidentialité (étant donné que le code SQL n’est plus passé en clair), améliore la sécurité d’accès aux données si l’on impose l’accès aux données uniquement par des procédures stockées. De plus il est possible de définir un contexte de sécurité propre à la procédure.

Il est possible par exemple de masquer la définition de la procédure stockée sur le serveur :

CREATE PROCEDURE MaProc @param int
WITH ENCRYPTION
AS
SELECT FirstName ,
LastName
FROM Person . Contact
WHERE ContactID > @param
GO

Sous SQL Server 2005 vous avez la possibilité de choisir le contexte d’exécution de l’ensemble de la procédure stockée, permettant ainsi d’accorder un privilège plus élevé pour l’exécution de celle-ci (par exemple autoriser la création de table, alors que l’utilisateur ne peut faire que des lectures / écritures de données).

CREATE PROCEDURE MaProc @param int
WITH EXECUTE AS 'toto'
AS
SELECT FirstName ,
LastName
FROM Person . Contact
WHERE ContactID > @param
GO

Et nativement la notion de « chaîne de propriété » vous permet si le propriétaire d’une table et d’une procédure stockée est le même d’interdire totalement l’accès à la table, mais d’autoriser l’exécution de la procédure stockée.

CREATE PROCEDURE MaProc @param int
AS
SELECT FirstName ,
LastName
FROM Person . Contact
WHERE ContactID > @param
GO

-- Interdit tout accès aux données de la table à tous
DENY SELECT , INSERT , UPDATE , DELETE ON Person . Contact TO public

-- Autorise l'execution de la procédure stockée à tous
GRANT EXEC ON MaProc TO public

EXEC MaProc 1

Dans cet exemple tout accès aux données est interdit directement, mais l’exécution de la procédure stockée est autorisée. Le propriétaire étant le même (dbo ici étant donné que j’étais logué en tant que sysadmin du serveur) il y a une relation de confiance entre les objets et l’exécution de la procédure stockée fonctionnera.

Réutilisation du plan d’exécution mis en cache

Là est le sujet le plus sensible et le plus controversé, l’intérêt des procédures stockées est de pouvoir réutiliser plus fréquemment le plan d’exécution. La re-génération de celui-ci étant consommateur de CPU, particulièrement sur les systèmes exécutant les requêtes en très grand nombre.

Prenez l’exécution d’une requête qui consomme 500 ms de CPU à l’exécution et 15 ms à la compilation et qui est exécutée 100 000 fois dans une journée, la consommation de CPU est d’environ 25 min par jour. La réutilisation du plan permettra d’économiser ce temps et de le consacrer à une autre tâche consommatrice de CPU (un tri par exemple).

Facilité de maintenance

Le fait de passer par des procédures stockées permet de gérer beaucoup plus facilement les dépendances entre les objets de la base de données. Toutes les requêtes étant soit des vues ou des procédures stockées, tout code SQL susceptible d’être exécuté sur le serveur y est déjà, rendant plus simple la maintenance du schéma et les vérification des dépendances.

On peut très facilement directement sur le serveur obtenir la liste des objets dépendant de la procédure stockée que l’on a écrite.

EXEC sp_depends 'MaProc'

Par contre il n’y a pas de dépendance forte entre un objet et une procédure stockée qui l’utilise. Rien ne vous interdit de créer une procédure stockées utilisant un objet d’existant pas encore ou de supprimer un objet actuellement utilisé dans une procédure stockée. Il n’y a pas actuellement de moyen de lier les 2 contrairement aux vues (option SCHEMABINDING).

Les inconvénients des procédures stockées

Passage d’arguments multiples et variables

Le passage d’un nombre de variable d’argument à la procédure stockée est complexe. En effet le fait de ne pas passer un argument à une procédure stockée impose que celui-ci ai une valeur par défaut. Dans l’exemple qui suit la première SP échoue et la seconde réussit puisqu’elle une valeur par défaut (NULL) sur son argument.

CREATE PROCEDURE MaProc5 @arg int , @arg2 int
AS
    WAITFORDELAY'00:00:00.500'
GO

EXEC MaProc5 1
GO

CREATEPROCEDURE MaProc6 @arg int, @arg2 int=NULL
AS
    WAITFORDELAY'00:00:00.500'
GO

EXEC MaProc6 1
GO

De plus tous les types sont NULLable sous SQL Server il faudra donc vérifier les arguments au tout début de la procédure stockée.

Passage de table

Le passage de table n’est pas possible directement dans une procédure stockée, il faut alors passer par une voie détournée. Soit on utilise une table temporaire dans laquelle on met les données qui seront utilisées par la procédure, soit des arguments de type texte (on passe alors une liste séparée par des virgules), soit de type xml pour SQL Server 2005 (et on passe par les fonctions XML du moteur).

CREATE TABLE #x ( liste int )
GO

CREATE PROCEDURE MaProc3
AS
SELECT *
FROM Person . Contact
WHERE ContactID IN ( SELECT liste
FROM #x )
GO

INSERT INTO #x ( liste ) VALUES ( 3 )
INSERT INTO #x ( liste ) VALUES ( 10 )
INSERT INTO #x ( liste ) VALUES ( 27 )

EXEC MaProc3

Ou alors en passant un argument de type texte contenant une liste de valeurs séparés par des virgules. Sous SQL Server 2005 les arguments de type XML peuvent être passé.

CREATE PROCEDURE MaProc4 @arg xml
AS
SELECT *
FROM Person . Contact
WHERE ContactID IN ( SELECT c . value ( '.' , 'int' )
FROM @arg . nodes ( '/x' ) AS T ( c ) )
GO

EXEC MaProc4 N '<x>1</x><x>7</x><x>10</x>'

Ici on a une liste passé sous forme de chaîne XML, on pourrait imaginer un contenu XML plus complexe sans problème. Le type xml de SQL Server 2005 peut contenir jusqu’à 2Go de données.

Métadonnées et valeurs / tables renvoyés

Une procédure stockée étant un lot de requêtes SQL, elle peut renvoyer un ou plusieurs jeux d’enregistrements, contrairement aux vues vous n’aurez pas la structure des données renvoyées ni le nombre de jeux d’enregistrement renvoyé.

Attention aussi aux paramètres de type OUTPUT et à la valeur de retour. La valeur de retour de la procédure stockée, n’est qu’un int elle ne servira donc généralement qu’à renvoyer un code d’erreur (0 si réussi). Les paramètres de type OUTPUT serviront de préférence à renvoyer les valeurs d’identity générées, des GUID générés et des nombres d’enregistrements affectés par les requêtes. Les données elles sont renvoyées par les jeux de données.

Possibilité de plan d’exécution faussé

Le plan d’exécution est généré à la première exécution de la procédure stockée (à partir de SQL Server 2000) avec les paramètres passés à ce moment là. Or si le plan d’exécution d’une requête varie fortement d’une exécution à l’autre à cause justement des valeurs de paramètres, ceci n’est pas reflété dans le cas d’une procédure stockée, on se contente de reprendre le plan du cache même si celui-ci n’est pas optimal. Voir plus loin dans la section traitant du plan et de sa mise en cache.

« Parameter Sniffing » ou non

Le moteur de base de données compile la procédure stockée à sa première exécution. Lors de cette phase le parameter sniffing consiste à remplacer tous les paramètres par la valeur qui est passé à la procédure stockée. Elle est alors compilée avec ces valeurs.

Quand par exemple vous passer par une variable ou que vous manipulé le paramètre à l’aide d’une fonction la valeur ne peut être détectée par le parameter sniffing le moteur chois donc de compiler par rapport aux données présentes dans la table et non par rapport à la valeur du paramètre.

-- Utilisation du Parameter Sniffing
-- Le paramètre est passé directement à la requête
CREATE PROCEDURE MaProc @param int
AS
SELECT FirstName ,
LastName
FROM Person . Contact
WHERE ContactID > @param
GO

-- Le Parameter Sniffing n'est pas utilisé
-- Le paramètre est passé dans une variable puis par la requête
CREATE PROCEDURE MaProc @param int
AS
DECLARE @var int

SET @var = @param

SELECT FirstName ,
LastName
FROM Person . Contact
WHERE ContactID > @var
GO

Dans la première procédure stockée le lien entre le paramètre et la requête est évident pour le moteur. Le plan est donc généré pour la procédure stockée à sa première exécution avec cette valeur. Dans la seconde le lien entre le paramètre et la requête est perdu, car l’on passe par une variable. Ce comportement est voulu, car dans certains cas on souhaite compiler la requête sans tenir compte de la valeur du paramètre à la première exécution. C’est exactement ce qu’il se passe dans cette situation.

Notez que le comportement de la seconde se produirait aussi si vous passez par une fonction directement sur le paramètre de la procédure stockée.

WHERE ContactID > NULLIF ( @param , 0 )

Il faut faire très attention dans l’écriture de la procédure stockée à ce genre de chose, car en fonctions des requêtes et des paramètres les performances de celles-ci risquent de varier énormément, c’est le cas de mon exemple. A l’opposé s’il y avait une égalité dans la requête au lieu du > le comportement serait exactement similaire dans les 2 cas, le champ étant clef (PK) de la table, le plan est alors invariable (ne dépend pas de la valeur du paramètre).

Les requêtes textuelles

Les requêtes textuelles sont envoyées à chaque fois au serveur. L’avantage principal est la manipulation possible de la requête avant son exécution sur le serveur. C’est ce qui en fait aussi son principal inconvénient celui de la sécurité car du code SQL ainsi manipulé peut vite de révéler dangereux pour cause de SQL Injection.

L’autre grosse problématique des requêtes textes est la mise en cache de leur plan. Contrairement à ce que l’on pense celui-ci est bien mis en cache, c’est la réutilisation du plan qui pose problème. Celle-ci n’est pas systématique et dépend du type de requête et du mode dans lequel est la base de données sur SQL Server 2005. Voir plus bas le détail sur la question.

Sinon les avantages / inconvénients sont en quelque sorte les inverses de ceux évoqués pour les procédures stockées.

Comment exécuter une requête textuelle ?

Au niveau du code net plusieurs solutions sont à votre disposition pour l’exécution d’une requête, j’en distinguerais 3 :

SqlCommand sans sqlParameter

La première consiste à concaténer une chaîne de caractères et à la passer directement au moteur de base de données.
SqlCommand cmd new SqlCommand ( "SELECT FirstName, LastName FROM Person.Contact WHERE ContactID " + i.ToString(),cnx);

Cette méthode présente beaucoup de désavantages, elle est tout d’abord très sensible à l’injection SQL, le plan de la requête est réutilisé seulement si rien ne change dans le texte de la requête. La requête exécutée sur le moteur est exactement celle que vous avez spécifiée.

SELECT FirstName , LastName FROM Person . Contact WHERE ContactID = 1

Au niveau du serveur de base de données la requête est interprétée comme un lot SQL simple. Dans certains cas le moteur est capable d’appliquer une paramétrisation simple et de conserver le plan pour ce type de requête même si la valeur passée change. La requête doit être la plus simple possible dans ce cas.

SQL Server 2005 introduit un mode appelé paramétrisation forcé (appliquez si possible le Service Pack 2 pour utiliser cette fonctionnalité).

ALTER DATABASE [AdventureWorks * SET PARAMETERIZATION FORCED

A partir de ce moment là le moteur transforme toutes les valeurs dans les requêtes par des paramètres et réutilise le plan quelque soit le paramètre qui lui est passé. Cela simule le fonctionnement d’une procédure stockée pour toutes les requêtes texte du serveur.

SqlCommand avec sqlParameter

La seconde méthode consiste à créer un SqlCommand avec des paramètres spécifiés dans la requête. SqlParameter nous aide à fournir la liste des paramètres de celle-ci avec ses valeurs.
SqlCommand cmd new SqlCommand ( "SELECT FirstName, LastName FROM Person.Contact WHERE ContactID @ContactId" , cnx);

Sql Parameter param = new Sql Parameter ( "@ContactId" , Sql DbType .Int, 4);

param.Value = i.ToString();

cmd.Parameters.Add(param) ;

Au niveau du serveur de base de données on constate que l’exécution directe de la requête est remplacée par l’appel de sp_executesql et la liste des paramètres est fournie. La méthode permet de diminuer le risque d’injection SQL, et permet la réutilisation du plan mis en cache lorsque la valeur passée en paramètre change.
exec sp_executesql N 'SELECT FirstName, LastName FROM Person.Contact WHERE ContactID @ContactId' , N '@ContactId int' , @ContactId 1

Là aussi le moindre ajout d’espace dans la requête provoque la non réutilisation du plan d’exécution de la requête.

SqlCommand avec SqlParameter et exécution de la méthode Prepare

Dernière méthode, on appelle en plus la méthode Prepare() elle permet une vérification plus stricte des paramètres passés à la requête et une réutilisation plus aisé de celle-ci au niveau de l’exécution. Un handle est généré à la première exécution de la requête, il y a juste à reprendre le handle et éventuellement changer la valeur des paramètres pour les exécutions suivantes. Très pratique pour une requête exécutée à plusieurs reprises.
SqlCommand cmd new SqlCommand ( "SELECT FirstName, LastName FROM Person.Contact WHERE ContactID @ContactId" , cnx);

Sql Parameter param = new Sql Parameter ( "@ContactId" , Sql DbType .Int, 4);

param.Value = i.ToString();

cmd.Parameters.Add(param);

cnx.Open();

cmd.Prepare();

On demande la préparation de la requête la première fois au niveau serveur par sp_prepexec.

declare @p1 int
set @p1 = NULLexec sp_prepexec @p1 output , N '@ContactId int' , N 'SELECT FirstName, LastName FROM Person.Contact WHERE ContactID @ContactId' , @ContactId 1
select @p1

Les fois suivantes seul le handle est à fournir (ici 1).

exec sp_execute 1 , @ContactId = 2

Au niveau de la réutilisation du plan et de l’injection SQL c’est exactement la même chose que sans le prepare. La méthode Prepare() peut être appelée dans le cas de procédures stockées ne fait rien dans ce cas.

Mise en cache du plan d’exécution, réutilisation et compilation

Les plans…

Qu’est ce qu’un plan ?

SQL Server lorsqu’il exécute une requête dispose de différente tâche pour le faire. Il a pour certaines opérations plusieurs choix. Par exemple les jointures peuvent être MERGE, HASH ou LOOP. Ces choix dépendant des données, des index et d’autres facteurs.

Pour chaque requête le serveur a donc plusieurs manières de produire le jeu de résultat et il s’efforce de trouver celle qui aura le plus bas coût (le moins d’impact sur les ressources du serveur et un temps d’exécution faible). Cette phase peut s’assimiler à un algorithme de résolution de jeu. Toutes les solutions ne peuvent être explorées donc on va au plus probable et au plus efficace. Plus le temps estimé est important pour la requête plus la phase de compilation peut être longue.

Une fois cette phase passé on a donc notre « compilation plan » qui est la manière dont le moteur va exécuter cette requête. L’ « execution plan » lui est la déclinaison du « compilation plan » pour chaque exécution simultanée de la même requête, il inclut en plus les paramètres de celle-ci.

Comment est exécutée une requête ?

Qu’elle soit procédure stockée ou non, l’exécution passe toujours par ces 5 phases :

  • Vérification syntaxique
  • Conversion en arbre « Parse Tree »
  • Compilation Génération du « Compiled Plan »
  • Exécution Génération du « Execution Plan »
  • Envoie du jeu de résultats au client

Les phases 3 et 4 peuvent être évitées si un plan est déjà présent dans la cache est trouvé. Pour la phase 2 seule les vues ont leur « Parse Tree » mis en cache et donc réutilisé si nécessaire si celui-ci est en cache. La phase 3 est la plus consommatrice de CPU, il est donc essentiel lorsque que beaucoup de requêtes sont exécutées sur le serveur d’essayer de diminuer l’impact de ces compilations.

… et le cache

Quand est ce que les plans sont réutilisés ?

Il faut déjà que la requête ait été exécutée une fois que la mémoire disponible soit suffisante pour que le système n’est pas exclus le plan de la requête du cache. Plus la requête est exécutée plus il y a de chance pour que le plan reste en cache.

Dans quels cas le plan en cache n’est pas réutilisé ?

Il y a des cas où le plan d’exécution en cache ne peut être réutilisé, dans ce cas un nouveau plan est généré. Ces situations, sans être exhaustives sont les suivantes :

  • Modification de paramètres de connexion

Essayez si possible de conserver les mêmes options SET (SET ANSI_NULLS, …) et si possible les mêmes paramètres de chaine de connexion.

  • Requête avec noms de schéma non précisé et utilisateur différent

Mettez toujours le nom du schéma devant un objet

  • L’ajout d’un simple espace à la requête dans le cas d’une requête texte

La moindre modification de la chaîne de caractère de la requête génèrera un nouveau plan

  • Une requête texte de plus de 8ko
  • Changement des structures de tables utilisées
  • Modifications des statistiques des données (faites automatiquement périodiquement en fonction des modifications des données des champs)

Consulter le contenu du cache

Pour savoir si le plan d’une requête est présent dans la cache vous avez à votre disposition sur SQL Server 2005 une DMV (Dynamic Management View) : sys.dm_exec_cached_plans. Cette vue permet de lister tous les plans actuellement présent dans le cache du serveur, ainsi que leur réutilisation éventuelle ce qui vous permettra de savoir si son plan est bien réutilisé.

Cette vue dynamique peut être utilisée conjointement avec :

  • sys.dm_exec_sql_text
  • sys.dm_exec_query_plan
  • sys.dm_exec_plan_attribute

Qui sont des fonctions, permettant d’obtenir le texte de la requête, le détail du plan, etc.

SELECT *
FROM sys.dm_exec_cached_plans
CROSS APPLY sys.dm_exec_sql_text ( plan_handle )

usecounts permet de savoir le nombre de fois que le plan a été ré-utilisé, objtype permet de savoir si vous avez à faire à une procédure, requête, etc.

Compilation / Recompilation

Un point important à prendre en compte pour les procédures stockées est la notion de recompilation. La procédure stockée est compilé quand elle est exécutée la première fois où si un plan existant n’est pas trouvé dans le cache. Or quel que soient les paramètres passés à la procédure stockée le plan ne changera pas. Cela est le comportement souhaité la plupart du temps, mais dans certains il faut recompiler la procédure pour obtenir un nouveau plan.

A noter que cette problématique s’applique aussi a cas des SqlCommand avec SqlParameter de la même manière. Le plan est mis en cache et réutilisé… Le risque de plan faussé dans cette situation est tout aussi réel.

Prenez les 2 requêtes suivantes, elles sont identiques au paramètre près de celle-ci. Elles ont un plan d’exécution différent du fait qu’elle ne traite pas la même quantité de données et n’utilisent donc pas l’index de « AddressEmail » de la même manière.

SELECT FirstName , LastName , EmailAddress FROM Person . Contact WHERE EmailAddress LIKE 'jay1@adventure-works.com'

SELECT FirstName , LastName , EmailAddress FROM Person . Contact WHERE EmailAddress LIKE 'j%'

Dans le cas d’une requête texte de ce type le plan est toujours correct. Prenons une procédure stockée.

CREATE PROCEDURE [dbo * . [MaProcTest] @param varchar ( 50 )

AS

SELECT FirstName , LastName , EmailAddress FROM Person . Contact WHERE EmailAddress LIKE @param

EXEC dbo.MaProcTest 'jay1@adventure-works.com'

EXEC dbo.MaProcTest 'j%'

Le deuxième requête lit 4951 pages, alors que la même requête en texte lit 569 pages ce qui est presque 10 fois plus. Le problème ici est que j’ai exécuté la première fois la procédure stockée avec un paramètre générant un plan correspondant à une recherche d’un seul enregistrement dans la table. Or la seconde fois j’utilise un argument recherchant une liste de valeurs, le plan de ma procédure est repris dans la cache et donc n’est pas le plan optimal pour ce paramètre.

Dans tous les cas du même genre il est nécessaire soit :

  • de recompiler manuellement la procédure pour supprimer le « mauvais » plan du cache
  • de créer la procédure avec WITH RECOMPILE
  • de l’exécuter avec le paramètre le plus fréquent

La première solution est à faire au cas par cas, j’exécute sp_recompile suivi du nom de la procédure stockée, ce qui va me générer un nouveau plan lors de la prochaine exécution de celle-ci. Mais si la prochaine exécution se fait avec le même paramètre qui m’a généré le « mauvais » plan mon problème reste identique.

Sous SQL Server 2005 il est possible de spécifier la valeur du ou des paramètres pour lesquels la procédure est compilée.

CREATE PROCEDURE [dbo * . [MaProcTest] @param varchar ( 50 )
AS
SELECT FirstName , LastName , EmailAddress FROM Person . Contact WHERE EmailAddress LIKE @param
OPTION ( OPTIMIZE FOR ( @param = '%' ))

On se trouve avec 569 pages lues à chaque fois dans cette situation. Le plan mis en cache correspond à celui qui aurait été généré avec

EXEC dbo.MaProcTest ' %'

Dernière possibilité faire fonctionner la procédure stockée comme une requête simple, qui se recompilera à chaque exécution. Le nombre de pages lues ici est strictement similaire à une requête texte.

CREATE PROCEDURE [dbo * . [MaProcTest] @param varchar ( 50 )
WITH RECOMPILE
AS
SELECT FirstName , LastName , EmailAddress FROM Person . Contact WHERE EmailAddress LIKE @param

Ici le plan sera le plan idéal que le moteur aura trouvé pour chaque cas, mais le serveur consommera le temps CPU nécessaire à la compilation à chaque exécution de la procédure.

Depuis SQL Server 2005, il est possible de forcer une recompilation au niveau de la requête, si vous avez plusieurs requêtes dans une procédure vous pouvez marquer une seule requête comme étant à recompiler.

CREATE PROCEDURE [dbo * . [MaProcTest] @param varchar ( 50 )
WITH RECOMPILE
AS
BEGIN
SELECT FirstName ,
LastName ,
EmailAddress
FROM Person . Contact
WHERE EmailAddress LIKE @param
OPTION ( RECOMPILE )

SELECT FirstName ,
LastName ,
EmailAddress
FROM Person . Contact
WHERE EmailAddress LIKE @param
END

La première requête est recompilée à chaque exécution, la seconde a son plan conservé en cache et réutilisé.

Conclusion

Dès lors que vous utilisez des requêtes paramétrées en ado.net le comportement est très similaire à celui des procédures stockées. Le plan est mis en cache dans les 2 cas, et le plan peut se trouver faussé dans les 2 cas. La différence est plus notable entre une requête texte non paramétrée et une procédure stockée, même si là aussi SQL Server 2005 apporte le mode forcé permettant d’avoir un comportement identique entre les 2 modes de requêtes.

Je pense que le point essentiel et l’avantage essentiel des procédures stockées est la latitude laissé aux DBA de paramétrer et gérer les requêtes (la mise en cache ou non, la recompilation de celle-ci, etc.). Les modifications de schémas d’en trouvent facilité et la sécurité améliorée elles aussi.

Pour ma part le choix est fait, les procédures stockées offrent des performances au moins égales à celles des requêtes textes, et m’offrent une marge de manœuvre très confortable au niveau du serveur de base de données.

Après votre choix est à faire j’espère que ce document vous aidera à prendre votre décision et à faire le bon choix : le votre !

Adresse d'origine

A voir également
Ce document intitulé « Sql server : requêtes sql texte (adhoc) contre procédures stockées » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.
Rejoignez-nous