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

10 errores de SP_EXECUTESQL que debe evitar para mejorar el SQL dinámico

¿Sabes lo poderosa que puede ser una herramienta como el SQL dinámico? Úselo de manera incorrecta y puede permitir que alguien se haga cargo de su base de datos. Además, puede haber demasiada complejidad. Este artículo tiene como objetivo presentar las trampas al usar SP_EXECUTESQL y ofrece los 10 errores más comunes que se deben evitar.

SP_EXECUTESQL es una de las formas en que puede ejecutar comandos SQL incrustados en una cadena. Construye esta cadena dinámicamente a través del código. Es por eso que llamamos a este SQL dinámico. Además de una serie de declaraciones, también puede pasarle una lista de parámetros y valores. De hecho, estos parámetros y valores difieren del comando EXEC. EXEC no acepta parámetros para SQL dinámico. ¡Sin embargo, ejecuta SP_EXECUTESQL usando EXEC!

Para un novato en SQL dinámico, así es como invoca esto.

EXEC sp_executesql <command string>[, <input or output parameters list>, <parameter value1>, <parameter value n>]

Usted forma la cadena de comandos que incluyen sentencias SQL válidas. Opcionalmente, puede pasar una lista de parámetros de entrada o salida y sus tipos de datos. Y finalmente, pasa una lista de valores separados por comas. Si pasa parámetros, necesita pasar valores. Más adelante, verá ejemplos correctos e incorrectos de esto a medida que siga leyendo.

Uso de SP_EXECUTESQL cuando no lo necesita

Así es. Si no lo necesita, no lo use. Si esto se convierte en los 10 mandamientos de SP_EXECUTESQL, este es el primero. Es porque este procedimiento del sistema puede ser fácilmente abusado. Pero, ¿cómo lo sabes?

Responda esto:¿Hay algún problema si el comando en su SQL dinámico se vuelve estático? Si no tienes nada que decir sobre este punto, entonces no lo necesitas. Ver el ejemplo.

DECLARE @sql NVARCHAR(100) = N'SELECT ProductID, Name FROM Production.Product ' +
			      'WHERE ProductID = @ProductID';
DECLARE @paramsList NVARCHAR(100) = N'@ProductID INT';
DECLARE @param1Value INT = 1;

EXEC sp_executesql @sql, @paramsList, @param1Value
GO

Como puede ver, el uso de SP_EXECUTESQL se completa con una cadena de comando, un parámetro y un valor. ¿Pero tiene que ser así? Seguramente no. Está perfectamente bien tener esto:

DECLARE @productID INT = 1;

SELECT ProductID, Name
FROM Production.Product
WHERE ProductID = @productID;

Pero podría avergonzarme cuando vea los ejemplos más adelante en el artículo. Porque voy a contradecir lo que estoy afirmando en este primer punto. Verá instrucciones breves de SQL dinámico que son mejores que las estáticas. Entonces, tenga paciencia conmigo porque los ejemplos solo probarán los puntos descritos aquí. Para el resto de ejemplos, simule por un momento que está viendo el código destinado a SQL dinámico.

Objetos y variables fuera de alcance

Ejecutar una serie de comandos SQL en una cadena usando SP_EXECUTESQL es como crear un procedimiento almacenado sin nombre y ejecutarlo. Sabiendo esto, los objetos como tablas temporales y variables fuera de la cadena de comando estarán fuera del alcance. Debido a esto, se producirán errores de tiempo de ejecución.

Al usar variables SQL

Mira esto.

DECLARE @extraText VARCHAR(10) = 'Name is '; -- note this variable
DECLARE @sql NVARCHAR(100) = N'SELECT @extraText + FirstName + SPACE(1) + LastName
                               FROM Person.Person WHERE BusinessEntityID = @BusinessEntityID';
DECLARE @paramList NVARCHAR(100) = N'@BusinessEntityID INT';
DECLARE @param1Value INT = 1;

