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.