Sqlserver
 sql >> Base de Dados >  >> RDS >> Sqlserver

10 armadilhas SP_EXECUTESQL a serem evitadas para um SQL dinâmico melhor


Você sabe o quão poderosa uma ferramenta como o SQL dinâmico pode ser? Use-o da maneira errada e você pode permitir que alguém assuma o controle de seu banco de dados. Além disso, pode haver muita complexidade. Este artigo tem como objetivo apresentar as armadilhas ao usar o SP_EXECUTESQL e oferece as 10 armadilhas mais comuns a serem evitadas.

SP_EXECUTESQL é uma das maneiras de executar comandos SQL incorporados em uma string. Você cria essa string dinamicamente por meio do código. É por isso que chamamos isso de SQL dinâmico. Além de uma série de instruções, você também pode passar para ele uma lista de parâmetros e valores. Na verdade, esses parâmetros e valores diferem do comando EXEC. EXEC não aceita parâmetros para SQL dinâmico. No entanto, você executa SP_EXECUTESQL usando EXEC!

Para um novato em SQL dinâmico, veja como você invoca isso.
EXEC sp_executesql <command string>[, <input or output parameters list>, <parameter value1>, <parameter value n>]

Você forma a sequência de comandos que incluem instruções SQL válidas. Opcionalmente, você pode passar uma lista de parâmetros de entrada ou saída e seus tipos de dados. E, finalmente, você passa uma lista de valores separados por vírgulas. Se você passar parâmetros, precisará passar valores. Mais tarde, você verá exemplos certos e errados disso enquanto lê.

Usando SP_EXECUTESQL quando você não precisa


Está certo. Se não precisa, não use. Se isso se tornar os 10 mandamentos do SP_EXECUTESQL, este é o primeiro. É porque este procedimento do sistema pode ser facilmente abusado. Mas como você sabe?

Responda isto:Existe algum problema se o comando em seu SQL dinâmico se tornar estático? Se você não tem nada a dizer sobre este ponto, então você não precisa disso. Veja o exemplo.
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 você pode ver, o uso de SP_EXECUTESQL é completo com uma string de comando, parâmetro e valor. Mas precisa ser assim? Certamente não. É perfeitamente bom ter isso:
DECLARE @productID INT = 1;

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

Mas eu posso ficar envergonhado quando você ver os exemplos mais adiante no artigo. Porque vou contradizer o que estou afirmando neste primeiro ponto. Você verá instruções SQL dinâmicas curtas que são melhores que estáticas. Então, tenha paciência comigo porque os exemplos só provarão os pontos descritos aqui. Para o resto dos exemplos, finja por um tempo que você está olhando para o código destinado ao SQL dinâmico.

Objetos e variáveis ​​fora do escopo


Executar uma série de comandos SQL em uma string usando SP_EXECUTESQL é como criar um procedimento armazenado sem nome e executá-lo. Sabendo disso, objetos como tabelas temporárias e variáveis ​​fora da string de comando estarão fora do escopo. Devido a isso, ocorrerão erros de tempo de execução.

Ao usar variáveis ​​SQL


Veja isso.
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

A variável @extraText é invisível aos comandos executados. Em vez disso, uma string literal ou a variável declarada dentro da string SQL dinâmica é muito melhor. De qualquer forma, o resultado é:

Você viu esse erro na Figura 1? Se você precisar passar um valor dentro da string SQL dinâmica, adicione outro parâmetro.

Ao usar tabelas temporárias


Tabelas temporárias no SQL Server também existem no escopo de um módulo. E aí, o que você achou desse 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

O código acima está executando 2 procedimentos armazenados em sucessão. A tabela temporária criada a partir do primeiro SQL dinâmico não será acessível ao segundo. Como resultado, você receberá um Nome de objeto inválido #TempNames erro.

Erro do SQL Server QUOTENAME


Aqui está outro exemplo que parecerá bom à primeira vista, mas causará um erro de nome de objeto inválido. Veja o código abaixo.
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 ser válido para SELECT, coloque o esquema e a tabela entre colchetes como este:[Schema].[Table] . Ou não os inclua (a menos que o nome da tabela inclua um ou mais espaços). No exemplo acima, nenhum colchete foi usado para a tabela e o esquema. Em vez de usar @Table como parâmetro, tornou-se um espaço reservado para REPLACE. O QUOTENAME foi usado.

QUOTENAME inclui uma string com um delimitador. Isso também é bom para lidar com aspas simples na string SQL dinâmica. O colchete é o delimitador padrão. Então, no exemplo acima, o que você acha que QUOTENAME fez? Verifique a Figura 2 abaixo.

