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.