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.