Dynamisk SQL är en sats som konstrueras och exekveras under körning och innehåller vanligtvis dynamiskt genererade SQL-strängdelar, inmatningsparametrar eller båda.
Olika metoder finns tillgängliga för att konstruera och köra dynamiskt genererade SQL-kommandon. Den aktuella artikeln kommer att utforska dem, definiera deras positiva och negativa aspekter och visa praktiska tillvägagångssätt för att optimera frågor i vissa vanliga scenarier.
Vi använder två sätt att köra dynamisk SQL:EXEC kommandot och sp_executesql lagrad procedur.
Använda kommandot EXEC/EXECUTE
För det första exemplet skapar vi en enkel dynamisk SQL-sats från AdventureWorks databas. Exemplet har ett filter som skickas genom den sammanlänkade strängvariabeln @AddressPart och exekveras i det sista kommandot:
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)
Observera att frågor byggda av strängsammansättning kan ge SQL-injektion sårbarheter. Jag rekommenderar starkt att du bekantar dig med detta ämne. Om du planerar att använda den här typen av utvecklingsarkitektur, särskilt i en webbapplikation som riktar sig till allmänheten, kommer det att vara mer än användbart.
Därefter bör vi hantera är NULL-värden i strängsammansättningar . Till exempel kan @AddressPart-instansvariabeln från föregående exempel ogiltigförklara hela SQL-satsen om den passerade detta värde.
Det enklaste sättet att hantera detta potentiella problem är att använda ISNULL-funktionen för att konstruera en giltig SQL-sats :
SET @SQLExec = 'SELECT * FROM Person.Address WHERE AddressLine1 LIKE ''%' + ISNULL(@AddressPart, ‘ ‘) + '%'''
Viktig! EXEC-kommandot är inte utformat för att återanvända cachade körningsplaner! Det kommer att skapa en ny för varje körning.
För att visa detta kommer vi att köra samma fråga två gånger, men med ett annat värde på indataparametern. Sedan jämför vi genomförandeplaner i båda fallen:
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 *%';
Använda utökad procedur sp_executesql
För att använda den här proceduren måste vi ge den en SQL-sats, definitionen av parametrar som används i den och deras värden. Syntaxen är följande:
sp_executesql @SQLStatement, N'@ParamNameDataType' , @Parameter1 = 'Value1'
Låt oss börja med ett enkelt exempel som visar hur man skickar ett uttalande och parametrar:
EXECUTE sp_executesql
N'SELECT *
FROM Person.Address
WHERE AddressLine1 LIKE ''%'' + @AddressPart + ''%''', -- SQL Statement
N'@AddressPart NVARCHAR(50)', -- Parameter definition
@AddressPart = 'a'; -- Parameter value
Till skillnad från EXEC-kommandot, sp_executesql utökad lagrad procedur återanvänder exekveringsplaner om de körs med samma sats men olika parametrar. Därför är det bättre att använda sp_executesql över EXEC kommando :
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%';
Dynamisk SQL i lagrade procedurer
Fram till nu har vi använt dynamisk SQL i skript. Men verkliga fördelar blir uppenbara när vi exekverar dessa konstruktioner i anpassade programmeringsobjekt – användarlagrade procedurer.
Låt oss skapa en procedur som kommer att leta efter en person i AdventureWorks-databasen, baserat på de olika inmatningsprocedurernas parametervärden. Från användarinmatningen kommer vi att konstruera ett dynamiskt SQL-kommando och köra det för att returnera resultatet till anropande användarapplikation:
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-parameter i sp_executesql
Vi kan använda sp_executesql med parametern OUTPUT för att spara värdet som returneras av SELECT-satsen. Som visas i exemplet nedan ger detta antalet rader som returneras av frågan till utdatavariabeln @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
Skydd mot SQL-injektion med sp_executesql-procedur
Det finns två enkla aktiviteter du bör göra för att minska risken för SQL-injektion avsevärt. Omge först tabellnamn inom parentes. För det andra, kontrollera i koden om tabeller finns i databasen. Båda dessa metoder finns i exemplet nedan.
Vi skapar en enkel lagrad procedur och exekverar den med giltiga och ogiltiga parametrar:
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'
Funktionsjämförelse av EXEC Command och sp_executesql lagrad procedur
EXEC-kommando | sp_executesql lagrad procedur |
Ingen återanvändning av cacheplan | Återanvändning av cacheplan |
Mycket sårbar för SQL-injektion | Mycket mindre sårbar för SQL-injektion |
Inga utdatavariabler | Stöder utdatavariabler |
Ingen parametrisering | Stöder parametrisering |
Slutsats
Det här inlägget demonstrerade två sätt att implementera den dynamiska SQL-funktionaliteten i SQL Server. Vi har lärt oss varför det är bättre att använda sp_executesql förfarande om det finns tillgängligt. Vi har också klargjort specificiteten för att använda EXEC-kommandot och kraven på att sanera användarinmatningar för att förhindra SQL-injektion.
För korrekt och bekväm felsökning av lagrade procedurer i SQL Server Management Studio v18 (och senare) kan du använda den specialiserade T-SQL Debugger-funktionen, en del av den populära dbForge SQL Complete-lösningen.