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

Execução SQL dinâmica no SQL Server


SQL dinâmico é uma instrução construída e executada em tempo de execução, geralmente contendo partes de strings SQL geradas dinamicamente, parâmetros de entrada ou ambos.

Vários métodos estão disponíveis para construir e executar comandos SQL gerados dinamicamente. O artigo atual irá explorá-los, definir seus aspectos positivos e negativos e demonstrar abordagens práticas para otimizar consultas em alguns cenários frequentes.

Usamos duas maneiras de executar SQL dinâmico:EXEC comando e sp_executesql procedimento armazenado.

Usando o comando EXEC/EXECUTE


Para o primeiro exemplo, criamos uma instrução SQL dinâmica simples do AdventureWorks base de dados. O exemplo tem um filtro que é passado pela variável de string concatenada @AddressPart e executado no ú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)

Observe que as consultas criadas por concatenação de strings podem fornecer vulnerabilidades de injeção de SQL. Aconselho vivamente que se familiarize com este tema. Se você planeja usar esse tipo de arquitetura de desenvolvimento, especialmente em um aplicativo da Web voltado para o público, será mais do que útil.

Em seguida, devemos lidar com valores NULL em concatenações de strings . Por exemplo, a variável de instância @AddressPart do exemplo anterior poderia invalidar a instrução SQL inteira se esse valor fosse passado.

A maneira mais fácil de lidar com esse problema potencial é usar a função ISNULL para construir uma instrução SQL válida :
SET @SQLExec = 'SELECT * FROM Person.Address WHERE AddressLine1 LIKE ''%' + ISNULL(@AddressPart, ‘ ‘) + '%'''



Importante! O comando EXEC não foi projetado para reutilizar planos de execução em cache! Ele criará um novo para cada execução.

Para demonstrar isso, executaremos a mesma consulta duas vezes, mas com um valor diferente de parâmetro de entrada. Em seguida, comparamos os planos de execução em ambos os 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 *%';

Usando o procedimento estendido sp_executesql


Para usar esse procedimento, precisamos fornecer uma instrução SQL, a definição dos parâmetros usados ​​nele e seus valores. A sintaxe é a seguinte:
sp_executesql @SQLStatement, N'@ParamNameDataType' , @Parameter1 = 'Value1'

Vamos começar com um exemplo simples que mostra como passar uma instrução e 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

Ao contrário do comando EXEC, o comando sp_executesql o procedimento armazenado estendido reutiliza os planos de execução se executados com a mesma instrução, mas com parâmetros diferentes. Portanto, é melhor 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 em procedimentos armazenados


Até agora, usávamos SQL dinâmico em scripts. No entanto, benefícios reais se tornam aparentes quando executamos essas construções em objetos de programação personalizados – procedimentos armazenados pelo usuário.

Vamos criar um procedimento que procurará uma pessoa no banco de dados AdventureWorks, com base nos diferentes valores de parâmetro do procedimento de entrada. A partir da entrada do usuário, construiremos o comando SQL dinâmico e o executaremos para retornar o resultado ao aplicativo de chamada do usuário:
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 SAÍDA em sp_executesql


Podemos usar sp_executesql com o parâmetro OUTPUT para salvar o valor retornado pela instrução SELECT. Conforme mostrado no exemplo abaixo, isso fornece o número de linhas retornadas pela consulta para a variável de saída @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

Proteção contra injeção de SQL com procedimento sp_executesql


Há duas atividades simples que você deve fazer para reduzir significativamente o risco de injeção de SQL. Primeiro, coloque os nomes das tabelas entre colchetes. Segundo, verifique no código se existem tabelas no banco de dados. Ambos os métodos estão presentes no exemplo abaixo.

Estamos criando um procedimento armazenado simples e executando-o com 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'

Comparação de recursos do comando EXEC e do procedimento armazenado sp_executesql

Comando EXEC procedimento armazenado sp_executesql
Sem reutilização do plano de cache Reutilização do plano de cache
Muito vulnerável à injeção de SQL Muito menos vulnerável à injeção de SQL
Nenhuma variável de saída Suporta variáveis ​​de saída
Sem parametrização Suporta parametrização

Conclusão


Esta postagem demonstrou duas maneiras de implementar a funcionalidade SQL dinâmica no SQL Server. Aprendemos por que é melhor usar o sp_executesql procedimento se estiver disponível. Além disso, esclarecemos a especificidade do uso do comando EXEC e as demandas para higienizar as entradas do usuário para evitar injeção de SQL.

Para a depuração precisa e confortável de procedimentos armazenados no SQL Server Management Studio v18 (e superior), você pode usar o recurso T-SQL Debugger especializado, uma parte da popular solução dbForge SQL Complete.