Módulo 2 · SQL Server / Procedures — Capítulo 10

SQL Dinâmico

Construindo queries como texto em tempo de execução — e como fazer isso sem abrir uma porta para SQL injection.

1. Quando SQL "fixo" não é suficiente

Todas as procedures até aqui têm uma query com estrutura fixa: os parâmetros mudam, mas as tabelas, colunas e cláusulas WHERE/ORDER BY são sempre as mesmas. Às vezes isso não é suficiente — por exemplo, permitir que o usuário escolha qual coluna ordenar, ou montar um WHERE com um número variável de condições. É para isso que existe SQL dinâmico: montar o comando como uma string e executá-lo.

2. sp_executesql — a forma correta

Existem duas formas de executar SQL dinâmico: EXEC(string) e sp_executesql. Use sempre a segunda.

SQL ordenação dinâmica com sp_executesql
CREATE PROCEDURE dbo.usp_ClienteListarOrdenado
    @OrdenarPor NVARCHAR(50) = 'Nome'
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @ColunasPermitidas TABLE (Coluna NVARCHAR(50));
    INSERT INTO @ColunasPermitidas VALUES ('Nome'), ('Email'), ('DataCadastro');

    IF NOT EXISTS (SELECT 1 FROM @ColunasPermitidas WHERE Coluna = @OrdenarPor)
        THROW 50000, 'Coluna de ordenação inválida.', 1;

    DECLARE @Sql NVARCHAR(MAX);
    SET @Sql = N'SELECT Id, Nome, Email, DataCadastro FROM dbo.Cliente ORDER BY '
             + QUOTENAME(@OrdenarPor);

    EXEC sp_executesql @Sql;
END;
GO
Por que não simplesmente concatenar e usar EXEC()

Se @OrdenarPor viesse de um campo de tela sem essa validação, alguém poderia enviar algo como Nome; DROP TABLE Cliente;--. Isso é SQL injection — a mesma classe de vulnerabilidade que existe em qualquer linguagem que monta comandos como texto. As três defesas usadas no exemplo acima — lista de valores permitidos, QUOTENAME() e sp_executesql — são a forma correta de mitigar isso.

3. QUOTENAME — obrigatório para identificadores dinâmicos

Nomes de tabela/coluna não podem ser parametrizados como valores (você não pode fazer ORDER BY @Coluna diretamente — T-SQL trataria isso como um valor literal, não como nome de coluna). Por isso, quando o nome de um identificador entra numa string dinâmica, ele precisa passar por QUOTENAME(), que o envolve em colchetes e neutraliza qualquer caractere especial:

SQL o que QUOTENAME faz
SELECT QUOTENAME('Nome');                  -- [Nome]
SELECT QUOTENAME('Nome]; DROP TABLE X;--'); -- [Nome]]; DROP TABLE X;--]
-- o resultado malicioso vira um NOME DE COLUNA inválido e inofensivo,
-- em vez de ser interpretado como comando

4. Parâmetros de valor continuam parametrizados

Diferente de nomes de coluna, valores (um Id, um texto de busca) devem sempre ser passados como parâmetros de verdade para sp_executesql — nunca concatenados na string:

SQL SQL dinâmico com parâmetros de valor tipados
CREATE PROCEDURE dbo.usp_ClienteBuscarDinamico
    @OrdenarPor NVARCHAR(50) = 'Nome',
    @Nome       NVARCHAR(100) = NULL
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @ColunasPermitidas TABLE (Coluna NVARCHAR(50));
    INSERT INTO @ColunasPermitidas VALUES ('Nome'), ('Email'), ('DataCadastro');

    IF NOT EXISTS (SELECT 1 FROM @ColunasPermitidas WHERE Coluna = @OrdenarPor)
        THROW 50000, 'Coluna de ordenação inválida.', 1;

    DECLARE @Sql NVARCHAR(MAX);
    SET @Sql = N'SELECT Id, Nome, Email FROM dbo.Cliente '
             + N'WHERE (@NomeParam IS NULL OR Nome LIKE ''%'' + @NomeParam + ''%'') '
             + N'ORDER BY ' + QUOTENAME(@OrdenarPor);

    EXEC sp_executesql
        @Sql,
        N'@NomeParam NVARCHAR(100)',
        @NomeParam = @Nome;
END;
GO

Note a diferença: @OrdenarPor (um identificador) passa por QUOTENAME e é concatenado direto na string, porque não existe outra forma de parametrizar nome de coluna. Já @Nome (um valor) é passado como parâmetro real de sp_executesql, com tipo declarado — exatamente como um parâmetro normal de procedure, ganhando a mesma proteção contra injection e o mesmo reaproveitamento de plano de execução.

5. Onde isso te leva

SQL dinâmico bem escrito resolve casos que o SQL estático não cobre, sem abrir brecha de segurança. O próximo capítulo volta a um assunto mais estrutural: triggers, e como (e quando) elas se relacionam com procedures.

📌 Resumo do capítulo

  • Prefira sempre sp_executesql a EXEC(string) — ele permite parâmetros tipados e reaproveita plano de execução.
  • Nomes de tabela/coluna dinâmicos sempre passam por QUOTENAME(), e idealmente validados contra uma lista de valores permitidos.
  • Valores (não identificadores) sempre vão como parâmetros reais de sp_executesql, nunca concatenados na string — essa é a defesa central contra SQL injection.

✏️ Praticando

  1. Implemente usp_ClienteBuscarDinamico e teste com diferentes valores de @OrdenarPor, incluindo um valor inválido para confirmar que o THROW dispara.
  2. Tente "quebrar" a procedure passando @OrdenarPor = 'Nome; SELECT 1--' e confirme que a validação contra a lista de colunas permitidas bloqueia isso antes mesmo de chegar no QUOTENAME.