Dynamisches SQL ist eine Anweisung, die zur Laufzeit erstellt und ausgeführt wird und normalerweise dynamisch generierte SQL-String-Teile, Eingabeparameter oder beides enthält.
Es stehen verschiedene Methoden zur Verfügung, um dynamisch generierte SQL-Befehle zu erstellen und auszuführen. Der aktuelle Artikel wird sie untersuchen, ihre positiven und negativen Aspekte definieren und praktische Ansätze zur Optimierung von Abfragen in einigen häufigen Szenarien demonstrieren.
Wir verwenden zwei Möglichkeiten, um dynamisches SQL auszuführen:EXEC Befehl und sp_executesql gespeicherte Prozedur.
Verwendung des EXEC/EXECUTE-Befehls
Für das erste Beispiel erstellen wir eine einfache dynamische SQL-Anweisung aus AdventureWorks Datenbank. Das Beispiel hat einen Filter, der durch die verkettete Zeichenfolgenvariable @AddressPart geleitet und im letzten Befehl ausgeführt wird:
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)
Beachten Sie, dass Abfragen, die durch Zeichenfolgenverkettung erstellt wurden, SQL-Injection-Schwachstellen aufweisen können. Ich würde Ihnen dringend raten, sich mit diesem Thema vertraut zu machen. Wenn Sie vorhaben, diese Art von Entwicklungsarchitektur zu verwenden, insbesondere in einer öffentlich zugänglichen Webanwendung, ist dies mehr als nützlich.
Als nächstes sollten wir mit NULL-Werten in Stringverkettungen umgehen . Beispielsweise könnte die Instanzvariable @AddressPart aus dem vorherigen Beispiel die gesamte SQL-Anweisung ungültig machen, wenn dieser Wert übergeben wird.
Der einfachste Weg, dieses potenzielle Problem zu lösen, besteht darin, die ISNULL-Funktion zu verwenden, um eine gültige SQL-Anweisung zu erstellen :
SET @SQLExec = 'SELECT * FROM Person.Address WHERE AddressLine1 LIKE ''%' + ISNULL(@AddressPart, ‘ ‘) + '%'''
Wichtig! Der EXEC-Befehl ist nicht dafür ausgelegt, zwischengespeicherte Ausführungspläne wiederzuverwenden! Es wird für jede Ausführung eine neue erstellt.
Um dies zu demonstrieren, führen wir dieselbe Abfrage zweimal aus, jedoch mit einem anderen Wert des Eingabeparameters. Dann vergleichen wir Ausführungspläne in beiden Fällen:
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 *%';
Verwenden der erweiterten Prozedur sp_executesql
Um diese Prozedur zu verwenden, müssen wir ihr eine SQL-Anweisung, die Definition der darin verwendeten Parameter und ihre Werte geben. Die Syntax lautet wie folgt:
sp_executesql @SQLStatement, N'@ParamNameDataType' , @Parameter1 = 'Value1'
Beginnen wir mit einem einfachen Beispiel, das zeigt, wie eine Anweisung und Parameter übergeben werden:
EXECUTE sp_executesql
N'SELECT *
FROM Person.Address
WHERE AddressLine1 LIKE ''%'' + @AddressPart + ''%''', -- SQL Statement
N'@AddressPart NVARCHAR(50)', -- Parameter definition
@AddressPart = 'a'; -- Parameter value
Im Gegensatz zum EXEC-Befehl ist der sp_executesql Erweiterte gespeicherte Prozeduren verwenden Ausführungspläne erneut, wenn sie mit derselben Anweisung, aber unterschiedlichen Parametern ausgeführt werden. Daher ist es besser, sp_executesql zu verwenden über EXEC Befehl :
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%';
Dynamisches SQL in gespeicherten Prozeduren
Bisher haben wir dynamisches SQL in Skripten verwendet. Echte Vorteile werden jedoch deutlich, wenn wir diese Konstrukte in benutzerdefinierten Programmierobjekten ausführen – benutzerdefinierten gespeicherten Prozeduren.
Lassen Sie uns eine Prozedur erstellen, die basierend auf den verschiedenen Parameterwerten der Eingabeprozedur nach einer Person in der AdventureWorks-Datenbank sucht. Aus der Benutzereingabe konstruieren wir einen dynamischen SQL-Befehl und führen ihn aus, um das Ergebnis an die aufrufende Benutzeranwendung zurückzugeben:
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 in sp_executesql
Wir können sp_executesql verwenden mit dem OUTPUT-Parameter, um den von der SELECT-Anweisung zurückgegebenen Wert zu speichern. Wie im folgenden Beispiel gezeigt, liefert dies die Anzahl der von der Abfrage zurückgegebenen Zeilen an die Ausgabevariable @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
Schutz vor SQL-Injection mit sp_executesql-Prozedur
Es gibt zwei einfache Maßnahmen, die Sie ergreifen sollten, um das Risiko einer SQL-Injection erheblich zu verringern. Schließen Sie zunächst Tabellennamen in Klammern ein. Überprüfen Sie zweitens im Code, ob Tabellen in der Datenbank vorhanden sind. Beide Methoden sind im folgenden Beispiel vorhanden.
Wir erstellen eine einfache gespeicherte Prozedur und führen sie mit gültigen und ungültigen Parametern aus:
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'
Funktionsvergleich des EXEC-Befehls und der gespeicherten Prozedur sp_executesql
EXEC-Befehl | sp_executesql gespeicherte Prozedur |
Keine Wiederverwendung des Cache-Plans | Wiederverwendung von Cache-Plänen |
Sehr anfällig für SQL-Injection | Viel weniger anfällig für SQL-Injection |
Keine Ausgabevariablen | Unterstützt Ausgabevariablen |
Keine Parametrisierung | Unterstützt Parametrisierung |
Schlussfolgerung
In diesem Beitrag wurden zwei Möglichkeiten zum Implementieren der dynamischen SQL-Funktionalität in SQL Server demonstriert. Wir haben gelernt, warum es besser ist, sp_executesql zu verwenden Verfahren, falls verfügbar. Außerdem haben wir die Besonderheiten der Verwendung des EXEC-Befehls und die Anforderungen zur Bereinigung von Benutzereingaben zur Verhinderung von SQL-Injection klargestellt.
Für das genaue und komfortable Debuggen von gespeicherten Prozeduren in SQL Server Management Studio v18 (und höher) können Sie die spezialisierte T-SQL-Debugger-Funktion verwenden, die Teil der beliebten dbForge SQL Complete-Lösung ist.