EXEC sp_executesql @sql, @paramList, @BusinessEntityId = @param1Value;
GO

La variable @texto adicional es invisible a los comandos ejecutados. En lugar de esto, una cadena literal o la variable declarada dentro de la cadena SQL dinámica es mucho mejor. De todos modos, el resultado es:

¿Viste ese error en la Figura 1? Si necesita pasar un valor dentro de la cadena SQL dinámica, agregue otro parámetro.

Al usar tablas temporales

Las tablas temporales en SQL Server también existen dentro del alcance de un módulo. Entonces, ¿qué opinas sobre este código?

DECLARE @sql NVARCHAR(200) = N'SELECT BusinessEntityID, LastName, FirstName, MiddleName
                               INTO #TempNames
                               FROM Person.Person
                               WHERE BusinessEntityID BETWEEN 1 and 100';

EXEC sp_executesql @sql;
EXEC sp_executesql N'SELECT * FROM #TempNames'
GO

El código anterior está ejecutando 2 procedimientos almacenados en sucesión. La tabla temporal creada a partir del primer SQL dinámico no será accesible para el segundo. Como resultado, obtendrá un Nombre de objeto no válido #TempNames error.

Error de QUOTENAME del servidor SQL

Aquí hay otro ejemplo que se verá bien a primera vista pero causará un error de nombre de objeto no válido. Vea el código a continuación.

DECLARE @sql NVARCHAR(100) = N'SELECT * FROM @Table';
DECLARE @tableName NVARCHAR(20) = 'Person.Person';

SET @sql = REPLACE(@sql,'@Table',QUOTENAME(@tableName));

PRINT @sql;
EXEC sp_executesql @sql;
GO

Para que sea válido para SELECT, encierre el esquema y la tabla entre corchetes como este:[Esquema].[Tabla] . O no los incluya en absoluto (a menos que el nombre de la tabla incluya uno o más espacios). En el ejemplo anterior, no se usaron corchetes tanto para la tabla como para el esquema. En lugar de usar @Table como parámetro, se convirtió en un marcador de posición para REEMPLAZAR. Se usó QUOTENAME.

QUOTENAME encierra una cadena con un delimitador. Esto también es bueno para tratar con comillas simples en la cadena SQL dinámica. El corchete es el delimitador predeterminado. Entonces, en el ejemplo anterior, ¿qué crees que hizo QUOTENAME? Consulte la Figura 2 a continuación.

La instrucción SQL PRINT nos ayudó a depurar el problema imprimiendo la cadena SQL dinámica. Ahora conocemos el problema. ¿Cuál es la mejor manera de arreglar esto? Una solución es el siguiente código:

DECLARE @sql NVARCHAR(100) = N'SELECT COUNT(*) FROM @Table';
DECLARE @tableName NVARCHAR(20) = 'Person.Person';

SET @sql = REPLACE(@sql,'@Table',QUOTENAME(PARSENAME(@tableName,2)) + '.'
                               + QUOTENAME(PARSENAME(@tableName,1)));
PRINT @sql;
EXEC sp_executesql @sql;
GO

Expliquemos esto.

Primero, @Table se utiliza como marcador de posición para REEMPLAZAR. Buscará la ocurrencia de @Table y reemplácelo con los valores correctos. ¿Por qué? Si se utiliza como parámetro, se producirá un error. Esa es también la razón para usar REEMPLAZAR.

Luego, usamos PARSENAME. La cadena que le pasamos a esta función la separará del esquema y la tabla. PARSENAME(@tableName,2) obtendrá el esquema. Mientras que PARSENAME(@tableName,1) conseguirá la mesa.

Finalmente, QUOTENAME incluirá los nombres de esquema y tabla por separado después de que PARSENAME haya terminado. El resultado es [Persona].[Persona] . Ahora, es válido.

Sin embargo, más adelante se mostrará una mejor manera de desinfectar la cadena SQL dinámica con nombres de objetos.

