Sqlserver
 sql >> Base de données >  >> RDS >> Sqlserver

Exécution SQL dynamique dans SQL Server

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.