Динамичният 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.