Sqlserver
 sql >> Datenbank >  >> RDS >> Sqlserver

Dynamische SQL-Ausführung in SQL Server

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.