Dynamiczny SQL to instrukcja skonstruowana i wykonana w czasie wykonywania, zwykle zawierająca dynamicznie generowane fragmenty ciągu SQL, parametry wejściowe lub jedno i drugie.
Dostępne są różne metody tworzenia i uruchamiania dynamicznie generowanych poleceń SQL. Obecny artykuł ma na celu ich zbadanie, zdefiniowanie ich pozytywnych i negatywnych aspektów oraz zademonstrowanie praktycznych metod optymalizacji zapytań w niektórych częstych scenariuszach.
Używamy dwóch sposobów wykonywania dynamicznego SQL:EXEC polecenie i sp_executesql procedura składowana.
Korzystanie z polecenia EXEC/EXECUTE
W pierwszym przykładzie tworzymy prostą instrukcję dynamicznego SQL z AdventureWorks Baza danych. Przykład ma jeden filtr, który jest przekazywany przez połączoną zmienną ciągu @AddressPart i wykonywany w ostatnim poleceniu:
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)
Należy pamiętać, że zapytania utworzone przez łączenie ciągów mogą powodować luki w zabezpieczeniach wstrzykiwania SQL. Gorąco radzę zapoznać się z tym tematem. Jeśli planujesz używać tego rodzaju architektury programistycznej, zwłaszcza w publicznie dostępnej aplikacji internetowej, będzie to więcej niż przydatne.
Następnie powinniśmy obsługiwać wartości NULL w konkatenacjach ciągów . Na przykład zmienna instancji @AddressPart z poprzedniego przykładu może unieważnić całą instrukcję SQL, jeśli zostanie przekazana ta wartość.
Najłatwiejszym sposobem rozwiązania tego potencjalnego problemu jest użycie funkcji ISNULL do skonstruowania poprawnej instrukcji SQL :
SET @SQLExec = 'SELECT * FROM Person.Address WHERE AddressLine1 LIKE ''%' + ISNULL(@AddressPart, ‘ ‘) + '%'''
Ważny! Polecenie EXEC nie jest przeznaczone do ponownego wykorzystywania buforowanych planów wykonania! Utworzy nowy dla każdego wykonania.
Aby to zademonstrować, dwukrotnie wykonamy to samo zapytanie, ale z inną wartością parametru wejściowego. Następnie porównujemy plany wykonania w obu przypadkach:
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 *%';
Korzystanie z procedury rozszerzonej sp_executesql
Aby skorzystać z tej procedury, musimy nadać jej instrukcję SQL, definicję użytych w niej parametrów oraz ich wartości. Składnia jest następująca:
sp_executesql @SQLStatement, N'@ParamNameDataType' , @Parameter1 = 'Value1'
Zacznijmy od prostego przykładu, który pokazuje, jak przekazać instrukcję i parametry:
EXECUTE sp_executesql
N'SELECT *
FROM Person.Address
WHERE AddressLine1 LIKE ''%'' + @AddressPart + ''%''', -- SQL Statement
N'@AddressPart NVARCHAR(50)', -- Parameter definition
@AddressPart = 'a'; -- Parameter value
W przeciwieństwie do polecenia EXEC, sp_executesql rozszerzona procedura składowana ponownie wykorzystuje plany wykonania, jeśli jest wykonywana z tą samą instrukcją, ale z różnymi parametrami. Dlatego lepiej użyć sp_executesql ponad EXEC polecenie :
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%';
Dynamiczny SQL w procedurach składowanych
Do tej pory w skryptach używaliśmy dynamicznego SQL. Jednak rzeczywiste korzyści stają się widoczne, gdy wykonujemy te konstrukcje w niestandardowych obiektach programistycznych – procedurach przechowywanych przez użytkownika.
Stwórzmy procedurę, która będzie szukać osoby w bazie danych AdventureWorks na podstawie różnych wartości parametrów procedury wejściowej. Z danych wprowadzonych przez użytkownika zbudujemy dynamiczne polecenie SQL i wykonamy je, aby zwrócić wynik do aplikacji wywołującej użytkownika:
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
Parametr OUTPUT w sp_executesql
Możemy użyć sp_executesql z parametrem OUTPUT, aby zapisać wartość zwróconą przez instrukcję SELECT. Jak pokazano w poniższym przykładzie, zapewnia to liczbę wierszy zwróconych przez zapytanie do zmiennej wyjściowej @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
Ochrona przed wstrzyknięciem SQL za pomocą procedury sp_executesql
Istnieją dwie proste czynności, które należy wykonać, aby znacznie zmniejszyć ryzyko wstrzyknięcia SQL. Najpierw umieść nazwy tabel w nawiasach. Po drugie, sprawdź w kodzie, czy w bazie danych istnieją tabele. Obie te metody są przedstawione w poniższym przykładzie.
Tworzymy prostą procedurę składowaną i wykonujemy ją z prawidłowymi i nieprawidłowymi parametrami:
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'
Porównanie funkcji polecenia EXEC i procedury składowanej sp_executesql
polecenie EXEC | procedura składowana sp_executesql |
Brak ponownego wykorzystania planu pamięci podręcznej | Ponowne wykorzystanie planu pamięci podręcznej |
Bardzo podatny na wstrzyknięcie SQL | Znacznie mniej podatne na wstrzyknięcie SQL |
Brak zmiennych wyjściowych | Obsługuje zmienne wyjściowe |
Brak parametryzacji | Obsługuje parametryzację |
Wniosek
W tym poście pokazano dwa sposoby implementacji funkcji dynamicznego SQL w SQL Server. Dowiedzieliśmy się, dlaczego lepiej jest używać sp_executesql procedurę, jeśli jest dostępna. Ponadto wyjaśniliśmy specyfikę korzystania z polecenia EXEC i wymagania dotyczące oczyszczenia danych wejściowych użytkownika w celu zapobiegania wstrzykiwaniu SQL.
Do dokładnego i wygodnego debugowania procedur składowanych w SQL Server Management Studio v18 (i nowszych) można użyć wyspecjalizowanej funkcji debugera T-SQL, będącej częścią popularnego rozwiązania dbForge SQL Complete.