sql >> Database >  >> RDS >> Sqlserver

Dynamische SQL-uitvoering in SQL Server

Dynamische SQL is een instructie die tijdens runtime is geconstrueerd en uitgevoerd en die meestal dynamisch gegenereerde SQL-tekenreeksdelen, invoerparameters of beide bevat.

Er zijn verschillende methoden beschikbaar om dynamisch gegenereerde SQL-opdrachten te construeren en uit te voeren. Het huidige artikel gaat ze onderzoeken, hun positieve en negatieve aspecten definiëren en praktische benaderingen demonstreren om zoekopdrachten in sommige veelvoorkomende scenario's te optimaliseren.

We gebruiken twee manieren om dynamische SQL uit te voeren:EXEC commando en sp_executesql opgeslagen procedure.

Het EXEC/EXECUTE-commando gebruiken

Voor het eerste voorbeeld maken we een eenvoudige dynamische SQL-instructie van de AdventureWorks databank. Het voorbeeld heeft één filter dat door de aaneengeschakelde tekenreeksvariabele @AddressPart wordt doorgegeven en in het laatste commando wordt uitgevoerd:

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)

Houd er rekening mee dat query's die zijn gemaakt door het samenvoegen van tekenreeksen, kwetsbaarheden voor SQL-injectie kunnen opleveren. Ik raad je ten zeerste aan om vertrouwd te raken met dit onderwerp. Als u van plan bent om dit soort ontwikkelingsarchitectuur te gebruiken, vooral in een openbare webtoepassing, zal dit meer dan nuttig zijn.

Vervolgens moeten we zijn NULL-waarden in aaneenschakelingen van tekenreeksen afhandelen . De instantievariabele @AddressPart uit het vorige voorbeeld kan bijvoorbeeld de hele SQL-instructie ongeldig maken als deze waarde wordt doorgegeven.

De gemakkelijkste manier om met dit potentiële probleem om te gaan, is door de functie ISNULL te gebruiken om een ​​geldige SQL-instructie samen te stellen :

SET @SQLExec = 'SELECT * FROM Person.Address WHERE AddressLine1 LIKE ''%' + ISNULL(@AddressPart, ‘ ‘) + '%'''


Belangrijk! Het EXEC-commando is niet ontworpen om uitvoeringsplannen in de cache opnieuw te gebruiken! Er wordt voor elke uitvoering een nieuwe gemaakt.

Om dit te demonstreren, voeren we dezelfde query twee keer uit, maar met een andere waarde van de invoerparameter. Vervolgens vergelijken we de uitvoeringsplannen in beide gevallen:

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 *%';

Uitgebreide procedure sp_executesql gebruiken

Om deze procedure te gebruiken, moeten we het een SQL-instructie geven, de definitie van de parameters die erin worden gebruikt en hun waarden. De syntaxis is als volgt:

sp_executesql @SQLStatement, N'@ParamNameDataType' , @Parameter1 = 'Value1'

Laten we beginnen met een eenvoudig voorbeeld dat laat zien hoe u een statement en parameters doorgeeft:

EXECUTE sp_executesql  
               N'SELECT *  
                     FROM Person.Address
	       WHERE AddressLine1 LIKE ''%'' + @AddressPart + ''%''', -- SQL Statement
              N'@AddressPart NVARCHAR(50)',  -- Parameter definition
             @AddressPart = 'a';  -- Parameter value

In tegenstelling tot het EXEC-commando, is de sp_executesql uitgebreide opgeslagen procedure hergebruikt uitvoeringsplannen als ze worden uitgevoerd met dezelfde instructie maar met verschillende parameters. Daarom is het beter om sp_executesql . te gebruiken meer dan EXEC commando :

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%';

Dynamische SQL in opgeslagen procedures

Tot nu toe gebruikten we dynamische SQL in scripts. Echte voordelen worden echter duidelijk wanneer we deze constructies uitvoeren in aangepaste programmeerobjecten - door de gebruiker opgeslagen procedures.

Laten we een procedure maken die een persoon zoekt in de AdventureWorks-database, op basis van de verschillende parameterwaarden van de invoerprocedure. Op basis van de gebruikersinvoer zullen we een dynamisch SQL-commando construeren en uitvoeren om het resultaat terug te sturen naar de aanroepende gebruikerstoepassing:

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

We kunnen sp_executesql . gebruiken met de parameter OUTPUT om de waarde op te slaan die wordt geretourneerd door de SELECT-instructie. Zoals in het onderstaande voorbeeld wordt getoond, levert dit het aantal rijen op dat door de query wordt geretourneerd aan de uitvoervariabele @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

Bescherming tegen SQL-injectie met sp_executesql Procedure

Er zijn twee eenvoudige activiteiten die u moet doen om het risico op SQL-injectie aanzienlijk te verminderen. Zet eerst de tabelnamen tussen haakjes. Ten tweede, controleer in de code of er tabellen in de database bestaan. Beide methoden zijn aanwezig in het onderstaande voorbeeld.

We creëren een eenvoudige opgeslagen procedure en voeren deze uit met geldige en ongeldige parameters:

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'

Functievergelijking van EXEC-opdracht en sp_executesql opgeslagen procedure

EXEC-opdracht sp_executesql opgeslagen procedure
Geen hergebruik van cacheplan Hergebruik van cacheplan
Zeer kwetsbaar voor SQL-injectie Veel minder kwetsbaar voor SQL-injectie
Geen uitvoervariabelen Ondersteunt uitvoervariabelen
Geen parametrisering Ondersteunt parametrisering

Conclusie

Dit bericht demonstreerde twee manieren om de dynamische SQL-functionaliteit in SQL Server te implementeren. We hebben geleerd waarom het beter is om de sp_executesql . te gebruiken procedure als deze beschikbaar is. We hebben ook de specificiteit verduidelijkt van het gebruik van de EXEC-opdracht en de eisen om gebruikersinvoer te zuiveren om SQL-injectie te voorkomen.

Voor het nauwkeurig en comfortabel debuggen van opgeslagen procedures in SQL Server Management Studio v18 (en hoger), kunt u de gespecialiseerde T-SQL Debugger-functie gebruiken, een onderdeel van de populaire dbForge SQL Complete-oplossing.


  1. Heroku Postgres-fout:PGError:FOUT:relatieorganisaties bestaan ​​niet (ActiveRecord::StatementInvalid)

  2. MySQL-dump per zoekopdracht

  3. Geeft COUNT(*) altijd een resultaat?

  4. Tool voor het scripten van tabelgegevens