Ejecutar SP_EXECUTESQL con una instrucción NULL

Hay días en los que estás molesto o desanimado. Se pueden cometer errores en el camino. Luego agregue una cadena SQL dinámica larga a la mezcla y NULL. ¿Y el resultado?

Nada.

SP_EXECUTESQL le dará un resultado en blanco. ¿Cómo? Al concatenar un NULL por error. Considere el siguiente ejemplo:

DECLARE @crlf NCHAR(2);
DECLARE @sql NVARCHAR(200) = N'SELECT' + @crlf +
	' p.Name AS Product' + @crlf +
	',v.Name AS Vendor' + @crlf +
	',v.AccountNumber' + @crlf +
	',p.ListPrice' + @crlf +
	'FROM Purchasing.ProductVendor pv' + @crlf +
	'INNER JOIN Production.Product p ON pv.ProductID = p.ProductID' + @crlf +
	'INNER JOIN Purchasing.Vendor v ON pv.BusinessEntityID = v.BusinessEntityID' + @crlf +
	'WHERE pv.BusinessEntityID = @BusinessEntityID';
DECLARE @paramsList NVARCHAR(100) = N'@BusinessEntityID INT';
DECLARE @BusinessEntityID INT = 1500;

PRINT @sql;
EXEC sp_executesql @sql, @paramsList, @BusinessEntityID
GO

Al principio, el código se ve bien. Sin embargo, los ojos de águila entre nosotros notarán el @crlf variable. ¿Es valioso? La variable no fue inicializada. Entonces, es NULL.

Pero, ¿cuál es el punto de esa variable? En una sección posterior, sabrá lo importante que es el formateo y la depuración. Por ahora, concentrémonos en el punto que nos ocupa.

Primero, la concatenación de una variable NULL a la cadena SQL dinámica dará como resultado NULL. Luego, PRINT imprimirá en blanco. Finalmente, SP_EXECUTESQL funcionará bien con la cadena SQL dinámica NULL. Pero no devuelve nada.

Los NULL pueden hipnotizarnos en un día que ya es malo. Tome un breve descanso. Relax. Luego regresa con la mente más clara.

Inserción de valores de parámetros

Desordenado.

Así es como se verán los valores incorporados en la cadena SQL dinámica. Habrá muchas comillas simples para cadenas y fechas. Si no tiene cuidado, los O'Brien y los O'Neil también causarán errores. Y dado que el SQL dinámico es una cadena, debe CONVERTIR o TRANSMITIR los valores a una cadena. He aquí un ejemplo.

DECLARE @shipDate DATETIME = '06/11/2011';
 DECLARE @productID INT = 750;
 DECLARE @sql NVARCHAR(1000);
 SET @sql = N'SELECT
  soh.ShipDate
 ,sod.ProductID
 ,SUM(sod.OrderQty) AS TotalQty
 ,SUM(sod.LineTotal) AS LineTotal
 FROM Sales.SalesOrderHeader soh
 INNER JOIN Sales.SalesOrderDetail sod ON soh.SalesOrderID = sod.SalesOrderID
 WHERE soh.ShipDate BETWEEN ' + '''' + CONVERT(VARCHAR(10), @shipDate, 101) + '''' + ' AND DATEADD(MONTH,1,' + '''' + CONVERT(VARCHAR(10), @shipDate, 101) + ''') ' +
 'AND sod.ProductID = ' + CAST(@productID AS VARCHAR(8)) +
 ' GROUP BY soh.ShipDate, sod.ProductID' +
 ' ORDER BY sod.ProductID';
 
 PRINT @sql;
 EXEC sp_executesql @sql;

Vi cadenas dinámicas más desordenadas que esta. Observe las comillas simples, CONVERT y CAST. Si se usaran parámetros, esto podría verse mejor. Puedes verlo a continuación

