Le SQL dynamique est une instruction construite et exécutée au moment de l'exécution, contenant généralement des parties de chaîne SQL générées dynamiquement, des paramètres d'entrée ou les deux.
Diverses méthodes sont disponibles pour construire et exécuter des commandes SQL générées dynamiquement. Le présent article va les explorer, définir leurs aspects positifs et négatifs et démontrer des approches pratiques pour optimiser les requêtes dans certains scénarios fréquents.
Nous utilisons deux manières d'exécuter du SQL dynamique :EXEC commande et sp_executesql procédure stockée.
Utilisation de la commande EXEC/EXECUTE
Pour le premier exemple, nous créons une instruction SQL dynamique simple à partir de AdventureWorks base de données. L'exemple a un filtre qui est passé par la variable de chaîne concaténée @AddressPart et exécuté dans la dernière commande :
USE AdventureWorks2019
-- Declare variable to hold generated SQL statement
DECLARE @SQLExec NVARCHAR(4000)
DECLARE @AddressPart NVARCHAR(50) = 'a'
-- Build dynamic SQL
SET @SQLExec = 'SELECT * FROM Person.Address WHERE AddressLine1 LIKE ''%' + @AddressPart + '%'''
-- Execute dynamic SQL
EXEC (@SQLExec)
Notez que les requêtes construites par concaténation de chaînes peuvent donner des vulnérabilités d'injection SQL. Je vous conseille vivement de vous familiariser avec ce sujet. Si vous envisagez d'utiliser ce type d'architecture de développement, en particulier dans une application Web destinée au public, ce sera plus qu'utile.
Ensuite, nous devons gérer les valeurs NULL dans les concaténations de chaînes . Par exemple, la variable d'instance @AddressPart de l'exemple précédent pourrait invalider l'intégralité de l'instruction SQL si cette valeur était transmise.
Le moyen le plus simple de gérer ce problème potentiel est d'utiliser la fonction ISNULL pour construire une instruction SQL valide :
SET @SQLExec = 'SELECT * FROM Person.Address WHERE AddressLine1 LIKE ''%' + ISNULL(@AddressPart, ‘ ‘) + '%'''
Important! La commande EXEC n'est pas conçue pour réutiliser les plans d'exécution mis en cache ! Il en créera un nouveau pour chaque exécution.
Pour le démontrer, nous allons exécuter la même requête deux fois, mais avec une valeur différente du paramètre d'entrée. Ensuite, nous comparons les plans d'exécution dans les deux cas :
USE AdventureWorks2019
-- Case 1
DECLARE @SQLExec NVARCHAR(4000)
DECLARE @AddressPart NVARCHAR(50) = 'a'
SET @SQLExec = 'SELECT * FROM Person.Address WHERE AddressLine1 LIKE ''%' + @AddressPart + '%'''
EXEC (@SQLExec)
-- Case 2
SET @AddressPart = 'b'
SET @SQLExec = 'SELECT * FROM Person.Address WHERE AddressLine1 LIKE ''%' + @AddressPart + '%'''
EXEC (@SQLExec)
-- Compare plans
SELECT chdpln.objtype
, chdpln.cacheobjtype
, chdpln.usecounts
, sqltxt.text
FROM sys.dm_exec_cached_plans as chdpln
CROSS APPLY sys.dm_exec_sql_text(chdpln.plan_handle) as sqltxt
WHERE sqltxt.text LIKE 'SELECT *%';
Utilisation de la procédure étendue sp_executesql
Pour utiliser cette procédure, nous devons lui donner une instruction SQL, la définition des paramètres utilisés et leurs valeurs. La syntaxe est la suivante :
sp_executesql @SQLStatement, N'@ParamNameDataType' , @Parameter1 = 'Value1'
Commençons par un exemple simple qui montre comment passer une instruction et des paramètres :
EXECUTE sp_executesql
N'SELECT *
FROM Person.Address
WHERE AddressLine1 LIKE ''%'' + @AddressPart + ''%''', -- SQL Statement
N'@AddressPart NVARCHAR(50)', -- Parameter definition
@AddressPart = 'a'; -- Parameter value
Contrairement à la commande EXEC, le sp_executesql la procédure stockée étendue réutilise les plans d'exécution s'ils sont exécutés avec la même instruction mais avec des paramètres différents. Par conséquent, il est préférable d'utiliser sp_executesql sur EXEC commande :
EXECUTE sp_executesql
N'SELECT *
FROM Person.Address
WHERE AddressLine1 LIKE ''%'' + @AddressPart + ''%''', -- SQL Statement
N'@AddressPart NVARCHAR(50)', -- Parameter definition
@AddressPart = 'a'; -- Parameter value
EXECUTE sp_executesql
N'SELECT *
FROM Person.Address
WHERE AddressLine1 LIKE ''%'' + @AddressPart + ''%''', -- SQL Statement
N'@AddressPart NVARCHAR(50)', -- Parameter definition
@AddressPart = 'b'; -- Parameter value
SELECT chdpln.objtype
, chdpln.cacheobjtype
, chdpln.usecounts
, sqltxt.text
FROM sys.dm_exec_cached_plans as chdpln
CROSS APPLY sys.dm_exec_sql_text(chdpln.plan_handle) as sqltxt
WHERE sqltxt.text LIKE '%Person.Address%';
SQL dynamique dans les procédures stockées
Jusqu'à présent, nous utilisions le SQL dynamique dans les scripts. Cependant, les avantages réels deviennent apparents lorsque nous exécutons ces constructions dans des objets de programmation personnalisés - des procédures stockées par l'utilisateur.
Créons une procédure qui recherchera une personne dans la base de données AdventureWorks, en fonction des différentes valeurs des paramètres de la procédure d'entrée. À partir de l'entrée utilisateur, nous allons construire une commande SQL dynamique et l'exécuter pour renvoyer le résultat à l'application utilisateur appelante :
CREATE OR ALTER PROCEDURE [dbo].[test_dynSQL]
(
@FirstName NVARCHAR(100) = NULL
,@MiddleName NVARCHAR(100) = NULL
,@LastName NVARCHAR(100) = NULL
)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @SQLExec NVARCHAR(MAX)
DECLARE @Parameters NVARCHAR(500)
SET @Parameters = '@FirstName NVARCHAR(100),
@MiddleName NVARCHAR(100),
@LastName NVARCHAR(100)
'
SET @SQLExec = 'SELECT *
FROM Person.Person
WHERE 1 = 1
'
IF @FirstName IS NOT NULL AND LEN(@FirstName) > 0
SET @SQLExec = @SQLExec + ' AND FirstName LIKE ''%'' + @FirstName + ''%'' '
IF @MiddleName IS NOT NULL AND LEN(@MiddleName) > 0
SET @SQLExec = @SQLExec + ' AND MiddleName LIKE ''%''
+ @MiddleName + ''%'' '
IF @LastName IS NOT NULL AND LEN(@LastName) > 0
SET @SQLExec = @SQLExec + ' AND LastName LIKE ''%'' + @LastName + ''%'' '
EXEC sp_Executesql @SQLExec
, @Parameters
, @[email protected], @[email protected],
@[email protected]
END
GO
EXEC [dbo].[test_dynSQL] 'Ke', NULL, NULL
Paramètre OUTPUT dans sp_executesql
Nous pouvons utiliser sp_executesql avec le paramètre OUTPUT pour enregistrer la valeur renvoyée par l'instruction SELECT. Comme le montre l'exemple ci-dessous, cela fournit le nombre de lignes renvoyées par la requête à la variable de sortie @Output :
DECLARE @Output INT
EXECUTE sp_executesql
N'SELECT @Output = COUNT(*)
FROM Person.Address
WHERE AddressLine1 LIKE ''%'' + @AddressPart + ''%''', -- SQL Statement
N'@AddressPart NVARCHAR(50), @Output INT OUT', -- Parameter definition
@AddressPart = 'a', @Output = @Output OUT; -- Parameters
SELECT @Output
Protection contre l'injection SQL avec la procédure sp_executesql
Il existe deux activités simples que vous devez effectuer pour réduire considérablement le risque d'injection SQL. Tout d'abord, placez les noms de table entre parenthèses. Deuxièmement, vérifiez dans le code si des tables existent dans la base de données. Ces deux méthodes sont présentes dans l'exemple ci-dessous.
Nous créons une procédure stockée simple et l'exécutons avec des paramètres valides et non valides :
CREATE OR ALTER PROCEDURE [dbo].[test_dynSQL]
(
@InputTableName NVARCHAR(500)
)
AS
BEGIN
DECLARE @AddressPart NVARCHAR(500)
DECLARE @Output INT
DECLARE @SQLExec NVARCHAR(1000)
IF EXISTS(SELECT 1 FROM sys.objects WHERE type = 'u' AND name = @InputTableName)
BEGIN
EXECUTE sp_executesql
N'SELECT @Output = COUNT(*)
FROM Person.Address
WHERE AddressLine1 LIKE ''%'' + @AddressPart + ''%''', -- SQL Statement
N'@AddressPart NVARCHAR(50), @Output INT OUT', -- Parameter definition
@AddressPart = 'a', @Output = @Output OUT; -- Parameters
SELECT @Output
END
ELSE
BEGIN
THROW 51000, 'Invalid table name given, possible SQL injection. Exiting procedure', 1
END
END
EXEC [dbo].[test_dynSQL] 'Person'
EXEC [dbo].[test_dynSQL] 'NoTable'
Comparaison des fonctionnalités de la commande EXEC et de la procédure stockée sp_executesql
Commande EXEC | procédure stockée sp_executesql |
Pas de réutilisation du plan de cache | Réutilisation du plan de cache |
Très vulnérable aux injections SQL | Beaucoup moins vulnérable aux injections SQL |
Aucune variable de sortie | Prend en charge les variables de sortie |
Pas de paramétrage | Prend en charge la paramétrisation |
Conclusion
Cet article a démontré deux manières d'implémenter la fonctionnalité SQL dynamique dans SQL Server. Nous avons appris pourquoi il est préférable d'utiliser le sp_executesql procédure si elle est disponible. De plus, nous avons clarifié la spécificité de l'utilisation de la commande EXEC et les demandes de nettoyage des entrées utilisateur pour empêcher l'injection SQL.
Pour un débogage précis et confortable des procédures stockées dans SQL Server Management Studio v18 (et versions ultérieures), vous pouvez utiliser la fonctionnalité spécialisée du débogueur T-SQL, qui fait partie de la solution populaire dbForge SQL Complete.