Sqlserver
 sql >> Database >  >> RDS >> Sqlserver

Esecuzione SQL dinamica in SQL Server

Dynamic SQL è un'istruzione costruita ed eseguita in fase di esecuzione, che di solito contiene parti di stringhe SQL generate dinamicamente, parametri di input o entrambi.

Sono disponibili vari metodi per costruire ed eseguire comandi SQL generati dinamicamente. Il presente articolo li esplorerà, definirà i loro aspetti positivi e negativi e dimostrerà approcci pratici per ottimizzare le query in alcuni scenari frequenti.

Usiamo due modi per eseguire SQL dinamico:EXEC comando e sp_executesql procedura memorizzata.

Utilizzo del comando EXEC/EXECUTE

Per il primo esempio, creiamo una semplice istruzione SQL dinamica da AdventureWorks Banca dati. L'esempio ha un filtro che viene passato attraverso la variabile stringa concatenata @AddressPart ed eseguito nell'ultimo comando:

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)

Si noti che le query create dalla concatenazione di stringhe possono fornire vulnerabilità di SQL injection. Consiglio vivamente di familiarizzare con questo argomento. Se prevedi di utilizzare questo tipo di architettura di sviluppo, specialmente in un'applicazione Web rivolta al pubblico, sarà più che utile.

Successivamente, dovremmo gestire i valori NULL nelle concatenazioni di stringhe . Ad esempio, la variabile di istanza @AddressPart dell'esempio precedente potrebbe invalidare l'intera istruzione SQL se passato questo valore.

Il modo più semplice per gestire questo potenziale problema è utilizzare la funzione ISNULL per costruire un'istruzione SQL valida :

SET @SQLExec = 'SELECT * FROM Person.Address WHERE AddressLine1 LIKE ''%' + ISNULL(@AddressPart, ‘ ‘) + '%'''


Importante! Il comando EXEC non è progettato per riutilizzare i piani di esecuzione memorizzati nella cache! Ne creerà uno nuovo per ogni esecuzione.

Per dimostrarlo, eseguiremo la stessa query due volte, ma con un valore diverso del parametro di input. Quindi, confrontiamo i piani di esecuzione in entrambi i casi:

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 *%';

Utilizzo della procedura estesa sp_executesql

Per utilizzare questa procedura, è necessario darle un'istruzione SQL, la definizione dei parametri utilizzati in essa e i loro valori. La sintassi è la seguente:

sp_executesql @SQLStatement, N'@ParamNameDataType' , @Parameter1 = 'Value1'

Iniziamo con un semplice esempio che mostra come passare un'istruzione e parametri:

EXECUTE sp_executesql  
               N'SELECT *  
                     FROM Person.Address
	       WHERE AddressLine1 LIKE ''%'' + @AddressPart + ''%''', -- SQL Statement
              N'@AddressPart NVARCHAR(50)',  -- Parameter definition
             @AddressPart = 'a';  -- Parameter value

A differenza del comando EXEC, sp_executesql la stored procedure estesa riutilizza i piani di esecuzione se eseguiti con la stessa istruzione ma parametri diversi. Pertanto, è meglio usare sp_executesql su EXEC comando :

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 dinamico nelle stored procedure

Finora abbiamo utilizzato l'SQL dinamico negli script. Tuttavia, i vantaggi reali diventano evidenti quando eseguiamo questi costrutti in oggetti di programmazione personalizzati:procedure memorizzate dall'utente.

Creiamo una procedura che cercherà una persona nel database AdventureWorks, in base ai diversi valori dei parametri della procedura di input. Dall'input dell'utente, costruiremo un comando SQL dinamico e lo eseguiremo per restituire il risultato all'applicazione utente chiamante:

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

Parametro OUTPUT in sp_executesql

Possiamo usare sp_executesql con il parametro OUTPUT per salvare il valore restituito dall'istruzione SELECT. Come mostrato nell'esempio seguente, fornisce il numero di righe restituite dalla query alla variabile di output @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

Protezione contro SQL injection con procedura sp_executesql

Ci sono due semplici attività che dovresti fare per ridurre significativamente il rischio di SQL injection. Innanzitutto, racchiudi i nomi delle tabelle tra parentesi. In secondo luogo, controlla nel codice se esistono tabelle nel database. Entrambi questi metodi sono presenti nell'esempio seguente.

Stiamo creando una semplice stored procedure e la stiamo eseguendo con parametri validi e non validi:

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'

Confronto delle funzionalità del comando EXEC e della stored procedure sp_executesql

Comando EXEC Procedura memorizzata sp_executesql
Nessun riutilizzo del piano cache Riutilizzo del piano cache
Molto vulnerabile all'iniezione SQL Molto meno vulnerabile all'iniezione SQL
Nessuna variabile di output Supporta le variabili di output
Nessuna parametrizzazione Supporta la parametrizzazione

Conclusione

Questo post ha dimostrato due modi per implementare la funzionalità SQL dinamica in SQL Server. Abbiamo imparato perché è meglio usare sp_executesql procedura se disponibile. Inoltre, abbiamo chiarito la specificità dell'utilizzo del comando EXEC e le richieste di disinfettare gli input degli utenti per prevenire l'iniezione di SQL.

Per il debug accurato e comodo delle stored procedure in SQL Server Management Studio v18 (e versioni successive), puoi utilizzare la funzionalità T-SQL Debugger specializzata, una parte della popolare soluzione dbForge SQL Complete.