DECLARE @shipDate DATETIME = '06/11/2011';
 DECLARE @productID INT = 750;
 DECLARE @sql NVARCHAR(1000);
 DECLARE @paramList NVARCHAR(500) = N'@shipDate DATETIME, @productID INT';
 SET @sql = N'SELECT
  soh.ShipDate
 ,sod.ProductID
 ,SUM(sod.OrderQty) AS TotalQty
 ,SUM(sod.LineTotal) AS LineTotal
 FROM Sales.SalesOrderHeader soh
 INNER JOIN Sales.SalesOrderDetail sod ON soh.SalesOrderID = sod.SalesOrderID
 WHERE soh.ShipDate BETWEEN @shipDate AND DATEADD(MONTH,1,@shipDate)
 AND sod.ProductID = @productID
  GROUP BY soh.ShipDate, sod.ProductID
  ORDER BY sod.ProductID';

PRINT @sql;
EXEC sp_executesql @sql, @paramList, @shipDate, @productID
GO

¿Ver? Menos comillas simples, sin CONVERT y CAST, y más limpio también.

Pero hay un efecto secundario aún más peligroso de los valores en línea.

Inyección SQL

Si viviéramos en un mundo donde todas las personas fueran buenas, nunca se pensaría en la inyección de SQL. Pero ese no es el caso. Alguien podría inyectar código SQL malicioso en el tuyo. ¿Cómo puede suceder esto?

Este es el escenario que vamos a utilizar en nuestro ejemplo:

  • Los valores se fusionan con la cadena SQL dinámica como nuestro ejemplo anterior. Sin parámetros.
  • La cadena SQL dinámica tiene un máximo de 2 GB con NVARCHAR(MAX). Mucho espacio para inyectar código malicioso.
  • Peor aún, la cadena SQL dinámica se ejecuta con permisos elevados.
  • La instancia de SQL Server acepta la autenticación de SQL.

¿Es esto demasiado? Para los sistemas administrados por un solo hombre, esto puede suceder. Nadie lo controla de todos modos. Para las empresas más grandes, a veces existe un departamento de TI laxo con la seguridad.

¿Sigue siendo esto una amenaza hoy en día? Lo es, según el proveedor de servicios en la nube Akamai en su informe Estado de Internet/Seguridad. Entre noviembre de 2017 y marzo de 2019, la inyección SQL representa casi dos tercios de todos los ataques a aplicaciones web. Esa es la más alta de todas las amenazas examinadas. Muy mal.

¿Quieres verlo por ti mismo?

Práctica de inyección SQL: mal ejemplo

Hagamos algo de inyección SQL en este ejemplo. Puede probar esto en su propio AdventureWorks base de datos. Pero asegúrese de que la autenticación de SQL esté permitida y de ejecutarla con permisos elevados.

DECLARE @lastName NVARCHAR(MAX) = 'Mu';
DECLARE @firstName NVARCHAR(MAX) = 'Zheng''; CREATE LOGIN sà WITH PASSWORD=''12345''; ALTER SERVER ROLE sysadmin ADD MEMBER sà; --';
DECLARE @crlf NCHAR(2) = nchar(13) + nchar(10);

DECLARE @sql NVARCHAR(MAX) = N'SELECT ' + @crlf +
' p.LastName ' + @crlf +
',p.FirstName ' + @crlf +
',a.AddressLine1 ' + @crlf +
',a.AddressLine2 ' + @crlf +
',a.City ' + @crlf +
'FROM Person.Person p ' + @crlf +
'INNER JOIN Person.BusinessEntityAddress bea ON p.BusinessEntityID = bea.BusinessEntityID ' + @crlf +
'INNER JOIN Person.Address a ON bea.AddressID = a.AddressID ' + @crlf +
'WHERE p.LastName = ' + NCHAR(39) + @lastName + NCHAR(39) + ' ' + @crlf +
'AND p.FirstName = '  + NCHAR(39) + @firstName + NCHAR(39);

-- SELECT @sql;	-- uncomment if you want to see what's in @sql					
EXEC sp_executesql @sql;
GO