A instrução SQL PRINT nos ajudou a depurar o problema imprimindo a string SQL dinâmica. Agora sabemos o problema. Qual é a melhor maneira de corrigir isso? Uma solução é o código abaixo:
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

Vamos explicar isso.

Primeiro, @Tabela é usado como um espaço reservado para REPLACE. Ele irá procurar a ocorrência de @Table e substitua-o pelos valores corretos. Por quê? Se for usado como parâmetro, ocorrerá um erro. Essa também é a razão para usar REPLACE.

Em seguida, usamos PARSENAME. A string que passamos para esta função irá separá-la em esquema e tabela. PARSENAME(@tableName,2) obterá o esquema. Enquanto PARSENAME(@tableName,1) vai pegar a mesa.

Por fim, QUOTENAME incluirá os nomes do esquema e da tabela separadamente após a conclusão de PARSENAME. O resultado é [Pessoa].[Pessoa] . Agora, é válido.

No entanto, uma maneira melhor de limpar a string SQL dinâmica com nomes de objetos será mostrada posteriormente.

Executando SP_EXECUTESQL com uma instrução NULL


Há dias em que você está chateado ou desanimado. Erros podem ser cometidos ao longo do caminho. Em seguida, adicione uma longa string SQL dinâmica à mistura e NULLs. E o resultado?

Nada.

SP_EXECUTESQL lhe dará um resultado em branco. Como? Concatenando um NULL por engano. Considere o exemplo abaixo:
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

A princípio, o código parece bom. No entanto, os olhos de águia entre nós notarão o @crlf variável. Seu valor? A variável não foi inicializada. Então, é NULO.

Mas qual é o objetivo dessa variável? Em uma seção posterior, você saberá como é importante na formatação e depuração. Por enquanto, vamos nos concentrar no ponto em questão.

Primeiro, a concatenação de uma variável NULL para a string SQL dinâmica resultará em NULL. Em seguida, PRINT imprimirá em branco. Finalmente, SP_EXECUTESQL funcionará bem com a string SQL dinâmica NULL. Mas não retorna nada.

NULLs podem nos hipnotizar em um dia já ruim. Faça uma pequena pausa. Relaxar. Então volte com uma mente mais clara.

Valores de parâmetro embutido


Bagunçado.

É assim que os valores embutidos na string SQL dinâmica se parecerão. Haverá muitas aspas simples para strings e datas. Se você não tomar cuidado, os O'Briens e O'Neils também causarão erros. E como o SQL dinâmico é uma string, você deve CONVERTER ou CAST os valores para string. Aqui está um exemplo.
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;

Eu vi cordas dinâmicas mais confusas do que isso. Observe as aspas simples, CONVERT e CAST. Se os parâmetros fossem usados, isso poderia parecer melhor. Você pode ver abaixo
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 aspas simples, sem CONVERT e CAST, e mais limpo também.

Mas há um efeito colateral ainda mais perigoso dos valores inline.

Injeção de SQL


Se vivêssemos em um mundo onde todas as pessoas fossem boas, a injeção de SQL nunca seria pensada. Mas esse não é o caso. Alguém pode injetar código SQL malicioso no seu. Como isso pode acontecer?

Aqui está o cenário que vamos usar em nosso exemplo:
  • Os valores são fundidos à string SQL dinâmica como nosso exemplo anterior. Sem parâmetros.
  • A string SQL dinâmica tem até 2 GB usando NVARCHAR(MAX). Muito espaço para injetar código malicioso.
  • Pior ainda, a string SQL dinâmica é executada com permissões elevadas.
  • A instância do SQL Server aceita autenticação SQL.

Isso é demais? Para sistemas gerenciados por um homem, isso pode acontecer. Ninguém o verifica de qualquer maneira. Para empresas maiores, às vezes existe um departamento de TI negligente com a segurança.

Isso ainda é uma ameaça hoje? É, de acordo com o provedor de serviços em nuvem Akamai em seu relatório State of the Internet/Security. Entre novembro de 2017 e março de 2019, a injeção de SQL representa quase dois terços de todos os ataques a aplicativos da web. Essa é a maior de todas as ameaças examinadas. Muito mal.

Você quer ver por si mesmo?

Prática de injeção de SQL: exemplo ruim


Vamos fazer alguma injeção de SQL neste exemplo. Você pode tentar isso em seu próprio AdventureWorks base de dados. Mas certifique-se de que a autenticação SQL seja permitida e que você a execute com permissões elevadas.
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

O código acima não representa o código real de uma empresa existente. Nem mesmo é chamado por um aplicativo. Mas isso ilustra a má ação. Então, o que temos aqui?

Primeiro, o código injetado criará uma conta SQL que se parece com sa , Mas isso não. E curta sa , isso tem administrador de sistema permissões. Eventualmente, isso será usado para acessar o banco de dados a qualquer momento com privilégios totais. Tudo é possível uma vez que isso é feito:roubar, excluir, adulterar dados corporativos, você escolhe.

