Sqlserver
 sql >> база данни >  >> RDS >> Sqlserver

Динамично изпълнение на SQL в SQL Server

Динамичният SQL е израз, конструиран и изпълняван по време на изпълнение, обикновено съдържащ динамично генерирани части от SQL низ, входни параметри или и двете.

Налични са различни методи за конструиране и изпълнение на динамично генерирани SQL команди. Настоящата статия ще ги проучи, ще определи техните положителни и отрицателни аспекти и ще демонстрира практически подходи за оптимизиране на заявките в някои чести сценарии.

Използваме два начина за изпълнение на динамичен SQL:EXEC команда и sp_executesql съхранена процедура.

Използване на команда EXEC/EXECUTE

За първия пример създаваме прост динамичен SQL израз от AdventureWorks база данни. Примерът има един филтър, който се предава през конкатенираната низова променлива @AddressPart и се изпълнява в последната команда:

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)

Имайте предвид, че заявките, изградени чрез конкатенация на низове, могат да доведат до уязвимости при инжектиране на SQL. Силно бих ви посъветвал да се запознаете с тази тема. Ако планирате да използвате този вид архитектура за разработка, особено в публично ориентирано уеб приложение, това ще бъде повече от полезно.

След това трябва да обработваме стойности NULL в конкатенации на низове . Например променливата на екземпляра @AddressPart от предишния пример може да обезсили целия SQL оператор, ако предаде тази стойност.

Най-лесният начин да се справите с този потенциален проблем е да използвате функцията ISNULL за конструиране на валиден SQL израз :

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


Важно! Командата EXEC не е предназначена да използва повторно кеширани планове за изпълнение! Той ще създаде нов за всяко изпълнение.

За да демонстрираме това, ще изпълним една и съща заявка два пъти, но с различна стойност на входния параметър. След това сравняваме плановете за изпълнение и в двата случая:

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

Използване на разширена процедура sp_executesql

За да използваме тази процедура, трябва да й дадем SQL оператор, дефиницията на параметрите, използвани в нея, и техните стойности. Синтаксисът е следният:

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

Нека започнем с прост пример, който показва как се предава израз и параметри:

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

За разлика от командата EXEC, sp_executesql разширената съхранена процедура използва повторно планове за изпълнение, ако се изпълнява със същия израз, но различни параметри. Затова е по-добре да използвате sp_executesql над EXEC команда :

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 в съхранените процедури

Досега използвахме динамичен SQL в скриптове. Въпреки това, реалните ползи стават очевидни, когато изпълняваме тези конструкции в персонализирани програмни обекти – потребителски съхранени процедури.

Нека създадем процедура, която ще търси човек в базата данни на AdventureWorks въз основа на различните стойности на параметрите на входната процедура. От въвеждането на потребителя ще изградим динамична SQL команда и ще я изпълним, за да върнем резултата към извикващото потребителско приложение:

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

OUTPUT параметър в sp_executesql

Можем да използваме sp_executesql с параметъра OUTPUT, за да запишете стойността, върната от оператора SELECT. Както е показано в примера по-долу, това осигурява броя на редовете, върнати от заявката към изходната променлива @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

Защита срещу SQL инжекция с процедура sp_executesql

Има две прости дейности, които трябва да направите, за да намалите значително риска от SQL инжектиране. Първо, оградете имената на таблици в скоби. Второ, проверете в кода дали таблици съществуват в базата данни. И двата метода присъстват в примера по-долу.

Създаваме проста съхранена процедура и я изпълняваме с валидни и невалидни параметри:

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'

Сравнение на функции на команда EXEC и съхранена процедура sp_executesql

Команда EXEC sp_executesql съхранена процедура
Без повторно използване на кеш план Повторна употреба на кеш план
Много уязвим към SQL инжекция Много по-малко уязвими към SQL инжекция
Няма изходни променливи Поддържа изходни променливи
Без параметризация Поддържа параметризация

Заключение

Тази публикация демонстрира два начина за внедряване на динамичната SQL функционалност в SQL Server. Научихме защо е по-добре да използвате sp_executesql процедура, ако е налична. Освен това изяснихме спецификата на използването на командата EXEC и изискванията за дезинфекция на въведените данни на потребителите за предотвратяване на SQL инжектиране.

За точното и удобно отстраняване на грешки на съхранени процедури в SQL Server Management Studio v18 (и по-нова версия), можете да използвате специализираната функция T-SQL Debugger, част от популярното решение 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. Показатели за производителност на SQL сървъра, за да останете пред играта

  2. FLOOR() Примери в SQL Server

  3. Как да обобщим полето за време в SQL Server

  4. В SQL, как можете да групирате по диапазони?

  5. Задействане за предотвратяване на вмъкване за дублиращи се данни от две колони