El código anterior no representa el código real de una empresa existente. Ni siquiera es invocable por una aplicación. Pero esto ilustra la mala acción. Entonces, ¿qué tenemos aquí?

Primero, el código inyectado creará una cuenta SQL que se parece a sa , pero no lo es. Y como sa , esto tiene sysadmin permisos Eventualmente, esto se usará para acceder a la base de datos en cualquier momento con todos los privilegios. Una vez hecho esto, todo es posible:robar, eliminar, manipular datos corporativos, lo que sea.

¿Se ejecutará este código? ¡Definitivamente! Y una vez que lo esté, la súper cuenta se creará en silencio. Y, por supuesto, la dirección de Zheng Mu aparecerá en el conjunto de resultados. Todo lo demás es normal. Shady, ¿no crees?

Después de ejecutar el código anterior, intente ejecutar este también:

SELECT IS_SRVROLEMEMBER('sysadmin','sà')

Si devuelve 1, está dentro, quienquiera que este es. Alternativamente, puede verificarlo en los inicios de sesión de seguridad de su servidor SQL en SQL Server Management Studio.

Entonces, ¿qué obtuviste?

Aterrador, ¿no? (Si esto es real, lo es).

Práctica de inyección SQL:buen ejemplo

Ahora, cambiemos un poco el código usando parámetros. Las demás condiciones siguen siendo las mismas.

DECLARE @lastName NVARCHAR(MAX) = 'Mu';
DECLARE @firstName NVARCHAR(MAX) = 'Zheng''; CREATE LOGIN sà WITH PASSWORD=''12345''; ALTER SERVER ROLE sysadmin ADD MEMBER sà; --';
DECLARE @crlf NCHAR(2) = nchar(13) + nchar(10);

DECLARE @sql NVARCHAR(MAX) = N'SELECT ' + @crlf +
' p.LastName ' + @crlf +
',p.FirstName ' + @crlf +
',a.AddressLine1 ' + @crlf +
',a.AddressLine2 ' + @crlf +
',a.City ' + @crlf +
'FROM Person.Person p ' + @crlf +
'INNER JOIN Person.BusinessEntityAddress bea ON p.BusinessEntityID = bea.BusinessEntityID ' + @crlf +
'INNER JOIN Person.Address a ON bea.AddressID = a.AddressID ' + @crlf +
'WHERE p.LastName = @lastName' + @crlf +
'AND p.FirstName = @firstName';

DECLARE @paramList NVARCHAR(300) = N'@lastName NVARCHAR(50), @firstName NVARCHAR(50)';

-- SELECT @sql;	-- uncomment if you want to see what's in @sql
EXEC sp_executesql @sql, @paramList, @lastName, @firstName;
GO

Hay 2 diferencias en el resultado en comparación con el primer ejemplo.

  • Primero, la dirección de Zheng Mu no aparecerá. El conjunto de resultados está en blanco.
  • Entonces, la cuenta renegada no se crea. Usar IS_SRVROLEMEMBER devolverá NULL.

¿Qué pasó?

Dado que se utilizan parámetros, el valor de @firstName es ‘Zheng”; CREAR LOGIN sà CON CONTRASEÑA=”12345”; ALT’ . Esto se toma como un valor literal y se trunca a solo 50 caracteres. Compruebe el parámetro de nombre en el código anterior. Es NVARCHAR(50). Es por eso que el conjunto de resultados está en blanco. Ninguna persona con un nombre así está en la base de datos.

Este es solo un ejemplo de inyección SQL y una forma de evitarlo. Hay más implicación en hacer algo real. Pero espero haber dejado claro por qué los valores en línea en SQL dinámico son malos.

Rastreo de parámetros

¿Ha experimentado un procedimiento almacenado de ejecución lenta desde una aplicación, pero cuando intentó ejecutarlo en SSMS se volvió rápido? Es desconcertante porque usaste los valores de parámetro exactos que se usan en la aplicación.