Este código será executado? Definitivamente! E assim que for, a super conta será criada silenciosamente. E, claro, o endereço de Zheng Mu aparecerá no conjunto de resultados. Todo o resto é normal. Sombrio, você não acha?

Depois de executar o código acima, tente executar isso também:
SELECT IS_SRVROLEMEMBER('sysadmin','sà')

Se retornar 1, ele está dentro, seja quem for é. Como alternativa, você pode verificá-lo nos Logins de Segurança do SQL Server no SQL Server Management Studio.

Então, o que você conseguiu?

Assustador, não é? (Se isso é real, é.)

Prática de injeção de SQL:bom exemplo


Agora, vamos alterar um pouco o código usando parâmetros. As outras condições continuam as mesmas.
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

Existem 2 diferenças no resultado em comparação com o primeiro exemplo.
  • Primeiro, o endereço de Zheng Mu não aparecerá. O conjunto de resultados está em branco.
  • Então, a conta renegada não é criada. Usar IS_SRVROLEMEMBER retornará NULL.

O que aconteceu?

Como os parâmetros são usados, o valor de @firstName é ‘Zheng”; CRIAR LOGIN sà COM SENHA=”12345”; ALT' . Isso é considerado um valor literal e truncado para apenas 50 caracteres. Verifique o parâmetro first name no código acima. É NVARCHAR(50). É por isso que o conjunto de resultados está em branco. Nenhuma pessoa com um primeiro nome como esse está no banco de dados.

Este é apenas um exemplo de injeção de SQL e uma maneira de evitá-lo. Há mais envolvido em fazer coisas reais. Mas espero ter deixado claro por que os valores embutidos no SQL dinâmico são ruins.

Sniffing de parâmetros


Você experimentou um procedimento armazenado de execução lenta de um aplicativo, mas quando tentou executá-lo no SSMS, ele ficou rápido? É desconcertante porque você usou os valores de parâmetro exatos usados ​​no aplicativo.

Isso é sniffing de parâmetros em ação. O SQL Server cria um plano de execução na primeira vez que o procedimento armazenado é executado ou recompilado. Em seguida, reutilize o plano para a próxima execução. Isso parece ótimo porque o SQL Server não precisa recriar o plano sempre. Mas há momentos em que um valor de parâmetro diferente precisa de um plano diferente para ser executado rapidamente.

Aqui está uma demonstração usando SP_EXECUTESQL e um SQL estático simples.
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

Este é muito simples. Verifique o plano de execução na Figura 3.

Agora vamos tentar a mesma consulta usando SQL estático.
DECLARE @ProductSubcategoryID INT = 23;
SELECT Name FROM Production.Product WHERE ProductSubcategoryID = @ProductSubcategoryID

Confira a Figura 4 e compare-a com a Figura 3.

Na Figura 3, Busca de índice e Loop Aninhado são usados. Mas na Figura 4, é uma Verificação de índice clusterizado . Embora não haja penalidade de desempenho discernível neste momento, isso mostra que a detecção de parâmetros não é apenas uma imaginação.

Isso pode ser muito frustrante quando a consulta se torna lenta. Você pode acabar usando técnicas como recompilar ou usar dicas de consulta para evitá-lo. Qualquer um destes tem desvantagens.

String SQL dinâmica não formatada em SP_EXECUTESQL


O que pode dar errado com este código?
DECLARE @sql NVARCHAR(100) = N'SELECT COUNT(*) AS ProductCount' +
                              'FROM Production.Product';
PRINT @sql;
EXEC sp_executesql @sql;

É curto e simples. Mas verifique a Figura 5 abaixo.

Erros ocorrem se você não se importa com um único espaço entre palavras-chave e objetos ao formar a string SQL dinâmica. Como na Figura 5, onde o ProductCount alias de coluna e a palavra-chave FROM não têm espaço entre elas. Torna-se confuso quando uma parte de uma string flui para a próxima linha de código. Isso faz você pensar que a sintaxe está correta.

Observe também que a string usou 2 linhas na janela de código, mas a saída de PRINT mostra 1 linha. Imagine se esta for uma string de comando muito, muito longa. É difícil encontrar o problema até que você formate corretamente a string na guia Mensagens.

Para resolver esse problema, adicione um retorno de carro e alimentação de linha. Você provavelmente notou uma variável @crlf dos exemplos anteriores. Formatar sua string SQL dinâmica com espaço e uma nova linha tornará a string SQL dinâmica mais legível. Isso é ótimo para depuração também.

