sql >> Base de Datos >  >> RDS >> Sqlserver

Ejecución de SQL dinámico en SQL Server

SQL dinámico es una declaración construida y ejecutada en tiempo de ejecución, que generalmente contiene partes de cadenas SQL generadas dinámicamente, parámetros de entrada o ambos.

Hay varios métodos disponibles para construir y ejecutar comandos SQL generados dinámicamente. El artículo actual los explorará, definirá sus aspectos positivos y negativos y demostrará enfoques prácticos para optimizar las consultas en algunos escenarios frecuentes.

Usamos dos formas de ejecutar SQL dinámico:EXEC comando y sp_executesql procedimiento almacenado.

Uso del comando EXEC/EXECUTE

Para el primer ejemplo, creamos una instrucción SQL dinámica simple a partir de AdventureWorks base de datos. El ejemplo tiene un filtro que se pasa a través de la variable de cadena concatenada @AddressPart y se ejecuta en el último comando:

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)

Tenga en cuenta que las consultas creadas por concatenación de cadenas pueden generar vulnerabilidades de inyección SQL. Le recomiendo encarecidamente que se familiarice con este tema. Si planea usar este tipo de arquitectura de desarrollo, especialmente en una aplicación web pública, será más que útil.

A continuación, debemos manejar valores NULOS en concatenaciones de cadenas . Por ejemplo, la variable de instancia @AddressPart del ejemplo anterior podría invalidar la instrucción SQL completa si se pasa este valor.

La manera más fácil de manejar este problema potencial es usar la función ISNULL para construir una instrucción SQL válida :

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


¡Importante! ¡El comando EXEC no está diseñado para reutilizar planes de ejecución almacenados en caché! Creará uno nuevo para cada ejecución.

Para demostrar esto, ejecutaremos la misma consulta dos veces, pero con un valor de parámetro de entrada diferente. Luego, comparamos los planes de ejecución en ambos casos:

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

Uso del procedimiento extendido sp_executesql

Para usar este procedimiento, necesitamos darle una declaración SQL, la definición de los parámetros usados ​​en él y sus valores. La sintaxis es la siguiente:

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

Comencemos con un ejemplo simple que muestra cómo pasar una declaración y parámetros:

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

A diferencia del comando EXEC, el sp_executesql El procedimiento almacenado extendido reutiliza los planes de ejecución si se ejecuta con la misma declaración pero con diferentes parámetros. Por lo tanto, es mejor usar sp_executesql sobre EXEC comando :

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

SQL dinámico en procedimientos almacenados

Hasta ahora usábamos SQL dinámico en scripts. Sin embargo, los beneficios reales se hacen evidentes cuando ejecutamos estas construcciones en objetos de programación personalizados:procedimientos almacenados por el usuario.

Vamos a crear un procedimiento que buscará a una persona en la base de datos de AdventureWorks, en función de los diferentes valores de los parámetros del procedimiento de entrada. A partir de la entrada del usuario, construiremos un comando SQL dinámico y lo ejecutaremos para devolver el resultado a la aplicación del usuario que llama:

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

Parámetro de SALIDA en sp_executesql

Podemos usar sp_executesql con el parámetro OUTPUT para guardar el valor devuelto por la sentencia SELECT. Como se muestra en el siguiente ejemplo, esto proporciona el número de filas devueltas por la consulta a la variable de salida @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

Protección contra inyección SQL con procedimiento sp_executesql

Hay dos actividades simples que debe realizar para reducir significativamente el riesgo de inyección SQL. Primero, encierre los nombres de las tablas entre paréntesis. En segundo lugar, verifique en el código si existen tablas en la base de datos. Ambos métodos están presentes en el siguiente ejemplo.

Estamos creando un procedimiento almacenado simple y ejecutándolo con parámetros válidos e inválidos:

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'

Comparación de funciones del comando EXEC y el procedimiento almacenado sp_executesql

Comando EXEC procedimiento almacenado sp_executesql
Sin reutilización del plan de caché Reutilización del plan de caché
Muy vulnerable a la inyección SQL Mucho menos vulnerable a la inyección SQL
Sin variables de salida Admite variables de salida
Sin parametrización Admite parametrización

Conclusión

Esta publicación demostró dos formas de implementar la funcionalidad de SQL dinámico en SQL Server. Hemos aprendido por qué es mejor usar sp_executesql procedimiento si está disponible. Además, hemos aclarado la especificidad del uso del comando EXEC y las demandas para desinfectar las entradas del usuario para evitar la inyección de SQL.

Para la depuración precisa y cómoda de los procedimientos almacenados en SQL Server Management Studio v18 (y superior), puede utilizar la característica especializada T-SQL Debugger, una parte de la popular solución dbForge SQL Complete.