Eso es detección de parámetros en acción. SQL Server crea un plan de ejecución la primera vez que se ejecuta o se vuelve a compilar el procedimiento almacenado. Luego, reutilice el plan para la próxima ejecución. Eso suena genial porque SQL Server no necesita volver a crear el plan cada vez. Pero hay veces que un valor de parámetro diferente necesita un plan diferente para ejecutarse rápido.

Aquí hay una demostración usando SP_EXECUTESQL y un SQL estático simple.

DECLARE @sql NVARCHAR(150) = N'SELECT Name FROM Production.Product WHERE ProductSubcategoryID = @ProductSubcategoryID';
DECLARE @paramList NVARCHAR(100) = N'@ProductSubcategoryID INT';
DECLARE @ProductSubcategoryID INT = 23;

EXEC sp_executesql @sql, @paramList, @ProductSubcategoryID

Éste es muy simple. Verifique el plan de ejecución en la Figura 3.

Ahora probemos la misma consulta usando SQL estático.

DECLARE @ProductSubcategoryID INT = 23;
SELECT Name FROM Production.Product WHERE ProductSubcategoryID = @ProductSubcategoryID

Consulte la Figura 4 y luego compárela con la Figura 3.

En la Figura 3, Búsqueda de índice y bucle anidado son usados. Pero en la Figura 4, es un Exploración de índice agrupado . Si bien no hay una penalización perceptible en el rendimiento en este punto, esto muestra que la detección de parámetros no es solo una imaginación.

Esto puede ser muy frustrante una vez que la consulta se vuelve lenta. Puede terminar usando técnicas como recompilar o usar sugerencias de consulta para evitarlo. Cualquiera de estos tiene inconvenientes.

Cadena SQL dinámica sin formato en SP_EXECUTESQL

¿Qué puede salir mal con este código?

DECLARE @sql NVARCHAR(100) = N'SELECT COUNT(*) AS ProductCount' +
                              'FROM Production.Product';
PRINT @sql;
EXEC sp_executesql @sql;

Es corto y simple. Pero consulte la Figura 5 a continuación.

Se producen errores si no le importa un solo espacio entre las palabras clave y los objetos al formar la cadena SQL dinámica. Como en la Figura 5, donde el ProductCount el alias de la columna y la palabra clave FROM no tienen espacios intermedios. Se vuelve confuso una vez que una parte de una cadena fluye hacia la siguiente línea de código. Te hace pensar que la sintaxis es correcta.

Observe también que la cadena usó 2 líneas en la ventana de código, pero la salida de PRINT muestra 1 línea. Imagínese si esta es una cadena de comando muy, muy larga. Es difícil encontrar el problema hasta que formatee correctamente la cadena desde la pestaña Mensajes.

Para resolver este problema, agregue un retorno de carro y un avance de línea. Probablemente haya notado una variable @crlf de los ejemplos anteriores. Formatear su cadena SQL dinámica con espacio y una nueva línea hará que la cadena SQL dinámica sea más legible. Esto también es excelente para la depuración.

Considere una instrucción SELECT con JOIN. Necesita varias líneas de código como el siguiente ejemplo.

DECLARE @sql NVARCHAR(400)
DECLARE @shipDate DATETIME = '06/11/2011';
DECLARE @paramList NVARCHAR(100) = N'@shipDate DATETIME';
DECLARE @crlf NCHAR(2) = NCHAR(13) + NCHAR(10);

set @sql = N'SELECT ' + @crlf +
 'soh.ShipDate ' + @crlf +
 ',sod.ProductID ' + @crlf +
 ',SUM(sod.OrderQty) AS TotalQty ' + @crlf +
 ',SUM(sod.LineTotal) AS LineTotal ' + @crlf +
 'FROM Sales.SalesOrderHeader soh ' + @crlf +
 'INNER JOIN Sales.SalesOrderDetail sod ON soh.SalesOrderID = sod.SalesOrderID ' + @crlf +
 'WHERE soh.ShipDate = @shipDate' + @crlf +
 'GROUP BY soh.ShipDate, sod.ProductID ' + @crlf +
 'ORDER BY sod.ProductID';

 PRINT @sql;
 EXEC sp_executesql @sql,@paramList,@shipDate
 GO

