sql >> Databáze >  >> RDS >> Sqlserver

Dynamické spouštění SQL na serveru SQL

Dynamický SQL je příkaz vytvořený a spouštěný za běhu, obvykle obsahující dynamicky generované části řetězce SQL, vstupní parametry nebo obojí.

Pro konstrukci a spouštění dynamicky generovaných SQL příkazů jsou k dispozici různé metody. Aktuální článek je prozkoumá, definuje jejich pozitivní a negativní aspekty a demonstruje praktické přístupy k optimalizaci dotazů v některých častých scénářích.

Dynamické SQL provádíme dvěma způsoby:EXEC příkaz a sp_executesql uložená procedura.

Použití příkazu EXEC/EXECUTE

Pro první příklad vytvoříme jednoduchý dynamický SQL příkaz z AdventureWorks databáze. Příklad má jeden filtr, který je předán přes zřetězenou řetězcovou proměnnou @AddressPart a proveden v posledním příkazu:

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)

Upozorňujeme, že dotazy sestavené zřetězením řetězců mohou způsobit zranitelnost vkládání SQL. Důrazně bych vám doporučil, abyste se s tímto tématem seznámili. Pokud plánujete použít tento druh vývojové architektury, zejména ve veřejné webové aplikaci, bude to více než užitečné.

Dále bychom měli zpracovat hodnoty NULL ve zřetězení řetězců . Například proměnná instance @AddressPart z předchozího příkladu by mohla zneplatnit celý příkaz SQL, pokud by byla předána tato hodnota.

Nejjednodušší způsob, jak vyřešit tento potenciální problém, je použít funkci ISNULL k vytvoření platného příkazu SQL :

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


Důležité! Příkaz EXEC není určen k opětovnému použití plánů provádění uložených v mezipaměti! Pro každé provedení vytvoří nový.

Abychom to demonstrovali, provedeme stejný dotaz dvakrát, ale s jinou hodnotou vstupního parametru. Poté porovnáme prováděcí plány v obou případech:

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

Použití rozšířené procedury sp_executesql

Abychom mohli tento postup použít, musíme mu zadat SQL příkaz, definici parametrů v něm použitých a jejich hodnoty. Syntaxe je následující:

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

Začněme jednoduchým příkladem, který ukazuje, jak předat příkaz a 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

Na rozdíl od příkazu EXEC, sp_executesql rozšířená uložená procedura znovu používá prováděcí plány, pokud jsou spuštěny se stejným příkazem, ale s jinými parametry. Proto je lepší použít sp_executesql přes EXEC příkaz :

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

Dynamické SQL v uložených procedurách

Doposud jsme ve skriptech používali dynamické SQL. Skutečné výhody se však projeví, když tyto konstrukce spustíme ve vlastních programovacích objektech – uživatelsky uložených procedurách.

Vytvořme proceduru, která bude hledat osobu v databázi AdventureWorks na základě různých hodnot parametrů vstupní procedury. Z uživatelského vstupu zkonstruujeme dynamický SQL příkaz a provedeme jej, abychom vrátili výsledek volající uživatelské aplikaci:

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 v sp_executesql

Můžeme použít sp_executesql s parametrem OUTPUT pro uložení hodnoty vrácené příkazem SELECT. Jak je ukázáno v příkladu níže, toto poskytuje počet řádků vrácených dotazem do výstupní proměnné @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

Ochrana proti vložení SQL pomocí procedury sp_executesql

Existují dvě jednoduché činnosti, které byste měli udělat, abyste výrazně snížili riziko vložení SQL. Nejprve uzavřete názvy tabulek do hranatých závorek. Za druhé v kódu zkontrolujte, zda v databázi existují tabulky. Obě tyto metody jsou uvedeny v příkladu níže.

Vytváříme jednoduchou uloženou proceduru a provádíme ji s platnými a neplatnými parametry:

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'

Porovnání funkcí příkazu EXEC a uložené procedury sp_executesql

Příkaz EXEC uložená procedura sp_executesql
Žádné opětovné použití plánu mezipaměti Opětovné použití plánu mezipaměti
Velmi zranitelné vůči SQL injection Mnohem méně zranitelné vůči SQL injection
Žádné výstupní proměnné Podporuje výstupní proměnné
Žádná parametrizace Podporuje parametrizaci

Závěr

Tento příspěvek demonstroval dva způsoby implementace dynamické funkce SQL v SQL Server. Zjistili jsme, proč je lepší používat sp_executesql postup, pokud je k dispozici. Také jsme objasnili specifičnost použití příkazu EXEC a požadavky na dezinfekci uživatelských vstupů, aby se zabránilo vkládání SQL.

Pro přesné a pohodlné ladění uložených procedur v SQL Server Management Studio v18 (a vyšší) můžete použít specializovanou funkci T-SQL Debugger, která je součástí oblíbeného řešení dbForge SQL Complete.


  1. Jak získat seznam tabulek bez omezení primárního klíče v databázi SQL Server - SQL Server / Výukový program T-SQL, část 58

  2. Jak můžeme definovat velikost výstupního parametru v uložené proceduře?

  3. Jak odstranit databázi v cPanel

  4. SQL vybrat vše, pokud je parametr null, jinak vrátit konkrétní položku