Considere uma instrução SELECT com JOIN. Ele precisa de várias linhas de código como o exemplo abaixo.
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 formatar a string, o @crlf variável é definida como NCHAR(13), um retorno de carro, e NCHAR(10), um avanço de linha. Ele é concatenado a cada linha para quebrar uma longa string da instrução SELECT. Para ver o resultado na aba Mensagens, usamos PRINT. Verifique a saída na Figura 6 abaixo.

Como você forma a string SQL dinâmica é com você. O que for mais conveniente para você para torná-lo claro, legível e fácil de depurar quando chegar a hora.

Nomes de objetos não higienizados


Você precisa definir dinamicamente nomes de tabela, exibição ou banco de dados por algum motivo? Então você precisa “higienizar” ou validar esses nomes de objetos para evitar erros.

Em nosso exemplo, usaremos um nome de tabela, embora o princípio de validação também possa se aplicar a visualizações. Como você lida com isso a seguir será diferente.

Anteriormente, usamos PARSENAME para separar o nome do esquema do nome da tabela. Também pode ser usado se a string tiver um nome de servidor e banco de dados. Mas neste exemplo, usaremos apenas nomes de esquema e tabela. Deixo o resto para suas mentes brilhantes. Isso funcionará independentemente de você nomear suas tabelas com ou sem espaços. Espaços em nomes de tabelas ou visualizações são válidos. Então, funciona para dbo.MyFoodCravings ou [dbo].[Meus desejos por comida] .

EXEMPLO

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

Em seguida, criamos um script que utilizará SP_EXECUTESQL. Isso executará uma instrução DELETE genérica para qualquer tabela com uma chave de 1 coluna. A primeira coisa a fazer é analisar o nome completo do objeto.
DECLARE @object NVARCHAR(128) = '[dbo].[My Favorite Bikes]';
DECLARE @schemaName NVARCHAR(128) = PARSENAME(@object,2);
DECLARE @tableName NVARCHAR(128) = PARSENAME(@object,1);

Desta forma separamos o esquema da tabela. Para validar ainda mais, usamos OBJECT_ID. Se não for NULL, então é 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

Observe também que usamos QUOTENAME. Isso garantirá que os nomes de tabela com espaço não acionem um erro ao colocá-los entre colchetes.

Mas que tal validar a coluna chave? Você pode verificar a existência da coluna da tabela de destino em 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

Agora, aqui está o script completo para o que queremos realizar.
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

O resultado deste script está na Figura 7 abaixo.

Você pode tentar isso com outras tabelas. Basta alterar o @object , @idkey e @id valores variáveis.

Nenhuma provisão para depuração


Erros podem acontecer. Portanto, você precisa conhecer a string SQL dinâmica gerada para encontrar a causa raiz. Não somos adivinhos ou mágicos para adivinhar a forma da string SQL dinâmica. Então, você precisa de um sinalizador de depuração.

Observe na Figura 7 anterior que a string SQL dinâmica é impressa na guia Mensagens do SSMS. Adicionamos um @isDebug BIT variável e defina-a como 1 no código. Quando o valor for 1, a string SQL dinâmica será impressa. Isso é bom se você precisar depurar um script ou procedimento armazenado como este. Basta defini-lo de volta para zero quando terminar a depuração. Se este for um procedimento armazenado, torne esse sinalizador um parâmetro opcional com um valor padrão de zero.

Para ver a string SQL dinâmica, você pode usar 2 métodos possíveis.
  • Use PRINT se a string for menor ou igual a 8.000 caracteres.
  • Ou use SELECT se a string tiver mais de 8.000 caracteres.

SQL dinâmico de baixo desempenho usado em SP_EXECUTESQL


SP_EXECUTESQL pode ser lento se você atribuir uma consulta de execução lenta a ele. Período. Isso ainda não envolve problemas com sniffing de parâmetros.

Portanto, comece estático com o código que você deseja executar dinamicamente. Em seguida, verifique o Plano de Execução e ESTATÍSTICAS IO. Veja se há índices ausentes que você precisa criar. Sintonize-o cedo.

O resultado final em SP_EXECUTESQL


Usar SP_EXECUTESQL é como empunhar uma arma poderosa. Mas quem o empunha precisa ser habilidoso nisso. Embora isso também não seja ciência de foguetes. Se você é um novato hoje, pode se tornar senso comum com a prática.

Esta lista de pegadinhas não está completa. Mas cobre os comuns. Se precisar de mais informações, confira esses links:
  • A maldição e as bênçãos do SQL dinâmico, por Erland Sommarskog
  • SP_EXECUTESQL (Transact-SQL), da Microsoft

Assim? Então compartilhe em suas redes sociais favoritas. Você também pode compartilhar conosco suas dicas testadas pelo tempo na seção Comentários.