Para formatear la cadena, @crlf variable se establece en NCHAR(13), un retorno de carro, y NCHAR(10), un salto de línea. Se concatena a cada línea para dividir una cadena larga de instrucción SELECT. Para ver el resultado en la pestaña Mensajes, usamos IMPRIMIR. Verifique la salida en la Figura 6 a continuación.

La forma en que forma la cadena SQL dinámica depende de usted. Lo que más le convenga para que sea claro, legible y fácil de depurar cuando llegue el momento.

Nombres de objetos sin desinfectar

¿Necesita establecer dinámicamente nombres de tablas, vistas o bases de datos por algún motivo? Luego, debe "desinfectar" o validar estos nombres de objetos para evitar errores.

En nuestro ejemplo, usaremos un nombre de tabla, aunque el principio de validación también puede aplicarse a las vistas. La forma en que lo enfrentes a continuación será diferente.

Anteriormente, usamos PARSENAME para separar el nombre del esquema del nombre de la tabla. También se puede usar si la cadena tiene un servidor y un nombre de base de datos. Pero en este ejemplo, usaremos solo nombres de esquema y tabla. Dejo el resto a sus mentes brillantes. Esto funcionará independientemente de si nombra sus tablas con o sin espacios. Los espacios en los nombres de tablas o vistas son válidos. Entonces, funciona para dbo.MyFoodCravings o [dbo].[Mis antojos de comida] .

EJEMPLO

Vamos a crear una tabla.

CREATE TABLE [dbo].[My Favorite Bikes]
(
	id INT NOT NULL,
	BikeName VARCHAR(50)
)
GO

Luego creamos un script que usará SP_EXECUTESQL. Esto ejecutará una instrucción DELETE genérica para cualquier tabla dada una clave de 1 columna. Lo primero que debe hacer es analizar el nombre completo del objeto.

DECLARE @object NVARCHAR(128) = '[dbo].[My Favorite Bikes]';
DECLARE @schemaName NVARCHAR(128) = PARSENAME(@object,2);
DECLARE @tableName NVARCHAR(128) = PARSENAME(@object,1);

De esta forma separamos el esquema de la tabla. Para validar más, usamos OBJECT_ID. Si no es NULL, entonces es válido.

IF NOT OBJECT_ID(QUOTENAME(@schemaName) + '.' + QUOTENAME(@tableName)) IS NULL
BEGIN
	PRINT @object + ' is valid!'
	-- do the rest of your stuff here
END
ELSE
BEGIN
        PRINT 'Invalid object name ' + @object
	-- if you need to do anything else, insert it here
END

Note también que usamos QUOTENAME. Esto asegurará que los nombres de las tablas con espacios no generen un error encerrándolos entre corchetes.

Pero, ¿qué hay de validar la columna clave? Puede comprobar la existencia de la columna de la tabla de destino en sys.columns .

IF (SELECT COUNT(*) FROM sys.columns
	    WHERE [object_id] = OBJECT_ID(QUOTENAME(@schemaName) + '.' + QUOTENAME(@tableName))
		  AND [name] = @idKey) > 0
BEGIN
     -- add miscellaneous code here, if needed
     EXEC sp_executesql @sql, @paramsList, @id
END
ELSE
BEGIN
     PRINT 'Invalid column name ' + @idKey + ' for object ' + @object
     -- if you need to do anything else, insert it here
END

Ahora, aquí está el guión completo de lo que queremos lograr.

