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.