Sqlserver
 sql >> Baza danych >  >> RDS >> Sqlserver

Dynamiczne wykonywanie SQL w SQL Server

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.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Czy możliwe jest wykonanie wielu aktualizacji za pomocą jednej instrukcji UPDATE SQL?

  2. Utwórz kolumnę obliczaną w SQL Server za pomocą T-SQL

  3. Sprawdź, czy tabela istnieje w SQL Server

  4. Jak zmienić bieżący format daty w SQL Server (T-SQL)

  5. SPRAWDŹ Ograniczenia w SQL Server