DECLARE @object NVARCHAR(128) = '[dbo].[My Favorite Bikes]';
DECLARE @schemaName NVARCHAR(128) = PARSENAME(@object,2);
DECLARE @tableName NVARCHAR(128) = PARSENAME(@object,1);
DECLARE @isDebug BIT = 1;
DECLARE @idKey NVARCHAR(128) = N'id';

DECLARE @sql NVARCHAR(200) = N'DELETE FROM @object WHERE @idKey = @id';
DECLARE @id INT = 0;
DECLARE @paramList NVARCHAR(100) = N'@id INT';

IF NOT OBJECT_ID(QUOTENAME(@schemaName) + '.' + QUOTENAME(@tableName)) IS NULL
BEGIN
   PRINT @object + ' is valid!'
   
   IF (SELECT COUNT(*) FROM sys.columns
       WHERE [object_id] = OBJECT_ID(QUOTENAME(@schemaName) + '.' + QUOTENAME(@tableName))
         AND [name] = @idKey) > 0
   BEGIN
       SET @sql = REPLACE(@sql, '@object', QUOTENAME(@schemaName) + '.' +          
                  QUOTENAME(@tableName));
       SET @sql = REPLACE(@sql, '@idkey', QUOTENAME(@idKey));
       IF @isDebug = 1
	   PRINT @sql;
       EXEC sp_executesql @sql, @paramList, @id
   END
   ELSE
       PRINT 'Invalid column name ' + @idKey + ' for object ' + @object
END
ELSE
BEGIN
   PRINT 'Invalid object name ' + @object
   -- if you need to do anything else, insert it here
END
GO

El resultado de este script se muestra en la Figura 7 a continuación.

Puedes probar esto con otras tablas. Simplemente cambie el @object , @idkey y @id valores variables.

Sin disposición para la depuración

Pueden ocurrir errores. Por lo tanto, necesita conocer la cadena SQL dinámica generada para encontrar la causa raíz. No somos adivinos ni magos para adivinar la forma de la cadena SQL dinámica. Por lo tanto, necesita un indicador de depuración.

Observe en la Figura 7 anterior que la cadena SQL dinámica se imprime en la pestaña Mensajes de SSMS. Agregamos un @isDebug BIT y configúrelo en 1 en el código. Cuando el valor es 1, se imprimirá la cadena SQL dinámica. Esto es bueno si necesita depurar una secuencia de comandos o un procedimiento almacenado como este. Simplemente vuelva a configurarlo en cero cuando haya terminado de depurar. Si se trata de un procedimiento almacenado, convierta esta marca en un parámetro opcional con un valor predeterminado de cero.

Para ver la cadena SQL dinámica, puede usar 2 métodos posibles.

  • Use PRINT si la cadena es menor o igual a 8000 caracteres.
  • O use SELECT si la cadena tiene más de 8000 caracteres.

SQL dinámico de bajo rendimiento utilizado en SP_EXECUTESQL

SP_EXECUTESQL puede ser lento si le asigna una consulta de ejecución lenta. Período. Esto no implica problemas con el rastreo de parámetros todavía.

Entonces, comience estático con el código que desea ejecutar dinámicamente. Luego, revisa el Plan de Ejecución y ESTADÍSTICAS IO. Vea si faltan índices que necesita crear. Sintonízalo temprano.

El resultado final en SP_EXECUTESQL

Usar SP_EXECUTESQL es como empuñar un arma poderosa. Pero el que lo maneja necesita ser hábil en eso. Aunque esto tampoco es ciencia espacial. Si eres un novato hoy, puede convertirse en sentido común con el tiempo y la práctica.

Esta lista de trampas no está completa. Pero cubre los comunes. Si necesita más información, consulte estos enlaces:

  • La maldición y las bendiciones de SQL dinámico, por Erland Sommarskog
  • SP_EXECUTESQL (Transact-SQL), de Microsoft

¿Me gusta esto? Luego, compártalo en sus plataformas de redes sociales favoritas. También puede compartir con nosotros sus consejos comprobados en la sección Comentarios.