Você está na página 1de 86

A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.

html

A Maldição e as Bênçãos do SQL Dinâmico


Um texto SQL por Erland Sommarskog, MVP do SQL Server. Última revisão: 30/08/2022.
Os direitos autorais se aplicam a este texto. Veja aqui as convenções de fontes usadas neste
artigo.

1. Introdução
Este é um texto sobre SQL dinâmico. Usado no lugar certo, o SQL dinâmico é um tremendo ativo, mas o
SQL dinâmico também é um dos recursos mais abusados no SQL Server. Frequentemente vejo posts em
fóruns de SQL que usam SQL dinâmico ou pedem por isso, quando não há necessidade, ou o desejo de
usar SQL dinâmico é devido a um erro anterior no design. Muitas vezes, essas postagens são de usuários
SQL inexperientes. É importante entender que o SQL dinâmico é um recurso avançado e, de preferência,
você não deve começar a usar o SQL dinâmico até dominar a gravação de consultas estáticas.

O quanto você deve usar o SQL dinâmico depende da sua função. Se você é um desenvolvedor de
aplicativos, você deve ser restritivo com o uso de SQL dinâmico. O SQL dinâmico certamente tem seu
lugar no código do aplicativo, mas, como veremos neste texto, ele também introduz complexidade, então
há todos os motivos para se segurar. Por outro lado, se você é um DBA, o SQL dinâmico é seu melhor
amigo, porque existem muitas operações de DBA que podem ser automatizadas com a ajuda do SQL
dinâmico. (E ao mesmo tempo em que você usa SQL dinâmico o dia todo, você deve tentar impedir que
os desenvolvedores grudem o SQL dinâmico no código do aplicativo!)

Alguns leitores podem perguntar: o que é SQL dinâmico? SQL dinâmico é quando você escreve código
SQL em uma variável de cadeia de caracteres e, em seguida, executa o conteúdo dessa variável. Isso pode
ser feito no código do cliente ou em um procedimento armazenado. Com uma definição estrita, o SQL
estático só pode ocorrer em procedimentos armazenados e scripts SQL, uma vez que o SQL no código do
cliente é sempre incorporado em literais de cadeia de caracteres na linguagem do lado do cliente. No
entanto, você pode argumentar que um cliente que só tem cadeias de caracteres SQL fixas está usando
SQL estático, e um dos objetivos do artigo é demonstrar como o código do cliente deve ser escrito para
conseguir isso.

O primeiro capítulo após esta introdução fornece os comandos básicos para executar o SQL dinâmico de
maneira adequada e discute as armadilhas que você pode encontrar. Este capítulo também demonstra
como você deve enviar consultas SQL do código do cliente de maneira adequada. O próximo capítulo é
muito importante: ele discute a injeção de SQL, uma ameaça contra a qual você deve sempre se proteger
quando trabalha com SQL dinâmico, não importa se você está usando SQL dinâmico em procedimentos
armazenados ou se está escrevendo código de cliente. Isso é seguido por um pequeno capítulo sobre os
aspectos de desempenho do SQL dinâmico e destaca algumas regras que você deve seguir. Este capítulo
também é amplamente aplicável tanto a procedimentos armazenados quanto ao código do cliente.

A partir deste ponto, no entanto, o artigo deixa o código do cliente de lado e discute apenas SQL dinâmico
em procedimentos armazenados e scripts SQL. O capítulo mais longo do artigo se concentra em um dos
grandes problemas do SQL dinâmico: se você não tiver a disciplina, o código que gera SQL dinâmico
pode facilmente se tornar ilegível e difícil de manter. Este capítulo é um guia de estilo com dicas para
ajudá-lo a escrever código que gera SQL dinâmico de uma maneira melhor. O penúltimo capítulo deste
artigo discute alguns bons casos de uso para SQL dinâmico. Este capítulo inclui uma seção sobre como
executar um pivô dinâmico – algo sobre o qual vejo perguntas diariamente em fóruns SQL. A última seção
aborda situações em que muitas vezes vejo o SQL dinâmico sendo abusado e que muitas vezes são
devidas a uma má escolha de design no início do processo.

Este artigo destina-se a uma ampla gama de trabalhadores SQL. Particularmente os primeiros capítulos
são escritos com o desenvolvedor SQL inexperiente em mente, e DBAs experientes e programadores
1 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

líderes podem achar o material excessivamente básico. No entanto, você ainda pode estar interessado em
ler este material para obter munição para manter seus devs sob controle para que eles não abusem do SQL
dinâmico. Programadores experientes podem descobrir coisas no capítulo do guia de estilo que você não
conhecia ou não havia pensado. Por outro lado, os programadores menos experientes podem achar a
última parte deste capítulo um pouco difícil de digerir, e você pode preferir desistir e passar para a seção
sobre, digamos, pivô dinâmico, se este for o problema que você tem em mãos.

2 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

Índice
1. Introdução
1.1 Versões SQL aplicáveis e banco de dados de
demonstração
2. Como executar o SQL dinâmico
2.1 sp_executesql
2.2 Armadilhas e armadilhas
2.3 EXEC()
2.4 Servidor vinculado EXEC() AT
2.5 Executando SQL dinâmico a partir do código do cliente
3. Cuidado com a injeção de SQL
3.1 Injeção de SQL no código do cliente
3.2 Injeção de SQL em SQL dinâmico em procedimentos
armazenados
3.3 Quotename e quotestring
3.4 A importância do nvarchar
3.5 Outros furos de injeção
4. Desempenho
5. Trabalhando com SQL dinâmico – um guia de estilo
5.1 Iniciar estático
5.2 A necessidade de depurar impressões
5.3 Mensagens de erro e números de linha
5.4 Espaçamento e formatação
5.5 Lidando com cadeias de caracteres aninhadas
5.6 Uma armadilha com cadeias de caracteres SQL longas
5.7 Recebendo nomes de tabelas e outros objetos como
parâmetros
5.8 Nomes dinâmicos de banco de dados e servidor
5.9 Fazendo algo em todos os bancos de dados
5.10 Cursores e SQL dinâmico
5.11 Uma maneira limpa de lidar com nomes dinâmicos
5.12 Saiba mais sobre nomes dinâmicos e cadeias de
caracteres aninhadas
5.13 Brincando com QUOTED_IDENTIFIER
5.14 Outros exemplos
6. Alguns bons casos de uso para SQL dinâmico
6.1 Condições de pesquisa dinâmica
6.2 Acessando fontes de dados remotas
6.3 Configurando sinônimos para acesso entre bancos de
dados
6.4 INSERÇÃO EM MASSA E CONJUNTO DE
OPENROW(BULK)
6.5 Tarefas de manutenção em geral
6.6 Pivô dinâmico – Transformando linhas em colunas
7. Situações em que você (provavelmente) não deve usar o SQL
dinâmico
7.1 Nomes de tabela dinâmica no código do aplicativo
7.2 Nomes de colunas dinâmicas

3 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

7.3 Definir aliases de coluna dinamicamente


7.4 Nomes de procedimentos dinâmicos
7.5 Dinâmico ORDER BY Condition
7.6 SELECIONE * FROM tbl WHERE col IN (@list)
7.7 Enviando uma cláusula WHERE como um parâmetro
7.8 Trabalhando com uma tabela com colunas
desconhecidas
7.9 BACKUP/RESTAURAÇÃO
8. Conclusão
8.1 Reconhecimento e Feedback 9. Revisões

4 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

1.1 Versões SQL aplicáveis e banco de dados de demonstração


Este artigo se aplica a todas as versões do SQL Server do SQL 2005 e posterior. Não há grandes diferenças
entre diferentes versões do SQL em relação ao SQL dinâmico desde esta versão. Há alguns aprimoramentos
menores que foram adicionados ao longo do caminho, e vou chamá-los à medida que chegarmos a eles.

No entanto, isso não significa que todos os exemplos no artigo serão executados no SQL 2005 como
estão, mas tomei a liberdade de adotar a sintaxe mais recente onde isso ajuda a tornar o código mais curto
ou conciso. Essas são pequenas coisas, e você pode modificar facilmente o código se quiser executar os
exemplos em uma versão mais antiga do SQL Server. Alguns desses recursos mais recentes aparecem
apenas em um ou dois lugares, e para estes eu aponto as diferenças à medida que aparecem. Há também
alguns recursos que eu uso repetidamente, e que eu listo abaixo e geralmente não menciono mais.

CRIAR OU ALTERAR. Ao longo do artigo, utilizo CRIAR OU ALTERAR PROCEDIMENTO. Essa sintaxe
foi introduzida no SQL 2016 SP1. Em versões anteriores, você terá que usar CREATE pela primeira
vez e, em seguida, alterar para ALTER se o mesmo procedimento for modificado.
IIF. IIF(condition, X, Y) é a abreviação de CASE WHEN condition THEN X ELSE Y END. O IIF foi
introduzido no SQL 2012, portanto, com versões mais antigas, você precisará substituir o IIF pela
expressão CASE correspondente .
INSERIR VALORES para várias linhas. Essa sintaxe foi introduzida no SQL 2008, portanto, no SQL
2005, você precisará reescrevê-la com várias instruções INSERT VALUES.
DECLARE com inicialização. Isso foi adicionado no SQL 2008 e, com o SQL 2005, você precisará
dividir isso em DECLARE + SET.

O banco de dados de demonstração para este artigo é NorthDynamic, que você pode criar executando o
script
NorthDynamic.sql. O banco de dados é muito pequeno, menos de 10 MB. O script é executado em todas
as versões do SQL
Servidor do SQL 2005 e superior. Nem todos os exemplos se baseiam nesse banco de dados; nesses casos,
o artigo tem instruções CREATE TABLE quando necessário. Eu recomendo que você trabalhe em tempdb
para esses exemplos.

NorthDynamic é uma versão ligeiramente modificada do antigo banco de dados de demonstração da


Microsoft Northwind, baseado na empresa fictícia Northwind Traders que floresceu entre 1996 e 1998,
isso explica por que todos os pedidos são deste período de tempo. (Se você está familiarizado com
Northwind e gostaria de saber quais são as diferenças: Eu substituí tipos de dados obsoletos para que o
banco de dados possa ser instalado com qualquer agrupamento. Também fiz algumas modificações no
esquema e nos dados para o benefício de alguns exemplos que eu queria incluir. Dei um novo nome ao
banco de dados caso eu queira outra variação de Northwind para outro artigo.)

2. Como executar o SQL dinâmico


Neste capítulo, veremos como executar SQL dinâmico de forma civilizada. Eu gosto de ressaltar que não
há nada nos exemplos deste capítulo que exija SQL dinâmico, e desse ponto de vista eles são maus
exemplos, porque você não deve usar SQL dinâmico quando não há razão para isso. No entanto, optei por
fazê-lo desta forma, porque queria focar no essencial e manter os exemplos simples. Casos de uso reais
virão nos capítulos posteriores.

2.1 sp_executesql
sp_executesql é o procedimento do sistema que você invoca para executar o SQL dinâmico. Uma maneira
de descrever esse procedimento é que ele cria um procedimento armazenado sem nome que é salvo no

5 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

cache do plano (mas não no disco) e, em seguida, executa o procedimento diretamente. Em chamadas
subsequentes para esse procedimento sem nome, o plano armazenado em cache é reutilizado. Neste ponto,
no entanto, não nos preocuparemos muito com as ramificações no cache do plano, mas salvaremos isso no
capítulo de desempenho . Por enquanto, basta pensar nisso como "cria um procedimento e o executa
diretamente".

E isso é uma coisa muito importante: um lote de SQL dinâmico é muito semelhante a um procedimento
armazenado. Sobre tudo o que você pode fazer em um procedimento armazenado, você pode fazer em um
lote de SQL dinâmico. (Há uma diferença importante em relação às permissões que abordarei na próxima
seção, e alguns itens mais marginais que abordarei neste texto.) E talvez o mais importante de tudo: você
pode usar parâmetros. Isso é algo que é muito essencial que você deve fazer uso. O inverso também se
aplica: não há sintaxe válida dentro do SQL dinâmico, ou seja, que não é válida em um procedimento
armazenado. Tudo o que é especial é que o SQL dinâmico é armazenado em uma cadeia de caracteres e,
em seguida, executado.

sp_executesql tem dois parâmetros fixos: @stmt e @params. @stmt é o texto do código SQL a ser
executado. O fato de o nome ser singular é um pouco enganoso: o código SQL não precisa ser uma única
instrução, mas pode ser um lote de qualquer comprimento com qualquer número de instruções. Como
você pode imaginar, @stmt é um parâmetro obrigatório. @params contém a lista de parâmetros para o
código em @stmt, e o formato dela é exatamente como o formato da lista de parâmetros para um
procedimento armazenado. @params não é obrigatório, mas assim como a maioria dos procedimentos
armazenados usa parâmetros, a maioria dos lotes de SQL dinâmico o faz. É na medida em que toda vez
que você vê uma chamada para sp_executesql sem parâmetros ao revisar o código, há todos os motivos
para verificar se o programador não está cometendo o erro de concatenar os valores de parâmetro na
cadeia de caracteres, o que é uma coisa muito ruim de se fazer. Depois @params siga os valores reais
para passar para a lista de parâmetros em @params da mesma maneira que você passa parâmetros para
um procedimento armazenado regular.

Aqui está um script de exemplo:


DECLARE @sql nvarchar(MAX),
@params Nvarchar(4000)

SELECIONE @sql = N'SELECT @cnt = COUNT(*)


DE dbo. Ordens
WHERE OrderDate >= @date
E OrderDate < dateadd(MONTH, 1, @date)

SELECT CompanyName
DE dbo. Clientes
WHERE CustomerID = @custid'

SELECIONE @params = N'@date data,


@cnt SAÍDA int,
@custid nchar(5) = N''ALFKI'''

DECLARE @cnt int,


@when data = '19980201'

SAÍDA DE sp_executesql @sql, @params, @when @cnt EXECUTIVOS


SELECT @cnt AS [Pedidos em Fev 1998]

EXECUTIVO sp_executesql @sql, @params,


@date = '19970801', @cnt = @cnt SAÍDA, @custid = 'VINET' SELECT @cnt AS [Pedidos em
julho de 1997]

O script começa com a declaração de duas variáveis que mais tarde passo para sp_executesql. @sql é a
variável que passarei para o parâmetro @stmt; Uso o nome @sql pois isso me parece mais natural.
Observe que o tipo de dados da variável é nvarchar(MAX). Você sempre deve usar esse tipo de dados para
6 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

seus lotes de SQL dinâmico, sem nenhuma exceção. Você deve usar nvarchar, e não pode usar varchar,
pela simples razão de que sp_executesql só aceita nvarchar e barfs se você tentar usar varchar. MAX, por
outro lado, não é um requisito para sp_executesql, mas serve para proteger sua sanidade. Ou seja, se você
usar um tipo limitado como nvarchar(1000), há o risco de que o lote que você compõe exceda o
comprimento especificado. Isso leva a truncamento silencioso e erros que podem deixá-lo louco enquanto
você está tentando entender o que está acontecendo.

Para @params, eu uso nvarchar(4000). Assim como acontece com @stmt, o tipo de dados deve ser
nvarchar; Varchar não é aceito. Quanto ao comprimento de 4000 que é principalmente por hábito. Você
teria que ter muitos parâmetros para preencher esse limite, mas, novamente, há poucas razões para correr
o risco de fazer um palpite em, digamos, 100, e depois obter erros porque era muito baixo.

Em seguida, configurei meu lote SQL atribuindo-o a @sql e, como é um pedaço estático de código, há
pouco a comentar sobre ele. Mais interessante é a atribuição a @params onde configurei a lista de
parâmetros. Há três parâmetros. O primeiro é um parâmetro "simples", enquanto o segundo é um
parâmetro OUTPUT e o terceiro tem um valor padrão. Valores padrão não é nada que seja comumente
usado com SQL dinâmico, mas eu o incluí aqui para enfatizar que a lista de parâmetros é exatamente
como a lista de parâmetros para um procedimento armazenado. Por outro lado, usar parâmetros OUTPUT
com SQL dinâmico é muito comum, já que você geralmente deseja obter dados escalares de volta do seu
lote de SQL dinâmico.

Antes de invocar meu SQL dinâmico, declaro duas variáveis locais. Vejamos novamente a chamada para
sp_executesql:
SAÍDA DE sp_executesql @sql, @params, @when @cnt EXECUTIVOS

O primeiro parâmetro é @sql, ou seja, meu texto SQL, e o segundo é @params, ou seja, minha lista de
parâmetros. Em seguida vem @when, minha variável local que iniciei em 1998-02-01. Esse valor é
passado para o parâmetro @date no lote SQL. Por último vem @cnt que é receber o valor do parâmetro
@cnt no SQL dinâmico. Embora tenham o mesmo nome, são entidades realmente diferentes; mais sobre
isso mais tarde. Assim como quando chamo um procedimento armazenado, preciso especificar OUTPUT
também para o parâmetro real. Eu não passo um valor para o parâmetro @custid, então o padrão para
esse parâmetro se aplica, e a segunda consulta retorna o nome Alfreds Futterkiste, que é o nome
completo do cliente com o ID ALFKI. (Quanto ao porquê do cliente
IDs no NorthDynamic são códigos em vez de IDs numéricos, eu não tenho ideia do que aconteceu na
Northwind Traders nos anos noventa, mas é meio fofo.)

Há um segundo sp_executesql de chamada para o mesmo lote SQL e parâmetros:


EXECUTIVO sp_executesql @sql, @params,
@date = '20180101', @cnt = @cnt SAÍDA, @custid = 'VINET'

Como você pode ver, nesta chamada eu uso parâmetros nomeados em vez de parâmetros posicionais, e
para os parâmetros de entrada @date e @custid eu uso literais para os parâmetros reais. Isso totalmente
alinhado com as chamadas para procedimentos armazenados regulares. Observe particularmente o valor
passado para o parâmetro @custid. Este é um varchar literal (não há N antes dele), enquanto @custid é
nchar(5). Ou seja, quando chegamos a esta parte da lista de parâmetros, somos livres para misturar
(var)char e n(var)char. A restrição que devemos passar nvarchar aplica-se apenas aos parâmetros fixos
@stmt e @params.

Para determinar que isso é como definir um procedimento armazenado e executá-lo ao mesmo tempo,
aqui está o mesmo código logicamente, mas com um procedimento armazenado "real":
CRIAR OU ALTERAR PROCEDIMENTO my_sp @date data,
@cnt SAÍDA int,
@custid nchar(5) = N'ALFKI' AS
7 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

SELECIONE @cnt = COUNT(*)


DE dbo. Ordens
WHERE OrderDate >= @date
E OrderDate < dateadd(MONTH, 1, @date)

SELECT CompanyName
DE dbo. Clientes ONDE CustomerID
= @custid ir
DECLARE @cnt int,
@when data = '19980201'

SAÍDA EXEC my_sp @when, @cnt


SELECT @cnt AS [Pedidos em Fev 1998]

EXEC my_sp @date = '19970701', @cnt = @cnt SAÍDA, @custid = 'VINET' SELECT @cnt AS
[Pedidos em julho de 1997]

Aqui está mais um exemplo para destacar um detalhe:


DECLARE @sql nvarchar(MAX) = 'SELECT CompanyName
DE dbo. Clientes
WHERE CustomerID = @custid'
EXECUTIVO sp_executesql @sql, N'@custid nchar(5)', 'VINET'
Como você vê, neste exemplo eu não tenho nenhuma variável @params, mas defino a lista de parâmetros
diretamente na chamada para sp_executesql. Observe o N que precede esse valor de parâmetro; Isso é
necessário para torná-lo um NVARCHAR literal. Eu tendo a usar esse padrão, se eu tiver uma lista de
parâmetros curta, mas quanto mais longa for a lista, maior a probabilidade de eu colocar a definição em
uma variável para tornar o código mais legível. Também é possível passar um valor literal para o
parâmetro @stmt, mas como o lote SQL é quase frequentemente criado a partir de partes diferentes, você
raramente fará essa prática.

2.2 Armadilhas e armadilhas


Agora você aprendeu a usar sp_executesql para executar SQL dinâmico. Veremos agora algumas
armadilhas que podem surpreendê-lo, se suas expectativas não estiverem de acordo com a funcionalidade
real.

Escopo das Variáveis


Considere este procedimento armazenado bastante absurdo:
CRIAR OU ALTERAR PROCEDIMENTOS mishap_sp COMO
DECLARE @tbl TABLE (um int NOT NULL)
DECLARAR @a int = 99

EXEC sp_executesql N'SELECT A FROM @tbl SELECT @a' go


EXECUTIVO mishap_sp

Quando executamos isso, recebemos duas mensagens de erro:


Msg 1087, Nível 15, Estado 2, Linha 1 Deve declarar a
variável de tabela "@tbl".
Msg 137, Nível 15, Estado 2, Linha 1 Deve declarar
a variável escalar "@a".

Se você tem prestado atenção, já entendeu por que recebemos esse erro: sp_executesql invoca um
procedimento armazenado sem nome. Ou seja, o código
SELECIONE UM DE @tbl SELECIONE @a

8 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

não faz parte do procedimento mishap_sp mas de um procedimento armazenado interno (e sem nome).
Como consequência, a instrução SELECT não pode acessar as variáveis em mishap_sp. No T-SQL, as
variáveis só são visíveis no escopo em que são declaradas; nunca em âmbitos internos.

Nota: escopo é uma palavra comumente usada em programação para indicar onde as variáveis (e outros itens) estão
visíveis. No T-SQL, um escopo pode ser um procedimento armazenado, gatilho, função ou simplesmente um script SQL.
Um lote de SQL dinâmico também é um escopo.

Se você deseja passar variáveis do procedimento circundante para o SQL dinâmico, você sempre deve
passá-las como parâmetros. Para variáveis de tabela, isso requer que você tenha definido um tipo de
tabela, como neste exemplo:
CREATE TYPE dbo.mytbltype AS TABLE (um int NOT NULL) ir
CRIAR OU ALTERAR PROCEDIMENTOS hap_sp COMO
DECLARE @mytbl dbo.mytbltype,
@a int = 99

EXEC sp_executesql N'SELECT A FROM @tbl SELECT @a',


N'@tbl dbo.mytbltype READONLY, @a int', @mytbl, @a go
EXEC hap_sp

Observação: se você ainda estiver no SQL 2005: parâmetros com valor de tabela foram introduzidos no SQL 2008.

O que podem ser parâmetros?


Algumas pessoas podem tentar:
EXEC sp_executesql N'SELECT * FROM @tblname', N'@tblname sysname', 'Funcionários'

Ou seja, eles esperam poder enviar um nome de tabela como parâmetro. (Como discutirei na seção Nomes
de tabela dinâmicos no código do aplicativo no último capítulo, o desejo de fazê-lo geralmente se deve a
um erro de design anterior.) Mas a mensagem de erro é:
Msg 1087, Nível 16, Estado 1, Linha 1
Deve declarar a variável de tabela "@tblname".

Não há nada de mágico com o SQL dinâmico. Não é possível colocar o nome de uma tabela em uma
variável e usá-la em uma cláusula FROM. Também não é possível colocar o nome de uma coluna em uma
variável e interpretá-la como tal. E o mesmo se aplica a quase todos os nomes de objeto, com uma
exceção: o nome de um procedimento usado em uma instrução EXEC. Você verá exemplos disso à medida
que o artigo avança.

Tabelas temporárias e SQL dinâmico


Em contraste com o exemplo de variável de tabela anterior, isso funciona:
CRIAR OU ALTERAR PROCEDIMENTOS hap_sp COMO
CREATE TABLE #temp(b int NOT NULL) EXEC
sp_executesql N'SELECT b FROM #temp' go
EXEC hap_sp

Isso ocorre porque uma tabela temporária é visível em todos os escopos internos invocados pelo módulo
que a criou.

Por outro lado, isso falha:


CRIAR OU ALTERAR PROCEDIMENTOS mishap_sp COMO
EXEC sp_executesql N'SELECT * EM #temp DO DBO. Clientes
SELECIONE * DE #temp
EXECUTIVO mishap_sp

9 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

A mensagem de erro é:
Msg 208, Nível 16, Estado 0, Procedimento mishap_sp, Linha 3 Nome de objeto
inválido '#temp'.

Uma tabela temporária local é descartada automaticamente quando o escopo onde ela foi criada é
encerrado. Ou seja, neste exemplo, #temp é descartada quando o lote de SQL dinâmico é encerrado e,
portanto, você não pode acessá-lo no procedimento armazenado circundante. Você precisa criar a tabela
com CREATE TABLE antes de invocar seu lote SQL. ("Mas eu não conheço o esquema, porque é
dinâmico!", pode objetar algum leitor. Se sim, você provavelmente cometeu um erro de design.
Voltaremos a isso no último capítulo deste artigo.)

SQL dinâmico e funções


Não é possível usar SQL dinâmico em funções definidas pelo usuário, ponto final. A razão é simples: uma
função no SQL Server não tem permissão para alterar o estado do banco de dados e, obviamente, o SQL
Server não pode verificar de antemão o que seu SQL dinâmico está fazendo. Ele também segue da regra
que você não pode chamar procedimentos armazenados em funções definidas pelo usuário e, como
aprendemos, um lote de SQL dinâmico é um procedimento armazenado sem nome.

Permissões
Isso é uma coisa que você precisa estar ciente, porque aqui está algo que é diferente dos procedimentos
armazenados normais. Considere este script onde criamos um usuário de teste que representamos para
testar permissões:
CRIAR OU ALTERAR PROCEDIMENTOS mishap_sp COMO
SELECT COUNT(*) AS custcnt FROM dbo. Clientes
EXEC sp_executesql N'SELECT COUNT(*) COMO empcnt FROM dbo. Funcionários vão
CRIAR USUÁRIO TESTUSER SEM LOGIN
CONCEDER EXECUTAR EM mishap_sp PARA
testuser ir
EXECUTE AS USER = 'testuser'
ir
EXECUTIVO mishap_sp
ir
REVERTER

A saída (com saída definida como texto no SSMS):


Custcnt ----------
91

(1 linha afetada)

Msg 229, Nível 14, Estado 5, Linha 1


A permissão SELECT foi negada no objeto 'Employees', banco de dados 'NorthDynamic', esquema 'dbo'.

Quando testuser executa mishap_sp, o SELECT contra Clientes é bem-sucedido graças a algo conhecido
como encadeamento de propriedade. Como o procedimento e a tabela têm o mesmo proprietário, o SQL
Server não verifica se testuser tem permissão na tabela Customers. No entanto, o SELECT que está no lote
de SQL dinâmico falha. Isso ocorre porque o lote de SQL dinâmico não é considerado como tendo um
proprietário. Como alternativa, o proprietário é considerado o usuário atual, que é testuser. Seja como for,
o encadeamento de propriedade não se aplica e, portanto, o SQL Server verifica se testuser tem
permissão SELECT em Funcionários e, como nunca concedemos nenhuma permissão para testuser, isso
falha.

Assim, se você considerar o uso de SQL dinâmico, precisará conversar com seu DBA ou com quem for
responsável pela segurança no banco de dados para verificar se isso é aceitável. Talvez a política seja que
os usuários só devem ter permissão para executar procedimentos armazenados, mas eles não devem ter
10 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

acesso direto às tabelas. Ainda é possível usar o SQL dinâmico com essa política em vigor, porque você
pode agrupar a permissão com o procedimento armazenado, se você assiná-lo com um certificado e, em
seguida, conceder a um usuário criado a partir desse certificado as permissões necessárias. Essa é uma
técnica que descrevo com muito mais detalhes no meu artigo Permissões de empacotamento em
procedimentos armazenados. Embora esta seja uma solução perfeitamente possível e válida, ela aumenta
um pouco a complexidade do seu sistema e, portanto, aumenta o obstáculo para o uso de SQL dinâmico.

Você está escondendo suas referências


Se você quiser saber onde uma determinada tabela é referenciada, o SQL Server oferece algumas maneiras
de determinar isso. Há as exibições de catálogo sys.sql_dependencies e
sys.sql_expression_dependencies, bem como o procedimento de sistema mais antigo sp_depends. Você
também pode usar Exibir dependências no menu de contexto de uma tabela no Pesquisador de Objetos no
SSMS; sob o capô, o SSMS usa sys.sql_expression_dependencies. No entanto, nenhum deles mostrará
referências feitas no SQL dinâmico, já que quando o SQL Server analisa um procedimento armazenado e
salva as dependências, o SQL dinâmico está apenas em um literal de cadeia de caracteres com o qual o
SQL Server não tem motivos para se preocupar naquele ponto.

Nota: No meu conto SQL, onde essa tabela é usada?, apresento uma solução que lê todo o código SQL em um banco de
dados em uma tabela indexada de texto completo que permite pesquisar o código usando operadores de texto completo
como CONTAINS e, assim, você pode encontrar referências também em SQL dinâmico.

Chintak Chhapia apontou um problema de natureza semelhante: quando você estiver procurando atualizar
sua instância do SQL Server para uma versão mais recente, convém usar o Supervisor de Atualização para
descobrir se há algum problema de compatibilidade em seu código ou se você está usando algum recurso
preterido. No entanto, o Supervisor de Atualização é totalmente cego para o que você faz no SQL
dinâmico.

A instrução RETURN
Além das diferenças com permissões, há mais algumas pequenas diferenças entre SQL dinâmico e
procedimentos armazenados. Nenhum deles é muito significativo, mas aqui está um que Jonathan Van
Houtte encontrou. Ele queria usar uma instrução RETURN com um valor de retorno específico em seu
SQL dinâmico, mas isso só é permitido em procedimentos armazenados regulares. Ou seja, isso falha:
EXEC sp_executesql N'IF @x = 0 RETURN 122', N'@x int', 0

A mensagem de erro é:
Msg 178, Nível 15, Estado 1, Linha 1
Uma instrução RETURN com um valor de retorno não pode ser usada nesse contexto.

Você ainda pode usar RETURN sem um valor de retorno no SQL dinâmico. Isso é legal:
EXEC sp_executesql N'IF @x = 0 RETURN', N'@x int', 0

2.3 EXEC()
EXEC() é uma maneira alternativa de executar SQL dinâmico. Existe por razões históricas, para mais, veja
a nota no final desta seção. Embora nunca haja nenhuma razão convincente para usar essa forma, há
muitos exemplos dela na natureza, então seria errado ficar em silêncio sobre ela.

EXEC() não suporta parâmetros, portanto, todos os valores precisam ser embutidos. Aqui está um exemplo,
semelhante ao que analisamos anteriormente:
DECLARE @sql varchar(MAX),
@when data = '1998-02-01',
@custid nchar(5) = N'ALFKI',
@empid int = 3
11 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

SELECIONE @sql =
'SELECIONAR CONTAGEM(*)
DE dbo. Ordens
WHERE OrderDate >= ''' + convert(char(8), @when, 112) + '''
E OrderDate < ''' + convert(char(8), dateadd(MONTH, 1, @when), 112) + '''
E EmployeeID = ' + convert(varchar(10), @empid) + '

SELECT CompanyName
DE dbo. Clientes
ONDE CustomerID = N''' + @custid + ''''

IMPRESSÃO @sql
EXEC(@sql)

Se você olhar para este código e compará-lo com o exemplo original com sp_executesql, o que você acha?
Você não tem a impressão de que isso parece muito mais complicado? A lógica das consultas é mais difícil
de seguir quando o SQL é dividido por todas essas aspas simples, vantagens e chamadas para converter. A
razão para essa cascata de aspas simples é que, para incluir uma única citação em um literal de cadeia de
caracteres, você precisa dobrá-la e, normalmente, as aspas simples aparecem no início ou no final de um
fragmento de cadeia de caracteres para que a aspa simples dupla dentro da cadeia de caracteres seja
seguida pela aspa simples que termina a cadeia de caracteres. (Se essa frase fez sua cabeça girar, você
meio que entendeu meu ponto. :-)

Quando se trata da conversão, os valores de data e hora constituem um desafio especial, já que você
precisa escolher um bom código de formato, para que a cadeia de caracteres de data construída não seja
mal interpretada. 112 é um bom código apenas para datas, 126 para valores com data e hora.

Você pode notar aqui que @sql é declarado como varchar(MAX), ao contrário de sp_executesql, EXEC()
aceita varchar e nvarchar.

Como você pode ver, adicionei um PRINT de diagnóstico, para que você possa ver o SQL real gerado.
Esta é a saída:
SELECIONAR CONTAGEM(*)
DE dbo. Ordens
WHERE OrderDate >= '19980201'
E OrderDate < '19980301'
E EmployeeID = 3
SELECT CompanyName
DE dbo. Clientes
ONDE CustomerID = N'ALFKI'

Isso é algo que voltarei no guia de estilo, mas vale a pena fazer o ponto já agora: quando você trabalha
com SQL dinâmico e constrói uma cadeia de caracteres de consulta, você sempre precisa de um PRINT de
diagnóstico, para que você possa ver o que você gerou no caso de receber uma mensagem de erro ou um
resultado inesperado.

A entrada para EXEC() não precisa ser uma única variável, mas pode ser uma concatenação de termos
simples, ou seja, variáveis e literais, como neste exemplo::
DECLARE @custid nchar(5) = N'ALFKI'

EXEC('SELECTNome da Empresa
DE dbo. Clientes
ONDE CustomerID = N''' + @custid + '''')

Chamadas de funções não são permitidas, portanto, isso falha com um erro de sintaxe:
DECLARAR @when data = '1998-02-01',
@empid int = 3

12 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

EXEC(
'SELECIONAR CONTAGEM(*)
DE dbo. Ordens
WHERE OrderDate >= ''' + convert(char(8), @when, 112) + '''
E OrderDate < ''' + convert(char(8), dateadd(MONTH, 1, @when), 112) + ''' E EmployeeID = ' +
convert(varchar(10), @empid) + )

Em qualquer caso, há poucas razões para usar esse padrão. Se as coisas derem errado, não há como
emendar em um PRINT de diagnóstico, portanto, é muito melhor armazenar a cadeia de caracteres SQL em
uma variável @sql.

No geral, há poucas razões para usar EXEC(), quando sp_executesql permite que você use parâmetros e,
assim, tornando o código muito mais legível. E, como veremos mais adiante neste artigo, legibilidade e
complexidade é apenas um dos motivos para ficar longe da concatenação de strings. Então eu poderia
torná-lo curto e dizer Não use EXEC(). Alguma vez. Mas isso não seria honesto, porque eu tendo a usar
EXEC() mesmo quando a cadeia de caracteres SQL não usa parâmetros. Ou seja, em vez de dizer

EXEC sp_executesql @sql

Na maioria das vezes, escrevo o mais curto:


EXEC(@sql)

Você verá isso mais de uma vez neste artigo. Mas você certamente poderia argumentar que isso é apenas
um mau hábito meu. Se você decidir sempre usar sp_executesql, não importa se seu SQL dinâmico usa
parâmetros ou não, você não estaria fazendo algo errado.

Antes de encerrar esta seção, devo acrescentar que, na verdade, há uma coisa que você pode fazer com
EXEC() que você não pode fazer com sp_executesql: você pode representar um usuário ou um logon
quando você executa seu lote SQL. Então, em vez da representação que fizemos acima quando testamos
permissões para testuser, poderíamos ter feito:
EXEC ('EXEC mishap_sp') COMO USUÁRIO = 'testuser'

Isso tem a vantagem de que você sabe com certeza que reverterá do contexto representado, mesmo que
haja um erro de execução. Mas, pessoalmente, não acho que isso supere o aumento da complexidade que
o SQL dinâmico incorre. Em qualquer caso, isso deve ser considerado como um recurso avançado.

Nota: EXEC() pode vir a ser usado em versões muito antigas do SQL Server. sp_executesql foi introduzido no SQL 7,
portanto, se você estiver no SQL 6.x, EXEC() é sua única opção. Você também pode ter que usar EXEC() no SQL 7 e
SQL 2000, se o SQL dinâmico pode exceder 4000 caracteres, uma vez que nvarchar(MAX) não está disponível nessas
versões. EXEC() permite que você contorne isso, já que você pode dizer EXEC(@sql1 + @sql2 + @sql3) e aceita que a
cadeia de caracteres resultante exceda 8000 bytes.

2.4 Servidor vinculado EXEC() AT


Esta é uma forma especial de EXEC() que permite executar um lote de comandos em um servidor
vinculado. Esta forma de EXEC() permite que você use parâmetros, se de uma maneira diferente de
sp_executesql.

Para jogar com isso, você pode configurar um servidor vinculado que remonta à sua instância atual da
seguinte maneira:
EXEC sp_addlinkedserver LOOPBACK, '', 'SQLNCLI', @datasrc = @@servername EXEC
sp_serveroption LOOPBACK, 'rpc out', 'true'

Você pode substituí -@@servername pelo nome de outra instância à qual você tem acesso, se desejar.
Apenas certifique-se de ter NorthDynamic nessa instância também.
13 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

Aqui está como você pode executar o lote de SQL dinâmico acima:
DECLARE @sql varchar(MAX),
@when data = '19980201',
@custid nchar(5) = 'VINET', int @cnt

SELECT @sql = 'SELECT ? = CONTAGEM(*)


FROM NorthDynamic.dbo.Orders
ONDE OrderDate >= ?
E OrderDate < dateadd(MONTH, 1, ?)

SELECT CompanyName
DE NorthDynamic.dbo.Customers
ONDE CustomerID = ?'
IMPRESSÃO @sql
EXEC(@sql, @cnt OUTPUT, @when, @when, @custid) EM LOOPBACK SELECT
@cnt AS [Pedidos em fevereiro de 1998]

Ao contrário sp_executesql, os parâmetros não têm nomes, mas o ponto de interrogação serve como um
detentor de parâmetros. O SQL Server passará os parâmetros fornecidos a EXEC() AT na ordem em que os
pontos de interrogação forem exibidos. Ou seja, @cnt vai para o primeiro ponto de interrogação e, como
você vê, podemos usar a palavra-chave OUTPUT aqui também. A próxima @when vem duas vezes na
lista de parâmetros, simplesmente porque é usada duas vezes no código SQL.

Há uma boa razão pela qual EXEC() AT não usa nomes de parâmetros como sp_executesql: o servidor
vinculado pode ser algo diferente do SQL Server, onde nomes começando com @ fariam pouco sentido.
O?
O marcador de parâmetro, por outro lado, é uma coisa padrão em ODBC e OLE DB, e o provedor OLE
DB para a fonte de dados remota alterará o ? no que fizer sentido na outra fonte de dados.

Para simplificar, usei EXEC() AT com um servidor vinculado que é outra instância do SQL Server. No
entanto, neste caso, é muito mais fácil usar sp_executesql com notação de quatro partes, como neste
exemplo:
DECLARE @sql nvarchar(MAX),
@params Nvarchar(4000)

SELECIONE @sql = N'SELECT @cnt = COUNT(*)


DE dbo. Ordens
WHERE OrderDate >= @date
E OrderDate < dateadd(MONTH, 1, @date)

SELECT CompanyName
DE dbo. Clientes
WHERE CustomerID = @custid'

SELECIONE @params = N'@date data,


@cnt SAÍDA int,
@custid nchar(5) = N''ALFKI'''

DECLARE @cnt int,


@when data = '19980201'

LOOPBACK EXEC. NorthDynamic.sys.sp_executesql @sql, @params, @when @cnt SAÍDA SELECT @cnt AS
[Pedidos em fevereiro de 1998]

Você pode notar que no exemplo com EXEC() AT, eu incluí o nome do banco de dados na cadeia de
caracteres SQL, mas eu poderia evitá-lo na versão com sp_executesql. Quando você invoca
sp_executesql (como qualquer outro procedimento do sistema) com um determinado nome de banco de
dados, sp_executesql será executado no contexto desse banco de dados. Portanto, há todas as razões para
usar esse padrão quando você deseja executar uma instrução SQL parametrizada em uma instância remota
14 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

do SQL Server. Mas quando seu servidor vinculado não está executando o SQL Server, EXEC() AT é
definitivamente seu amigo.

Lembre-se de que há algumas restrições em quais tipos de dados você pode usar em servidores
vinculados. Por exemplo, você não pode usar o tipo de dados xml. Você pode passar tipos de LOB (por
exemplo, nvarchar(MAX)), mas apenas como parâmetros de entrada; você não pode recebê-los como
parâmetros OUTPUT.

2.5 Executando SQL dinâmico a partir do código do cliente


A diferença entre procedimentos armazenados e código de cliente a esse respeito é que em procedimentos
armazenados você só usa SQL dinâmico ocasionalmente, mas no código do cliente você pode dizer que
faz isso o tempo todo, já que o código SQL está sempre dentro de um literal de cadeia de caracteres na
linguagem do lado do cliente. No entanto, isso não significa que você sempre terá que usar concatenação
de cadeia de caracteres para construir a cadeia de caracteres. Au contraire, este é um caso excepcional.
Contanto que você esteja trabalhando apenas com consultas de baunilha simples em tabelas específicas,
suas consultas devem estar contidas em um único literal de cadeia de caracteres que contém a consulta
completa e onde os valores de entrada do usuário são passados como parâmetros. Ou de uma maneira
diferente: não importa se você está fazendo código SQL dinâmico em um procedimento armazenado ou
em código de cliente, qualquer coisa que possa ser uma variável, conforme permitido pela sintaxe SQL,
deve ser passada como parâmetro e não ser inserida na cadeia de caracteres de consulta por meio de
concatenação. Nesta seção, aprenderemos como fazer isso no .NET e também começaremos a examinar
os muitos problemas com a inserção de valores de parâmetro.

Nota: Se você estiver trabalhando em um ambiente diferente do .NET, você ainda deve ler esta seção. O padrão que
apresento aqui se aplica a todos os ambientes cliente do SQL Server, embora o nome dos objetos e os métodos sejam
diferentes. E embora o .NET ofereça suporte a parâmetros nomeados no estilo @param, alguns ambientes oferecem
suporte apenas a marcadores de parâmetros, normalmente ?, como vimos exemplos na seção anterior.

Também devo salientar que esta seção visa o código do cliente onde você realmente escreve código SQL. Se você
estiver usando um ORM como o Entity Framework, você pode estar usando uma linguagem como Linq que gera o
código SQL. Por razões irrelevantes para este artigo, eu não sou amigo de ORMs, mas pelo menos eles mantêm os
usuários longe de escrever SQL dinâmico ruim, e eu não vou abordar Linq e similares neste artigo.

Para esta seção, não usaremos as tabelas no NorthDynamic, mas trabalharemos com esta tabela:
CREATE TABLE typetbl (ident int NOT NULL IDENTITY, intcol int NOT NULL,
deccol decimal(8,2) NOT NULL, fltcol float NOT NULL, strcol nvarchar(25) NOT
NULL, dtcol datetime NOT NULL,
RESTRIÇÃO pk_datatyptbl CHAVE PRIMÁRIA (ident)

Embora você possa criá-lo no NorthDynamic, recomendo que você crie a tabela no tempdb, e é isso que
assumirei no texto a seguir.

Digamos que queremos inserir uma linha nesta tabela e temos os valores em variáveis intval, decval etc e
queremos recuperar o valor de ident para a linha inserida. Aqui está um método para fazer isso:
vazio estático público InsertTbl () {
using(SqlConnection cn = new SqlConnection(strConn)) { using(SqlCommand
cmd = new SqlCommand()) { cmd.CommandType = CommandType.Text;

cmd.CommandText =
@"INSERT dbo.typetbl(intcol, strcol, fltcol, deccol, dtcol)
VALORES(@intval, @strval, @fltval, @decval, @dtval) SELECIONE @ident =
scope_identity()";

cmd.Parameters.Add("@intval", SqlDbType.Int). Valor = intval; cmd.Parameters.Add("@strval",


SqlDbType.NVarChar, 25). Valor = strval; cmd.Parameters.Add("@fltval", SqlDbType.Float). Valor =

15 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

fltval; cmd.Parameters.Add("@decval", SqlDbType.Decimal). Valor = decval;


cmd.Parâmetros["@decval"]. Precisão = 8; cmd.Parâmetros["@decval"]. Escala = 2;
cmd.Parameters.Add("@dtval", SqlDbType.DateTime). Valor = dtval; cmd.Parameters.Add("@ident",
SqlDbType.Int);
cmd.Parameters["@ident"]. Direção = ParameterDirection.Output;

cmd.Conexão = cn; Cn. Aberto();


cmd.ExecuteNonQuery();

int identval = Convert.ToInt32(cmd.Parameters["@ident"]. Valor); Console.WriteLine("A linha


inserida tem id " + identval. ToString()); }}
}

Primeiro, criamos objetos de conexão e comando (dentro usando cláusulas para garantir que os recursos
sejam liberados assim que saírem do escopo) e definimos o tipo de comando como texto. Em seguida, em
conformidade com o que disse no início desta seção, definimos o texto do comando como uma única
cadeia de caracteres estática que inclui algumas variáveis T-SQL. Ou, mais precisamente, são parâmetros.

No conjunto de instruções a seguir, definimos esses parâmetros com o método Add da classe
SqlParameterCollection. Há algumas sobrecargas desse método, das quais as duas mais comumente
usadas aparecem neste exemplo. O primeiro parâmetro é o nome do parâmetro. (Parece que o @ pode
ficar de fora, mas recomendo que você sempre o inclua.) O segundo parâmetro é um valor da enumeração
SqlDbType (que está no namespace System.Data). Essa enumeração basicamente tem uma entrada para
cada tipo de dados no SQL Server, com o nome sendo o mesmo que o tipo de dados do SQL Server, mas
com maiúsculas iniciais e algumas maiúsculas médias também para apimentar. Existem alguns desvios
com alguns tipos menos usados que não abordarei aqui. Para obter uma lista completa, consulte o.NET
Framework referência. Para tipos que vão com um comprimento, ou seja, tipos de cadeia de caracteres e
binários, usamos uma sobrecarga que leva um terceiro parâmetro que é o comprimento. (Para tipos de
LOB, como nvarchar(MAX), você especifica -1 para o comprimento.) O exemplo inclui um valor
decimal. Não há sobrecarga para definir um parâmetro decimal com precisão e escala, portanto,
precisamos definir essas propriedades separadamente, como visto no exemplo de código acima.

O método Add retorna o objeto SqlParameter recém-criado que nos permite definir a propriedade Value
diretamente e, portanto, para todos, exceto @decval podemos definir um parâmetro em uma única linha
de código. Eu não incluí isso no exemplo, mas para passar explicitamente um valor NULL, defina a
propriedade Value como System.DBNull.Value.

Por @ident não há nenhum valor a ser definido, já que este é um parâmetro de saída, que indicamos
definindo o
Propriedade de direção . (Esta propriedade tem como padrão ParameterDirection.Input.)

Uma vez que os parâmetros tenham sido definidos, abrimos a conexão, executamos o lote SQL e, em
seguida, recuperamos o valor IDENTITY gerado do parâmetro de saída e o imprimimos.

Neste ponto, você pode querer tentar o código. O arquivo clientcode.cs inclui InsertTbl, bem como um
método BadInsert que veremos mais tarde. Há também um Main que analisa a linha de comando e tem
um manipulador de exceção simples. Na parte superior do arquivo você vê o seguinte:
Cadeia de conexão, alterar servidor e banco de dados conforme necessário!
cadeia de caracteres estática privada
strConn = @"Segurança Integrada=SSPI;" +
@"Fonte de dados=(local); Catálogo Inicial=tempdb;";

Constantes para a demonstração. int intval estático


privado = 123456;
string estática privada strval = "alfa (α) e beta (β)"; fltval duplo estático privado
= 17,0/3,0; decval decimal estático privado = 456,76M; privado estático
DateTime dtval = DateTime.Now;
16 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

Salve e compile o programa. (Veja aqui se você precisar de ajuda na compilação.) Abra um prompt de
comando para mover para a pasta onde você tem o executável e executá-lo como:
Cliente

Você deve ver uma saída como esta:


A linha inserida tem id 1

Você também pode executar um SELECT contra typetbl para ver se os valores foram inseridos.

Vamos agora investigar o que acontece em termos de T-SQL. Você pode usar o Profiler para capturar o
comando enviado ao SQL Server. (Você também pode usar o XEvent Profiler em versões recentes do
SSMS). Você verá algo assim: (Eu reformatei a saída para legibilidade):
declarar @p8 int set
@p8=2
exec sp_executesql N'INSERT dbo.typetbl(intcol, strcol, fltcol, deccol, dtcol)
VALORES(@intval, @strval, @fltval, @decval, @dtval)
SELECIONE @ident = scope_identity()',
N'@intval int, @strval nvarchar(25), @fltval flutuação,
@decval decimal(8,2), @dtval datetime, @ident saída int»,
@intval=123456, @strval=N'alfa (α) e beta (β)', @fltval=5,66666666666666667,
@decval=456.76, @dtval='2018-09-22 00:07:35.193', @ident=@p8 saída selecione @p8

Então esse era o cerne da questão! Os parâmetros @ não são apenas uma abstração no .NET, mas o lote é
passado para sp_executesql exatamente como foi inserido e todos os parâmetros fazem parte da lista de
parâmetros em
@params.

A história completa é um pouco diferente, no entanto. O .NET não compõe o código acima, mas o que
você vê é uma representação textual do que em grande parte é um fluxo binário de dados. O que aparece
no rastreamento é um evento RPC (RPC = Remote Procedure Call). Ou seja, o .NET diz ao SQL Server
para executar um procedimento armazenado, desta vez sp_executesql, com tantos parâmetros e, em
seguida, envia os parâmetros em formato binário. Se você fosse espionar o fio com o Wireshark ou
similar, você veria a instrução INSERT, mas não as aspas ao redor. Se você examinasse os bytes que
precedem a palavra-chave INSERT, encontraria o comprimento da cadeia de caracteres. O mesmo vale
para os parâmetros @params e @strval. Você não seria capaz de discernir os valores de @intval, @fltval,
@decval e @dtval, a menos que esteja familiarizado com o formato binário.

Nota: A variável @p8 na saída Trace é uma maneira muito engraçada que o Profiler usa para indicar o valor de retorno
dos parâmetros de saída. Na realidade, @p8 não existe. E, particularmente, o valor 2 nunca é passado para o parâmetro
@ident.

O leitor pode achar que isso é um pouco demais de detalhes minuciosos para realmente chamar sua
atenção, então deixe-me resumir quais são os pontos importantes até agora:

Quando você define um ou mais parâmetros para seu lote SQL, isso resulta em uma chamada para
sp_executesql.
Os valores de parâmetro são passados em um formato binário sem qualquer sintaxe SQL em torno deles.
(Por que isso importa, será aparente mais tarde.)

E acima de tudo:

Quando você passa valores para um lote SQL do código do cliente, seja .NET ou qualquer outra coisa,
você sempre deve usar instruções parametrizadas.

17 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

O último ponto deve ser realmente supérfluo, porque o que você viu é a norma que qualquer
desenvolvedor de software profissional deve seguir. Mas, infelizmente, vejo demasiados exemplos em
fóruns de utilizadores e noutros locais que indicam que nem sempre é esse o caso. Por conseguinte, temos
de analisar o que muitas pessoas fazem e por que razão não o devem fazer. No clientcode.cs também há
um método BadInsert que se parece com isto:
vazio estático público BadInsert () {

using(SqlConnection cn = new SqlConnection(strConn)) {


using(SqlCommand cmd = new SqlCommand()) { cmd.CommandType =
CommandType.Text; cmd.CommandText =
@"INSERT typetbl (intcol, strcol, fltcol, deccol, dtcol)
VALORES(" + intval. ToString() + @",
'" + strval + @"',
" + FLTVAL. ToString() + @",
" + Decval. ToString() + @",
'" + dtval. ToString() + @"')
SELECIONE scope_identity()"; Console.WriteLine(cmd.CommandText);

cmd.Conexão = cn; Cn. Aberto();

Objeto res = cmd.ExecuteScalar();

int identval = Convert.ToInt32(res);


Console.WriteLine("A linha inserida tem id " + identval. ToString());
}}
}

À primeira vista, pode parecer mais curto, e alguns leitores podem até achar mais simples do que o
InsertTbl, já que não há chamada para métodos extras. "Nós apenas construímos a cadeia de caracteres de
consulta, vamos nos ater ao princípio KISS e pular as coisas extravagantes". (KISS = Mantenha isso
estúpido e simples.) Mas, como veremos, isso é apenas estúpido – não é nada simples. O fato de eu ter me
sentido compelido a adicionar uma escrita diagnóstica do texto de comando resultante pode ser um
primeiro indício disso.

Se você estiver aquecido nele, você pode experimentá-lo imediatamente executando isso na janela de
linha de comando:
código do cliente BAD

Isso invocará BadInsert. No entanto, devo avisá-lo que para um resultado completamente bem sucedido
todos os pontos abaixo devem ser verdadeiros:

1. Em suas configurações regionais no Windows (ou localidade no Linux), o separador decimal deve ser
um ponto e não uma vírgula.
2. O formato de data em suas configurações regionais deve corresponder à configuração de formato de
data no SQL Server.
3. Seu banco de dados deve ter um agrupamento grego.

Quando eu executei o programa no meu computador, eu tenho esta saída:


INSERT typetbl (intcol, strcol, fltcol, deccol, dtcol)
VALORES(123456,
'alpha (a) and beta (ß)',
5,66666666666667,
456,76,
'2018-09-22 23:25:57')
SELECIONE scope_identity()
EXCEPTION THROWN: Há menos colunas na instrução INSERT do que os valores especificados na cláusula VALUES.
O número de valores na cláusula VALUES deve corresponder ao número de colunas especificadas na instrução INSERT.

18 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

Como você vê, uma exceção foi lançada e nenhum dado foi inserido. Além disso, a mensagem de erro
parece misteriosa no início. Quando voltamos ao código .NET e contamos colunas e valores, não podemos
encontrar nenhuma incompatibilidade. Mas se você olhar mais de perto para o comando gerado, poderá
encontrar vírgulas nos valores de deccol e fltcol, o que bagunça a sintaxe T-SQL. Essas vírgulas
apareceram, porque ToString respeita as configurações regionais, e eu tenho minhas configurações
regionais no Windows definidas como sueco, onde o delimitador decimal é uma vírgula e não um ponto.
Há obviamente uma correção simples: use algo mais sofisticado do que ToString que permite controlar o
formato. No entanto, imagine um programador ingênuo morando em, digamos, Austrália, onde ponto é o
delimitador decimal, que escreve o acima. O código funciona, passa em todos os testes e é enviado. Então,
em algum momento, atinge uma parte do mundo, por exemplo, a Europa continental, onde vírgula é o
delimitador decimal. E os usuários europeus descobrem que o software trava assim que o iniciam. Você
pode imaginar como isso afetará a reputação do fornecedor do software.

Para poder executar o programa, alterei minhas configurações regionais para Inglês (Austrália). Agora eu
tenho um erro diferente:
INSERT typetbl (intcol, strcol, fltcol, deccol, dtcol)
VALORES(123456,
'alpha (a) and beta (ß)',
5.66666666666667,
456.76,
'22/09/2018 23:33:51')
SELECIONE scope_identity()
EXCEÇÃO LANÇADA: A conversão de um tipo de dados varchar em um tipo de dados datetime resultou em um valor fora do
intervalo.
A declaração foi encerrada.

Desta vez, os valores decimais e flutuantes são bons, mas em vez disso, o valor para dtcol causa um
problema. Minha configuração de idioma no SQL Server é us_english e a configuração de formato de data
é mdy. Ou seja, o SQL Server espera que o valor seja mês/dia/ano. Quando corri isso no dia 22 do mês,
recebi o erro. Para os primeiros doze dias do mês, o valor teria sido inserido – mas para onze desses dias
teria sido inserido erroneamente! Novamente, isso é algo que pode ser resolvido pela formatação: a data
deve estar em um formato seguro no SQL Server, por exemplo, YYYY-MM-DDThh:mm:ss.fff. (O T neste
formato representa a si mesmo.) Mas um desenvolvedor ingênuo nos EUA nunca notará o problema,
porque ele tem a mesma configuração no Windows e no SQL Server. (E não é melhor com um
desenvolvedor sueco ingênuo. O formato sueco AAAA-MM-DD é interpretado corretamente com a
configuração mdy.) Nesse caso, o software pode até funcionar em sites em países como Alemanha ou
Austrália, onde as datas são escritas como dia/mês/ano, se tiverem o formato de data definido como dmy
no SQL Server. Mas, mais cedo ou mais tarde, haverá um descompasso e haverá erros. Ou, pior ainda,
datas incorretas.

Nota: para obter uma discussão mais detalhada sobre formatos de data, consulte o artigo O guia definitivo para os tipos
de dados datetime do SQL Server MVP Tibor Karaszi.

Fiz mais uma tentativa de rodar o programa. Desta vez, defini minhas configurações regionais para inglês
(Estados Unidos). Eu tenho esta saída:
INSERT typetbl (intcol, strcol, fltcol, deccol, dtcol)
VALORES(123456,
'alpha (a) and beta (ß)',
5.66666666666667,
456.76,
'22/09/2018 23:50:47')
SELECT scope_identity() A linha inserida tem id
4

19 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

Êxito! A linha foi inserida. No entanto, quando olhei para os dados, notei um problema. Em strcol eu vi
isso:
alpha (a) and beta (ß)

Se você olhar de perto, você pode ver que o alfa grego foi substituído por um latim minúsculo regular a
minúsculo, e em vez de um beta minúsculo, há um alemão "scharfes s". Isso se deve ao fato de que não há
N antes do literal de cadeia de caracteres na instrução SQL. Assim, este é um literal varchar, e para
varchar, o repertório de caracteres é definido pela página de código para o agrupamento. Minha colação
para o teste foi um
Finnish_Swedish agrupamento, que usa a página de código 1252 para varchar, e esta página de código não
inclui alfa e beta. Portanto, o SQL Server os substituiu por caracteres de fallback que considerou
adequados.

Nota: o leitor observador pode notar que a mesma substituição ocorreu também na saída na janela de linha de comando
ao executar o programa. Isso não é devido ao N ausente, mas porque o ambiente de linha de comando não tem suporte a
Unicode.

Há mais problemas com o BadInsert. O programa clientcode permite que você substitua o valor de strval
na linha de comando. Execute isto:
código do cliente BAD Brian O'Brien

Eu tinha mudado de volta minhas configurações regionais para sueco quando eu executei isso, mas
realmente não importa.
INSERT typetbl (intcol, strcol, fltcol, deccol, dtcol)
VALORES(123456,
'Brian O'Brien',
5,66666666666667,
456,76,
'2018-09-23 00:07:36')
SELECIONE scope_identity()
EXCEÇÃO LANÇADA: Sintaxe incorreta perto de 'Brien'.
Aspas não fechadas após a cadeia de caracteres ') SELECT scope_identity()'.

Por causa da aspa simples no nome de Brian, desta vez recebi um erro de sintaxe. Agora veja isso:
código do cliente GOOD Brian O'Brien

GOOD invoca InsertTbl e a linha é inserida sem qualquer dor.

Sim, podemos corrigir o BadInsert para que ele dobre as aspas em strval. Mas isso vai tornar o código
SQL mais fácil de ler?

E isso nos leva à essência deste exercício. InsertTbl mostra uma maneira muito estruturada de escrever
código de acesso a dados. Você define a instrução SQL em uma única cadeia de caracteres, que está livre
de qualquer interrupção da sintaxe .NET, exceto para as aspas duplas circundantes. Em seguida, é seguida
por uma definição dos parâmetros de forma simples, mas estruturada. No BadInsert, a instrução SQL é
misturada com a sintaxe .NET, o que significa que quanto mais variáveis houver, mais difícil será ver a
floresta SQL para todas as árvores .NET.

Com BadInsert você pode obter um choque entre as configurações regionais no cliente e a interpretação
em SQL
Servidor que possui suas próprias configurações e regras, diferente do .NET e do Windows. Por outro
lado, com InsertTbl, todos os valores são interpretados consistentemente no programa cliente de acordo

20 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

com as configurações regionais no computador do usuário e são transformados em um formato binário


inequívoco que o SQL Server sempre interpreta da mesma maneira.

Quando se trata do erro final, deixando de fora o N, este é um erro que você poderia cometer com
InsertTbl também, como você poderia erroneamente usar SqlDbType.VarChar em vez de
SqlDbType.NVarChar, o que levaria à mesma mutilação dos caracteres gregos. Ainda assim, a interface
do .NET meio que incentiva você a ser exato com os tipos de dados.

O leitor atento deve ter notado que há mais uma diferença entre os dois métodos. Em InsertTbl, uso um
parâmetro de saída para obter o valor id da linha inserida, enquanto em BadInsert eu retorno o id com uma
instrução SELECT e uso ExecuteScalar para executar o lote e obter o valor dessa maneira. Ambas as
abordagens são perfeitamente válidas, e eu queria mostrar exemplos com ambas. Qual você usa é uma
questão de gosto e o que se encaixa melhor com o momento. (Obviamente, se você tiver mais de um valor
de retorno, ExecuteScalar não é uma opção.)

Alguns leitores podem achar que ter que especificar o tipo de dados exato e detalhes como comprimento
de cadeias de caracteres e precisão/escala para decimal é um pouco de um fardo. Certamente o .NET, que
visa aumentar a produtividade do desenvolvedor, poderia fazer melhor? Realmente. Se você não
especificar o comprimento de uma cadeia de caracteres ou parâmetro binário, o .NET usará o
comprimento do valor fornecido, e o similar será verdadeiro para valores decimais. Além disso, o
SqlParameterCollection classe fornece o método AddWithValue que infere o tipo do valor que você
passa. NUNCA USE NADA DISSO, PONTO FINAL! Veja bem, embora isso seja um aumento para a
produtividade do desenvolvedor, é um revés para a produtividade do DBA, que provavelmente será quem
terá que descobrir por que o acesso ao banco de dados é lento. Por que é assim, é algo que voltarei no
capítulo de performance. Mas se você quiser saber mais detalhes aqui e agora, você pode ler o post
AddWithValue is Evil do MVP do SQL Server Dan Guzman.

Você pode argumentar que espelhar o comprimento das cadeias de caracteres no aplicativo .NET causa um
problema de manutenção e você não deseja alterar o código se a coluna for alterada de nvarchar(25) para
nvarchar(50). Se isso for uma preocupação, você pode usar 100 como um valor padrão para todas as
colunas "curtas" e 4000 para todos os nvarchar mais longos e 8000 para varchar longo. (Para Parâmetros
MAX, você deve usar -1 como observado acima.) Ainda há situações em que isso pode afetar o
desempenho, mas isso é algo muito mais sutil, e não é nada que eu discutirei neste artigo. E o impacto não
é tão abrangente quanto quando você não especifica nenhum comprimento.

Nesta seção, eu só olhei para o .NET, mas qualquer que seja o ambiente que você esteja usando, criar uma
cadeia de caracteres SQL concatenando a entrada do usuário é totalmente inaceitável, se você estiver
escrevendo código em um contexto profissional como funcionário ou consultor. Você sempre deve usar
parâmetros onde a sintaxe SQL permitir. Isso pode parecer uma coisa muito dura de dizer, mas nesta seção
só olhamos para isso do ponto de vista da facilidade do desenvolvedor. Escrever e manter código com
cadeias de caracteres SQL parametrizadas fixas é muito mais fácil do que inserir valores de parâmetro.
Examinaremos uma razão ainda mais importante no próximo capítulo, a partir da qual ficará claro por que
uso palavras tão fortes. Observe que continuaremos trabalhando com typetbl e o programa clientcode,
então não os abandone agora.

3. Cuidado com a injeção de SQL


Quando você trabalha com SQL dinâmico, você precisa estar ciente da injeção de SQL. Bem, "consciente"
é um pouco de eufemismo. Toda vez que você cria código dinamicamente a partir de alguma entrada, você
deve se fazer estas perguntas:

1. A entrada pode ser criada para que o código SQL gerado execute algo diferente do pretendido?
2. Isso pode ser utilizado por um usuário mal-intencionado?
21 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

3. Posso me proteger contra isso?

Neste capítulo, veremos como as duas primeiras coisas podem acontecer e quais são seus meios de
proteção. A primeira seção discute a injeção de SQL no código do cliente, mas isso não significa que você
pode ignorar esta seção se fizer apenas programação T-SQL, porque as lições neste capítulo são de
natureza geral e se aplicam a todos. As seções restantes deste capítulo abordam aspectos específicos do T-
SQL.

3.1 Injeção de SQL no código do cliente


O que é injeção de SQL? A injeção de SQL é quando um usuário insere a sintaxe SQL em um campo de
entrada ou similar e faz com que o aplicativo execute algo diferente do esperado. Aqui está um exemplo
clássico, Exploits of a Mom, da banda desenhada xkcd.

Para os leitores que nunca ouviram falar do conceito de injeção de SQL, isso pode não se registrar
imediatamente, mas vamos tentar com nosso programa clientcode. Vamos primeiro criar uma tabela
Alunos:
CREATE TABLE Alunos (um int NOT NULL)

(Já que vamos tentar derrubá-lo, as colunas realmente não importam.)

Para ver se o xkcd pode estar puxando nossas pernas, tentamos o nome dado na tira:
código de cliente BAD Robert'); DROP TABLE Estudantes; --

Não, isso não deu certo:


INSERT typetbl (intcol, strcol, fltcol, deccol, dtcol)
VALORES(123456,
'Roberto'); DROP TABLE Estudantes; --',
5,66666666666667,
456,76,
'2018-09-23 21:47:00')
SELECIONE scope_identity()
EXCEÇÃO LANÇADA: sintaxe incorreta perto de '5'

Mas se você olhar mais de perto para isso, você pode ver que xkcd está em algo. Essa única citação depois
de Robert parece se encaixar na sintaxe. A única citação que originalmente estava no BadInsert ainda está
lá – mas está escondida atrás dos dois hífens que iniciam um comentário. Acontece que introduzi novas
linhas na instrução gerada para facilitar a leitura e, portanto, o número na próxima linha causa um erro de
sintaxe. Muitos desenvolvedores não se preocupariam em adicionar novas linhas e, com a instrução em
uma única linha, o comando gerado teria sido sintaticamente correto como tal. Ele ainda teria falhado
devido à incompatibilidade do número de colunas entre a lista INSERT e VALUES.

22 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

Nada disso são obstáculos que não podem ser superados. Em seguida, tente o seguinte:
código de cliente BAD Robert', 1, 1, 1); DROP TABLE Estudantes; SELECIONE 1 ONDE 1 EM ('1

Pode parecer uma grande bobagem, mas olhe para o resultado:


INSERT typetbl (intcol, strcol, fltcol, deccol, dtcol)
VALORES(123456,
'Roberto', 1, 1, 1); DROP TABLE Estudantes; SELECIONE 1 ONDE 1 EM ('1',
5,66666666666667,
456,76,
'2018-09-23 21:51:48')
SELECIONE scope_identity()
A linha inserida tem id 1

O lote foi concluído sem erros e se você executar SELECT * FROM Students em uma janela de consulta,
descobrirá que a tabela não está lá.

Para entender como o BadInsert foi explorado, vamos ver as técnicas usadas. Para que a instrução INSERT
original funcione, adicionamos o dígito 1 quantas vezes forem necessárias para fazer com que VALUES
corresponda a INSERT. E ficamos felizes em ver que 1 foi aceito para todos os valores (pode ter havido
erros de conversão ou violações de restrição). Para que o resto da sintaxe funcionasse, tivemos que
incorporar essa lista de valores em uma nova instrução, e um SELECT com IN parecia a escolha mais fácil.
Não nos preocupamos com uma tabela no SELECT, porque não há nada que diga que uma coluna ou uma
variável deve vir antes de IN – ela pode muito bem ser literal. Por fim, tivemos que considerar a citação
que se pretende fechar o valor em strval e a vírgula que se segue. Como você pode ver, isso foi alcançado
no final '1.

Apenas como um contraste: recrie a tabela Students e execute a mesma novamente, mas substituindo
BAD por GOOD. Isso também é concluído com sucesso, mas quando você verifica a tabela Alunos, ela
ainda está lá. Se você procurar em typetbl, encontrará os primeiros 25 caracteres da cadeia de caracteres
de injeção e, em fltcol, deccol e dtcol, encontrará os valores constantes do arquivo clientcode.cs, ou seja,
os valores pretendidos. Por outro lado, na fila inserida no ataque de injeção vemos outra coisa. (O
resultado de todos esses 1s.) Você também pode observar que a coluna ident para a linha adicionada com
BadInsert tem um valor diferente do que foi relatado na saída acima. Isso ocorre porque o ExecuteScalar
pegou esse 1 do SELECT extra que injetamos.

Foi fácil para nós compor a string de injeção, porque tínhamos acesso ao código-fonte. Você pode
argumentar que seria muito mais difícil para um intruso que não vê o código descobrir como o ataque de
injeção deve ser criado, mas esse não é realmente o caso. Imagine que o BadInsert está realmente dentro
de um aplicativo web que é aberto a todos na Internet e strval obtém seu valor de uma URL. Ou, ainda,
um biscoito. O usuário malvado pode primeiro tentar um tipo simples de exploração na faixa xkcd. Se o
usuário tiver sorte, o servidor Web exibirá a mensagem de erro do SQL Server e, nesse caso, é uma tarefa
relativamente simples de descobrir. Isso já seria uma má prática em si; A prática recomendada é
configurar o servidor Web para mostrar uma mensagem de erro genérica e gravar apenas a mensagem de
erro real em um arquivo de log local. Mas isso ainda não é um obstáculo, apenas uma pequena lombada
que pode parar alguém que está tentando encontrar um furo de injeção manualmente. Na vida real, os
intrusos usam ferramentas automatizadas para encontrar brechas de injeção de SQL. Um site que é escrito
no estilo de BadInsert terá, naturalmente, muitos furos de injeção para tentar. Um colega MVP fez uma
demonstração desse programa para mim. Ele direcionou para alguns sites, e o programa levou talvez dez
segundos para determinar que um site era vulnerável à injeção de SQL.

É muito importante entender que as pessoas que se envolvem nisso não são adolescentes solitários que
querem se exibir. Não, a razão pela qual as pessoas gastam seu tempo em encontrar buracos de injeção de
SQL significa m-o-n-e-y. Se o banco de dados lida com dinheiro sozinho, o dinheiro pode ser feito
diretamente no sistema. O dinheiro também pode ser feito roubando as informações no banco de dados e
23 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

vendendo-as. Ou o intruso pode usar a máquina do SQL Server como uma entrada para o resto da rede
para infectar todos os PCs da organização em preparação para atacar um terceiro em um ataque DDOS
que o intruso é pago para executar. Ou o intruso pode instalar ransomware e chantagear o site. Ou...

Só para dar um exemplo da vida real: a Marriott anunciou em 2018 que quase 500 milhões de dados de
clientes haviam vazado em um grande vazamento de dados. E um dos meios no ataque foi, sim, a injeção
de SQL. Você pode procurar por Marriott e injeção de SQL em seu mecanismo de pesquisa favorito para
encontrar artigos sobre o incidente. Aqui também está um link direto para um dos artigos que encontrei.

Embora os maiores perigos estejam com aplicativos Web expostos na Internet, isso não significa que você
pode ignorar o risco de injeção de SQL em aplicativos de intranet ou clientes Windows. Pode haver
funcionários mal-intencionados ou descontentes que querem roubar dados ou apenas causar uma bagunça
em geral. E pode haver computadores na organização que foram infectados com malware que tentam
atacar todas as máquinas que encontrarem.

Então, como você se protege contra a injeção de SQL? Na tira xkcd, a mãe fala sobre higienizar as
entradas do banco de dados. O que ela presumivelmente está aludindo é que as aspas simples na cadeia de
entrada devem ser dobradas. Mas isso por si só não protege você contra a injeção de SQL; há ataques de
injeção que empregam a aspa única. Portanto, não é incomum ver sugestões de que alguns caracteres não
devem ser permitidos entrada, mas esse não é um caminho muito frutífero a seguir, uma vez que esses
caracteres podem fazer parte de valores de entrada perfeitamente legítimos. Imagine que Brian O'Brien
descobre que não pode digitar seu nome, mas é informado de que isso inclui um personagem ilegal!

Não, há uma maneira muito melhor de parar a injeção de SQL e você já viu: instruções parametrizadas.

Quando os valores de entrada são passados como parâmetros através de SqlParameterCollection e,


finalmente, para sp_executesql, a injeção de SQL não pode acontecer, porque não há lugar para injetar
nada. Dito isso, ainda há mais um cuidado a ser tomado: o aplicativo deve ser executado com um conjunto
limitado de permissões. É muito comum que aplicativos da Web sejam executados com permissões
elevadas ou até mesmo como sa. E isso é muito ruim. Afinal, mesmo que você já tenha aprendido como
evitar a injeção de SQL, basta um programador júnior que não tenha e haja um buraco de injeção de SQL.
Por conseguinte, um pedido nunca deve ser concedido mais do que a adesão a db_datareader e
db_datawriter. Isso limitará os danos de um buraco de injeção de SQL. Melhor ainda, o aplicativo só deve
usar procedimentos armazenados durante todo o processo. Nesse caso, tudo o que você precisa conceder
são permissões EXECUTE. (Você pode fazer isso no nível de esquema ou banco de dados, sem necessidade
de fazê-lo por procedimento.)

Nota: alguns leitores podem vir com objeções como "nosso aplicativo cria tabelas dinamicamente" etc, então ele deve ter
permissões elevadas. Nesse caso, você deve empacotar essas permissões dentro de procedimentos armazenados, e
descrevo isso em detalhes no meu artigo Permissões de empacotamento em procedimentos armazenados. Os aplicativos
nunca devem ser executados com permissões elevadas, ponto final!

3.2 Injeção de SQL em SQL dinâmico em procedimentos armazenados


Quando você trabalha com SQL dinâmico em um procedimento armazenado, é claro que você deve usar
sp_executesql com uma instrução parametrizada para se proteger contra a injeção de SQL em valores de
entrada simples, como parâmetros de pesquisa, etc. No entanto, na maioria dos casos, quando você cria
uma cadeia de caracteres SQL dinamicamente dentro de um procedimento armazenado, é porque há
algum valor de entrada que você não pode passar como um parâmetro, mas que deve ser inserido na
cadeia de caracteres SQL, por exemplo, um nome de tabela.

Aqui está um exemplo. Charlie Brown é o DBA de uma universidade onde cada aluno tem seu próprio
pequeno banco de dados onde eles podem fazer o que quiserem. Charlie quer ser útil, então ele agenda um
trabalho que percorre todas as tabelas nos bancos de dados dos alunos para desfragmentá-las. Aqui está o
núcleo desse script:
24 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

DECLARE @sql varchar(MAX)

DECLARAR table_cur CURSOR ESTÁTICO LOCAL PARA


SELECIONE 'ALTERAR ÍNDICE TUDO EM [' + nome + '] RECONSTRUIR ' DE sys.tables

ABRIR table_cur

ENQUANTO 1 = 1
COMEÇAR
FETCH table_cur EM @sql
SE @@fetch_status <> 0
QUEBRAR

IMPRESSÃO @sql
EXEC(@sql)
FIM

DESALOCAR table_cur

Este não é um roteiro muito bom. Charlie adicionou colchetes ao redor do nome da mesa, para que o script
não falhe se o aluno tiver criado tabelas com espaços ou outros personagens engraçados no nome. Uma
falha é que ele ignorou que as tabelas não têm estar no esquema dbo, mas esse é o pequeno problema.
Um dos estudantes da universidade é Joe Cool. Aqui está um script para criar seu banco de dados:
CREATE LOGIN JoeCool COM SENHA = 'CollegeKid'
CRIAR BANCO DE DADOS JoeCool
ALTERAR AUTORIZAÇÃO NO BANCO DE DADOS::JoeCool TO JoeCool
SELECIONE is_srvrolemember('sysadmin', 'JoeCool')

O SELECT final informa que Joe não é membro do sysadmin. Mas essa é exatamente a ambição de Joe,
então em seu banco de dados, ele cria isso:
USE JoeCool go
CREATE TABLE tbl (um int NOT NULL)
CRIAR TABELA [tbl]] RECONSTRUIR; ALTER FUNÇÃO DE SERVIDOR sysadmin ADD MEMBER JoeCool --
]
(um int NÃO NULO)

Sim, essa última mesa tem um nome engraçado. Tão engraçado quanto Bobbie Tables na tira xkcd acima.
À noite, o trabalho do Agente de Charlie é executado, sendo executado com permissões sysadmin, é
claro. Execute o lote de cursor acima no banco de dados JoeCool e veja as instruções (adicionei uma
quebra de linha para legibilidade)
ALTER INDEX ALL ON [tbl] REBUILD
ALTER INDEX ALL ON
[TBL] RECONSTRUIR; ALTER FUNÇÃO DE SERVIDOR sysadmin ADD MEMBER JoeCool --] REBUILD

Agora execute isso novamente:


SELECIONE is_srvrolemember('sysadmin', 'JoeCool')

Joe agora é sysadmin.

Veja como Joe construiu seu nome de tabela que imprimo para maior clareza: tbl] REBUILD; ALTERAR
FUNÇÃO DE SERVIDOR SYSADMIN ADICIONAR MEMBRO JoeCool --. (Você também pode
executar uma consulta sobre sys.tables para vê-lo.) Ele começou com o nome de outra tabela no banco de
dados. Ele então adicionou um colchete direito, que ele dobrou no script CREATE TABLE, já que estava
dentro de um nome entre colchetes. O objetivo do colchete é equilibrar o colchete esquerdo no script de
Charlie. Joe também adicionou REBUILD para tornar o comando sintaticamente completo. Ele então
adicionou o comando que queria executar e, no final, adicionou personagens de comentários para matar a
última parte do comando de Charlie.
25 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

Nota: o comando ALTER SERVER ROLE que Joe usa foi introduzido no SQL 2012. Você pode usar
sp_addsrvrolemember para o mesmo truque no SQL 2008 e versões anteriores.

Há uma mudança muito simples que Charlie pode fazer em seu roteiro para parar o ataque de Joe. Antes
de olharmos para ele, vamos primeiro privar Joe de sua participação no sysadmin:
ALTER FUNÇÃO DE SERVIDOR sysadmin DROP MEMBER JoeCool

Agora altere a instrução SELECT no cursor para ler:


SELECIONE 'ALTERAR ÍNDICE ALL ON ' + quotename(name) + ' REBUILD ' FROM sys.tables

Execute o cursor novamente. Esta é a saída (novamente com uma quebra de linha para ajustar a largura da
página):
ALTER INDEX ALL ON [tbl] REBUILD
ALTER INDEX ALL ON
[TBL]] RECONSTRUIR; ALTER FUNÇÃO DE SERVIDOR sysadmin ADD MEMBER JoeCool --] REBUILD

À primeira vista, pode parecer idêntico ao anterior. Contudo:


SELECIONE is_srvrolemember('sysadmin', 'JoeCool')

agora retorna 0. Joe não foi promovido a sysadmin. Se você olhar mais de perto, você pode ver o porquê:
o colchete direito no nome da tabela de Joe agora é dobrado, então a declaração agora atinge o objetivo de
Charlie: reconstruir o índice na tabela com o nome engraçado. Esse é o objetivo da função quotename.
Antes de olharmos mais de perto para esta importante função, gostaria de fazer um comentário sobre o
exemplo em si. Normalmente, você pode pensar em um nome de tabela dinâmica como algo que vem
através de um procedimento e que, em última análise, pode vir do lado de fora do servidor. Mas, como
você viu neste exemplo, o invasor também pode vir de dentro da instância do SQL Server.

3.3 Quotename e quotestring


Sempre que você criar instruções SQL dinâmicas onde uma variável contém o nome de algo, você deve
usar quotename. quotename retorna sua entrada cercada por colchetes, e todos os colchetes direitos são
dobrados como visto aqui:
SELECT quotename('a'), quotename('[a]')
A saída é:
[a] [[a]]]

Dessa forma, você se protege da injeção de SQL em nomes de metadados. Quando você recebe um nome
de tabela como um parâmetro em um procedimento armazenado, há mais algumas precauções que estão
mais relacionadas ao design da interface do que à injeção de SQL como tal, portanto, veremos isso mais
adiante no capítulo do guia de estilo.

QuoteName aceita um segundo parâmetro que permite especificar um delimitador alternativo. Aqui estão
alguns exemplos para ilustrar isso:
SELECT quotename('Brian O''Brien', '''') -- 'Brian O''Brien'
SELECT quotename('double (") quote', '"') -- "double ("") quote"
SELECT quotename('(direita) paren', '(') -- ((direita)) paren)
SELECT quotename('{brace} position', '}') -- {{brace}} position}
SELECT quotename('right <angle>', '<') -- <right <angle>>>
SELECT quotename('back'tick', ''') -- 'back''tick'
SELECT quotename('no $ money', '$') -- NULL

26 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

Em todos os casos, o delimitador de fechamento é dobrado. Como você pode ver, para os delimitadores
emparelhados, você pode passar o delimitador esquerdo ou direito, mas é sempre o delimitador de
fechamento que é duplicado. Como o último exemplo sugere, você não pode passar nenhum caractere
como delimitador, mas todos os suportados são mostrados acima. Destes, o mais importante é o primeiro,
que você precisa usar se quiser incorporar um valor de cadeia de caracteres em sua instrução. Como
exemplo, aqui está um lote que cria um banco de dados com o nome definido dinamicamente:
DECLARE @dbname sysname = 'Meu Novo Banco de Dados',
@datadir Nvarchar(128),
@logdir Nvarchar(128),
@sql nvarchar (MAX)
SET @datadir = convert(nvarchar(128), serverproperty('InstanceDefaultDataPath'))
SET @logdir = convert(nvarchar(128), serverproperty('InstanceDefaultLogPath'))

SELECIONE @sql = 'CREATE DATABASE ' + quotename(@dbname, '"') + '


ON (NOME = ' + quotename(@dbname, '''') + ',
FILENAME = ' + quotename(@datadir + @dbname + '.mdf', '''') + ') FAÇA LOGON (NAME = '
+ quotename(@dbname + '_log', '''') + ',
FILENAME = ' + quotename(@logdir + @dbname + '.ldf', '''') + ')' PRINT @sql
EXEC(@sql)

Esta foi a declaração gerada na minha máquina:


CREATE DATABASE "Meu Novo Banco de Dados"
ON (NAME = 'Meu Novo Banco de Dados',
FILENAME = 'S:\MSSQL\SQLXI\Meu Novo Banco de Dados.mdf')
LOGON (NOME = 'Meu Novo Database_log',
FILENAME = 'S:\MSSQL\SQLXI\Meu Novo Banco de Dados.ldf')

Observação: este exemplo não é executado no SQL 2008 e versões anteriores. Esses dois parâmetros para serverproperty
foram introduzidos no SQL 2012.

Observe que o que passo para quotename é o caminho completo para os dois arquivos de banco de
dados, ou seja, a concatenação de cadeia de caracteres ocorre dentro de quotename. Para o exemplo, usei
aspas duplas para delimitar o nome do banco de dados, em vez dos colchetes mais comuns. (Este último é
específico da Microsoft, enquanto a aspa dupla é o padrão ANSI). Quanto aos outros delimitadores que
você pode usar com quotename, as situações em que você consegue usá-los são poucas e distantes entre
si.

Quotename é uma função muito conveniente, mas há uma armadilha que você precisa tomar cuidado.
Como o nome sugere, quotename destina-se a ser usado com nomes de objetos. Sua entrada é limitada a
128 caracteres (que é o comprimento máximo de um identificador no SQL Server), como visto neste
exemplo:
DECLARE @x nvarchar(255) = replicate('''', 128)
SELECT quotename(@x, '''')
SET @x = replicate('''', 129)
SELECT quotename(@x, '''')

O primeiro SELECT retorna uma longa lista de aspas simples (mais precisamente 258 delas!), mas o
segundo retorna NULL. Assim, desde que você use apenas quotename para delimitar identificadores entre
colchetes, você está seguro. Mas quando você usa quotename para delimitar valores de cadeia de
caracteres em aspas simples, você precisa considerar que o valor pode exceder 128 caracteres de
comprimento. O exemplo CREATE DATABASE acima é certamente vulnerável a esse risco. O tipo de
retorno para os dois parâmetros serverproperty é nvarchar(128) e essa também é a definição de sysname.
Assim, o nome completo do arquivo pode ter até 260 caracteres.

Se você quiser eliminar esse risco, poderá usar as funções definidas pelo usuário quotestring e
quotestring_n. Ambos aceitam nvarchar(MAX) como entrada e retornam o mesmo tipo de dados. Eles
27 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

encapsulam suas entradas em aspas simples e dobram quaisquer aspas simples dentro das cadeias de
caracteres. quotestring_n também ataca um N para tornar a string retornada um nvarchar literal. Com o
quotestring, você mesmo precisa atender a isso. Eles não são um built-in, mas listados abaixo:
CREATE FUNCTION quotestring(@str nvarchar(MAX)) RETORNA nvarchar(MAX) COMO BEGIN
DECLARE @ret nvarchar(MAX),
@sq nchar(1) = ''''
SELECIONE @ret = substituir(@str, @sq, @sq + @sq)
RETORNO(@sq + @ret + @sq)
FIM
CREATE FUNCTION quotestring_n(@str nvarchar(MAX)) RETORNA nvarchar(MAX) COMO BEGIN
DECLARE @ret nvarchar(MAX),
@sq nchar(1) = ''''
SELECIONE @ret = substituir(@str, @sq, @sq + @sq)
RETURN('N' + @sq + @ret + @sq) FIM

Veremos exemplos de uso dessas funções ao longo deste artigo.

Você pode perguntar: quando devo usar quotename (ou quotestring)? Toda vez que você insere um nome
de objeto ou um valor de cadeia de caracteres de uma variável em sua cadeia de caracteres SQL, sem
nenhuma exceção. Veja o exemplo de banco de dados acima: os dois caminhos provavelmente não são
vetores de ataque, já que você precisa ser sysadmin ou um administrador do Windows para alterá-los. Mas
o nome do banco de dados pode vir de uma fonte não confiável (como nosso garoto universitário Joe
Cool) e conter algo malicioso. E mesmo que tudo isso seja hipotético para você, ainda há um argumento
de robustez geral: se qualquer um dos nomes incluiria uma única citação, por que o comando deveria
falhar com um erro de sintaxe?

3.4 A importância do nvarchar


Esta é uma armadilha de injeção de SQL que eu não estava ciente de mim mesmo, até que eu por acaso
estava olhando para o blog de Remus Rusanu para outra coisa como eu estava trabalhando neste artigo.

Há mais uma falha no roteiro de Charlie Brown acima, e que Joe Cool explora ao criar esta tabela:

CREATE TABLE [tbl〛 RECONSTRUIR; ALTER SERVER ROLE sysadmin ADD MEMBER JoeCool --]
(a int NOT NULL)

Esse personagem engraçado depois do último caractere em tbl é o caractere Unicode U+301B, Right
White Square Bracket. A noite chega, e o script aprimorado de Charlie usando aspas é executado e isso é
impresso (com quebras de linha adicionadas):
ALTER INDEX ALL ON [tbl] REBUILD
ALTER INDEX ALL ON
[TBL]] RECONSTRUIR; ALTER FUNÇÃO DE SERVIDOR sysadmin ADD MEMBER JoeCool --] REBUILD
ALTER INDEX ALL ON
[TBL] RECONSTRUIR; ALTER FUNÇÃO DE SERVIDOR sysadmin ADD MEMBER JoeCool --] REBUILD
Observe que o engraçado caractere de colchete duplo agora é um colchete direito regular. Se você
executar
SELECT is_srvrolemember('sysadmin', 'JoeCool') você vê que

Joe Cool é novamente sysadmin.

A razão pela qual isso aconteceu é que Chaiile foi desleixado em seu roteiro quando declarou a variável
@sql :
DECLARE @sql varchar(MAX)

28 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

É varchar, não nvarchar. Quando os dados são convertidos de nvarchar para varchar, qualquer caractere
na cadeia de caracteres Unicode que não está disponível no conjunto de caracteres menor usado para
varchar é substituído por um caractere de fallback. E, neste caso, o caractere de fallback é um colchete
direito simples. quotename não ajudou aqui, porque quotename trabalhou na entrada de sys.tables que é
nvarchar e a conversão para varchar aconteceu depois que quotename tinha feito seu trabalho.

Corrija a declaração de @sql e tire Joe do sysadmin:


ALTER FUNÇÃO DE SERVIDOR sysadmin DROP MEMBER JoeCool

Altere a declaração de variável e execute o script de Charlie novamente. Desta vez, a saída é:
ALTER INDEX ALL ON [tbl] REBUILD
ALTER INDEX ALL ON
[TBL]] RECONSTRUIR; ALTER FUNÇÃO DE SERVIDOR sysadmin ADD MEMBER JoeCool --] REBUILD
ALTER INDEX ALL ON
[tbl〛 RECONSTRUIR; ALTER FUNÇÃO DE SERVIDOR sysadmin ADD MEMBER JoeCool --] REBUILD

O colchete branco direito ainda está lá e is_srvrolemember nos informa que Joe não é sysadmin.

Esse caractere não é o único, mas há mais caracteres Unicode que podem ser usados para tais explorações.
Por exemplo, em seu blog Remus menciona Modifier Letter Apostrophe, U+02BC, que é substituído por
uma única citação quando a string é lançada para varchar.

Também podemos usar esse exemplo para chamar a atenção para sp_executesql. Como sp_executesql
exige que você use nvarchar para sua variável @sql, você não pode abrir esse tipo de buraco, isso só
pode acontecer com você se você usar EXEC().

Nota: Embora o principal objetivo desses exemplos seja demonstrar que a injeção de SQL pode entrar onde você não
espera, o grande erro de Charlie é outro: ele está executando com permissões elevadas quando não deveria. Sempre que
você, como um DBA de nível de servidor, executar código em um banco de dados de usuário no qual você não pode
confiar, você deve colocar entre colchetes seus comandos em EXECUTE AS USER = 'dbo' e REVERT. Isso coloca você
no banco de dados atual, e você não tem mais permissões no nível do servidor, e um usuário como Joe não pode
explorá-las para se tornar sysadmin, ou qualquer outra coisa que ele mesmo não possa fazer, supondo que ele tenha
db_owner direitos no banco de dados. Existem outros ataques que Joe pode executar além dos ataques de injeção de
SQL que vimos. Por exemplo, ele poderia adicionar um gatilho DDL que é acionado no ALTER INDEX e que o
adiciona ao sysadmin quando o script de Charlie é executado.

3.5 Outros furos de injeção


Em todos os exemplos até agora, o vetor de ataque foi onde uma variável que deveria conter um único
token foi concatenada à cadeia de caracteres SQL. Como é um token único, a proteção com
parametrização e quotename é simples.

Se você tiver variáveis que se destinam a conter vários tokens, pode ser muito difícil se proteger contra a
injeção de SQL. Aqui está um exemplo, que ainda vejo com muita frequência:
SELECIONE @list = '1,2,3,4'
SELECT @sql = 'SELECT * FROM tbl WHERE id IN (' + @list + ')'

Como você garante que @list realmente é uma lista de valores separados por vírgulas e não algo como '0)
DROP
BANCO DE DADOS ABC --'? A resposta neste caso específico é que você não usa SQL dinâmico, mas deve
usar uma função de lista para tabela. Consulte meu breve artigo Matrizes e lista no SQL Server para obter
alternativas.

Outro exemplo é este: digamos que você tenha um procedimento armazenado que retorna um conjunto de
resultados e já esteja usando SQL dinâmico para produzir esse conjunto de resultados. Você decide
29 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

reforçá-lo adicionando um parâmetro para especificar a ordem de classificação. Pode parecer tão simples
quanto:
CRIAR OU ALTERAR PROCEDIMENTO my_dynamic_sp .....
@sortcols nvarchar(200) = NULL AS ...
SE @sortcols NÃO FOR NULO
SELECIONE @sql += ' ORDER BY ' + @sortcols ...

Mas, como você já entendeu, esse procedimento está aberto à injeção de SQL. Nesse caso, você pode
argumentar que tem tudo sob controle: você está mostrando os nomes das colunas para o usuário em uma
lista suspensa ou em algum outro dispositivo de interface do usuário, e não há nenhum lugar onde o
usuário possa digitar qualquer coisa. Isso pode ser verdade hoje, mas pode mudar daqui a dois anos,
quando for decidido adicionar uma interface web, e de repente as colunas de classificação vêm como
parâmetros em uma URL (que o usuário, é claro, tem todos os poderes para manipular). Quando você
estiver escrevendo um procedimento armazenado para uso do aplicativo, você nunca deve fazer
suposições sobre o cliente, mesmo se você escrever o código do cliente também. Menos ainda quando se
trata de sérios riscos de segurança, como injeção de SQL.

Existem várias maneiras de fazer a pele deste gato. Você pode analisar @sortcols e, em seguida, aplicar
quotename nas colunas individuais. Pessoalmente, acho que prefiro usar um parâmetro com valor de
tabela de três colunas (uma coluna para a posição, uma para o nome da coluna e uma coluna de bits para
ASC/DESC). Ou eu prefiro separar totalmente o cliente dos nomes das colunas e fazer algo assim:

SELECIONE @sql += @sortcol1 CASO


QUANDO 'col1' ENTÃO 'col1'
QUANDO 'col2' ENTÃO 'col2'
...
ELSE 'col1' -- Precisa ter um padrão.
END + ' ' + CASE @isdesc1 QUANDO 0 ENTÃO 'ASC' ELSE 'DESC' END

Se col2 fosse renomeado para outra coisa, o cliente não seria afetado.

E para generalizar isso: sempre que você pensa em passar variáveis com texto SQL de vários tokens para
seu procedimento armazenado em um aplicativo, você precisa elaborar uma solução para seu
procedimento armazenado para que ele não seja aberto à injeção de SQL. Isso não é nada simples, já que
você precisa analisar fragmentos de código SQL, algo para o qual o T-SQL não está bem equipado. Pode
haver todas as razões para reconsiderar totalmente a sua abordagem. Por exemplo, se você tem a ideia de
passar uma cláusula WHERE completa, provavelmente não deve usar um procedimento armazenado, mas
construir todo o SQL no cliente. (Passando um A cláusula WHERE do cliente para o servidor não é apenas
ruim devido ao risco de injeção de SQL, mas também porque introduz um acoplamento estreito entre
cliente e servidor que será uma dor de cabeça de manutenção a longo prazo.) Não há razão para usar
procedimentos armazenados por causa disso. Existem cenários que são muito dinâmicos onde você deseja
dar aos usuários a capacidade de recuperar dados com um alto grau de flexibilidade. Tais cenários são
melhor resolvidos no lado do cliente e, em tais soluções, o código SQL provavelmente será cortado em
pequenos fragmentos que são agrupados. Obviamente, você ainda deve se proteger contra a injeção de
SQL e ainda deve usar parâmetros para valores em cláusulas WHERE. Os nomes de colunas e tabelas nunca
devem vir de campos de texto ou URLs, mas de fontes sobre as quais você tem controle total.

Até agora código de aplicação. Para um utilitário DBA puro, você pode se permitir ser um pouco mais
liberal. Para dar um exemplo, em meu artigo Packaging Permissions in Stored Procedures, apresento um
procedimento armazenado GrantPermsToSP do qual um parâmetro é um TVP com permissões a serem
inseridas em uma instrução GRANT. Eu não faço nenhuma validação desse parâmetro, e o procedimento
está realmente aberto para injeção de SQL. No entanto, como se destina a ser chamado de SSMS por
pessoas que estão na função db_owner, não há nada que eles possam ganhar explorando o orifício de
injeção – eles não podem executar comandos mais poderosos através do procedimento do que eles
mesmos podem executar diretamente. E essa é a quintessência: a injeção de SQL é uma ameaça, quando
30 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

permite que os usuários elevem suas permissões além do que podem fazer por conta própria a partir do
SSMS ou similar. (Lembre-se de que, para usuários da Web que não têm acesso direto ao SQL Server, isso
significa que qualquer comando que eles possam injetar é uma elevação.) A partir disso, se você assinar
seu procedimento de utilitário com um certificado para empacotar permissões com o procedimento para
permitir que usuários com privilégios baixos executem algum comando privilegiado de forma controlada
(e é exatamente para isso que você usa o GrantPermsToSP), você deve absolutamente certificar-se de que
não tem nenhum buraco de injeção de SQL.

4. Desempenho
Você pode perguntar se há alguma diferença de desempenho entre o uso de SQL dinâmico e SQL estático
em procedimentos armazenados. A resposta é que, desde que você observe algumas diretrizes ao compor
seu SQL dinâmico, quase não há diferença. (Bem, se você produzir uma besta de 2000 linhas em seu
cliente e passá-la para o SQL Server, provavelmente deve considerar movê-la para um procedimento
armazenado, pois há um custo para passar todo esse código pelo fio.)

Quando você constrói seu SQL dinâmico, há duas coisas que você precisa observar para evitar causar
estragos com o desempenho. Uma delas você já aprendeu. Ou seja, você deve sempre parametrizar suas
instruções e não embutir valores de parâmetros simples por vários motivos:

Isso torna o código mais fácil de ler.


Facilita o tratamento de datas.
Isso torna mais fácil lidar com entradas como Brian O'Brien.
Você se protege contra a injeção de SQL.

Agora você vai aprender mais um motivo: é melhor para o desempenho. Não tanto para sua própria
consulta individual, mas para o servidor como um todo. Isso está relacionado a como o SQL Server
armazena em cache os planos de consulta. Para um procedimento armazenado, o SQL Server procura o
plano de consulta no cache pelo nome do procedimento armazenado. Um lote de SQL dinâmico não tem
um nome. Em vez disso, o SQL Server calcula um valor de hash a partir do texto da consulta e faz uma
pesquisa de cache nesse hash. O hash é computado no texto da consulta como está, sem qualquer
normalização de espaços, superior/inferior, comentários, etc. E o mais importante, não de valores de
parâmetro embutidos na cadeia de caracteres. Diga que você tem algo assim:
cmd.CommandText = @"SELECT ...
DE...
JUNTAR...
ONDE col1 = " + IntValue.Tostring();

Diga agora que esse texto de comando é chamado para IntValue = 7, 89 e 2454. Isso resulta em três
entradas de cache diferentes. Agora imagine que a consulta tem vários parâmetros de pesquisa e há uma
infinidade de usuários passando valores diferentes. Haverá muitas entradas de cache, e isso não é bom
para o SQL Server. Quanto mais entradas houver, maior o risco de colisões de hash. (Ou seja, dois textos
de consulta diferentes têm o mesmo valor de hash, quando o SQL Server precisa comparar os textos de
consulta diretamente.) Além disso, o cache começa a consumir muita memória que poderia ser melhor
usada em outros lugares. Eventualmente, isso pode levar a que os planos sejam expulsos do cache, de
modo que as consultas recorrentes precisem ser recompiladas. E por último, mas não menos importante:
toda essa compilação terá um grande impacto na CPU.

Eu gosto de deixar você ciente de que, quando o SQL Server calcula o valor de hash, isso inclui a lista de
parâmetros. Assim, essas duas chamadas resultarão em duas entradas de cache:
EXEC sp_executesql N'SELECT COUNT(*) DE Clientes WHERE City = @city', N'@city nvarchar(6)',
N'London'
31 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

EXEC sp_executesql N'SELECT COUNT(*) DE Clientes WHERE City = @city', N'@city nvarchar(7)',
N'Abidjan'

Observe a diferença no comprimento da declaração do parâmetro @city.

E isso é o que você obtém se no .NET definir o parâmetro desta maneira: cmd.Parameters.Add("@strval",

SqlDbType.NVarChar). Valor = cidade; Quando você deixar de fora o comprimento, o .NET definirá o

comprimento a partir do valor real de cidade. Então é por isso que você

deve sempre definir o comprimento explicitamente para parâmetros de cadeia de caracteres e binários. E
fique longe de AddWithValue.

Obs: Sendo uma pessoa honesta, preciso confessar que menti um pouco aqui. Para consultas muito simples, o SQL
Server sempre empregará a parametrização automática, para que as constantes sejam substituídas por espaços reservados
para parâmetros. A instrução INSERT no programa clientcode é um exemplo de tal consulta. Há também uma
configuração de banco de dados, parametrização forçada, sob a qual o SQL Server substitui qualquer constante em uma
consulta por um parâmetro. Essa configuração destina-se, na verdade, a ser um encobrimento para aplicativos mal
escritos que inserem valores de parâmetro embutidos em cadeias de caracteres SQL. Mas você nunca deve assumir que a
parametrização automática salvará o show, mas você deve sempre usar instruções parametrizadas.

Nesta nota, também devo ressaltar que há uma opção de configuração otimizada para cargas de trabalho ad hoc. Quando
isso é definido, o plano para uma cadeia de caracteres de consulta não parametrizada não é armazenado em cache na
primeira vez que aparece, mas apenas uma consulta de shell é armazenada no cache. O plano será armazenado em cache
somente se a mesma cadeia de caracteres de consulta aparecer uma segunda vez. Isso reduz o espaço ocupado pela
memória, mas as colisões de hash ainda estarão lá. É geralmente considerado uma prática recomendada ter essa opção
ativada, mas com um aplicativo bem escrito, isso realmente não deve importar.

Observe que isso se aplica a valores que vêm como entrada do usuário ou de alguma outra fonte. Uma
consulta pode ter uma condição como
E customers.isactive = 1

Ou seja, a consulta é conectada para retornar apenas clientes ativos. Neste caso, não há absolutamente
nenhuma razão para tornar o 1 um parâmetro.

Há mais uma medida que você precisa tomar para reduzir o lixo de cache: sempre qualificar suas tabelas
para o esquema, mesmo que todas as tabelas estejam no esquema dbo. Ou seja, você deve escrever:
cmd.CommandText = @"SELECT ...
DE dbo.sometable
JUNTE-SE dbo.someothertable ON ...
ONDE col1 = @id"

A razão para isso é a seguinte: Suponha dois usuários Jack e Jill. Jack tem o esquema padrão Jack e Jill
tem o esquema padrão Jill. Se você deixar de fora dbo e dizer apenas FROM sometable, o SQL Server
precisará considerar que a qualquer momento pode aparecer uma tabela chamada Jack.sometable ou Jill.sometable, caso
em que as consultas de Jack e Jill devem ir contra essas respectivas tabelas. Por esse motivo, Jack e Jill
não podem compartilhar a mesma entrada de cache, mas tem que haver uma entrada de cache cada para
eles. Se houver muitos usuários, isso pode levar a muitas entradas de cache para a mesma consulta.

Observe que isso se aplica a todos os objetos que pertencem a um esquema, não apenas tabelas. Se você
perder um único objeto, a entrada de cache não poderá ser compartilhada por usuários com esquemas
padrão diferentes. Aqui está uma lista de tipos de objeto para os quais você precisa especificar o esquema:

32 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

Tabelas.
Modos de exibição.
Funções definidas pelo usuário de todos os tipos.
Procedimentos armazenados.
Sinônimos.
Filas do Service Broker.
Seqüências.
Tipos definidos pelo usuário de todos os tipos. (Tipos de tabela, tipos de CLR etc).
Coleções de esquema XML.

Lembre-se de que isso não se aplica apenas às instruções no próprio SQL dinâmico, mas também à lista de
parâmetros, que também faz parte do que é armazenado em cache. Como exemplo, tivemos essa
afirmação no início do texto:
EXEC sp_executesql N'SELECT A FROM @tbl SELECT @a',
N'@tbl dbo.mytbltype READONLY, @a int', @mytbl, @a

Nota: Quando você cria um usuário com CREATE USER, o esquema padrão é definido como dbo, portanto, há uma boa
chance de que todos os usuários tenham o mesmo esquema padrão e, portanto, possam compartilhar planos de consulta
armazenados em cache mesmo se você deixar o esquema de fora. Mas não há razão para supor que isso seja sempre
verdade. Há comandos mais antigos para criar usuários, e eles também criarão um esquema para o usuário e o tornarão o
esquema padrão.

Nesta seção, usei apenas exemplos em código de cliente, mas tudo o que discuti também se aplica ao SQL
dinâmico criado em um procedimento armazenado.

5. Trabalhando com SQL dinâmico – um guia de estilo


Você aprendeu os comandos básicos para SQL dinâmico e foi avisado sobre a injeção de SQL. Agora é a
hora de aprender mais sobre SQL dinâmico de um ponto de vista prático. Você pode pensar que escrever
SQL dinâmico é uma tarefa simples: construir cadeias de caracteres, quão difícil pode ser? Por
experiência, posso dizer que é mais difícil acertar do que pode parecer inicialmente.

Escrever um bom SQL dinâmico não é apenas uma questão de habilidade. Não, talvez a virtude mais
importante seja a disciplina. Se você for consistente e cuidadoso ao escrever seu SQL dinâmico, você terá
menos bugs e seu código será mais fácil de ler e manter. Por outro lado, código mal escrito que gera SQL
dinâmico tende a ser muito difícil, para não dizer impossível, de ler e entender. O código gerado pode ser
facilmente enterrado no código T-SQL que o gera com todas as suas aspas simples e vantagens.
Geralmente, quanto menos deles você tiver, mais legível será o seu código e menor será o risco de erros.
Você já aprendeu o dispositivo mais importante: usar instruções parametrizadas. Mas como não se pode
parametrizar tudo, isso não basta. Neste capítulo darei algumas dicas sobre como melhorar seu SQL
dinâmico de diferentes ângulos: como torná-lo mais legível, mais fácil de solucionar problemas, mais
robusto e mais seguro.

5.1 Iniciar estático


Antes de começar a escrever SQL dinâmico, você precisa ter uma boa compreensão de como será a
consulta que você pretende gerar. A melhor maneira é começar escrevendo uma consulta totalmente
estática, onde você codifica temporariamente as partes que deseja que sejam dinâmicas. Depois de ter a
consulta estática funcionando, você pode usá-la como um ponto de partida para escrever o código que cria
a consulta dinâmica. Se você tentar resolver tudo de uma vez, você pode encontrar-se em uma situação em

33 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

que você não sabe se é a lógica de consulta como tal que está errada, ou se você criou a consulta da
maneira errada.

5.2 A necessidade de depurar impressões


Se você é novo no SQL dinâmico, a primeira coisa que precisa aprender é usar impressões de depuração.
Todos os procedimentos armazenados que lidam com SQL dinâmico devem incluir um parâmetro final
@debug:
@debug bit = 0 AS

E em todos os lugares no código onde você está prestes a executar algum SQL dinâmico, você deve ter:
SE @debug = 1 @sql DE IMPRESSÃO
EXECUTIVO sp_executesql @sql, @params, @par1, ...

Isso é absolutamente essencial, porque de vez em quando sua cadeia de caracteres SQL não fará o que
você pretendia, e pode até mesmo falhar em compilar. Se você receber uma mensagem de erro e não vir a
aparência da cadeia de caracteres, você está em uma sala completamente escura. Por outro lado, se você
vir o código SQL, o erro pode ser imediatamente aparente.

Se a cadeia de caracteres SQL for muito longa, a instrução poderá sair como truncada. A instrução PRINT
imprime no máximo 8000 bytes, portanto, com nvarchar(MAX) para sua variável @sql, você verá apenas
até 4000 caracteres. Uma maneira de evitar isso é usar SELECT em vez de PRINT. Isso fornece a cadeia de
caracteres em uma célula de grade no SSMS e, quando você copiá-la para uma janela de texto, você pode
descobrir que é tudo uma linha. Felizmente, as versões modernas do SSMS têm uma configuração para
controlar isso. No SSMS, vá para Tools->Options e marque a configuração Retain CR/LF on copy or save
realçada como nesta imagem:

Você também pode ver que aumentei o máximo de dados não XML para dois milhões (o padrão é 65536)
para evitar truncamento com cadeias de caracteres muito longas. Observe se você alterar essas
configurações, nenhuma delas afetará as janelas de consulta abertas no momento, mas somente as janelas
abertas após a alteração.
34 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

Nota: Para ver o máximo acima de 65536, você precisa ter o SSMS 18.2 ou posterior. A caixa de seleção para reter
CR/LF foi introduzida no SSMS 16 e está desmarcada por padrão. Se você não vir essa configuração, terá uma versão
mais antiga do SSMS. No SSMS 2012 e 2014, os CR/LF são sempre mantidos e, nos SSMS 2005 e 2008, são sempre
substituídos por um único espaço. Em qualquer caso, como o SSMS é um download gratuito, há poucas razões para usar
uma versão antiga.

Embora isso funcione, não é isento de problemas:

Se você copia dados com frequência para o Excel, talvez prefira manter a configuração Reter
desmarcada. (Porque se houver quebras de linha nos dados, o mapeamento para células no Excel será
bagunçado.)
Há situações em que conjuntos de resultados extras do procedimento armazenado podem bagunçar as
coisas, por exemplo, se você usar INSERT-EXEC, então você prefere imprimir o código na guia
Mensagens em
SSMS.
Se o procedimento produz vários lotes de SQL dinâmico, pode ser mais prático ter todos eles coletados
na guia Mensagens, em vez de ter que copiar vários conjuntos de resultados da grade de consulta.

Recebi uma sugestão de Jonathan Van Houtte que aborda o primeiro ponto (mas não os outros dois) que
faz uso dos recursos XML no SQL Server e SSMS:
SELECIONE @sql COMO [processing-instruction(x)] PARA XML PATH('')

Na célula de grade, você obtém uma cadeia de caracteres XML clicável e, quando clica nela, ela é aberta
em uma nova janela onde você vê o texto SQL com o CR/LF mantido. O código é encapsulado em uma
tag <?x, mas além disso, não há mutilação devido ao XML. Somente se a cadeia de caracteres SQL
incluísse a sequência ?>, você poderia ter problemas.

Meu colega de trabalho Jan Swedin veio com a solução definitiva para esse problema: um procedimento
armazenado que divide o texto em blocos de 8000 bytes, tomando cuidado para que cada pedaço termine
com uma quebra de linha e, em seguida, ele execute um PRINT em cada bloco. Aqui está uma versão
adaptada de seu procedimento:
CRIAR OU ALTERAR PROCEDIMENTO SystemPrint @str nvarchar(max) AS
SET XACT_ABORT, NOCOUNT ON;
DECLARE @noofchunks int,
@chunkno int = 0,
@pos int = 0, -- posição inicial da substring
@len int; -- comprimento da substring

SET @str = substituir(@str, nchar(13), '');


SET @noofchunks = teto(len(@str) / 4000.0);

ENQUANTO @chunkno < @noofchunks


COMEÇAR
SET @len = 4000 - charindex(nchar(10) COLLATE Latin1_General_BIN2,
reverse(substring(@str, @pos, 4000));
PRINT substring(@str, @pos, @len);
CONJUNTO @chunkno += 1;
SET @pos += @len + 1; -- Acumulação + LF END

Observação: há um problema conhecido com esse procedimento. Se o comprimento da cadeia de caracteres SQL estiver
próximo de um bloco completo, por exemplo, 7998 caracteres, os últimos caracteres na cadeia de caracteres podem não
ser impressos. Resta ao leitor entender como isso pode acontecer e corrigir o problema.

Não importa qual desses métodos você adota, sempre tenha uma impressão de depuração do seu SQL
dinâmico. E, sim, quero dizer SEMPRE!

35 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

5.3 Mensagens de erro e números de linha


Quando você trabalha com SQL dinâmico, você sempre encontrará mensagens de erro. Às vezes o erro
está relacionado ao código que gera o SQL dinâmico, às vezes vem do próprio SQL dinâmico. Em ambos
os casos, pode ser um erro de compilação ou um erro em tempo de execução. Escusado será dizer que
você precisa entender a origem do erro, para que você comece a olhar para a parte certa do código.
Infelizmente, isso é algo mais complicado do que tem que ser, devido a um recurso em versões recentes
do SSMS.

Para esta seção, trabalharemos com a tarefa de criar um logon SQL, um usuário de banco de dados para
esse logon e adicionar o usuário à função PlainUsers. A entrada para a operação é o nome de login e a
senha. Primeiro, veremos a situação em que o SQL dinâmico é gerado dentro de um procedimento
armazenado. Copie esse código, que tem vários erros, em uma janela de consulta vazia.
USE tempdb go
SE NÃO EXISTIR (SELECIONE * sys.database_principals ONDE NOME = 'PlainUsers')
CREATE ROLE PlainUsers vão
CRIAR OU ALTERAR PROCEDIMENTO add_new_user @name sysname,
@password Nvarchar(50),
@debug bit = 0 AS
DECLARAR @sql nvarchar(MAX)
SELECIONE @sql = 'CREATE LOGIN' + quotename(@name) + '
COM SENHA = ' + quotename(@password, '''') + ' MUST_CHANGE, CHECK_EXPIRATION
CRIAR USUÁRIO ' + quotename(@name) + '
ALTER ROLE PlainUsers ADD MEMBER ' + quotename(@name) IF @debug = 1
IMPRESSÃO @sql
EXEC sp_exectesql @sql GB
EXEC add_new_user 'nisse', 'TotaL1y#and0m', 1,

Nota: Se você estiver executando esses exemplos no SQL 2008 ou anterior, você pode observar que o código usa a
sintaxe ALTER ROLE ADD MEMBER que foi adicionada no SQL 2012. No entanto, como estamos explorando
mensagens de erro, isso não deve ser de muita importância prática para você.

A primeira mensagem de erro que encontramos quando tentamos este pedaço de código não tem relação
com o SQL dinâmico como tal, mas serve para destacar um comportamento em versões modernas do
SSMS que é importante entender para esta seção. Essa é a saída quando você executa o script no SSMS
2014 ou posterior.
O módulo 'add_new_user' depende do objeto ausente 'sp_exectesql'. O módulo ainda será criado; no entanto, ele não pode
ser executado com êxito até que o objeto exista. Msg 102, Nível 15, Estado 1, Linha 19 Sintaxe incorreta perto de ','.

Se você clicar duas vezes na mensagem de erro, descobrirá que você acaba na linha da instrução EXEC
após o procedimento e, de fato, há uma vírgula à direita que não deve estar lá. E se você olhar na barra de
status, verá que ela diz Ln 19, o que concorda com a mensagem de erro. No entanto, esse não é o número
de erro relatado pelo SQL Server. Lembre-se de que o SSMS envia um lote, conforme definido pelo
separador go, por vez para o SQL Server, que não tem conhecimento sobre sua janela de consulta. A
chamada para add_new_user é um lote de linha única, portanto, o número de linha real relatado pelo SQL
Server é a Linha 1. Esse também é o número de linha que você veria se fosse executá-lo no SSMS 2012
ou anterior. Você pode se convencer desse fato salvando o script em um arquivo e executando-o através do
SQLCMD, que não faz ajustes. Por que isso é importante entender vai prevalecer em poucos instantes.

Remova a vírgula ofensiva (ou substitua-a por ponto-e-vírgula) e tente novamente. Desta vez, a saída é:
O módulo 'add_new_user' depende do objeto ausente 'sp_exectesql'. O módulo ainda será criado; no entanto, ele não pode ser
executado com êxito até que o objeto exista.
CRIAR LOGIN [nisse]
COM SENHA = 'TotaL1y#and0m' MUST_CHANGE,
CHECK_EXPIRATION
CRIAR USUÁRIO [nisse]

36 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

ALTER ROLE PlainUsers ADD MEMBER [nisse]


Msg 2812, Nível 16, Estado 62, Procedimento add_new_user, Linha 12 [Linha de Início em Lote 18] Não foi possível
localizar o procedimento armazenado 'sp_exectesql'.

(A peça Batch Start Line 18, aparece somente no SSMS 16 e posterior. Se você estiver usando o SSMS
2014 e versões anteriores, não verá essa parte.) Observe aqui que a mensagem de erro inclui o nome do
procedimento add_new_user. Isso é muito importante. Isso nos informa que o erro ocorreu durante a
compilação ou execução do procedimento armazenado. Como o nome está presente, o erro não pode vir
do SQL dinâmico. (Lembre-se de que o SQL dinâmico é um procedimento armazenado sem nome
próprio.) O SSMS vê o nome do procedimento e percebe que há pouco sentido em modificar o número da
linha do SQL Server, e o número da linha mostrado aqui é realmente a linha 12 no procedimento
armazenado, não a linha 12 na janela de consulta. (Neste exemplo específico, o procedimento está
presente na janela de consulta, mas muitas vezes você chamará procedimentos armazenados não visíveis
na janela atual.)

Corrija o erro de digitação e execute novamente o procedimento. Agora obtemos essa saída no SSMS 16 e
posterior.
CRIAR LOGIN [nisse]
COM SENHA = 'TotaL1y#and0m' MUST_CHANGE,
CHECK_EXPIRATION
CRIAR USUÁRIO [nisse]
ALTER ROLE PlainUsers ADD MEMBER [nisse]]
Msg 156, Nível 15, Estado 1, Linha 22
Sintaxe incorreta perto da palavra-chave 'CREATE'.

Não há nenhum nome de procedimento na mensagem de erro, portanto, o erro não pode vir diretamente de
dentro add_new_user. Poderia vir do SQL dinâmico? Ou poderia vir de algo diretamente na janela de
consulta? Podemos tentar clicar duas vezes na mensagem de erro e ver onde chegamos. Se você tentar
isso, você pode não ser capaz de fazer muito sentido dele, mas se você tentar várias vezes com o cursor
colocado em linhas diferentes, você acabará percebendo que o cursor permanece onde você estava. Você
também pode receber uma caixa de mensagem do SSMS sobre algo estar fora dos limites. Seja como for,
se você verificar mais de perto, perceberá que a mensagem de erro diz linha 22, mas há apenas 19 linhas
no script. E o SQL dinâmico é apenas cinco linhas de código, então de onde vieram essas 22?

Acima, o SSMS foi útil e modificou o número de linha do SQL Server para um número de linha na janela
de consulta, e é isso que está acontecendo aqui também. Mas qual é o ponto de modificar o número de
linha do SQL dinâmico? Obviamente, queremos ver o número de linha real conforme relatado pelo SQL
Server. Tentar mapear isso para um número de linha na janela de consulta parece simplesmente bobo. A
resposta é que não há como o SSMS discernir se a mensagem de erro vem do código SQL diretamente
presente na janela de consulta ou de algum lote de SQL dinâmico executado a partir dessa janela. As
informações de erro fornecidas pelo SQL Server não têm esses detalhes, portanto, o SSMS apenas assume
que o erro vem de algo diretamente presente na janela.

Isso leva a uma situação muito constrangedora. Neste exemplo, talvez não seja tão difícil, já que o SQL
dinâmico tem apenas cinco linhas, mas digamos que você gere um lote maior de SQL dinâmico
consistindo de 50 a 100 linhas de código. Neste caso, você realmente quer saber o número da linha dentro
do lote, para que você possa encontrar onde o erro realmente está.

O processo é o seguinte. Você primeiro encontra o início do lote que você realmente executou, que neste
caso é esta linha:
EXEC add_new_user 'nisse', 'TotaL1y#and0m', 1

37 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

A barra de status nos informa que esta é a linha 19. Se o erro estivesse na primeira linha do SQL
dinâmico, o SQL Server teria relatado a Linha 1 e o SSMS teria dito 19. Agora eram 22, e 22-19+1 = 4.
Assim, de acordo com o SQL Server, o erro está na linha 4. O erro real é que a sintaxe correta para o
CHECK_EXPIRATION cláusula é CHECK_EXPIRATION = ON, mas o SQL Server não detecta o erro até ver
o CREATE na próxima linha. Se você adicionar a sintaxe ausente e tentar novamente, descobrirá que o lote
é executado com êxito. (Se você tiver o SQL 2012 ou posterior, conforme observado acima.)

Aqui eu tinha um procedimento armazenado e, portanto, era simples dizer se o erro era do código que
gerou o SQL dinâmico ou o próprio SQL dinâmico. Mas digamos que queremos tornar isso um script
simples, porque queremos usá-lo em diferentes servidores ou bancos de dados em diferentes ocasiões.
Cole o script abaixo na mesma janela de consulta do procedimento acima, adicionando um separador go
após a chamada ao add_new_user e, em seguida, execute o que você colou:
DECLARE @name sysname = 'linda',
@password nvarchar(50) = 'UlTr@-TOp_$ecre1', @debug bit = 1

DECLARAR @sql nvarchar(MAX)


SELECIONE @sql = 'CREATE LOGIN' + quotename(@name) + '
COM SENHA = ''' quotename(@password, '''') + ' MUST_CHANGE,
CHECK_EXPIRATION = LIGADO
CRIAR USUÁRIO ' + quotename(@name) + '
ALTER ROLE PlainUsers ADD MEMBER ' + quotename(@name) IF @debug = 1
IMPRESSÃO @sql
EXEC sp_executesql @sql

Esta é a saída:
Msg 102, Nível 15, Estado 1, Linha 27 Sintaxe incorreta perto
de 'quotename'.

Essa mensagem vem do SQL dinâmico ou do próprio script? Neste exemplo, não há nenhum nome de
procedimento para nos guiar. Mas há algumas pistas. Uma pista é a aparência de quotename na
mensagem de erro, isso sugere que é um erro no próprio script, já que quotename não parece ser usado
dentro do SQL dinâmico. Outra pista é que o conteúdo do @sql não é impresso, o que seria se a execução
tivesse atingido a linha que invoca o SQL dinâmico. Podemos obter mais uma pista clicando duas vezes
na mensagem de erro para ver se acertamos algo que corresponde à mensagem de erro. Desta vez,
terminamos nesta linha:
COM SENHA = ''' quotename(@password, '''') + ' MUST_CHANGE

Se olharmos mais de perto, podemos notar que deve haver um + entre a sequência de aspas simples e o
nome da citação. Insira o caractere de adição e execute novamente:
CRIAR LOGIN [linda]
COM SENHA = ''UlTr@-TOp_$ecre1' MUST_CHANGE,
CHECK_EXPIRATION = LIGADO
CRIAR USUÁRIO [linda]
ALTER ROLE PlainUsers ADD MEMBER [linda] Msg 102, Nível 15,
Estado 1, Linha 22
Sintaxe incorreta perto de 'UlTr@'.
Msg 105, Nível 15, Estado 1, Linha 22
Aspas não fechadas após a cadeia de caracteres ' MUST_CHANGE,
CRIAR USUÁRIO [linda]
CHECK_EXPIRATION = LIGADO
ALTER ROLE PlainUsers ADD MEMBER [linda]'.

Como vemos o SQL impresso, isso é um forte indício de que o erro está no SQL dinâmico. Como o texto
UlTr@ aparece no texto da mensagem, o texto por si só nos ajuda a localizar onde está o erro. Mas
digamos que a situação não é tão feliz, mas precisamos traduzir o número de erro do SSMS para um

38 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

número de linha no SQL dinâmico. Coloque o cursor na primeira linha do lote e, neste exemplo, esta é a
linha com a primeira DECLARE. Essa é a linha 21 e, portanto, o número da linha que o SQL Server relata é
22-21+1 = 2. Se o lote tiver uma ou mais linhas em branco antes da DECLARE, você deverá colocar o
cursor na primeira dessas linhas em branco – se tiver incluído essas linhas em branco quando selecionou o
texto da consulta a ser executado. Ou seja, o número base é a primeira linha do que você selecionou na
janela de consulta.

Observe que isso destaca a importância de ter esses PRINTs de depuração. Se você não vir a saída de
depuração, poderá suspeitar que a mensagem de erro está no próprio script e, inversamente, se você vir a
saída de depuração, é provável que o erro esteja no SQL dinâmico. (Embora tenhamos visto uma exceção
a essa regra com o erro ortográfico de sp_executesql acima.)

Se você tiver um script que produz vários lotes de SQL dinâmico, as coisas podem ficar realmente
complicadas. Se você receber uma mensagem de erro após a impressão de um lote de SQL dinâmico, o
erro pode estar nesse lote – ou pode vir do código que gera o próximo lote. Você terá que usar seu bom
senso para descobrir o que está acontecendo. Se você clicar duas vezes na mensagem de erro e a linha em
que você acabar não tem relação com a mensagem de erro, isso é uma indicação de que o erro está no
SQL dinâmico.

5.4 Espaçamento e formatação


Como eu disse anteriormente, uma virtude importante quando você trabalha com SQL dinâmico é a
disciplina, e isso realmente entra em cena quando se trata de espaçamento. Você precisa tomar cuidado
para que diferentes partes do seu SQL dinâmico não se choquem umas com as outras. Aqui está um
exemplo em que o desleixo é recompensado com uma mensagem de erro:
DECLARAR @sql nvarchar(MAX)
DECLARE @tblname sysname = 'dbo. Clientes
SELECT @sql = 'SELECT CompanyName, ContactName, Endereço
DE' + @tblname +
'ONDE Cidade = @city'
IMPRESSÃO @sql
EXEC sp_executesql @sql, N'@city nvarchar(15)', 'Londres'

A saída é:
SELECT CompanyName, ContactName, Endereço
FROMdbo.CustomersWHERE City = @city Msg 102, Level
15, State 1, Line 36 Sintaxe incorreta perto de '.'.

O FROM e o WHERE foram colados com o nome da tabela devido aos espaços ausentes na cadeia de
caracteres SQL. (Como observado acima, o número da linha depende exatamente de onde na janela você
colou o comando. Eu executei o acima em uma janela em que eu já tinha algum código, e o SSMS relatou
o erro para estar na linha 36.)

Você deve criar o hábito de garantir que sempre haja espaço em branco antes e depois das palavras-chave
quando você dividir a cadeia de caracteres para trazer o conteúdo de uma variável ou o que quer que seja.
O espaço em branco inclui nova linha e, como o T-SQL permite que literais de cadeia de caracteres sejam
executados sobre limites de linha, você deve fazer uso disso. Este é um exemplo melhor do que foi dito
acima:
DECLARAR @sql nvarchar(MAX)
DECLARE @tblname sysname = 'dbo. Clientes
SELECT @sql = 'SELECT CompanyName, ContactName, Endereço
DE ' + @tblname + '
ONDE Cidade = @city'
IMPRESSÃO @sql
EXEC sp_executesql @sql, N'@city nvarchar(15)', 'Londres'
39 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

O lote SQL agora é executado com êxito. Esta é a cadeia de caracteres SQL que é impressa:
SELECT CompanyName, ContactName, Endereço
DE dbo. Clientes
ONDE Cidade = @city

O recuo é um pouco engraçado, mas o código é perfeitamente legível.

Se você estiver vindo de uma linguagem como Visual Basic que não permite literais de sequência de
sequência de caracteres para executar quebras de linha, você pode preferir algo assim:
DECLARAR @sql nvarchar(MAX)
DECLARE @tblname sysname = 'dbo. SELECT @sql dos clientes = 'SELECT
CompanyName, ContactName, Address ' +
' DE ' + @tblname +
' ONDE Cidade = @city '
IMPRESSÃO @sql
EXEC sp_executesql @sql, N'@city nvarchar(15)', 'Londres'

Embora este código como tal certamente seja bastante fácil de ler, eu ainda recomendo fortemente contra
isso por mais de um motivo. Uma delas é que o SQL dinâmico será uma única linha longa, portanto, se
houver uma mensagem de erro, ela sempre será relatada como na linha 1 (depois de aplicar as
transformações na seção anterior). Se a mensagem de erro for enigmática e o texto SQL for longo, você
pode ficar no escuro sobre onde o erro realmente está. Uma razão mais geral é que, na minha experiência,
os limites entre todas essas vantagens e citações é uma área perigosa onde os erros facilmente se insinuam
porque acidentalmente deixamos de fora um ou outro. Então, quanto menos tivermos, melhor.

Como eu disse no início deste capítulo: quando você estiver prestes a criar algo com SQL dinâmico,
primeiro escreva uma consulta estática onde você tenha valores de exemplo para o que você precisa ter
dinâmico em tempo de execução. Isso serve a dois propósitos:

Certifique-se de que a sintaxe da consulta como tal esteja correta.


Você obtém a formatação que você gosta da consulta.

Gosto de ressaltar que a formatação do SQL dinâmico é importante por dois motivos:

1. Se houver um erro de compilação no código, o número da linha ajudará você a encontrar onde o erro
está. (Não importa que você tenha que passar pelos aros que discuti na seção anterior.)
2. O código é executado, mas não produz os resultados esperados. Certamente ajuda se a consulta estiver
formatada de uma maneira que você está acostumado quando você está tentando entender o que está
errado com ela.

Às vezes, você usa várias instruções para criar seu SQL dinâmico, porque você está pegando partes
diferentes de lugares diferentes. Um excelente exemplo é quando você escreve algo para dar suporte a
condições de pesquisa dinâmica. Uma passagem típica em tal procedimento poderia ser:
SE @orderid NÃO FOR NULO
DEFINA @sql += ' E O.OrderID = @orderid'

(Observe o espaço antes de AND!) Agora, se você adicionar muitos desses fragmentos SQL, você os
obterá todos em uma única linha, que pode não ser tão legível. Ao mesmo tempo, colocar a cotação única
de fechamento na próxima linha pareceria engraçado. A maneira como eu costumo lidar com isso é que eu
introduzo uma variável que eu normalmente chamo de @nl (para newline):
DECLARE @nl nchar(2) = char(13) + char(10)

E então eu escrevo o acima como

40 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

SE @orderid NÃO FOR NULO


DEFINA @sql += ' E O.OrderID = @orderid' + @nl

Isso me dá quebras de linha no meu SQL dinâmico. Esta pequena dica pode não registrar com você
imediatamente, mas eu achei essa abordagem muito valiosa para me ajudar a escrever código que gera
SQL dinâmico, de modo que tanto o código em si quanto o código gerado seja claramente legível.

5.5 Lidando com cadeias de caracteres aninhadas


Uma das coisas mais charmosas (ironia pretendida) ao trabalhar com SQL dinâmico são literais de cadeia
de caracteres aninhados. Para incluir uma única aspa em uma cadeia de caracteres no T-SQL, você precisa
dobrá-la. Você já aprendeu que, quando se trata de cadeias de caracteres onde os valores vêm de variáveis
e onde a sintaxe não permite que você use as variáveis diretamente, você deve usar quotename ou
quotestring para obter o valor na cadeia de caracteres SQL para se proteger contra a injeção de SQL. Isso
tem o efeito colateral de que a quantidade de aspas aninhadas é reduzida.

Para deixar claro do que estou falando, essa não é a maneira correta de fazê-lo:
CRIAR OU ALTERAR PROCEDIMENTO create_db @dbname sysname,
@sizeinmb smallint = 1200,
@logsize smallint = 200,
@debug bit = 0 AS

DECLARE @datadir nvarchar(128) =


convert(nvarchar(128), serverproperty('InstanceDefaultDataPath')),
@logdir nvarchar(128) =
convert(nvarchar(128), serverproperty('InstanceDefaultLogPath')),
@sql nvarchar (MAX)
SELECIONE @sql =
N'CRIAR BANCO DE DADOS ' + quotename(@dbname) + '
NA PRIMÁRIA (NOME = N''' + @dbname + ''',
NOME DO ARQUIVO = N''' + @datadir + @dbname + '.mdf'',
TAMANHO = ' + convert(varchar, @sizeinmb) + ' MB) FAÇA LOGON (NOME =
N''' + @dbname + '_log'',
NOME DO ARQUIVO = N''' + @logdir + @dbname + '.ldf'',
TAMANHO = ' + convert(varchar, @logsize) + ' MB)'
SE @debug = 1 @sql DE IMPRESSÃO
EXEC(@sql)

Como um lembrete: o acima nunca será executado corretamente no SQL 2008 ou anterior, pois os dois parâmetros para
serverproperty foram adicionados no SQL 2012.

Esse código falhará se houver uma única aspa no nome do banco de dados ou em qualquer um dos
caminhos de instalação. E está aberto à injeção de SQL, o que importaria se create_db fosse exposto a
usuários simples para permitir que eles criassem um banco de dados de forma controlada. (O
procedimento teria que ser executado com permissões mais altas do que os próprios usuários.) Acrescente
a isso que não é trivial escrever. Levei algumas tentativas para acertar todas as citações, especialmente
todas aquelas Ns. Como repetição, aqui está a maneira correta de fazê-lo:
CRIAR OU ALTERAR PROCEDIMENTO create_db @dbname sysname,
@sizeinmb smallint = 1200,
@logsize smallint = 200,
@debug bit = 0 AS

DECLARE @datadir nvarchar(128) =


convert(nvarchar(128), serverproperty('InstanceDefaultDataPath')),
@logdir nvarchar(128) =
convert(nvarchar(128), serverproperty('InstanceDefaultLogPath')),
@sql nvarchar (MAX)
SELECIONE @sql =
N'CRIAR BANCO DE DADOS ' + quotename(@dbname) + '
41 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

NO PRIMÁRIO (NOME = N' + quotename(@dbname, '''') + ',


NOME DO ARQUIVO = N' + dbo.quotestring(@datadir + @dbname + '.mdf') + ',
TAMANHO = ' + convert(varchar, @sizeinmb) + ' MB)
LOGON (NOME = N' + quotename(@dbname + '_log', '''') + ',
NOME DO ARQUIVO = N' + dbo.quotestring(@logdir + @dbname + '.ldf') + ',
TAMANHO = ' + convert(varchar, @logsize) + ' MB)'
SE @debug = 1 @sql DE IMPRESSÃO
EXEC(@sql)

Aqui eu usei quotename para os nomes internos dos arquivos, pois eles podem no máximo ter 128
caracteres, mas quotestring para os caminhos de arquivo. Mas não seria errado usar aspas para todos os
quatro.

Em termos de legibilidade pode ser uma questão de gosto que você prefere, mas sugiro que a segunda seja
mais fácil de escrever. E, além disso, é seguro para injeção de SQL.

Veremos algumas técnicas avançadas para lidar com cadeias de caracteres aninhadas nas seções Mais
sobre nomes dinâmicos e cadeias de caracteres aninhadas e Jogando com QUOTED_IDENTIFIER.

Nota: Uma versão melhor do create_db limparia @dbname antes de construir os caminhos de arquivo, para que ele não
inclua nenhum caractere com significado especial para o sistema de arquivos, por exemplo, barras inclinadas para frente
ou para trás. Deixei isso de fora, pois extrapola o escopo deste artigo.

5.6 Uma armadilha com cadeias de caracteres SQL longas


Aqui está algo que você pode encontrar se você concatenar cadeias de caracteres SQL que são bastante
longas. Dê uma olhada no script no arquivo longquery.sql. Você encontra ali uma consulta que é
aparentemente sem sentido. Para obter uma demonstração para esta seção, fiz uma longa consulta nos
fóruns do MSDN, mas para proteger o pôster, substituí os nomes das colunas originais por nomes de
produtos da NorthDynamic. A consulta original não usava SQL dinâmico, mas introduzi um nome de
banco de dados dinâmico por causa do exemplo desta seção.

Observação: para obter a saída descrita nesta seção, você precisa ter o SQL 2012 ou posterior. No SQL 2008, você
obterá erros adicionais devido ao uso do IIF.

O comportamento esperado ao executar o script é que receberemos uma mensagem de erro nos dizendo
que
View_XVUnitsProducts é um objeto inválido, já que não há essa exibição no tempdb. No entanto, quando
executamos o script, obtemos um erro diferente:
Msg 102, Nível 15, Estado 1, Linha 49 Sintaxe incorreta perto
de 'View_XVUnits'.

Também podemos ver no conjunto de resultados que @sql tem exatamente 4000 caracteres e, se
olharmos para o conteúdo de @sql, podemos ver que a cadeia de caracteres está truncada. Estamos
usando nvarchar(MAX) para nossa variável @sql, e ainda assim a consulta está truncada?

O motivo subjacente é encontrado no sistema de tipo do SQL Server. Se você tiver uma operação com
dois operandos do mesmo tipo de dados, o resultado será do mesmo tipo de dados que os operandos.
Nesse contexto, nvarchar(20) e nvarchar(40) podem ser considerados o mesmo tipo de dados, enquanto
nvarchar(MAX) é um tipo de dados diferente. No script, a variável @sql é construída a partir de um
número de literais nvarchar intercalados com algumas chamadas para citarnome. Todos os literais de
cadeia de caracteres têm menos de 4000 caracteres. Por esta razão, o tipo da expressão é nvarchar(4000).
O fato de haver uma variável nvarchar(MAX) no lado esquerdo do operador de atribuição não altera isso.
Assim, o resultado líquido é que a expressão de cadeia de caracteres é silenciosamente truncada.

42 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

Nota: para um tempero extra, tente substituir tempdb no script por NorthDynamic. A maneira como o truncamento
atinge desta vez, a mensagem de erro definitivamente não é fácil de entender. Pelo menos desde que você não olhe para
a saída de depuração.

A princípio, pode parecer que, para fazer isso funcionar, temos que encapsular cada cadeia de caracteres
em converter ou converter para alterar o tipo de dados para nvarchar(MAX), o que tornaria o código
mais difícil de ler. Felizmente, não é tão ruim assim. O SQL Server tem uma regra que diz que, quando
dois tipos diferentes se encontram na mesma expressão, o tipo com menor precedência no sistema de tipos
do SQL Server é convertido para o tipo com precedência mais alta (se existir uma conversão implícita, ou
seja). Por esta razão, é suficiente introduzir um valor inicial de nvarchar(MAX) no início da
concatenação, e isso causa a conversão em bola de neve através de toda a concatenação. Aqui está uma
possível solução (estou mostrando apenas o início do script):
DECLARE @sql nvarchar(MAX),
@dbname sysname = 'tempdb'

SELECIONE @sql = cast('' AS nvarchar(MAX)) + N'

A mensagem de erro agora é a esperada:


Msg 208, Nível 16, Estado 1, Linha 2
Nome de objeto inválido 'tempdb.dbo.View_XVUnitsProducts'.

Também podemos ver no conjunto de resultados que a cadeia de caracteres tem 4423 caracteres. Ou seja,
adicionando uma string cast vazia ao nvarchar(MAX) conseguimos que a consulta funcionasse com o
mínimo de lixo.

A variável @sql em si pode servir para a tarefa, e em muitos casos ela vem naturalmente, porque você
adiciona ao @sql corda peça por peça. Neste exemplo pode ser assim:
DECLARE @sql nvarchar(MAX) = '',
@dbname sysname = 'tempdb'

SELECIONE @sql = @sql + N'

Mas há duas novas armadilhas escondidas aqui. O primeiro é um que você notaria rapidamente. Se você
não conseguir inicializar @sql para a cadeia de caracteres vazia, a cadeia inteira será NULL e nada será
executado. A outra armadilha é que você (ou outra pessoa) pode ser tentado a introduzir o operador de
atalho +=:
DECLARE @sql nvarchar(MAX) = '',
@dbname sysname = 'NorthDynamic'

SELECIONE @sql += N'

Isso trará de volta o erro de sintaxe, porque no lado direito do operador +=, agora há apenas cadeias de
caracteres curtas e, portanto, não há conversão implícita para nvarchar(MAX). Por esta razão, adicionar
cast('' AS nvarchar(MAX)) + N' é uma boa precaução de segurança.

5.7 Recebendo nomes de tabelas e outros objetos como parâmetros


Há situações em que você deseja receber o nome de uma tabela ou algum outro objeto como um
parâmetro. Ter nomes de tabelas como parâmetro para um procedimento em um aplicativo não é a melhor
das ideias, algo que discutirei mais adiante no último capítulo deste artigo. Mas certamente há situações
em que isso pode ser desejável para uma tarefa de DBA. Considere este procedimento que pode não ser o
melhor exemplo de um caso de uso prático, mas serve como um exemplo curto e conciso:

43 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

USE NorthDynamic ir
CRIAR OU ALTERAR PROCEDIMENTO GetCount @tblname nvarchar(1024) COMO
DECLARAR @sql nvarchar(MAX)
SELECIONE @sql = 'SELECT COUNT(*) FROM ' + @tblname
IMPRESSÃO @sql
EXEC sp_executesql @sql ir
EXEC GetCount 'dbo. Clientes

O exemplo é executado, mas, como você pode ver, o procedimento está aberto para injeção de SQL.
Também não é capaz de lidar com nomes com caracteres especiais. Aprendemos que devemos usar o nome
de aspas para resolver isso. Mas se tentarmos:
CRIAR OU ALTERAR PROCEDIMENTO GetCount @tblname nvarchar(1024) COMO
DECLARAR @sql nvarchar(MAX)
SELECIONE @sql = 'SELECT COUNT(*) FROM ' + quotename(@tblname)
IMPRESSÃO @sql
EXEC sp_executesql @sql ir
EXEC GetCount 'dbo. Clientes

Recebemos uma mensagem de erro:


SELECIONE COUNT(*) DE [dbo. Clientes]
Msg 208, Nível 16, Estado 1, Linha 7 Nome de objeto
inválido 'dbo. clientes'.

Alguns leitores podem ficar intrigados com isso, porque à primeira vista parece correto. Não existe uma
tabela com esse nome no NorthDynamic? não, há uma tabela Customers no esquema dbo e, com
colchetes aplicados corretamente, isso é escrito como [dbo].[Clientes]. Mas quando há apenas um par de
colchetes, o SQL Server usa dbo. Clientes como um nome de tabela na notação de uma parte e o procura
no esquema padrão do usuário atual. Ou seja, o ponto faz parte do nome; não é um separador quando está
dentro dos colchetes.

Aqui está outro exemplo onde dá errado:


EXEC GetCount '[Detalhes do pedido]'

A saída:
SELECT COUNT(*) FROM [[Detalhes do pedido]]]
Msg 208, Nível 16, Estado 1, Linha 10
Nome de objeto inválido '[Detalhes do pedido]'.

Desta vez, falhou, porque o chamador já tinha colocado o nome entre colchetes.

A maneira de resolver isso é primeiro usar a função parsename para obter qualquer parte do banco de
dados no @tblname. ParseName é uma função que retorna as várias partes de uma especificação de objeto
com as partes numeradas da direita para a esquerda. Ou seja, 1 é o próprio nome do objeto, 2 é o esquema,
3 é o nome do banco de dados e 4 é o nome de qualquer servidor vinculado. Se uma parte estiver ausente
da especificação, NULL será retornado. Em seguida, use a função object_id para obter a ID do objeto para
a tabela/exibição e a função db_id para obter a ID do banco de dados em @tblname ou a ID do banco de
dados atual, se nenhum componente de banco de dados estiver presente. Finalmente, você usa a função
de metadados db_name para converter a id do banco de dados de volta para um nome junto com as
funções
object_schema_name e object_name para obter os nomes do esquema e do próprio objeto a partir dos IDs
do objeto e do banco de dados. Você encapsula cada componente em quotename. Aqui está um exemplo,
com comentários adicionados para explicar as várias etapas.
CRIAR OU ALTERAR PROCEDIMENTO GetCount @tblname nvarchar(1024) COMO
DECLARE @sql nvarchar(MAX),
@object_id int,
44 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

@db_id int

- Apoiar nomes em servidores vinculados exigiria muito mais trabalho. SE parsename(@tblname, 4)


NÃO FOR NULO
COMEÇAR
RAISERROR('Tabelas em um servidor diferente não são suportadas.', 16, 1) RETURN 1
FIM

-- Obter id do objeto e validar.


SELECIONE @object_id = object_id(@tblname)
SE @object_id FOR NULL
COMEÇAR
RAISERROR('No such table/view "%s".', 16, 1, @tblname) RETORNAR 1
FIM

-- Obtenha a ID do banco de dados para o objeto.


SE parsename(@tblname, 3) NÃO FOR NULO
SELECT @db_id = db_id(parsename(@tblname, 3))
MAIS
SELECIONE @db_id = db_id()

-- Obtenha o nome normalizado.


SELECIONE @tblname = quotename(db_name(@db_id)) + '.' +
quotename(object_schema_name(@object_id, @db_id)) + '.' + quotename(object_name(@object_id,
@db_id))

SELECIONE @sql = 'SELECT COUNT(*) FROM ' + @tblname PRINT


@sql
EXEC sp_executesql @sql

Como diz o comentário, o procedimento não suporta objetos em servidores vinculados, pois isso seria
muito mais complicado, e deixo isso como um exercício para o leitor que precisa disso. Esse
procedimento oferece suporte a objetos em outros bancos de dados, mas, em muitos casos, você pode se
permitir restringir uma pesquisa como essa ao banco de dados atual. Nesse caso, você deve adicionar
parsename(@tblname, 3) NÃO É NULO à verificação inicial e alterar a mensagem de erro de acordo.

Aqui estão alguns casos de teste:


CREATE DATABASE [extra-banco de dados]
ir
USE [extra-banco de dados] ir
CRIAR ESQUEMA extra;
ir
CREATE TABLE extra.testtable(a int NOT NULL)
INSERT extra.testtable(a) SELECT TOP 65 object_id FROM sys.objects go
USE NorthDynamic ir
EXEC GetCount 'dbo. Clientes
EXEC GetCount '[Detalhes do pedido]'
EXEC GetCount 'sys.schemas'
EXEC GetCount 'msdb.dbo.sysjobs'
EXEC GetCount '[extra-database].extra.testtable' EXEC GetCount
'OTHERSERVER.master.sys.databases' ir
DROP DATABASE [extra-banco de dados]

Ao executar os testes, obtemos essa saída na guia Mensagens (com as mensagens "linhas afetadas"
omitidas):
SELECIONE COUNT(*) DE [NorthDynamic]. [dbo]. [Clientes]
SELECIONE COUNT(*) DE [NorthDynamic]. [dbo]. [Detalhes do pedido]
SELECIONE COUNT(*) DE [NorthDynamic]. [sys]. [esquemas]
SELECIONE COUNT(*) DE [msdb]. [dbo]. [sysjobs]
SELECT COUNT(*) FROM [extra-database]. [extra]. [testtable]
Msg 50000, Nível 16, Estado 1, Procedimento GetCount, Linha 9 [Linha de Início em Lote 50] Tabelas em um
servidor diferente não é suportado.
45 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

John Smart leu isso e gostou da ideia. Ele decidiu empacotar o acima em uma função definida pelo
usuário,
SanitizeObjectName, que ele teve a gentileza de compartilhar. Isso permite que você execute
SELECIONE dbo. SanitizeObjectName('Detalhes do pedido')

e volte [NorthDynamic].[ DBO]. [Detalhes do pedido]. Seu roteiro também inclui a função
SanitizeObjectNameEx que tem mais alguns sinos e assobios. Ele permite que você solicite um erro se o
objeto não puder ser encontrado, e você pode especificar um esquema padrão diferente do dbo.

Você pode aplicar o mesmo padrão a outros tipos de objetos. Para coisas como procedimentos
armazenados e outras coisas que vivem em sys.objects , o exemplo funciona como dado (salve para o
SELECT COUNT(*) obviamente). Para outras classes de objetos, há, em muitos casos, funções de
metadados que você pode usar da mesma maneira; Ou seja, você pode traduzir para ID e, em seguida,
voltar para um nome que você embrulhar em quotename. Infelizmente, nem todas as funções de
metadados entendem os identificadores citados, como atesta este exemplo:
SELECT user_id('dbo'), user_id('[dbo]') -- Retorna 1, NULL

Felizmente, essas são funções para classes de objetos que não vivem em um esquema e que também não
podem ser endereçadas em bancos de dados, portanto, os nomes sempre consistem em um único
componente. Isso permite uma solução mais fácil: verifique se o nome já está entre colchetes e, nesse
caso, remova-os antes de aplicar o nome entre aspas. Aqui está um exemplo com alguns casos de teste:
CRIAR USUÁRIO NormalUser SEM LOGIN
CREATE USER AlsoNormal WITHOUT LOGIN
CREATE USER [Espaço usuário] SEM LOGIN ir
CRIAR OU ALTERAR PROCEDIMENTO MyDropUser @username sysname COMO
DECLARE @sql nvarchar(MAX)

SELECIONE @sql = 'DROP USER' +


quotename (CASO QUANDO esquerda(@username, 1) = '[' E direita(@username, 1) = ']'
ENTÃO substring(@username, 2, len(@username) - 2)
OUTRA U@username
FIM)
IMPRIMIR @sql
EXEC(@sql) ir
EXEC MyDropUser 'NormalUser'
EXEC MyDropUser '[AlsoNormal]'
EXEC MyDropUser 'Usuário espacial'

Este método não lida exatamente com tudo. Por exemplo, se um nome de usuário realmente começa e
termina entre colchetes, é provável que as coisas deem errado. Mas pode ser bom o suficiente para você.

Alguns leitores podem pensar que, se o nome já estiver entre colchetes, poderíamos tomar a string como
está sem envolvê-la entre aspas, mas isso seria errado. Todos dizem depois de mim, por favor: injeção de
SQL!

5.8 Nomes dinâmicos de banco de dados e servidor


Não é incomum que o nome do banco de dados seja dinâmico. Pode ser que você queira fazer algo em
vários bancos de dados ou porque você tem bancos de dados diferentes para diferentes anos, clientes ou
o que quer que seja. Uma variação desse tema é que, no mesmo procedimento armazenado, você deseja
executar comandos no banco de dados local e no banco de dados mestre . Neste último caso, não há
realmente nada dinâmico com o nome do banco de dados, mas você precisa alternar bancos de dados.
Muitas pessoas fazem como neste exemplo:
DECLARE @dbname sysname = 'NorthDynamic',
@sql nvarchar (MAX)
46 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

SELECIONE @sql = 'USE ' + quotename(@dbname) + '


SELECIONE * DE dbo. Clientes ONDE CustomerID = @custid' PRINT @sql
EXECUTIVO sp_executesql @sql, N'@custid nchar(5)', NLFKI'

Isso funciona, mas há uma maneira diferente que eu acho mais limpa:
DECLARE @dbname sysname = 'NorthDynamic',
@sql nvarchar(MAX),
@sp_executesql Nvarchar(200)
SELECT @sp_executesql = quotename(@dbname) + '.sys.sp_executesql'
SELECT @sql = N'SELECT * FROM dbo. Clientes ONDE CustomerID = @custid' PRINT @sql
EXECUTIVO @sp_executesql @sql, N'@custid nchar(5)', N'ALFKI'

Estou fazendo uso de duas coisas aqui:

1. EXEC aceita uma variável para o nome do procedimento.


2. Um procedimento do sistema é executado no contexto do banco de dados que foi chamado, também
com notação de três partes.

Você poderia argumentar que uma vantagem com a instrução USE é que a impressão de depuração deixa
claro em qual banco de dados o lote SQL é executado. Isso certamente é verdade, mas você pode resolver
isso adicionando um comentário à cadeia de caracteres SQL.

Aprendemos na seção sobre EXEC() AT que podemos usar sp_executesql para executar comandos em um
servidor vinculado que é outra instância do SQL Server. Se quiser que o nome do servidor seja dinâmico,
você pode usar a mesma técnica:
DECLARE @servername sysname = 'YOURSERVER',
@dbname sysname = 'NorthDynamic',
@sql nvarchar(MAX),
@sp_executesql Nvarchar(200)
SELECIONE @sp_executesql = quotename(@servername) + '.' +
quotename(@dbname) + '.sys.sp_executesql' SELECT @sql = N'SELECT *
FROM dbo. Clientes ONDE CustomerID = @custid'
IMPRESSÃO @sql
EXECUTIVO @sp_executesql @sql, N'@custid nchar(5)', N'ALFKI'

Uma vez que o USE não é fácil de aplicar neste caso, esta solução surge como realmente poderosa aqui.

Devo acrescentar que essa técnica é boa quando você deseja executar comandos DDL ou uma consulta
ocasional em outro banco de dados ou servidor, mas se você tiver uma abundância de consultas entre
bancos de dados ou entre servidores em seu aplicativo onde você não deseja codificar os nomes de banco
de dados/servidor, você deve usar sinônimos em vez disso, o que o dispensa de usar SQL dinâmico.
Discutirei isso mais adiante na seção Configurando sinônimos para acesso entre bancos de dados.

5.9 Fazendo algo em todos os bancos de dados


Há situações em que você deseja executar uma ação em todos os bancos de dados (ou uma seleção de
bancos de dados), e essa ação pode incluir uma operação em um conjunto de tabelas, exibições ou
qualquer coisa nos bancos de dados. Existem dois procedimentos de sistema que são populares para este
fim, sp_MSforeachdb e sp_MSforeachtable. Eles não são documentados e não são suportados, mas você
pode facilmente encontrar exemplos na web.

Pessoalmente, não gosto desses procedimentos, pois o código resultante tende a ser menos legível. Se eu
quiser fazer algo em todos os bancos de dados, prefiro escrever meu próprio loop de cursor – afinal, isso
não é realmente ciência de foguetes. E se eu quiser executar a mesma ação em vários objetos, prefiro
escrever uma consulta no catálogo do sistema para gerar as instruções. Se for apenas um one-off, eu

47 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

simplesmente copiar o resultado em uma janela de consulta e executá-lo. Mas se eu precisar dele em um
script, eu concateno as linhas em uma única cadeia de caracteres e executo essa cadeia de caracteres.

Vou dar-lhe um exemplo de que você pode usar um boilerplate para suas necessidades e, em seguida, toco
brevemente outro exemplo no final desta seção. A tarefa é que precisamos conceder permissões SELECT em
todos os modos de exibição para uma determinada função em todos os bancos de dados onde essa função
existe. Como estamos concedendo apenas seleção em um determinado tipo de objeto, não podemos
conceder permissão no nível de esquema ou banco de dados – não queremos dar aos usuários acesso às
tabelas base – portanto, precisamos conceder acesso por objeto. (Colocando todas as exibições em um
esquema separado, para que pudéssemos conceder acesso ao esquema? Essa é uma ótima ideia! Mas
assuma, por exemplo, que isso não é viável. Talvez não tenhamos pensado apenas nisso quando fizemos
nosso design original e agora estamos presos a ele.) Aqui está o roteiro:
DECLARE @db sysname,
@sp_executesql Nvarchar(500),
@query nvarchar(MAX),
@dbstmts nvarchar (MAX),
@role sysname = 'hemline'

SELECIONE @query =
«SE user_id(@role) NÃO FOR NULO
COMEÇAR
SELECIONE @dbstmts =
(SELECIONE ''GRANT SELECT ON '' +
quotename(s.name) + ''.'' + quotename(v.name) + '' TO '' + quotename(@role) +
char(13) + char(10)
DE sys.views v
JOIN sys.schemas s ON s.schema_id = v.schema_id
ONDE NÃO EXISTE (SELECIONE *
DE sys.database_permissions dp
ONDE dp.major_id = v.object_id
E dp.type = ''SL''
E dp.grantee_principal_id = user_id(@role)) PARA XML PATH(''''), TYPE).value(''.'',
''nvarchar(MAX)'')
FIM
MAIS
SELECIONE @dbstmts = NULL'

DECLARE dbcur CURSOR STATIC LOCAL FOR


SELECT quotename(name) FROM sys.databases
ONDE database_id > 4
E estado = 0 -- Somente bancos de dados online
E is_read_only = 0
ENCOMENDAR PELO nome

ABRIR dbcur

ENQUANTO 1 = 1
COMEÇAR
FETCH dbcur EM @db
SE @@fetch_status <> 0
QUEBRAR

SELECIONE @sp_executesql = @db + '.sys.sp_executesql'

EXEC @sp_executesql @query, N'@dbstmts nvarchar(MAX) OUTPUT, @role sysname',


@dbstmts SAÍDA, @role
IMPRIMIR @db
SE @dbstmts NÃO FOR NULO
COMEÇAR
IMPRESSÃO @dbstmts
EXECUTIVO @sp_executesql @dbstmts
FIM

48 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

FIM

DESALOCAR dbcur

O script começa com uma série de variáveis. @db é o banco de dados com o qual estamos trabalhando
atualmente no loop e @sp_executesql manterá o nome de três partes para sp_executesql em @db, como
discutimos na seção anterior. @query é a consulta que executamos no @db para gerar @dbstmts que
executa o trabalho real. Todas essas quatro variáveis fazem parte do boilerplate, mas a última @role é
específica para este exemplo: este é o papel ao qual concederemos direitos. (Achei melhor colocar o nome
da função em uma variável, caso quiséssemos executar essa ação para mais de uma função.)

Primeiro montamos @query. Primeiro, ele verifica se @role está presente nesse banco de dados (mas não
se importa que isso seja realmente uma função, mas qualquer entidade de banco de dados será aceita). Se
@role estiver presente, @query executará a consulta principal em sys.views para obter os modos de
exibição , filtrando os modos de exibição aos quais @role já tem acesso. A lista SELECT cria uma
instrução GRANT para cada exibição, a instrução é seguida por CR+LF para tornar a saída fácil de ler,
caso queiramos inspecioná-la.

Você pode não ter visto isso antes: essa é uma maneira de converter um conjunto de resultados em uma
cadeia de caracteres concatenada. Funciona, mas a sintaxe não é das mais bonitas. Se você não tiver
necessidade de oferecer suporte ao SQL 2016 e versões anteriores, poderá usá string_agg que é muito
mais limpo. Há uma versão com string_agg abaixo. O próximo passo é configurar um cursor sobre
sys.databases para que possamos fazer um loop sobre eles. O cursor é STATIC LOCAL. Está totalmente
fora do escopo deste artigo, mas eu recomendo fortemente que você sempre configure seus cursores dessa
maneira, pois caso contrário pode haver surpresas desagradáveis. (O supracitado sp_MSforeachdb usa
DYNAMIC GLOBAL e há relatos de sp_MSforeachdb ignorando bancos de dados.) Muitas vezes você vai
querer modificar a filtragem para que você inclua apenas bancos de dados de seu interesse. Neste
exemplo, excluo os bancos de dados regulares do sistema, master, tempdb, model e msdb, mas não faço
nenhum esforço para filtrar bancos de dados para SSRS, replicação etc. Para este exemplo em particular,
confio que a verificação do @role dentro @query filtrará esses bancos de dados de qualquer maneira. Mas
se você estiver fazendo algo como criar um gatilho DDL em todos os bancos de dados de usuários, você
realmente precisa ter cuidado para não criá-los em bancos de dados adicionados pelo sistema.

Tenho mais dois filtros: um para excluir bancos de dados que não estão online e outro para excluir bancos
de dados somente leitura. Sugiro que estes são filtros que você incluirá com mais frequência do que não.
Tudo dependendo de suas necessidades, você pode querer adicionar mais condições para excluir / incluir
bancos de dados.

Dentro do loop do cursor é bastante simples. Primeiro, executamos @query para obter as instruções a
serem executadas no banco de dados atual no loop. Em muitos casos, quando você usa essa técnica,
@query terá apenas um parâmetro, ou seja, o parâmetro de saída @dbstmts. No entanto, neste exemplo
em particular, também enviamos @role como parâmetro. É uma questão de gosto, mas optei por sempre
imprimir o nome do banco de dados, mesmo que não haja nada para executar para um determinado banco
de dados. Finalmente, no loop, imprimimos @dbstmts e executamos o SQL retornado de @query.

Aqui está uma instrução alternativa para criar @query que usa a função string_agg que é mais bonita do
que o PARA XML PATH hocus pocus, mas como observado requer SQL 2017 ou posterior:
SELECIONE @query =
«SE user_id(@role) NÃO FOR NULO
COMEÇAR

49 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

SELECIONE @dbstmts = string_agg(convert(nvarchar(MAX), ''GRANT SELECT ON


'') +
quotename(s.name) + ''.'' + quotename(v.name) +
'' TO '' + quotename(@role), nchar(13) + nchar(10))
DE sys.views v
JOIN sys.schemas s ON s.schema_id = v.schema_id
ONDE NÃO EXISTE (SELECIONE *
DE sys.database_permissions dp
ONDE dp.major_id = v.object_id
E dp.type = ''SL''
E dp.grantee_principal_id = user_id(@role)) FIM
MAIS
SELECIONE @dbstmts = NULL'

A conversão para nvarchar(MAX) é necessária para evitar truncamento se o resultado for maior que 4000
caracteres.

Você verá mais de concatenação com PARA CAMINHO XML e string_agg em seções posteriores, e não
menos importante na seção sobre pivô dinâmico.

Algum tempo atrás, houve uma pergunta em um fórum onde o cartaz queria encontrar uma certa string em
seu servidor, onde ela tinha muitos bancos de dados. Então essa era a tarefa: em quais tabelas e em quais
bancos de dados você pode encontrar uma determinada cadeia de caracteres? Achei que deveria ajudá-la,
então parti usando o clichê acima. E, acredite, foi difícil acertar quando há várias camadas de SQL
dinâmico. Não vou ocupar espaço com o script aqui, mas você o encontra em searchalldatabases.sql.

5.10 Cursores e SQL dinâmico


Cursores são algo que você deve usar com moderação, e as situações em que você precisa usar SQL
dinâmico e um cursor em combinações são poucas e distantes entre si. No entanto, tenho uma dica para
compartilhar, então aqui vai.

Eu sempre digo às pessoas para configurar seus cursores desta maneira:


DECLARE cur CURSOR ESTÁTICO LOCAL PARA
SELECIONAR...

STATIC garante que você não obtenha o tipo de cursor padrão, que é DINÂMICO e que é propenso a um
desempenho realmente ruim. LOCAL significa que o escopo do cursor é local para o procedimento
armazenado. Ou seja, quando o procedimento sai, o cursor desaparece. O padrão é que um cursor seja
global para o seu processo. Isso pode resultar em erros desagradáveis se o loop do cursor for interrompido
no meio do caminho e você reinserir o procedimento na mesma conexão, pois o cursor ainda existe neste
ponto.

No entanto, LOCAL não funciona se você, por qualquer motivo, deseja configurar o cursor com SQL
dinâmico. Se você tentar isso:
DECLARE @name sysname,
@sql nvarchar (MAX)

SELECIONE @sql =
'DECLARE cur CURSOR ESTÁTICO LOCAL PARA
SELECIONE TOP (5) CompanyName FROM dbo. Clientes
ENCOMENDAR POR CustomerID'

EXEC sp_executesql @sql

ABRIR cur

Você recebe essa mensagem de erro para a instrução OPEN:


50 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

Msg 16916, Nível 16, Estado 1, Linha 9


Um cursor com o nome 'cur' não existe.

Você já deve saber o porquê: o código dentro @sql é um procedimento armazenado próprio, e LOCAL
significa local para esse escopo.

Mas há uma saída que aprendi originalmente com Anthony Faull. Você pode usar uma variável de cursor:
DECLARE @name sysname,
@sql nvarchar(MAX),
@cur CURSOR

SELECIONE @sql =
'SET @cur = CURSOR ESTÁTICO PARA
SELECIONE TOP (5) CompanyName FROM dbo. Clientes
ENCOMENDAR POR CustomerID
ABERTO @cur'

EXEC sp_executesql @sql, N'@cur SAÍDA DO CURSO', SAÍDA @cur

ENQUANTO 1 = 1
COMEÇAR
FETCH @cur EM @name
SE @@fetch_status <> 0
QUEBRAR

IMPRIMIR @name
FIM

As variáveis de cursor estão no produto há muito tempo (desde o SQL 7), mas continuam sendo um
daqueles recursos obscuros que poucas pessoas conhecem e ainda menos usam. (Até Anthony me enviar
seu exemplo, eu nunca os havia considerado.)

Preste atenção ao fato de que o cursor é configurado com SET em vez de DECLARE. Eu ainda especifico
STATIC, mas LOCAL não é necessário (nem é permitido), já que o escopo do cursor é o mesmo da variável
que contém o cursor. Observe que a instrução OPEN deve estar dentro do SQL dinâmico. Se você colocá-
lo após a chamada para SQL dinâmico, você receberá este erro:
Msg 16950, Nível 16, Estado 2, Linha 13

A variável '@cur' não tem atualmente um cursor alocado para ela.

Este é um dos poucos casos em que há uma diferença entre os procedimentos armazenados sem nome criados com
sp_executesql e procedimentos armazenados "reais". Com este último, você precisa inserir a palavra-chave VARIANDO
entre CURSOR e OUTPUT na declaração de parâmetro. Mas VARIAR não é permitido com sp_executesql, e nem é
necessário.

5.11 Uma maneira limpa de lidar com nomes dinâmicos


Estamos começando a chegar às seções mais avançadas deste capítulo. Embora as ideias nesta seção seja
algo para o qual todos possam ter uso às vezes, os leitores inexperientes podem preferir pular para o
próximo capítulo neste momento.

O SQL dinâmico é difícil de ler e manter devido à mistura da sintaxe no procedimento de hospedagem e
do SQL dinâmico. Os pontos mais graves são onde você divide a cadeia de caracteres SQL para emendar
em uma variável para algo que não pode ser um parâmetro. Por exemplo, analisamos anteriormente o
procedimento create_db que contém esta afirmação:
SELECIONE @sql =
N'CRIAR BANCO DE DADOS ' + quotename(@dbname) + '
NO PRIMÁRIO (NOME = N' + quotename(@dbname, '''') + ',
51 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

NOME DO ARQUIVO = N' + dbo.quotestring(@datadir + @dbname + '.mdf') + ',


TAMANHO = ' + convert(varchar, @sizeinmb) + ' MB)
LOGON (NOME = N' + quotename(@dbname + '_log', '''') + ',
NOME DO ARQUIVO = N' + dbo.quotestring(@logdir + @dbname + '.ldf') + ',
TAMANHO = ' + convert(varchar, @logsize) + ' MB)'

Mas você pode evitar todas essas aspas simples, plusses e chamadas de função. Aqui está uma versão do
@sql que diz o que você quer fazer de forma muito mais sucinta:
SELECIONE @sql =
N'CREATE BANCO DE DADOS @dbname
NO PRIMÁRIO (NOME = @datafilename, NOME DO ARQUIVO = @datafilepath,
TAMANHO = @sizeinmb MB)
LOGON (NOME = @logfilename, NOME DO ARQUIVO = @logfilepath,
TAMANHO = @logsize MB)'

Não, isso não é sintaxe legal e essas coisas começando com @ não são realmente variáveis, mas sim são
espaços reservados que você substitui pelos valores reais com a ajuda da função de substituição. Aqui
está o código completo da nova versão do create_db juntamente com um caso de teste:
CRIAR OU ALTERAR PROCEDIMENTO create_db @dbname sysname,
@sizeinmb smallint = 1200,
@logsize smallint = 200,
@debug bit = 0 AS

DECLARE @datadir nvarchar(128) =


convert(nvarchar(128), serverproperty('InstanceDefaultDataPath')),
@logdir nvarchar(128) =
convert(nvarchar(128), serverproperty('InstanceDefaultLogPath')),
@sql nvarchar (MAX)

SELECIONE @sql =
N'CREATE BANCO DE DADOS @dbname
NO PRIMÁRIO (NOME = @datafilename, NOME DO ARQUIVO = @datafilepath,
TAMANHO = @sizeinmb MB)
LOGON (NOME = @logfilename, NOME DO ARQUIVO = @logfilepath,
TAMANHO = @logsize MB)'

SELECT @sql = replace(@sql, '@dbname', quotename(@dbname))


SELECT @sql = replace(@sql, '@datafilename',
'N' + quotename(@dbname, '''')) SELECT @sql = replace(@sql,
'@datafilepath',
dbo.quotestring_n(@datadir + @dbname + '.mdf'))
SELECT @sql = replace(@sql, '@sizeinmb', convert(varchar, @sizeinmb))
SELECIONE @sql = replace(@sql, '@logfilename',
'N' + quotename(@dbname + '_log', '''')) SELECT @sql = replace(@sql,
'@logfilepath',
dbo.quotestring_n(@logdir + @dbname + '.ldf')) SELECT @sql = replace(@sql, '@logsize',
convert(varchar, @logsize))

SE @debug = 1 @sql DE IMPRESSÃO


EXEC(@sql)
ir
EXEC create_db [Test'Dβ], @debug = 1 GB
DROP DATABASE [Test'Dβ]

Cada substituição também garante que o valor seja cotado adequadamente. Você pode observar que @sql
tem variáveis/espaços reservados diferentes para o nome do banco de dados e o nome do arquivo de dados
primário, embora o valor seja o mesmo. Isso é necessário, já que o valor deve ser cotado de forma
diferente nos dois locais. Você também pode observar que, neste exemplo, eu uso quotestring_n para os
caminhos de arquivo, já que eles potencialmente podem ter mais de 128 caracteres. Note também que há
alguma importância em ter o N antes dos literais de corda em todos os lugares, ou então a letra grega β
será substituída por seu sósia, o alemão "scharfes-s", ß.
52 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

Este exemplo pode não ser totalmente persuasivo. Embora a cadeia de caracteres @sql como tal seja mais
concisa e fácil de entender, o comprimento total de create_db mais do que dobrou, então eu esperaria que
muitos leitores preferissem a versão anterior. Mas imagine uma cadeia de caracteres SQL mais longa que
executa mais de vinte linhas ou mais, com apenas algumas "variáveis" exclusivas que são repetidas várias
vezes na cadeia de caracteres. Agora, essa técnica está se tornando mais atraente. Meu pensamento é que
este é um truque para ter em sua caixa de ferramentas, quando você sente que a concatenação de cordas
está ameaçando sua sanidade mental, mas isso não é nada que você usaria em todos os casos quando você
trabalha com nomes dinâmicos. Use o que você acha que funciona melhor para a situação.

Exploraremos mais possibilidades na próxima seção.

5.12 Saiba mais sobre nomes dinâmicos e cadeias de caracteres aninhadas


Nesta seção, daremos uma olhada em mais uma abordagem para lidar com nomes dinâmicos, e também
veremos mais algumas técnicas para lidar com cadeias de caracteres aninhadas. O que apresento nesta
seção é mais direcionado a usuários avançados com uma boa compreensão do que estão fazendo, e
leitores menos experientes podem achar o material um pouco desconcertante demais.

Se olharmos para a solução para create_db na seção anterior, é um pouco frustrante que tenhamos tantas
variáveis/espaços reservados derivados do parâmetro de entrada @dbname. Mas, em vez de substituir os
valores completos e adicionar delimitadores de cadeia de caracteres e tudo quando usamos substituir,
poderíamos ter espaços reservados dentro dos literais de cadeia de caracteres, algo assim:
SELECIONE @sql =
N'CREATE BANCO DE DADOS @dbname
NO PRIMÁRIO (NOME = N''%(dbname)'',
NOME DO ARQUIVO = N''%(datadir)%(dbname).mdf'',
TAMANHO = %(sizeinmb) MB)
FAÇA LOGON (NOME = N''%(dbname)_log'',
NOME DO ARQUIVO = N''%(logdir)%(dbname).ldf'',
TAMANHO = %(logsize) MB)'

Se você está dizendo que gosta mais da solução anterior, não vou discutir com você, mas fingir por um
segundo que você realmente gosta disso e aguentar, porque você pode apreciar alguns dos dispositivos
que apresento para lidar com strings aninhadas. Quanto ao estilo do %(espaço reservado), isso é apenas algo
escolhido para ter em vez @placeholder, mas você usa o que preferir.

Quando substituímos os espaços reservados, devemos considerar o risco de que os valores incluam aspas
simples, então tem que ser algo assim:
SELECT @sql = replace(@sql, '%(dbname)', replace(@dbname, ''', ''''''''''
Ai! Não era bonito. Felizmente, não precisamos escrever código assim, mas podemos introduzir duas
variáveis locais para preservar nossa sanidade:
DECLARE @sq nchar(1) = '''',
@sqsq nchar(2) = '''' + ''''

Outra coisa que é um pouco de tensão no olho é todas as aspas simples dobradas na cadeia de caracteres
SQL. Este exemplo pode ser um pouco tolerável, mas se o seu SQL dinâmico tiver muitos literais de
cadeia de caracteres constantes, você pode realmente ansiar por uma alternativa. O que você acha disso:
SELECIONE @sql =
N'CREATE BANCO DE DADOS @dbname
NO PRIMÁRIO (NOME = N"%(dbname)",
NOME DO ARQUIVO = N"%(datadir)%(dbname).mdf",
TAMANHO = %(sizeinmb) MB)
FAÇA LOGON (NOME = N"%(dbname)_log",
NOME DO ARQUIVO = N"%(logdir)%(dbname).ldf",
TAMANHO = %(logsize) MB)'
53 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

SELECT @sql = replace(@sql, '"', @sq)

Ou seja, aqui eu uso aspas duplas, e então eu substituo as aspas duplas por aspas simples depois de ter
construído meu SQL dinâmico.

Embora isso possa ser mais brando com os olhos, essa é uma técnica que você deve usar com alguns
cuidados. Você não pode fazer isso, se você em algum lugar concatenar valores na cadeia de caracteres em
vez de usar espaços reservados. Considere, por exemplo, esta mistura:
SELECIONE @sql =
N'CREATE BANCO DE DADOS @dbname
NO PRIMÁRIO (NOME = N"%(dbname)",
NOME DO ARQUIVO = N' + dbo.quotestring(@datadir + @dbname + '.mdf') + ',
TAMANHO = %(sizeinmb) MB)
FAÇA LOGON (NOME = N"%(dbname)_log",
NOME DO ARQUIVO = N"%(logdir)%(dbname).ldf",
TAMANHO = %(logsize) MB)'
SELECT @sql = replace(@sql, '"', @sq)

Se @datadir ou @dbname incluíssem uma aspa dupla, você poderia obter resultados inesperados,
incluindo erros de sintaxe. E seu código estaria aberto à injeção de SQL.

A partir disso, se você usar aspas duplas como um delimitador de cadeia de caracteres alternativo, também
deverá usar variáveis/espaços reservados que você substituirá para todos os valores de entrada. E também
muito importante: você deve substituir as aspas duplas por aspas simples antes de enfrentar qualquer um
dos espaços reservados.

Escusado será dizer que, se a cadeia de caracteres SQL incluir aspas duplas naturalmente (por exemplo,
XML, predicados de texto completo), você precisará encontrar um delimitador diferente para poder usar
essa técnica.

Somando todas essas peças, aqui está uma nova versão do create_db:
CRIAR OU ALTERAR PROCEDIMENTO create_db @dbname sysname,
@sizeinmb smallint = 1200,
@logsize smallint = 200,
@debug bit = 0 AS

DECLARE @datadir nvarchar(128) =


convert(nvarchar(128), serverproperty('InstanceDefaultDataPath')),
@logdir nvarchar(128) =
convert(nvarchar(128), serverproperty('InstanceDefaultLogPath')), @sql nvarchar(MAX),
@sq nchar(1) = '''',
@sqsq nchar(2) = '''' + ''''

SELECIONE @sql =
N'CREATE BANCO DE DADOS @dbname
NO PRIMÁRIO (NOME = N"%(dbname)",
NOME DO ARQUIVO = N"%(datadir)%(dbname).mdf",
TAMANHO = %(sizeinmb) MB)
FAÇA LOGON (NOME = N"%(dbname)_log",
NOME DO ARQUIVO = N"%(logdir)%(dbname).ldf",
TAMANHO = %(logsize) MB)'

SELECT @sql = replace(@sql, '"', @sq)


SELECT @sql = replace(@sql, '@dbname', quotename(@dbname))
SELECT @sql = replace(@sql, '%(dbname)', replace(@dbname, @sq, @sqsq))
SELECT @sql = replace(@sql, '%(datadir)', replace(@datadir, @sq, @sqsq))
SELECT @sql = replace(@sql, '%(logdir)', replace(@logdir, @sq, @sqsq))
SELECT @sql = replace(@sql, '%(sizeinmb)', convert(varchar, @sizeinmb))
SELECT @sql = replace(@sql, '%(logsize)', convert(varchar, @logsize))

54 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

SE @debug = 1 @sql DE IMPRESSÃO


EXEC(@sql)
ir
EXEC create_db [Test'Dβ], @debug = 1 GB
DROP DATABASE [Test'Dβ]

5.13 Brincando com QUOTED_IDENTIFIER


Há mais uma abordagem para cadeias de caracteres aninhadas que menciono com alguma hesitação, uma
vez que depende de funcionalidade preterida. Abaixo está um trecho de um script para coletar muitos
dados para todos os bancos de dados de usuários em um servidor. (Este é um script que envio aos clientes
que desejam que eu analise o desempenho de seus aplicativos.)
CREATE TABLE #sql (sql nvarchar(MAX) NOT NULL) ir
SET QUOTED_IDENTIFIER OFF
ir
INSERIR #sql (sql)
VALORES ("
INSERIR tempdb.. Procs
SELECIONE db_name(), o.type, s.name, o.name o.object_id
FROM sys.objects o
JOIN sys.schemas s ON o.schema_id = s.schema_id
ONDE o.type IN ('P', 'PC', 'FN', 'FS') ...

INSERIR tempdb.. qs_runtime_stats_interval


SELECIONE db_name() COMO dbname, * DE sys.query_store_runtime_stats_interval
") ir
SET QUOTED_IDENTIFIER EM
MOVIMENTO
DECLARE @sql nvarchar(MAX),
@sp_executesql Nvarchar(200)
SELECIONE @sql = SQL DE #sql

DECLARE cur CURSOR ESTÁTICO LOCAL PARA


SELECT quotename(name) + '.sys.sp_executesql'
FROM sys.databases
ONDE database_id >= 5
E estado = 0
E is_read_only = 0

ABRIR cur

ENQUANTO 1 = 1
COMEÇAR
FETCH CUR EM @sp_executesql
SE @@fetch_status <> 0
INTERVALO

EXECUTIVO @sp_executesql @sql


FIM
Eu faço uso disso com SET QUOTED_IDENTIFIER OFF, a aspa dupla agora serve como um delimitador de
cadeia de caracteres em pé de igualdade com a aspa simples. Isso me permite delimitar a cadeia de
caracteres SQL entre aspas duplas, e não há necessidade de dobrar todas as aspas simples. O que significa
que posso facilmente copiar o código SQL para uma janela de consulta no SSMS e testar. (Não há
nenhuma parte dinâmica no código SQL aqui; a única coisa que requer SQL dinâmico é que eu quero
executá-lo em vários bancos de dados.)

Você deve estar ciente de que SET QUOTED_IDENTIFIER OFF é uma opção herdada e há vários recursos
no SQL Server que exigem que essa configuração seja ON, por exemplo, métodos de tipo XML e índices
filtrados para nomear dois. Para evitar esses acidentes, isolei essa configuração em um único lote onde
insiro a cadeia de caracteres SQL em uma tabela temporária. E a razão para a tabela temporária é

55 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

exatamente para me permitir ter esse lote isolado, senão eu poderia ter atribuído o texto a @sql
diretamente.

Escusado será dizer que isso não vai funcionar bem, se o seu lote SQL já inclui aspas duplas, por exemplo,
porque você está lidando com XML ou JSON.

5.14 Outros exemplos


No meu site há alguns exemplos do que eu acho que é SQL dinâmico bem trabalhado. Esses exemplos são
longos demais para serem incluídos aqui, até porque ocuparia muito espaço para explicar o propósito do
código. Então eu deixo que basta apenas dar os links para explorar esse código por conta própria, se você
quiser inspiração para como escrever um bom SQL dinâmico.

Assinatura de certificado
Já me referi ao meu artigo Permissões de empacotamento em procedimentos armazenados algumas vezes
neste artigo. Este artigo inclui um procedimento armazenado GrantPermsToSP e um script
GrantPermsToSP_server.sql que automatizam o processo de criação de um certificado, assinatura de um
procedimento, criação de um usuário/logon a partir desse certificado e concessão a esse usuário as
permissões necessárias e, assim, empacotando as permissões no procedimento. O script no nível do
servidor também copia o certificado do banco de dados do usuário para o mestre. Acho que ambos são
bons exemplos de SQL dinâmico, e você reconhecerá algumas das técnicas que apresentei neste capítulo.

O procedimento armazenado é certamente mais fácil de digerir, uma vez que opera dentro de um único
banco de dados. O script do lado do servidor é mais complexo. Ele também inclui uma coisa que não
discuto em outro lugar neste artigo: como executar uma ação em todos os servidores em um grupo de
disponibilidade.

beta_lockinfo
beta_lockinfo é um procedimento de monitoramento que retorna informações sobre a atividade atual no
servidor: você pode ver os bloqueios, quem está bloqueando quem, a instrução atual de cada processo
com o plano de consulta e muito mais informações. Não há fim do SQL dinâmico neste procedimento. A
peça mais longa é o lugar onde eu traduzo os vários ids em sys.dm_tran_locks para nomes, que podem ser
nomes de tabelas, nomes de índices, etc. Aqui eu faço uso de algumas das técnicas que discuti
anteriormente neste guia de estilo. Há também algumas coisas mais esotéricas. Eu escrevi a primeira
versão deste procedimento para SQL 6.5, e naquela época não havia grades na ferramenta de consulta,
mas toda a saída era baseada em texto. Para otimizar a propriedade da tela, adicionei código para tornar a
largura das colunas dinâmica (uma técnica que roubei de sp_who2). Ele ainda está lá se você seguir o
parâmetro @textmode.

Para uma dosagem máxima de SQL dinâmico, acho que a versão para SQL 2012-2014 é a melhor. (Porque
essa versão inclui uma parte que só é executada no SQL 2014, que eu manipulei com SQL dinâmico em
vez de criar uma nova versão do procedimento.)

6. Alguns bons casos de uso para SQL dinâmico


Neste capítulo, veremos alguns exemplos em que o SQL dinâmico é útil. Algumas das seções deste
capítulo são apenas uma discussão rápida, talvez com indicações em outros lugares, enquanto outras
incluem soluções reais sobre como fazer as coisas. Se você acha que seu caso de uso favorito está
faltando, deixe-me uma linha - mas primeiro leia o próximo capítulo onde eu discuto situações em que o
SQL dinâmico é menos provável de ser a solução certa na minha opinião. Quero dizer, apenas para o caso
de sua sugestão ser abordada lá. :-)

56 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

6.1 Condições de pesquisa dinâmica


Um requisito que aparece em mais de um aplicativo é que os usuários desejam ser capazes de pesquisar os
dados usando muitas condições de pesquisa diferentes. Por exemplo, em um sistema de pedidos, os
usuários podem querer encontrar pedidos por ID do pedido, nome do cliente, intervalo de data, ID do
produto, etc e, muitas vezes, em combinação. Para um conjunto de dados de qualquer tamanho, é
impossível obter um bom desempenho, a menos que você observe que precisa de planos de consulta
diferentes para diferentes condições de pesquisa. Isso pode ser obtido com uma consulta bem escrita em
SQL estático, desde que você adicione a dica de consulta OPTION (RECOMPILE) no final da consulta. No
entanto, essas soluções tendem a quebrar, quando as condições são mais complexas por natureza. Além
disso, se as pesquisas forem muito frequentes, a recompilação pode se tornar um problema de
desempenho em si. A alternativa é construir a consulta de pesquisa com SQL dinâmico, onde é mais fácil
adicionar condições mais complexas, e como cada combinação de condições de pesquisa obtém seu
próprio plano em cache, isso reduz consideravelmente a quantidade de compilação.

Tenho um artigo separado no meu site intitulado Condições de Pesquisa Dinâmica onde discuto como
implementar pesquisas com SQL estático + OPTION (RECOMPILE), bem como como implementar tais
pesquisas com SQL dinâmico, e assim não abordarei mais este tópico neste artigo.

6.2 Acessando fontes de dados remotas


Já analisamos um pouco o uso de SQL dinâmico com servidores vinculados e, particularmente, o caso em
que você deseja que o nome do servidor vinculado seja definido em tempo de execução. Nesta seção,
trabalharemos com casos em que o nome do servidor vinculado é estático. Além disso, veremos apenas
consultas que, como tal, podem ser escritas com SQL totalmente estático. Mas onde ainda precisamos
recorrer ao SQL dinâmico para obter um desempenho aceitável.

A maneira mais simples de obter dados de um servidor remoto é fazer referência a tabelas por notação de
quatro partes, ou seja, server.database.schemaname.tablename. Às vezes, isso funciona muito bem, mas
também há situações em que o otimizador decide, por um motivo ou outro, arrastar uma grande tabela
remota em sua totalidade para o servidor local para aplicar uma condição WHERE que filtra todas, exceto
um punhado de linhas.

Quando isso acontece, você começa a procurar alternativas. O mais próximo em mãos é o OPENQUERY,
que permite executar uma consulta de passagem na fonte de dados remota e, portanto, garante que a
filtragem será executada remotamente. Isso geralmente envia você para o terreno do SQL dinâmico,
porque o OPENQUERY requer uma cadeia de caracteres SQL fixa como entrada. Não é possível passar o
texto da consulta em uma variável e nem a consulta pode ser parametrizada; Todos os valores de
parâmetro devem ser alinhados à consulta. (Exatamente o que eu tenho insistido neste artigo que você
nunca deve fazer!) Isso pode levar a alguns encontros bastante feios com SQL dinâmico.

Há uma boa razão pela qual o OPENQUERY não aceita uma variável para a consulta: o SQL Server precisa saber a
forma e o formato do conjunto de resultados que retorna da consulta em tempo de compilação. E como o plano é
armazenado em cache, o conjunto de resultados deve ser conhecido e corrigido. Assim, a consulta não pode ser uma
variável que produz resultados com formas diferentes em execuções diferentes. Quanto ao motivo pelo qual o
OPENQUERY não aceita parâmetros, tenho menos certeza, mas pode ser que eles simplesmente não tenham vindo por
aí para implementá-lo.

A melhor solução é evitar completamente o OPENQUERY e, em vez disso, executar uma consulta dinâmica
parametrizada e receber o resultado em uma tabela temporária, que você pode usar no restante da
consulta. Se o servidor remoto for outra instância do SQL Server, esse é um padrão que já vimos algumas
vezes.
INSERIR #temp (....)
SERVIDOR EXEC.db.sys.sp_executesql @query, @params, @par1, @par2, ...

57 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

Isso será executado no servidor remoto no contexto do banco de dados remoto, portanto, em @query
você pode fazer referência a tabelas em notação de duas partes, sem necessidade de especificar o nome do
banco de dados na consulta. Se o servidor vinculado for algo diferente do SQL Server, você não poderá
usá sp_executesql, mas anteriormente no artigo analisamos EXEC() AT, que também oferece suporte a
parâmetros.

Há duas razões pelas quais essa abordagem é preferível ao OPENQUERY:

O SQL dinâmico é menos feio – normalmente @query é uma cadeia de caracteres de consulta fixa.
Você evita o lixo do cache do plano no servidor remoto enviando cadeias de caracteres de consulta
diferentes para valores de parâmetros diferentes. (Isso se aplica se o servidor remoto for outra instância
do SQL Server. Ele também pode ou não se aplicar a outras fontes de dados remotas.)

No entanto, pode haver situações em que esta solução não é do seu agrado e você acha que o OPENQUERY
funciona melhor para você, então vamos ver como fazer isso de maneira adequada. Antes de
continuarmos, precisamos de um servidor vinculado para ter algo com o que brincar. Configuramos um
servidor de loopback na seção sobre EXEC() AT, mas caso você não o tenha mais por perto, aqui estão os
comandos novamente:
EXEC sp_addlinkedserver LOOPBACK, '', 'SQLNCLI', @datasrc = @@servername EXEC
sp_serveroption LOOPBACK, 'rpc out', 'true'

Como eu disse antes, um servidor de loopback como este nos permite executar os scripts de teste a partir
de uma única janela de consulta. Se preferir, você pode usar uma segunda instância como seu servidor
vinculado. Apenas certifique-se de ter NorthDynamic nessa instância.

Se você começar a usar o OPENQUERY sem pensar muito por trás dele, você pode acabar escrevendo algo
assim:
CRIAR OU ALTERAR PROCEDIMENTO get_remote_data @searchstr nvarchar(40) COMO
DECLARAR @sql nvarchar(MAX)
SELECIONE @sql = 'SELECT * FROM OPENQUERY(LOOPBACK, ' +
''SELECT * FROM NorthDynamic.dbo.Customers ' +
'WHERE CompanyName LIKE N''''%' + @searchstr + '%'''''''
IMPRESSÃO @sql
EXEC sp_executesql @sql ir
EXEC get_remote_data 'Feed'

Só posso recomendar que você siga esse caminho, se você já tomou uma decisão consciente de perder a
cabeça. Você realmente gosta da ideia de olhar para algo assim por um dia inteiro, tentando entender se a
razão pela qual isso não funciona é porque você tem uma única citação a mais ou a menos em algum
lugar? Ou se esses personagens deveriam estar em outro lugar? O acima funciona – mas posso garantir
que não acertei na minha primeira tentativa. E, embora possa estar funcionando, isso está aberto para
injeção de SQL – tanto no servidor remoto quanto no servidor local.

Quando você trabalha com OPENQUERY, você precisa adotar uma abordagem sistemática ao criar sua
consulta dinâmica e fazê-lo em duas etapas. Primeiro, crie a consulta remota e, em seguida, incorpore-a à
consulta local, como neste exemplo:
CRIAR OU ALTERAR PROCEDIMENTO get_remote_data @searchstr nvarchar(40) COMO
DECLARE @remotesql nvarchar(MAX),
@localsql nvarchar (MAX)

SELECIONE @remotesql =
'SELECT * FROM NorthDynamic.dbo.Customers
ONDE CompanyName LIKE ' + dbo.quotestring_n('%' + @searchstr + '%')
SELECIONE @localsql = 'SELECT * FROM OPENQUERY(LOOPBACK, ' + dbo.quotestring_n(@remotesql) + ')'
IMPRIMIR @localsql

58 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

EXEC sp_executesql @localsql ir


EXEC get_remote_data 'rápido'

Note que devemos usar quotestring_n aqui para @remotesql, já que muito provavelmente excederá 128
caracteres, então quotename não é uma opção viável. Poderíamos usar quotename para @searchstr já que
tem no máximo 40 caracteres, mas parece lógico usar o mesmo para ambos. Gosto de ressaltar que usar
aspas ou quotestring_n não é apenas uma questão de ser bonito. O literal de cadeia de caracteres passado
para OPENQUERY deve ser nvarchar, ou então @searchstr poderia ser usado para injeção de SQL no
servidor remoto como o truque que Joe Cool jogou na seção A importância de nvarchar.

Passaremos agora para outra situação em que o desempenho pode ser um desafio.
SELECIONAR...
FROM server.db.dbo.bigremotetbl r
JUNTE-SE dbo.tinylocaltbl l ON r.somecol = l.somecol

Como os nomes sugerem, bigremotetbl é uma mesa grande, com milhões de linhas ou mais, enquanto
tinylocaltbl tem apenas um punhado. Para um bom desempenho, você deseja que o otimizador se contente
com um plano em que o conteúdo de tinylocaltbl.somecol seja enviado para o servidor remoto e a parte
da consulta em relação ao bigremotetbl seja avaliada lá. O otimizador certamente é capaz de produzir tal
plano, mas por um motivo ou outro, ele poderia ter a ideia de trazer todas as linhas em bigremotetbl para o
servidor local. Vou ter que admitir que não estou inclinado a arriscar que vai dar certo. Quando me deparo
com uma situação como essa, prefiro passar os dados na tabela local para o servidor remoto e executar
uma consulta lá como uma prevenção de desastre.

Meu método preferido é empacotar os dados na tabela local em uma cadeia de caracteres XML que passo
para o servidor remoto com sp_executesql. Na consulta remota, eu destruo os dados XML em uma tabela
temporária local que eu associo à tabela principal no servidor remoto. Aqui está um exemplo em que a
tabela temporária #getthese corresponde a tinylocaltbl enquanto Customers no NorthDynamic serve como
um token bigremotetbl.
CRIAR #getthese TABELA (custid nchar(5) NÃO É CHAVE PRIMÁRIA NULA)
INSERIR #getthese(custid)
VALORES(N'ALFKI'), (N'BERGS'), (N'VINET')

ir
DECLARE @xmlstr nvarchar(MAX),
@query nvarchar (MAX)
SELECT @xmlstr = (SELECT custid FROM #getthese FOR XML RAW, ROOT('root'))

SELECIONE @query = '


DECLARE @xml xml = cast(@xmlstr AS xml)

CRIAR #xmldata TABELA (custid nchar(5) NÃO É CHAVE PRIMÁRIA NULA)

INSERIR #xmldata(custid)
SELECIONE T.c.value(''@custid'', ''nchar(5)'')
DE @xml.nodes(''/root/row'') AS T(c)

SELECT C.CustomerID, C.CompanyName, C.ContactName, C.City


DE dbo. Clientes C
ONDE EXISTE (SELECIONE * DE #xmldata x ONDE C.CustomerID = x.custid)'

LOOPBACK EXEC. NorthDynamic.sys.sp_executesql @query, N'@xmlstr nvarchar(MAX)', @xmlstr

Ao formar a cadeia de caracteres XML, eu uso FOR XML RAW, que é o mais fácil de usar para essa
finalidade. Você pode observar que o tipo de dados de @xmlstr é nvarchar e não xml. Isso ocorre porque,
como já observamos algumas vezes, o tipo de dados xml não é suportado em chamadas para servidores
vinculados. Em vez disso, eu lanço @xmlstr para xml no lado remoto, para que eu possa destruí-lo em
#xmldata.
59 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

Você pode perguntar se a tabela temporária no servidor remoto é necessária, não poderíamos usá
@xml.nós diretamente? De fato, podemos, mas então o otimizador não teria nenhuma pista sobre os dados
e faria uma suposição cega que poderia levar a um desempenho ruim. Por esse motivo, geralmente é uma
boa ideia destruir dados XML em uma tabela temporária, para que o otimizador obtenha algumas
estatísticas para trabalhar.

O leitor pode achar isso excessivamente complicado e perguntar se não poderíamos usar um parâmetro
com valor de tabela. A resposta é a mesma que para o tipo de dados xml: não suportado com servidores
vinculados. O que é uma alternativa viável no SQL 2016 e posterior é usar JSON em vez disso, se você
tão chique. Como o JSON é tratado como nvarchar no SQL Server de qualquer maneira, isso remove a
conversão inicial em @query acima.

Uma alternativa é usar OPENQUERY e transformar a tabela temporária local em uma lista IN:
DECLARE @getlist nvarchar(MAX),
@remotequery nvarchar (MAX),
@localquery nvarchar (MAX)

SELECIONE @getlist =
(SELECIONE dbo.quotestring_n(custid) + ', '
DE #getthese
PARA CAMINHO XML(''), TYPE).value('.', 'nvarchar(MAX)')
SELECT @getlist = substring(@getlist, 1, len(@getlist) - 1)

-- SELECIONE @getlist = string_agg(dbo.quotestring_n(custid), ',') DE #getthese

SELECIONE @remotequery =
N'SELECT C.CustomerID, C.CompanyName, C.ContactName, C.City
DE NorthDynamic.dbo.Customers C
ONDE C.CustomerID IN (' + @getlist + ')'

SELECIONE @localquery =
N'SELECT CustomerID, CompanyName, ContactName, Cidade
DE OPENQUERY(LOOPBACK, ' + dbo.quotestring_n(@remotequery) + ')'

IMPRESSÃO @localquery
EXECUTIVO sp_executesql @localquery

A lista separada por vírgulas é salva em @getlist, que construímos com a ajuda do FOR XML PATH. Não,
não estamos realmente usando XML aqui, esta é apenas essa maneira engraçada de construir uma cadeia
de caracteres a partir de um conjunto de resultados que já vimos algumas vezes neste artigo. No SQL 2017
e posterior, podemos usar o string_agg mais direto , aqui comentado. Depois de termos @getlist,
construímos a consulta em duas etapas, como fizemos acima: primeiro criamos @remotequery e, em
seguida, incorporamos em @localquery.

Gosto menos desta solução. Isso causa lixo no cache do plano no servidor remoto, já que os valores em
#getlist se tornam constantes na consulta remota. Além disso, o operador IN só é útil quando há uma
única coluna de chave. Com o EXISTS, você também pode lidar com condições de várias chaves. Então,
novamente, a solução com OPENQUERY deve funcionar com qualquer fonte de dados remota, enquanto a
solução com envio por XML teria que ser retrabalhada se o servidor vinculado for algo diferente do SQL
Server.

6.3 Configurando sinônimos para acesso entre bancos de dados


Você pode ter uma situação em que dois bancos de dados em um servidor precisam fazer referência um ao
outro por um motivo ou outro. Se você codificar referências com notação de três partes como
otherdb.dbo.tbl, você terá um problema se quiser configurar um segundo ambiente para teste ou qualquer
outra coisa na mesma instância. Se todas as referências cruzadas forem por chamadas de procedimento,
você poderá criar nomes de procedimento dinamicamente:
60 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

SELECT @otherdb = otherdb FROM configtable


SELECIONE @spname = @otherdb + '.dbo.some_sp' EXEC
@spname

Mas isso é menos palatável se você também quiser fazer referências a exibições ou tabelas no outro banco
de dados, já que você precisaria usar SQL dinâmico para obter o nome do banco de dados na consulta.
Felizmente, não há necessidade de fazer isso, já que você pode usar sinônimos. Por exemplo:
CREATE SYNONYM otherdb.thattable FOR MyOtherDB.dbo.thattable

Agora você pode consultar otherdb.thattable como se fosse uma tabela local, mas a consulta realmente
acessará essa tabela em MyOtherDB. Neste exemplo, coloquei o sinônimo em um esquema específico
para referências ao outro banco de dados. Não há nenhum requisito para fazer isso, mas isso também
funciona:
CREATE SYNONYM dbo.thattable FOR MyOtherDB.dbo.thattable

No entanto, ter um esquema separado ajuda você a ver a que o nome se refere. O esquema também facilita
a administração dos sinônimos com os procedimentos que apresento nesta seção. A saber, se você tem
muitos objetos aos quais precisa fazer referências, você precisa de muitos sinônimos, e manter isso
manualmente não é realmente prático. Este é um lugar onde o SQL dinâmico é útil, e apresentarei dois
procedimentos armazenados, setup_synonyms e retarget_synonyms.

setup_synonyms configura sinônimos para todas as tabelas, exibições e procedimentos armazenados no


@otherdb e coloca os sinônimos no @synschema de esquema. A lista de tipos de objeto é codificada, mas
você pode modificá-la para atender às suas necessidades. Todos os sinônimos existentes no @synschema
são descartados antes que os novos sejam criados, para que você possa usá setup_synonyms para uma
configuração inicial, bem como para uma atualização quando novos objetos tiverem sido adicionados ao
outro banco de dados. Você também pode usá-lo para redirecionar os sinônimos para se referir a um banco
de dados diferente, por exemplo, quando você tiver restaurado um conjunto de bancos de dados com
novos nomes na mesma instância. As instruções para soltar e criar sinônimos estão dentro de uma
transação, de modo que, se algo der errado, tudo é revertido. Sinta-se à vontade para adaptar o
procedimento às suas necessidades.
CRIAR OU ALTERAR PROCEDIMENTO setup_synonyms @synschema sysname @otherdb sysname COMO
BEGIN TRY
DECLARAR @sp_executesql nvarchar(600),
@dropsql nvarchar(MAX),
@createquery nvarchar(MAX),
@createsql nvarchar (MAX)

-- Validar parâmetros de entrada.


SE schema_id(@synschema) FOR NULO
RAISERROR('Esquema "%s" não existe.', 16, 1, @synschema)

SE db_id(@otherdb) FOR NULO


RAISERROR('Banco de dados "%s" não existe.', 16, 1, @otherdb)

-- Configurar referência para sp_executesql.


SELECT @sp_executesql = quotename(@otherdb) + '.sys.sp_executesql'

-- Execute uma consulta para gerar o SQL para descartar sinônimos existentes.
SELECIONE @dropsql =
(SELECIONE 'SOLTAR SINÔNIMO' + quotename(@synschema) + '.' + quotename(name) + char(13) +
char(10)
FROM sys.sinônimos
ONDE schema_id = schema_id(@synschema)
PARA CAMINHO XML(''), TYPE).value('.', 'nvarchar(MAX)')

-- Configure uma consulta para criar os novos sinônimos. Isso é SQL dinâmico, já que
-- é executado no outro banco de dados, do qual o nome é um parâmetro.
61 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

SELECIONE @createquery = 'SELECIONE @sql =


(SELECIONE '' CRIAR SINÔNIMO '' +
quotename(@schema) + ''.'' + quotename(o.name) + '' FOR '' + quotename(db_name()) + ''.'' +
quotename(s.name) + ''.'' + quotename(o.name) + char(13) + char(10)
FROM sys.objects o
JOIN sys.schemas s ON o.schema_id = s.schema_id
ONDE o.type IN (''P'', ''U'', ''V'')
PARA CAMINHO XML(''''), TYPE).value(''.'', ''nvarchar(MAX)'')'

-- Execute a consulta para obter as instruções.


EXEC @sp_executesql @createquery, N@schema sysname, @sql nvarchar(MAX) OUTPUT',
@synschema, @createsql SAÍDA

-- Imprimir e executar os extratos dentro de uma transação


INICIAR TRANSAÇÃO
IMPRESSÃO @dropsql
EXECUTIVO (@dropsql)
IMPRESSÃO @createsql
EXECUTIVO (@createsql)
CONFIRMAR TRANSAÇÃO
FIM TENTATIVA
COMECE A CAPTURAR
SE @@trancount > 0 TRANSAÇÃO DE REVERSÃO
; ARREMESSO
DE FIM DE
CAPTURA
Nota: o manipulador CATCH usa o ; Instrução THROW que foi introduzida no SQL 2012. Para um teste rápido, você
pode substituir ; THROW com uma instrução RAISERROR, se você estiver no SQL 2008 e versões anteriores. Para
obter mais informações sobre tratamento de erros, consulte Parte Um da minha série Tratamento de erros e transações no
SQL Server.

Pode haver situações em que você não deseja criar sinônimos para cada objeto no outro banco de dados,
por exemplo, porque os desenvolvedores só devem usar objetos de interface definidos oficialmente. Em
caso afirmativo, você definiria seus sinônimos em um script e não usaria setup_synonyms. No entanto,
você ainda pode ter o
precisa redefinir todos os sinônimos para se referir a um banco de dados diferente, e para isso você usaria
retarget_synonyms. Ele atualiza todos os sinônimos existentes no @synschema substituindo o nome do
banco de dados existente por @otherdb. Assim como setup_synonyms, todo o trabalho é feito dentro de
uma transação.
CRIAR OU ALTERAR PROCEDIMENTO retarget_synonyms @synschema sysname @otherdb sysname COMO
BEGIN TRY
DECLARAR @sql nvarchar(MAX)

-- Validar parâmetros de entrada.


SE schema_id(@synschema) FOR NULO
RAISERROR('Esquema "%s" não existe.', 16, 1, @synschema)

SE db_id(@otherdb) FOR NULO


RAISERROR('Banco de dados "%s" não existe.', 16, 1, @otherdb)

-- Execute uma consulta para gerar o SQL para descartar sinônimos existentes.
SELECIONE @sql =
(SELECIONE 'DROP SYNONYM' + quotename(@synschema) + '.' + quotename(name) + char(13) + char(10)
+
'CREATE SYNONYM' + quotename(@synschema) + '.' + quotename(name) +
' FOR ' + quotename(@otherdb) + '.' +
quotename(parsename(base_object_name, 2)) + '.' +
quotename(parsename(base_object_name, 1)) + char(13) + char(10)
FROM sys.sinônimos
ONDE schema_id = schema_id(@synschema)
PARA CAMINHO XML(''), TYPE).value('.', 'nvarchar(MAX)')

62 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

-- Imprima e execute os extratos dentro de uma transação.


INICIAR TRANSAÇÃO
IMPRESSÃO @sql
EXECUTIVO (@sql)
CONFIRMAR TRANSAÇÃO
FIM TENTATIVA
COMECE A CAPTURAR
SE @@trancount > 0 TRANSAÇÃO DE REVERSÃO
; JOGAR
FIM DA CAPTURA

Deixo esses procedimentos sem comentários do ponto de vista do SQL dinâmico, pois eles se baseiam em
conceitos que já discutimos, mas você é bem-vindo para estudar os princípios que eu emprego. Como em
exemplos anteriores em que usamos PARA CAMINHO XML para criar uma lista de valores, você pode usar
string_agg no SQL 2017 ou posterior para simplificar o código

O outro banco de dados pode existir em outra instância do SQL Server. Você pode usar sinônimos neste
caso, bem como neste exemplo:
CRIAR SINÔNIMO otherdb.sometbl PARA SERVER.otherdb.dbo.sometbl

Modificar esses procedimentos para oferecer suporte a um parâmetro de @server opcional é bastante
simples. Você precisará alterar @createquery no setup_synonyms para que o código @sql seja retornado
como um conjunto de resultados que você captura com INSERT-EXEC, já que o SQL Server não oferece
suporte a tipos MAX para parâmetros OUTPUT.

6.4 BULK INSERT e OPENROWSET(BULK)


Se você quiser carregar dados de um arquivo com BULK INSERT ou OPENROWSET(BULK), muitas vezes
você se encontra usando SQL dinâmico, porque eles exigem que os nomes de todos os arquivos no
comando (arquivo de dados, arquivo de formato, arquivo de erro) sejam literais de cadeia de caracteres,
mas muitas vezes o(s) nome(s) do(s) arquivo(s) são fornecidos em tempo de execução. Como você está
trabalhando com cadeias de caracteres, as advertências gerais se aplicam. Ou seja, use um de quotename
ou quotestring. Este último é mais seguro, já que um caminho de arquivo certamente pode ter mais de 128
caracteres.

Aqui está um exemplo com BULK INSERT:


DECLARE @datafile nvarchar(250) = 'C:\temp\mydata.bcp',
@formatfile nvarchar(250) = 'C:\temp\mydata.fmt', @sql nvarchar(MAX)

SELECIONE @sql = 'BULK INSERT mytable FROM ' + dbo.quotestring_n(@datafile) + ' WITH
(FORMATFILE = ' + dbo.quotestring_n(@formatfile) + ')' PRINT @sql
EXEC(@sql)

O comando gerado é:
INSERÇÃO EM MASSA MYTABLE DE N'C:\temp\mydata.bcp'
COM (FORMATFILE = N'C.\temp\mydata.fmt')

Não estou ocupando espaço aqui com uma tabela de exemplo e arquivos de exemplo, portanto, a menos
que você mesmo os crie, o comando falhará com um erro de que a tabela ou arquivo está faltando quando
você executá-lo.

Aqui está um exemplo com OPENROWSET(BULK):


DECLARE @datafile nvarchar(250) = 'C:\temp\mydata.bcp',
@formatfile nvarchar(250) = 'C:\temp\mydata.fmt',
@sql nvarchar (MAX)

63 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

SELECIONE @sql =
«INSERIR mytable (a, b, c)
SELECIONE a, b, c DE OPENROWSET(BULK ' + dbo.quotestring_n(@datafile) + ', FORMATFILE = ' +
dbo.quotestring_n(@formatfile) + ') AS t'
IMPRESSÃO @sql
EXEC(@sql)

Pode-se argumentar que, da perspectiva do SQL dinâmico, seria mais limpo ter o INSERT fora do SQL
dinâmico e usar o INSERT-EXEC. No entanto, existem algumas otimizações quando você diz INSERT ...
SELECIONE... FROM OPENROWSET(BULK) que você perde se colocar o INSERT fora do SQL dinâmico.
Da mesma forma, existem algumas dicas para o comando INSERT que só são aplicáveis a
OPENROWSET(BULK). Então, por esse motivo, é melhor manter o INSERT dentro do SQL dinâmico.

6.5 Tarefas de manutenção em geral


Há muitas tarefas de DBA que podem ser automatizadas com a ajuda do SQL dinâmico. Aqui estão apenas
alguns exemplos:

Alterar muitos logins para ativar/desativar políticas de senha.


Criando logons e usuários a partir de uma tabela importada de um arquivo do Excel.
Adicionar um gatilho DDL a muitos bancos de dados para impor alguma política.
Adicionar colunas de auditoria padrão a tabelas que não as possuem.
Um script executado em janelas de manutenção para aumentar o tamanho dos arquivos de dados que
têm menos de 10% de espaço livre.

E não tem fim. Contanto que você jogue o DBA e não esteja escrevendo código de aplicativo, o SQL
dinâmico é quase sempre um jogo justo. Apenas algumas ressalvas:

Não perca a seção sobre BACKUP/RESTORE no próximo capítulo.


Se você está pensando em automatizar a manutenção de índices e estatísticas, primeiro visite
http://ola.hallengren.com

e olhe para sua solução, que é mais ou menos o padrão de fato no mundo do SQL Server. Ola já fez o
trabalho para você.
E embora o SQL dinâmico no T-SQL possa ser um martelo muito bom, nem sempre é a ferramenta
certa. Particularmente, se você quiser fazer algo em muitos servidores, você deve realmente estar
usando o Powershell. E não importa se é uma tarefa para um único servidor ou muitos, é sempre uma
boa ideia verificar se as pessoas legais em dbatools.io têm algo para você.

6.6 Pivô dinâmico – Transformando linhas em colunas


O resultado desse problema é que você tem um conjunto de dados com duas ou mais chaves, por exemplo,
vendas por produto e funcionário. Você deseja apresentar os dados como uma matriz com uma coluna por
funcionário. Você não deseja codificar os funcionários, mas se um novo funcionário ingressar no
departamento de vendas, a saída deverá incluir automaticamente esse funcionário.

Vamos deixar isso claro desde o início: esta é uma operação não relacional. Uma consulta em um banco de
dados relacional retorna uma tabela com um número fixo de colunas, onde cada coluna representa um
atributo distinto no conjunto de dados que está sendo retornado. Um pivô dinâmico é a antítese disso. Ou
mais direto ao ponto: é um dispositivo de apresentação. E, logicamente, o melhor lugar para esses
dispositivos é na camada de apresentação e você deve esperar que um bom escritor de relatórios forneça
essa funcionalidade. Para mencionar alguns, sei que no SSRS o relatório Tablix pode fazer isso, e tenho a
impressão de que no Power BI você pode usar o Matrix Visual. O Excel tem suporte a pivôs há muito

64 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

tempo. Se você estiver recebendo os dados em, digamos, um programa .NET, poderá dinamizar
facilmente os dados em C#.

Eu poderia ter parado aqui e colocado esta seção no próximo capítulo como um exemplo do que você não
deve usar SQL dinâmico para. No entanto, ainda pode haver situações em que você prefere implementar o
pivoting no T-SQL. Para um exemplo muito óbvio: você é um DBA e sua camada de apresentação é
SSMS. Neste caso, não há outra opção. E mesmo se você tiver um programa cliente, talvez ache mais
conveniente fazer o pivotamento em T-SQL. Além disso, vejo perguntas quase diariamente nos fóruns de
SQL que monitoro que pedem um pivô dinâmico. (Não importa que essas perguntas geralmente são de
usuários inexperientes que, em muitos casos, seria melhor explorar as opções do lado do cliente.) Então,
por esses motivos, decidi escrever esta seção para mostrar como implementar um pivô dinâmico de forma
estruturada.

Nota: você pode ter sido encaminhado para esta seção diretamente em resposta a uma pergunta do fórum. Se você está
ansioso para encontrar uma solução para o seu problema em mãos, você pode continuar lendo. Mas esteja ciente de que
escrevi esta seção sob a suposição de que você leu pelo menos os capítulos 2 e 3, então se você realmente não entender
do que estou falando, vá para o topo do artigo e volte aqui quando tiver concluído esses capítulos. Se você quiser brincar
com os exemplos, encontrará informações sobre o banco de dados de demonstração NorthDynamic e as versões SQL
aplicáveis na seção curta Versões SQL aplicáveis e banco de dados de demonstração no início do artigo.

Como sempre, quando trabalhamos com SQL dinâmico, devemos primeiro implementar uma solução
estática, para que saibamos a sintaxe do que vamos gerar. Digamos que queremos ver como diferentes
produtos vendem nos cinco países mais importantes para a NorthDynamic Trading: Brasil, França,
Alemanha, Reino Unido e EUA. Alguns leitores podem, neste momento, esperar ver uma solução usando
o operador PIVOT. No entanto, acho que este operador é de pouco valor. Na verdade, tão pouco valor, que
nem me preocupei em aprendê-lo. Em vez disso, usaremos algo que chamo de filtros CASE dentro de uma
função agregada. Aqui está uma consulta sobre este tema:
SELECT P.ProductName,
SUM(CASO C.País QUANDO 'Brasil' ENTÃO OD. Valor FIM) AS Brasil,
SUM(CASO C.País QUANDO «França» ENTÃO OD. Montante END) AS França,
SUM(CASO C.País QUANDO «Alemanha» ENTÃO OD. Montante END) AS Alemanha,
SUM(CASO C.País quando «Reino Unido» e od. Montante END) COMO UK,
SUM(CASO C.País QUANDO «EUA» E OD. Quantidade FIM) COMO EUA DE dbo.
Encomendas O
JUNTE-SE AO DBO. [Detalhes do pedido] OD EM O.OrderID = OD. ID do pedido
JUNTE-SE AO DBO. Clientes C ON C.CustomerID = O.CustomerID
JUNTE-SE AO DBO. Produtos P ON P.ProductID = OD. Productid
WHERE C.Country IN ('Brasil', 'França', 'Alemanha', 'EUA', 'Reino Unido')
GRUPO POR P.ProductName
ORDEM POR P.ProductName

Como você vê, temos cinco chamadas para a função agregada SOMA, uma para cada país, e dentro delas
temos uma expressão CASE com uma única cláusula WHEN e nenhuma ELSE. A parte sobre nenhum MAIS
é importante. Para o SUM, ele funcionaria com a adição de ELSE 0, mas isso não funcionaria com outras funções
agregadas porque produziria resultados incorretos. São essas expressões CASE de alternativa única que eu
chamo de filtros CASE.

Nota: se você não entendeu imediatamente o que eu quis dizer com resultados incorretos, mas quer saber: Substitua
SUM por AVG e execute a consulta com e sem ELSE 0 para ver a diferença. Com ELSE 0 na consulta, todos os pedidos
contam, não apenas os do Brasil, França etc, então as médias são muito baixas.

Os leitores que estão familiarizados com o operador PIVOT podem se perguntar por que eu sou a favor do
acima. É verdade que certamente é um pouco mais detalhado do que uma consulta usando PIVOT. No
entanto, quando geramos a consulta com SQL dinâmico, isso removerá a natureza repetitiva do código,
como você verá mais adiante. O que é muito mais importante é que o esquema acima é muito flexível.
Digamos que queremos mais duas colunas: uma para todos os outros países e um total geral por produto.
65 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

Isso é muito simples, basta remover a cláusula WHERE e adicionar mais uma SOMA filtrada e outra não
filtrada:
SELECT P.ProductName,
SUM(CASO C.País QUANDO 'Brasil' ENTÃO OD. Valor FIM) AS Brasil,
SUM(CASO C.País QUANDO «França» ENTÃO OD. Montante END) AS França,
SUM(CASO C.País QUANDO «Alemanha» ENTÃO OD. Montante END) AS Alemanha,
SUM(CASO C.País quando «Reino Unido» e od. Montante END) COMO UK,
SUM(CASO C.País QUANDO «EUA» E OD. Montante FIM) COMO EUA,
SUM(CASO EM QUE C.País NÃO EM ('Brasil', 'França', 'Alemanha', 'EUA', 'Reino Unido')
ENTÃO OD. Quantidade
FIM) COMO Outros,
SOMA(OD. Valor) AS [Total Geral]
DE dbo. Encomendas O
JUNTE-SE AO DBO. [Detalhes do pedido] OD EM O.OrderID = OD. OrderID
JOIN dbo. Clientes C ON C.CustomerID = O.CustomerID
JUNTE-SE AO DBO. Produtos P ON P.ProductID = OD. Productid
AGRUPAR POR CONJUNTOS DE AGRUPAMENTO (P.ProductName, ())
ORDEM POR AGRUPAMENTO(P.ProductName), P.ProductName

Nota: Enquanto eu estava nele, eu também adicionei um total geral por coluna com a ajuda da palavra-chave
GROUPING SETS. No entanto, como isso não tem relação com SQL dinâmico, deixo isso sem explicação e indico você
para os Manuais Online se quiser saber mais. Essa sintaxe foi introduzida no SQL 2008 e não é executada no SQL 2005.

Antes de nos aprofundarmos na geração de consultas de pivô dinamicamente, veremos mais um exemplo.
Uma consulta dinâmica sempre inclui funções agregadas com filtros CASE e, na maioria dos casos, a
agregação vem naturalmente, porque você está procurando uma soma, uma média etc. Mas,
ocasionalmente, você tem um conjunto de dados com uma chave de duas colunas que deseja apresentar
em forma de matriz sem qualquer agregação. Como exemplo, podemos tomar a visão
ProductCountrySales no NorthDynamic:
CREATE VIEW ProdutoPaísVendas AS
SELECT P.ProductName, C.Country, SUM(OD. Valor) AS TotAmount FROM dbo.
Encomendas O
JUNTE-SE AO DBO. [Detalhes do pedido] OD EM O.OrderID = OD. ID do pedido
JUNTE-SE AO DBO. Clientes C ON C.CustomerID = O.CustomerID
JUNTE-SE AO DBO. Produtos P ON P.ProductID = OD. Productid
GRUPO POR P.ProductName, C.Country

Gostamos de usar esse modo de exibição para retornar os mesmos dados do primeiro exemplo, em vez de
usar as tabelas base. Mas como a SOMA está dentro da exibição, não há necessidade aparente de realizar
qualquer agregação. Ainda assim, precisamos de uma função agregada, porque essa é a única maneira de
obter dados para o mesmo produto em uma única linha. O truque é usar MIN ou MAX:
SELECT ProductName,
MIN(CASE País QUANDO 'Brasil' ENTÃO TotAmount END) COMO Brasil,
MIN(CASO País QUANDO 'França' ENTÃO TotAmount END) COMO França,
MIN(CASE País QUANDO 'Alemanha' ENTÃO TotAmount END) AS Alemanha,
MIN(CASE País QUANDO 'UK' ENTÃO TotAmount END) COMO UK,
MIN(CASE País QUANDO 'EUA' ENTÃO TotAmount END) COMO EUA
DE dbo. ProductCountrySales
WHERE Country IN ('Brasil', 'França', 'Alemanha', 'EUA', 'Reino Unido')
GRUPO POR ProductName

A razão pela qual isso funciona é porque os filtros CASE garantem que cada MIN (ou MAX) veja apenas
um único valor não-NULL.

Vamos agora passar para gerar consultas de pivô dinamicamente, para que possamos obter qualquer
número de colunas em nosso pivô. Um procedimento armazenado que gera e executa um pivô dinâmico
normalmente consiste em seis etapas:

66 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

1. Identificar os valores que definirão as colunas na consulta e salvá-los em uma tabela temporária.
2. Criando a parte inicial da consulta, ou seja, SELECT + colunas iniciais.
3. Gerar todas as chamadas filtradas por caso necessárias para a função agregada com a ajuda dos dados
na tabela temporária e adicioná-los à cadeia de caracteres de consulta.
4. Adicionar quaisquer colunas finais no conjunto de resultados desejado à cadeia de caracteres de
consulta.
5. Adicionando o restante da consulta: cláusulas FROM, JOIN, WHERE, GROUP BY e ORDER BY.
6. Executando a consulta gerada.

Vamos passar por essas etapas com o procedimento ProductCountrySales_sp. O objetivo deste
procedimento é retornar as vendas por produto e país durante um período, incluindo apenas o produto e os
países que realmente tiveram vendas durante esse período. Ou seja, se nenhum cliente de, digamos,
Bélgica, comprou qualquer coisa durante o período, a Bélgica não deve ser incluída no conjunto de
resultados. As colunas de país devem ser ordenadas primeiro em ordem alfabética por continente e, em
seguida, por país dentro do continente. Nesta primeira versão, não incluímos nenhum subtotal por
continente, mas veremos isso no final da seção.

Vamos passar por este procedimento passo a passo, embora não na ordem acima, mas sim a ordem que
você deve seguir quando você implementar uma solução para um pivô dinâmico. Começamos com um
shell vazio, com apenas o passo 6 incluído:
CRIAR OU ALTERAR PROCEDIMENTO ProductCountrySales_sp @fromdate data,
@todate data,
@debug bit = 0 AS
COMECE A TENTAR
DECLARE @lineend char(3) = '," + char(13) + char(10),
@sql nvarchar (MAX)

-- Mais código será adicionado à medida que avançamos.

SE @debug = 1
IMPRESSÃO @sql
EXEC sp_executesql @sql, N'@fromdate data, @todate data', @fromdate, @todate END TRY
COMECE A CAPTURAR
SE @@trancount > 0 TRANSAÇÃO DE REVERSÃO
; JOGAR
FIM DA CAPTURA

O procedimento leva três parâmetros. Além @fromdate e @todate ditadas pela funcionalidade desejada,
há também um parâmetro @debug. Como apontei algumas vezes, esse parâmetro é obrigatório quando
você trabalha com SQL dinâmico para que você possa inspecionar o SQL dinâmico se as coisas derem
errado. A última instrução no procedimento é executar o SQL dinâmico que aprenderemos a gerar no texto
a seguir. Podemos ver que essa cadeia de caracteres SQL terá @fromdate e @todate como parâmetros, é
claro que não incorporaremos os valores desses parâmetros na cadeia de caracteres SQL.

A lógica do procedimento é envolvida em TRY-CATCH. Isso não tem nada a ver com SQL dinâmico, mas
estou incluindo por uma questão de práticas recomendadas. Se você quiser saber mais sobre o tratamento
de erros e por que o bloco CATCH se parece, consulte a Parte Um da minha série Tratamento de erros e
transações no SQL Server.

Há duas variáveis locais. Como você deve imaginar, é @sql onde construiremos nossa consulta. @lineend
é semelhante ao @nl que introduzi na seção Espaçamento e Formatação, mas além do CR-LF, há também
uma vírgula. Como as linhas que geramos terminarão em vírgula, faz todo o sentido ter a vírgula na
variável. Isso também se deve a uma restrição com a função string_agg que voltarei.

67 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

Vejamos agora o passo 1. Nesta etapa, montamos a tabela #pivotcols. (Embora você possa usar qualquer
nome, estou usando esse nome como um nome padrão neste texto). Logicamente, essa tabela precisa ter
três colunas:

1. Um que define a ordem das colunas, normalmente chamado de colno. Esta é normalmente uma
coluna int.
2. Um que especifica os valores para os filtros CASE, normalmente chamados filterval. O tipo de dados
para esta coluna deve corresponder à coluna de origem da qual foi retirado.
3. Um que define os nomes das colunas no conjunto de resultados da consulta gerada, normalmente
chamado colname. O tipo de dados desta coluna é sempre sysname. (Porque é um nome de
metadados.)

Os valores em todas as três colunas são sempre exclusivos.

Às vezes, você pode usar a mesma coluna para mais de uma finalidade. Neste exemplo específico,
filterval e colname teriam os mesmos valores, portanto, ignoramos filterval e usamos colname em ambos
os lugares. Se os requisitos tivessem solicitado uma lista alfabética rigorosa das colunas de saída, também
poderíamos ter ignorado o colno.

É assim que definimos e preenchemos #pivotcols para ProductCountrySales_sp:


CREATE TABLE #pivotcols (colno int NOT NULL PRIMARY KEY, colname sysname NOT
NULL UNIQUE)
INSERT #pivotcols (colno, colname)
SELECT row_number() OVER(ORDEM POR Cou.Continent, Cou.Country), Cou.Country
DE dbo. Países Cou
ONDE EXISTE (SELECIONE *
DE dbo. Clientes C
JUNTE-SE AO DBO. Pedidos O ON O.CustomerID = C.CustomerID
ONDE C.Country = Cou.Country
E O.OrderDate ENTRE @fromdate E @todate)

Suspeito que muitos leitores teriam escrito isso como uma junção sobre as três tabelas, mas nesse caso
você teria que jogar um DISTINTO para eliminar duplicatas. Usar o EXISTS é uma maneira mais limpa de
atingir o objetivo. Na verdade, estamos pedindo países que tiveram vendas no período.

Agora pulamos para a etapa 5, a parte em que configuramos o corpo da consulta após a lista SELECT. É
uma boa ideia escrever esta parte antes de executar as etapas 2, 3 e 4, porque é aqui que você determina a
lógica de seleção da consulta, e é aqui que você define os aliases que você usará nas etapas anteriores.
Para este exemplo, esta etapa não é particularmente complicada:
SELECIONE @sql += '
DE dbo. Encomendas O
JUNTE-SE AO DBO. [Detalhes do pedido] OD EM O.OrderID = OD. ID do pedido
JUNTE-SE AO DBO. Clientes C ON C.CustomerID = O.CustomerID
JUNTE-SE AO DBO. Produtos P ON P.ProductID = OD. Productid
ONDE O.OrderDate ENTRE @fromdate E @todate
GRUPO POR P.ProductName
ENCOMENDAR POR P.ProductName'

Observe que estamos adicionando à variável @sql com o operador +=. (Relembre o Advertência sobre o
uso desse operador se a cadeia de caracteres anexada for composta por várias cadeias de caracteres
longas.)

Agora que temos os aliases, podemos voltar para a etapa 2, que neste exemplo é uma linha única:
SELECT @sql = 'SELECT P.ProductName' + @lineend

68 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

ProductName é a única coluna que vem antes das colunas dinâmicas.

A etapa 3 é a peça central de um pivô dinâmico da perspectiva do SQL dinâmico. É aqui que construímos
as linhas SUM/CASE a partir do conteúdo de #pivotcols. Vamos primeiro ver como construir uma única
linha, antes de olhar para concatená-los juntos na variável @sql, para que não estejamos tentando fazer
muitas coisas ao mesmo tempo. O que estamos vendo é construir uma linha como esta:
SUM(CASE País QUANDO 'Brasil' ENTÃO TotAmount END) COMO Brasil,

Há duas ocorrências do Brasil aqui. Um é um literal de cadeia de caracteres e o segundo é um alias de


coluna. Aprendemos que devemos sempre usar variáveis onde isso é sintaticamente possível, então
podemos pensar que devemos usar uma variável no lugar do literal de cadeia de caracteres. No entanto,
isso nos falha aqui, porque os parâmetros só funcionam quando temos um número fixo deles, e não
sabemos de antemão quantos precisaremos. Assim, precisamos embutir o literal de cadeia de caracteres e
o alias de coluna (que nunca pode ser uma variável de qualquer maneira). Quando fazemos isso,
precisamos tomar as precauções usuais para evitar a injeção de SQL e evitar acidentes sintáticos quando o
nome do país inclui caracteres especiais. Também podemos querer adicionar algum recuo para que a saída
de depuração seja mais legível. Isso leva a algo assim:
SELECT concat(space(7), ' SUM(CASE C.Country WHEN ', quotename(colname, ''''),
' ENTÃO OD. Montante END) AS ', quotename(colname), @lineend) DE #pivotcols

Você pode notar que eu uso a função concat em vez de usar o operador +. Isso realmente não importa
aqui, mas se você tiver uma coluna de filtragem separada em #pivotcols isso pode ser um valor inteiro ou
de data. Nesse caso, o concat é mais prático porque todas as suas entradas são implicitamente convertidas
em cadeias de caracteres, de modo que o código não precisa estar repleto de um tipo explícito. O espaço
de chamada(7) adiciona sete espaços de recuo.

Nota: A função concat foi introduzida no SQL 2012, portanto, se você ainda estiver no SQL 2008 ou anterior, precisará
usar o operador +.

Em ambos os lugares onde o colname aparece, ele é enrolado em aspas para que possamos lidar com
nomes de países como Costa Rica e Costa do Marfim sem erros de sintaxe. Como colname é do tipo
sysname, não precisamos nos preocupar que o valor de entrada para quotename é muito longo e resultará
em NULL sendo retornado. Também adicionamos @lineend à cadeia de caracteres para obter a vírgula
final e uma quebra de linha para uma saída bonita.

Para ter qualquer uso para essas linhas, precisamos concatená-las em uma única cadeia de caracteres. Já
fizemos isso algumas vezes neste artigo, mas em nenhum lugar é tão essencial quanto com o pivô
dinâmico, então vamos dar uma olhada um pouco mais de perto em como fazer isso. Há duas opções: a
função mais direta string_agg, que está disponível no SQL 2017 e posterior, e a menos intuitiva FOR XML
PATH, disponível desde o SQL 2005. Ambos possuem algumas peculiaridades que exigem alguma
modificação no SELECT acima. Aqui está o código completo para a etapa 3 com string_agg:
; COM sumcaselines AS ( SELECT colno,
convert(nvarchar(MAX),
concat(espaço(7), ' SOMA(CASO C.País QUANDO N', quotename(colname, ''''),
' ENTÃO OD. Montante END) AS ', quotename(colname))) AS sumcase
DE #pivotcols
)
SELECIONE @sql += string_agg(sumcase, @lineend) DENTRO DO GRUPO (ORDER BY COLNO) DE
SUMCASELINES

A razão pela qual eu uso um CTE aqui é simplesmente para ter a concatenação de cadeia de caracteres
separada da chamada para string_agg, mas se você preferir colocar a concatenação dentro string_agg,
você pode fazê-lo. Você pode observar que eu modifiquei o SELECT acima em dois aspectos. A primeira é

69 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

que eu envolvi a operação concat em uma conversão para nvarchar(MAX). Se o pivô tiver muitas
colunas, pode ser que o comprimento total da concatenação exceda 8000 bytes, o que significa que o
resultado que sai de string_agg deve ser nvarchar(MAX). No entanto, o tipo de saída de string_agg é o
mesmo que o tipo da entrada, portanto, se a entrada for nvarchar(n), você não poderá obter mais de 4000
caracteres de volta. Assim, para obter nvarchar(MAX) de volta de string_agg, você precisa enviar
nvarchar(MAX) em. (Felizmente, se você esquecer o converter e o resultado é mais de 8000 bytes,
string_agg gera um erro e não trunca a cadeia de caracteres silenciosamente.) A outra modificação é que
@lineend está ausente da concatenação principal, mas passou como o segundo argumento para
string_agg, isto é, o delimitador para a concatenação. (Esse argumento deve ser um literal de cadeia de
caracteres constante ou uma variável. Não pode ser uma expressão como '', + @nl. Essa é uma das razões
pelas quais @lineend inclui a vírgula)

Preste atenção também à cláusula DENTRO do GRUPO, que não vimos em nossos encontros anteriores
com string_agg. Essa cláusula nos permite especificar a ordem dentro da cadeia de caracteres
concatenada, e a ordem já está definida pela coluna colno em #pivotcols, então usamos essa coluna.

Esta é a solução com FOR XML PATH:


SELECIONE @sql +=
(SELECT concat(space(7), ' SUM(CASE C.Country WHEN ', quotename(colname, ''''),
' ENTÃO OD. Montante END) AS ', quotename(colname), @lineend)
DE #pivotcols
ENCOMENDAR POR colno
PARA CAMINHO XML(''), TYPE).value('.', 'nvarchar(MAX)')

SELECT @sql = substring(@sql, 1, len(@sql) - len(@lineend))

string_agg tem um lugar especial para o delimitador entre as cadeias de caracteres e entende que ele não
deve ser adicionado após a última string. PARA XML PATH é projetado principalmente para uma finalidade
diferente, portanto, ele não tem esse recurso. Por esta razão, temos que adicionar uma segunda instrução
para remover o @lineend final, pois caso contrário essa vírgula nos daria erros de sintaxe. Para o pedido
no nome do país, usamos uma cláusula regular ORDER BY. Não há necessidade aqui de lançar para
nvarchar(MAX) aqui, mas isso é feito implicitamente neste caso.

Você pode se perguntar o que todas essas coisas engraçadas na última linha realmente significam e por
que você precisa delas. Mas é muito complicado explicar neste contexto, por isso vou deixá-lo no escuro.
Você precisará confiar em mim que isso funciona. E, sim, você precisa de todo esse gobbledygook. Mas
ele sempre parece o mesmo, então você pode apenas copiá-lo e colá-lo quando precisar.

Nota: Muitas pessoas que postam exemplos com PARA CAMINHO XML, encapsulam a consulta FOR XML na função
stuff para remover o delimitador à direita. No entanto, acho que isso torna o código difícil de ler, e é por isso que prefiro
removê@lineend em uma declaração separada.

O último passo a ser observado é o passo 4, que é muito simples neste primeiro exemplo: como não temos
colunas extras, ele está vazio. Voltaremos a esse passo no próximo exemplo.

Aqui está o código completo para ProductCountrySales_sp com as duas alternativas da etapa 3 dentro de
quadros. Ou seja, você só incluiria um desses quadros ao criar o procedimento.
CRIAR OU ALTERAR PROCEDIMENTO ProductCountrySales_sp @fromdate data,
@todate data,
@debug bit = 0 AS
COMECE A TENTAR
DECLARE @lineend char(3) = '," + char(13) + char(10),
@sql nvarchar (MAX)

CREATE TABLE #pivotcols (colno int NOT NULL PRIMARY KEY, colname sysname NOT
NULL UNIQUE)
70 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

INSERT #pivotcols (colno, colname)


SELECT row_number() OVER(ORDEM POR Cou.Continent, Cou.Country), Cou.Country
DE dbo. Países Cou
ONDE EXISTE (SELECIONE *
DOS Clientes C
JOIN Orders O ON O.CustomerID = C.CustomerID
ONDE C.Country = Cou.Country
E O.OrderDate ENTRE @fromdate E @todate)

SELECT @sql = 'SELECT P.ProductName' + @lineend


; COM sumcaselines AS ( SELECT colno,
convert(nvarchar(MAX),
concat(espaço(7), ' SOMA(CASO C.País QUANDO N', quotename(colname, ''''),
' ENTÃO OD. Montante END) AS ', quotename(colname))) AS sumcase FROM #pivotcols

)
SELECIONE @sql += string_agg(sumcase, @lineend) DENTRO DO GRUPO (ORDER BY COLNO) DE
SUMCASELINES
SELECIONE @sql +=
(SELECT concat(space(7), ' SUM(CASE C.Country WHEN ', quotename(colname, ''''),
' ENTÃO OD. Montante END) AS ', quotename(colname), @lineend) DE #pivotcols
ENCOMENDAR POR colno
PARA CAMINHO XML(''), TYPE).value('.', 'nvarchar(MAX)')

SELECT @sql = substring(@sql, 1, len(@sql) - len(@lineend))

SELECIONE @sql += '


DE dbo. Encomendas O
JUNTE-SE AO DBO. [Detalhes do pedido] OD EM O.OrderID = OD. ID do pedido
JUNTE-SE AO DBO. Clientes C ON C.CustomerID = O.CustomerID
JUNTE-SE AO DBO. Produtos P ON P.ProductID = OD. Productid
ONDE O.OrderDate ENTRE @fromdate E @todate
GRUPO POR P.ProductName
ENCOMENDAR POR P.ProductName'

SE @debug = 1
IMPRESSÃO @sql

EXEC sp_executesql @sql, N'@fromdate data, @todate data', @fromdate, @todate END TRY
COMECE A CAPTURAR
SE @@trancount > 0 TRANSAÇÃO DE REVERSÃO
; JOGAR
FIM DA CAPTURA

Vejamos agora alguns exemplos de execuções. Esta chamada cobre todo o período em que os
NorthDynamic Traders estiveram ativos, por isso são todas as ordens na base de dados:
EXECUTIVO ProductCountrySales_sp '19960101', '19981231', 1

Aqui está o início do conjunto de resultados, dividido em três imagens para ajustar a largura da página:

71 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

Você também pode procurar na guia Mensagens para ver a consulta gerada. Aqui está uma versão
abreviada:
SELECIONE P.ProductName ,
SUM(CASO C.País QUANDO «Costa do Marfim»» ENTÃO OD. Montante END) AS [Costa do Marfim],
SUM(CASO C) País QUANDO «Áustria» ENTÃO OD. Montante END) AS [Áustria],
SUM(CASO C.País QUANDO «Bélgica» ENTÃO OD. Montante END) AS [Bélgica],
...
SUM(CASO C.País QUANDO 'Brasil' ENTÃO OD. Montante END) AS [Brasil],
SUM(CASO C.País QUANDO 'Venezuela' ENTÃO OD. Valor FIM) AS [Venezuela]
DE dbo. Encomendas O
JUNTE-SE AO DBO. [Detalhes do pedido] OD EM O.OrderID = OD. ID do pedido
JUNTE-SE AO DBO. Clientes C ON C.CustomerID = O.CustomerID
JUNTE-SE AO DBO. Produtos P ON P.ProductID = OD. Productid
ONDE O.OrderDate ENTRE @fromdate E @todate
GRUPO POR P.ProductName
ORDEM POR P.ProductName

Preste atenção ao primeiro país, a Costa do Marfim (lembre-se que os países são classificados
principalmente por continentes, então a África vem em primeiro lugar). Sem o nome de aspas, esse nome
de país teria produzido erros de sintaxe, devido à aspa simples e ao espaço no nome.

Se você executá-lo por apenas duas semanas em julho de 1997, você terá muito menos colunas:
EXEC ProductCountrySales_sp '19970701', '19970714', 1 Estas são as quatro

primeiras linhas no conjunto de resultados, que tem apenas seis

colunas:

E aqui está a lista SELECT:


SELECIONE P.ProductName ,
SUM(CASO C) País QUANDO «Áustria» ENTÃO OD. Montante END) AS [Áustria],
SUM(CASO C.País QUANDO «Dinamarca» ENTÃO OD. Montante END) AS [Dinamarca],
SUM(CASO C.País QUANDO «Alemanha» ENTÃO OD. Montante END) AS [Alemanha],
SUM(CASO C.País QUANDO «Itália» ENTÃO OD. Montante END) AS [Itália],
SUM(CASO C.País QUANDO 'Canadá' ENTÃO OD. Montante END) AS [Canadá], SUM(CASE C.País
QUANDO 'EUA' ENTÃO OD. Montante FIM) COMO [EUA],
SUM(CASO C.País QUANDO 'Brasil' ENTÃO OD. Valor END) AS [Brasil]
DE Ordens O ...

72 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

Passaremos agora ao próximo exemplo, que é ProductEmployeeSales_sp. Neste procedimento, queremos


ver as vendas por produto e funcionário durante um período, com os funcionários como colunas,
ordenados por suas vendas totais. Só devemos ter colunas para funcionários que realmente tiveram vendas
no período. Neste exemplo, também queremos totais gerais, tanto por produto quanto por funcionário.

Isso leva a algumas mudanças em relação ao exemplo anterior. Para começar, precisamos de uma coluna
de filtragem separada em #pivotcols, já que queremos filtrar por EmployeeID, enquanto para os nomes de
coluna queremos os nomes dos funcionários. Em seguida, devido à coluna Total Total, a etapa 4 não está
vazia. Como se vê, essa etapa não se manifesta por uma linha de código separada, mas sim por
modificações no código para as etapas 3 e 5. A coluna Total Geral em si é simplesmente adicionada
primeiro ao segmento com as cláusulas FROM-JOIN. E por causa desta coluna, a última coluna dinâmica
deve agora ser seguida por uma vírgula. Na solução com string_agg, isso significa que precisamos
adicionar um @lineend extra. Por outro lado, na variante com FOR XML PATH, isso significa que devemos
deixar de fora a operação de substring que remove o @lineend final, então essa parte é realmente mais
curta quando temos colunas extras.

Como também queremos totais gerais por funcionário, precisamos usar a cláusula GROUPING SETS na
cláusula GROUP BY, da mesma maneira que no exemplo anterior com uma consulta estática. (O que, como
observado, tem pouco a ver com o pivô dinâmico como tal, mas como não é incomum pedi-lo, adicionei
isso ao exemplo.)

Aqui está o código completo, com as duas variações para a etapa 3 em quadros. Salientei também as
diferenças em relação ao primeiro exemplo.
CRIAR OU ALTERAR PROCEDIMENTO ProductEmployeeSales_sp @fromdate data,
@todate data,
@debug bit = 0 AS
COMECE A TENTAR
DECLARE @lineend char(3) = '," + char(13) + char(10),
@sql nvarchar (MAX)

CREATE TABLE #pivotcols (colno int NOT NULL PRIMARY KEY, filterval int NOT NULL
UNIQUE,
colname sysname NÃO É NULL EXCLUSIVO)

; COM OrderAgg AS (
SELECIONE EmployeeID, SUM(OD. Valor) Valor AS
DE dbo. Encomendas O
JUNTE-SE AO DBO. [Detalhes do pedido] OD EM O.OrderID = O.OrderID
ONDE OrderDate ENTRE @fromdate E @todate
GRUPO POR O.EmployeeID
)
INSERT #pivotcols(colno, filterval, colname)
SELECIONE row_number() OVER(ORDER BY OA. Valor DESC, E.EmployeeID), E.EmployeeID,
E.FirstName + ' ' + E.LastName
DE dbo. Empregados E
JOIN OrderAgg OA ON E.EmployeeID = OA. ID do funcionário

SELECT @sql = 'SELECT P.ProductName' + @lineend


; COM sumcaselines AS ( SELECT colno,
convert(nvarchar(MAX),
concat(space(7), ' SUM(CASE O.EmployeeID WHEN ', filterval, ' ENTÃO OD. Montante END)
AS ', quotename(colname))) AS sumcase
DE #pivotcols
)
SELECIONE @sql += string_agg(sumcase, @lineend) DENTRO DO GRUPO (ORDER BY colno) +
@lineend FROM sumcaselines

73 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

SELECIONE @sql +=
(SELECT concat(space(7), ' SUM(CASE O.EmployeeID WHEN ', filterval,
' ENTÃO OD. Montante END) AS ', quotename(colname), @lineend) DE #pivotcols
ENCOMENDAR POR colno
PARA CAMINHO XML(''), TYPE).value('.', 'nvarchar(MAX)')

-- Nada aqui.

SELECIONE @sql += '


SOMA(OD. Valor) AS [Total Geral]
DE dbo. Encomendas O
JUNTE-SE AO DBO. [Detalhes do pedido] OD EM O.OrderID = OD. ID do pedido
JUNTE-SE AO DBO. Produtos P ON P.ProductID = OD. Productid
ONDE O.OrderDate ENTRE @fromdate E @todate
AGRUPAR POR CONJUNTOS DE AGRUPAMENTO (P.ProductName, ())
ORDEM POR AGRUPAMENTO (P.ProductName), P.ProductName'

SE @debug = 1
IMPRESSÃO @sql
EXEC sp_executesql @sql, N'@fromdate data, @todate data', @fromdate, @todate END TRY
COMECE A CAPTURAR
SE @@trancount > 0 TRANSAÇÃO DE REVERSÃO
; JOGAR
FIM DA CAPTURA

Você pode notar que há um CTE ao construir #pivotcols. Esse CTE calcula o valor das vendas para os
funcionários durante o período de forma relacional, para que possamos usar esse valor de vendas para
ordenar as colunas.

Aqui está um exemplo de chamada:


EXECUTIVO ProductEmployeeSales_sp '19970201', '19970214', 1

Ele lista quatro (de nove) funcionários. Aqui estão as primeiras linhas no conjunto de resultados

O último exemplo é ProductContinentSales_sp. É semelhante ao ProductCountrySales_sp, mas aqui


queremos subtotais por continente, bem como um total geral. Para simplificar, ignoramos se um país tem
vendas no período, mas incluímos todos os países dos quais há pelo menos um cliente. Deixo o exemplo
sem comentários, mas vou salientar que filterval está novamente ausente de #pivotcols, mas por outro
lado há uma coluna de ajuda que é usada na etapa 3 onde queremos filtrar o nome do país para as colunas
regulares, enquanto para as colunas sub-totais queremos filtrar no nome do continente. Mostro esta etapa
somente com FOR XML.
CRIAR OU ALTERAR PROCEDIMENTO ProductContinentSales_sp @fromdate data,
@todate data,
@debug bit = 1 AS
COMECE A TENTAR
DECLARE @lineend char(3) = '," + char(13) + char(10), @sql nvarchar(MAX)

CREATE TABLE #pivotcols (colno int NOT NULL PRIMARY KEY, colname sysname NOT NULL
UNIQUE, IsContinent bit NOT NULL)

INSERT #pivotcols (colno, colname, IsContinent)


SELECIONE row_number() OVER(ORDEM POR CONTINENTE, ISCONTINENTE, PAÍS),
IIF(IsContinent = 1, Continente, País), IsContinent
DE (SELECIONE DISTINTA C.País, cou. Continente, 0 AS IsContinente
74 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

DE dbo. Clientes C
JUNTE-SE AO DBO. Países cou ON C.País = cou. País UNIÃO TODOS
SELECIONE NULL DISTINTO, cou. Continente, 1 AS IsContinente
DE dbo. Clientes C
JUNTE-SE AO DBO. Países cou ON C.País = cou. País) AS u

SELECT @sql = 'SELECT P.ProductName' + @lineend

SELECIONE @sql +=
(SELECT concat(espaço(7), ' SOMA(CASO', ),
IIF(IsContinente = 1, 'cou. Continente', «C.País»),
' WHEN ', quotename(colname, ''''),
' ENTÃO OD. Montante END) AS ', quotename(colname), @lineend) DE #pivotcols
ENCOMENDAR POR colno
PARA CAMINHO XML(''), TYPE).value('.', 'nvarchar(MAX)')

SELECIONE @sql += '


SOMA(OD. Valor) AS [Total Geral]
DE dbo. Encomendas O
JUNTE-SE AO DBO. [Detalhes do pedido] OD EM O.OrderID = OD. ID do pedido
JUNTE-SE AO DBO. Clientes C ON C.CustomerID = O.CustomerID
JUNTE-SE AO DBO. Países cou ON cou. País = C.País
JUNTE-SE AO DBO. Produtos P ON P.ProductID = OD. Productid
ONDE O.OrderDate ENTRE @fromdate E @todate
AGRUPAR POR CONJUNTOS DE AGRUPAMENTO (P.ProductName, ())
ORDEM POR AGRUPAMENTO(P.ProductName), P.ProductName'

SE @debug = 1
IMPRESSÃO @sql
EXEC sp_executesql @sql, N'@fromdate data, @todate data', @fromdate, @todate
FIM TENTATIVA
COMECE A CAPTURAR
SE @@trancount > 0 TRANSAÇÃO DE REVERSÃO
; JOGAR
FIM DE
PEGAR ir
EXECUTIVO ProductContinentSales_sp '19960101', '19981231', 1

Como o conjunto de resultados é bastante amplo, não o incluo na íntegra aqui, mas mostro apenas as duas
últimas colunas que incluem dois totais continentais, bem como o total geral.

7. Situações em que você (provavelmente) não deve usar o SQL


dinâmico
Neste capítulo, discutirei situações em que você pode estar inclinado a usar SQL dinâmico, mas onde essa
pode não ser a melhor das escolhas. Pode ser que você tenha cometido um erro anteriormente em seu
design, ou também pode ser que haja uma alternativa que não requer SQL dinâmico.

7.1 Nomes de tabela dinâmica no código do aplicativo


Não é incomum ver perguntas em fóruns onde o cartaz diz que o nome da tabela para uma consulta em um
procedimento armazenado tem que ser dinâmico. Se você sentir vontade de fazer isso para algo que será
executado como uma parte regular de um aplicativo, é muito provável que você esteja latindo a árvore

75 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

errada. Para esclarecer, estou falando de consultas – SELECT, INSERT, UPDATE, DELETE, MERGE. Para
algo que envolve DDL pode ser uma coisa diferente, mas você raramente tem DDL em execução como
parte das operações diárias de um aplicativo.

Muito provavelmente, isso é resultado de algum erro no início do projeto. Em um banco de dados
relacional, uma tabela destina-se a modelar uma entidade exclusiva com um conjunto distinto de atributos
(ou seja, as colunas). Normalmente, entidades diferentes exigem operações diferentes. Portanto, se você
acha que deseja executar a mesma consulta em várias tabelas, algo não é como deveria ser.

Ao longo dos anos, observei mais de uma razão pela qual as pessoas querem fazer isso, e nesta seção
abordarei alguns equívocos. Se você encontrou uma situação em que você queria usar nomes de tabela
dinâmica no código do aplicativo que eu não abordo aqui, por favor, deixe-me uma linha em
esquel@sommarskog.se.

Tabelas temporárias exclusivas do processo


Esta é uma confusão de usuários SQL muito inexperientes, que aprenderam SQL dinâmico muito cedo.
Eles querem uma tabela de trabalho que seja exclusiva para o usuário atual, então eles continuam e criam
uma tabela como WorkTable_98 onde 98 é o spid do processo atual e todo o acesso a essa tabela tem que
ser por meio de SQL dinâmico. A maioria dos leitores deste texto sabe que a verdadeira solução é usar
uma tabela temporária local, com um nome começando com um único sinal numérico (#). Cada tabela é
visível apenas para o processo atual e é automaticamente descartada quando o escopo onde foi criada é
encerrado. Assim, não há necessidade de criar tabelas nomeadas dinamicamente nesse caso.

O excesso de zelo na vontade de generalizar


Não é incomum em um banco de dados que há muitas tabelas de pesquisa que parecem todas iguais: há
um id, um nome e talvez algumas colunas de auditoria genéricas. Isso leva algumas pessoas a pensar que
devem generalizar isso, então, em vez de ter um conjunto de procedimentos CRUD (Criar, Ler, Atualizar,
Excluir) para cada tabela, elas querem um único conjunto onde o nome da tabela é um parâmetro.

Decorre do que disse acima que isso está errado. As tabelas modelam entidades diferentes, e deve haver
um conjunto de procedimentos para cada tabela exatamente por esse motivo. Se você acha que isso
significa muita codificação chata, você pode escrever um programa que gera esses procedimentos, então
adicionar uma nova tabela de pesquisa é muito fácil. Minha experiência é que essas tabelas nem sempre
permanecerão cópias de carbono umas das outras. Ou seja, durante a vida útil do sistema atributos
adicionais serão identificados para algumas dessas tabelas de pesquisa, portanto, com o tempo, esses
procedimentos se desviarão uns dos outros simplesmente porque devem ser diferentes.

Uma observação secundária aqui: se você não usa procedimentos armazenados, mas cria suas instruções SQL no código
do cliente, é uma coisa totalmente diferente. Aqui você pode ter uma classe genérica para todas essas tabelas de pesquisa
que tem o código genérico comum, e você pode instanciar subclasses para cada tabela de pesquisa, adicionando
propriedades extras para colunas extras, substituindo métodos etc. Mas o T-SQL não se presta a esse tipo de coisa.

Não sei, mas talvez algumas pessoas que querem fazer isso pensem que estão economizando recursos
reduzindo o número de procedimentos armazenados. Mas tenha em mente que cada lote de SQL dinâmico
é um procedimento armazenado sem nome, portanto, é tudo igual. Se você tiver cem procedimentos para
cem tabelas, ou se tiver um único procedimento para cem tabelas, esse procedimento produzirá cem
procedimentos armazenados sem nome produzindo cem planos de consulta, assim como cem
procedimentos estáticos.

Aplicativos multilocatários
Quando você projeta um aplicativo multilocatário, há algumas possibilidades. Uma delas é ter um único
banco de dados e ter um TenantID em cada tabela. Em seguida, você carrega o ID do locatário em todos
os procedimentos armazenados. Este não é de forma alguma um projeto incomum. No entanto, isso traz

76 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

um risco: se um desenvolvedor casual se atrapalhar, os dados de um locatário podem ser expostos para
outro locatário.

Por esse motivo, algumas pessoas preferem ter um banco de dados (ou um esquema) por locatário. Mas
depois pensam que só querem um conjunto de procedimentos armazenados que tem como parâmetro o
nome de uma base de dados de inquilinos, "porque tudo o resto seria um pesadelo de manutenção". Acho
que isso é totalmente errado. Ter esse único conjunto de procedimentos armazenados é um pesadelo total
de manutenção, já que quase todas as linhas de código precisam estar repletas de SQL dinâmico. Não,
você deve ter um conjunto de procedimentos armazenados em cada banco de dados (ou em cada esquema)
e, em seguida, precisa desenvolver métodos para distribuir e distribuir alterações de código e tabela para
todos os bancos de dados ou esquemas. Nos dias de DevOps isso não deve ser muito complicado.

Nota: Obviamente, se você não usar procedimentos armazenados, mas enviar todas as consultas do código do cliente,
esse cenário é bastante simples. Para um aplicativo de vários bancos de dados, o banco de dados é determinado pela
cadeia de conexão. Para um aplicativo de vários esquemas, você provavelmente teria logons pessoais em que cada
usuário tem o esquema padrão apropriado.

Atributo como parte do nome da tabela


Nesse padrão, fica muito claro que é um caso de várias tabelas modelando a mesma entidade. Em vez de
um único
Tabela de pedidos, há Orders_ALFKI, Orders_VINET etc com uma tabela por cliente. Ou Orders_1996,
Orders_1997 etc com uma tabela por ano. Ou seja, o que realmente é um atributo da entidade e deve ser
uma coluna em uma única tabela foi adicionado como parte do nome da tabela. Este é raramente, ou
nunca, um design correto.

O trem exato de pensamentos que leva a esse projeto não está totalmente claro para mim. Mas talvez as
pessoas que usam esse padrão não tenham absorvido totalmente o modo relacional de pensar. Este tópico
do fórum pode servir como uma ilustração. Neste caso em particular, parece que nunca ocorreu ao cartaz
que o seu CodNeg fosse uma coluna na sua tabela temporária.

Particionamento caseiro
Parece que, às vezes, quando as pessoas colocam um valor de chave no nome da tabela, elas têm uma
preocupação de que uma única tabela seria muito grande para o SQL Server. Essa ansiedade não é muito
bem fundamentada quando se trata de operações regulares do dia a dia. Com índices adequados, você
pode encontrar rapidamente os dados que está procurando, e não importa se o tamanho total da tabela é de
1 TB. E se você precisa digitalizar todos esses dados, você precisa digitalizá-los, não importa se é uma
tabela ou cem tabelas. Mas é mais fácil escrever a consulta se for uma única tabela ou um modo de
exibição.

Dito isso, há situações em que você pode querer particionar seus dados. Aqui estão algumas razões
possíveis:

Você deseja distribuir os dados em vários grupos de arquivos para espalhar a carga, possivelmente
colocando dados mais antigos em unidades mais lentas.
Você quer ser capaz de envelhecer dados antigos rapidamente. Por exemplo, digamos que você queira
reter todos os pedidos para 12
Meses. Então, quando junho de 2020 começar, você quer se livrar de todos os pedidos de junho de
2019 e não quer executar uma instrução DELETE, que levaria muito tempo para ser concluída.
Um cenário bastante avançado: clientes diferentes têm padrões muito diferentes, então você deseja
planos diferentes para a mesma consulta dependendo do cliente e, para isso, deseja histogramas
diferentes nas estatísticas para clientes diferentes.

Mas em nenhum desses casos faz sentido criar Orders_201906, Orders_201907 etc, ou

77 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

Orders_ALFKI, Orders_VINET etc e, em seguida, use SQL dinâmico para acessar a tabela que você
precisa para o momento. A saber, o SQL Server fornece não um, mas dois recursos de particionamento
diferentes: tabelas particionadas e exibições particionadas. Vou dar uma visão geral muito rápida deles
aqui e, em seguida, fornecer links para leitura adicional.

Tabelas particionadas é o recurso mais popular. Eles foram introduzidos no SQL 2005 e por muito tempo
estavam disponíveis apenas no Enterprise Edition, mas isso mudou com o lançamento do SQL 2016 SP1 e
agora estão disponíveis em todas as edições. Para criar uma tabela particionada, primeiro crie uma função
de partição para definir os intervalos para as partições. Em seguida, você define um esquema de partição
que mapeia os intervalos de partição para diferentes grupos de arquivos e, finalmente, cria a tabela nesse
esquema de partição, usando-a como se fosse um grupo de arquivos. O recurso suporta até 15 000
partições para uma única tabela.

As exibições particionadas existem há ainda mais tempo, elas já foram introduzidas no SQL 2000 e
sempre estiveram disponíveis em todas as edições do SQL Server. Uma exibição particionada consiste em
várias tabelas individuais que têm uma estrutura comum e todas têm as mesmas colunas de chave
primária. Para a coluna de particionamento, que deve fazer parte da PK, cada tabela tem uma restrição
CHECK definida para que um determinado valor caiba em, no máximo, uma tabela. A exibição em si
consiste em um número de consultas SELECT em relação a essas tabelas unidas
com UNION ALL. Com visualizações particionadas, você realmente teria tabelas como Orders_201906 ou
Orders_ALFKI, mas todo o acesso seria por meio da exibição Ordens. Quando você consulta o modo de
exibição, o plano inclui todas as tabelas no modo de exibição, mas o plano é tal que apenas as tabelas
qualificadas pela chave de particionamento são realmente acessadas em tempo de execução. Eu não vi
nenhum limite máximo para o número de tabelas que você pode ter em uma visão particionada, mas eu
diria que 100 seria muito, e na prática você deve ficar abaixo de 20.

Uma forma muito especializada de exibições particionadas são exibições particionadas distribuídas, onde
as tabelas estão em várias instâncias do SQL Server. Este é um recurso bastante avançado, e só é
suportado no Enterprise Edition.

Normalmente, todas as tabelas em uma exibição particionada têm o mesmo conjunto de colunas, mas há
espaço para alguma flexibilidade, portanto, se você achar que precisa de novas colunas na visualização
Pedidos a partir de janeiro de 2021, poderá deixar as tabelas Orders_202012 e anteriores inalteradas e
apenas colocar NULL no SELECT dessas tabelas na definição de exibição. Ou se você precisar ter colunas
extras para o cliente ALFKI, e somente para esse cliente, poderá ter essas colunas em Orders_ALFKI e
não incluí-las na exibição. As consultas que acessam essas colunas acessariam Orders_ALFKI
diretamente e não a exibição Ordens.

Dos três marcadores que listei acima, as tabelas particionadas e as exibições particionadas suportam as
duas primeiras, mas você só pode alcançar o último marcador com exibições particionadas, onde cada
tabela tem seu próprio histograma. (Embora seja possível obter resultados bons o suficiente com
estatísticas filtradas quando os dados estão em uma única tabela.)

Quando se trata do segundo ponto, a comutação de partição, esta é uma operação que requer várias etapas,
não menos importante se mais de uma tabela (lógica) estiver envolvida, por exemplo, uma tabela Orders
e uma tabela OrderDetails.
Se isso vai acontecer no dia 1º de cada mês, você certamente deseja automatizar essa operação em um
procedimento armazenado que você agenda. Não importa se você usa tabelas particionadas ou exibições
particionadas, esse procedimento precisará usar um monte de SQL dinâmico para criar uma nova partição
ou uma nova tabela e se livrar da antiga. Não estou incluindo um exemplo de como fazer isso aqui, mas
gosto de ressaltar que este é um exemplo de bom uso do SQL dinâmico. Você tem SQL dinâmico para
executar uma tarefa específica dentro de um único procedimento armazenado. Compare isso com nomes
de tabelas dinâmicas em que você coloca o código em todos os lugares.

78 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

Esta foi uma breve introdução ao particionamento no SQL Server. Para uma introdução um pouco mais
longa com exemplos de código, etc, a MVP da plataforma de dados, Catherine Wilhemsen, tem uma
postagem de blog de duas partes Particionamento de tabela no SQL Server, que abrange tabelas
particionadas. Stefan Delmarco tem um artigo muito bom sobre exibições particionadas, SQL Server 2000
Partitioned Views. (Este artigo é um pouco antigo, mas o recurso não mudou muito desde então.)

7.2 Nomes de colunas dinâmicas


E há usuários que misteriosamente pensam que o SQL Server pode ler suas mentes, portanto, se o fizerem
SELECIONE @colname DA TBL

Na verdade, o SQL Server retornará a coluna da qual o nome está em @colname. Comum também é que
eles queiram fazer:
ATUALIZAÇÃO tbl
DEFINIR @colname = @value
ONDE keycol = @id

Mas isso só definirá a variável @colname para o valor de @value – se houver uma linha na tabela em
que keycol é igual a @id.

Se você se lembra do que eu disse na seção anterior, sabe por que isso está errado: cada coluna deve
modelar um atributo exclusivo da entidade que a tabela modela. Por essa razão, atributos diferentes (ou
seja, colunas) tendem a ter domínios diferentes para os valores que podem tomar, o que torna o acima um
pouco inútil.

Assim, eu diria que se você sente que precisa fazer algo como o acima, há um erro em algum lugar no
design. Uma situação que posso pensar é que você tem uma tabela com colunas como
SalesAmt_Jan, SalesAmt_Feb etc. O design adequado seria, é claro, ter uma subtabela SalesAmts com
uma linha por mês. No entanto, você pode estar na situação infeliz de que você está preso a um design
como este. Nesse caso, minha preferência é usar uma expressão IIF, como pode ser chato:
ATUALIZAÇÃO tbl
SET SalesAmt_Jan = IIF(@colname = 'SalesAmt_Jan', @salesamt, SalesAmt_Jan), SalesAmt_Feb = IIF(@colname
= 'SalesAmt_Feb', @salesamt, SalesAmt_Feb),
...
DE tbl
ONDE keycol = @id

No SQL 2008 e versões anteriores, você teria que usar CASE em vez de IIF.

Outro equívoco que ocasionalmente senti é que alguns desenvolvedores têm a impressão de que atualizar
vinte colunas usará mais recursos do que atualizar uma única. Uma instrução UPDATE normal em um
procedimento armazenado que oferece suporte a uma interface do usuário pode ter a seguinte aparência:
ATUALIZAÇÃO tbl
SET col1 = @val1, col2 =
@val2,
... col20 = @val20
WHERE keycol = @id

Agora, digamos que o usuário realmente só alterou o valor de col1 e as variáveis de @val2 para @val20
mantêm os valores atuais de col2 para col20. Neste caso, eles acham que isso é mais enxuto no servidor:
ATUALIZAÇÃO tbl
SET col1 = @val1
ONDE keycol = @id

79 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

Eu realmente executei alguns testes sobre isso, e minha conclusão é que se não há índices nas colunas col2
a col20 não há diferença alguma. Descobri que a quantidade de log de transações produzido era
exatamente a mesma para as duas instruções. Quando qualquer uma das colunas extras estava presente em
um índice, eu vi algum aumento na quantidade de log de transações produzido. No entanto, como isso
normalmente é uma questão de atualizações de linha única, o impacto real provavelmente será totalmente
insignificante. Por outro lado, se você estiver gerando instruções UPDATE que incluem apenas as
colunas para as quais há alterações, você gerará uma instrução UPDATE para cada combinação de
colunas, e cada uma delas resultará em uma entrada de cache, portanto, estaria ocupando mais recursos
dessa maneira.

7.3 Definir aliases de coluna dinamicamente


O desejo aqui é fazer algo assim:
SELECIONE COL COMO @myname DA tbl

Imagino que o cenário mais comum seja um pivô semidinâmico. Ou seja, o número de colunas é fixo, mas
você deseja que o cabeçalho reflita o conteúdo da coluna. Como exemplo, digamos que você deseja
retornar as vendas por funcionário nos últimos três meses, com os funcionários como linhas e os meses
como colunas, e deseja ano-mês no cabeçalho da coluna. Em muitos casos, isso é algo que você pode
manipular na camada de apresentação. Se você ainda fizer isso no SQL, poderá implementá-lo como um
pivô dinâmico de acordo com o padrão que apresentei anteriormente. Mas é de fato possível alcançar esse
objetivo específico sem SQL dinâmico. Insira o resultado em uma tabela temporária com nomes de coluna
fixos e renomeie as colunas conforme desejado. Aqui está um procedimento para fazer isso para o
exemplo dado. Embora as condições sejam um pouco mais complexas, você pode reconhecer o padrão de
SOMA com filtros CASE na seção sobre pivô dinâmico.

CRIAR OU ALTERAR PROCEDIMENTO AmountPerEmployee @yearmonth char(6) COMO


COMECE A TENTAR
CREATE TABLE #temp (EmployeeID int NOT NULL PRIMARY KEY, month1 decimal(10,
2) NOT NULL, month2 decimal(10, 2) NOT NULL, month3 decimal(10, 2) NOT NULL)

DECLARE @month1 char(6) =


convert(char(6), dateadd(MÊS, -2, @yearmonth + '01'), 112),
@month2 char(6) =
convert(char(6), dateadd(MONTH, -1, @yearmonth + '01'), 112), @nextmon date = dateadd(MONTH, 1,
@yearmonth + '01')

INSERT #temp (EmployeeID, month1, month2, month3)


SELECT EmployeeID,
SOMA(CASO EM QUE O.OrderDate >= @month1 + '01' E
O.OrderDate < @month2 + '01' ENTÃO OD. Montante END),
SOMA(CASO EM QUE O.OrderDate >= @month2 + '01' E
O.OrderDate < @yearmonth + '01' ENTÃO OD. Montante END),
SOMA(CASO EM QUE O.OrderDate >= @yearmonth + '01' E
O.OrderDate < @nextmon ENTÃO OD. Montante FIM)
DAS Encomendas O
JOIN (SELECT OrderID, SUM(Amount) AS Amount
FROM [Detalhes do pedido]
GROUP BY OrderID) COMO OD EM O.OrderID = OD. ID do pedido
ONDE O.OrderDate >= @month1 + '01'
E O.OrderDate < @nextmon
GRUPO POR EmployeeID

EXEC tempdb.. sp_rename '#temp.month1', @month1, 'COLUNA'


EXEC tempdb.. sp_rename '#temp.month2', @month2, 'COLUNA'
EXEC tempdb.. sp_rename '#temp.month3', @yearmonth, 'COLUNA'

SELECIONE E.FirstName, E.LastName, t.*


DOS Funcionários E
JOIN #temp t ON E.EmployeeID = t.EmployeeID
80 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

FIM TENTATIVA
COMECE A CAPTURAR
SE @@trancount > 0 TRANSAÇÃO DE REVERSÃO
; JOGAR
FIM DE
PEGAR ir
EXEC AmountPerEmployee '199802'

Observe que essa técnica requer que você retorne as colunas da tabela temporária com SELECT *, como
você não sabe os nomes, não pode listá-los.

Por apenas três meses, um pivô dinâmico completo seria um pouco exagerado. Por outro lado, se a
exigência seria de 24 meses, a solução acima não é realista; o pensamento de variáveis de 24 meses é
apenas repelente. Você ainda pode ficar com a ideia principal, e ter os meses em uma tabela temporária, e
então você faz um loop sobre essa tabela temporária para a parte de renomeação. No entanto, você ainda
precisaria de 24 linhas SUM/CASE na lista SELECT, e não é improvável que você prefira um pivô totalmente
dinâmico nesse caso.

7.4 Nomes de procedimentos dinâmicos


Nesse caso, o programador está procurando chamar um procedimento armazenado do qual o nome é
definido em tempo de execução. Talvez porque o nome do banco de dados é dinâmico, talvez por algum
outro motivo. Não há necessidade de SQL dinâmico aqui, e você viu a solução várias vezes neste
documento: EXEC aceita uma variável para o nome do procedimento:
EXECUTIVO @spname

Ainda... Não posso deixar de notar que, com alguma frequência, vejo até programadores experientes
construindo uma instrução SQL dinâmica para lidar com essa situação.

7.5 Dinâmico ORDER BY Condition


Se o procedimento armazenado retornar uma consulta estática, mas você quiser permitir que o usuário
selecione a ordem das linhas, o melhor é classificar os dados do lado do cliente. Até porque, então, você
pode permitir que o usuário classifique novamente os dados sem ter que executar novamente a consulta.
Existem vários controles de grade no mercado para ajudá-lo com isso.

Mas se você quiser fazer isso em SQL, você pode usar CASE:
ORDEM POR CASO @ordercol = THATCOL ENTÃO THATCOL END,
CASO @ordercol = THISCOL ENTÃO thiscol END,
...
OPÇÃO (RECOMPILAR)

A adição de OPTION (RECOMPILE) pode ajudá-lo a obter um plano de consulta que se encaixe melhor
com a condição de classificação selecionada.

É certo que, se você tiver várias colunas de classificação e também quiser permitir que o usuário selecione
entre ASC e DESC, isso pode sair do controle, e uma solução com SQL dinâmico pode ser menos
complexa de implementar. Mas nesse caso você deve ter cuidado e não apenas fazer algo como
@sql += ' ORDEM POR ' + @sortcols

uma vez que faria você amplamente aberto para injeção de SQL. Eu discuti brevemente possíveis
abordagens anteriormente na seção Outros furos de injeção. No meu artigo Condições de Pesquisa
Dinâmica, tenho alguns exemplos mais detalhados.

81 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

7.6 SELECT * FROM tbl WHERE col IN (@list)


Esse é um dos equívocos mais comuns entre usuários SQL inexperientes. Eles fazem algo como:
SELECIONE @list = '1,2,3'
SELECT * FROM tbl WHERE col IN (@list)

E aí eles se perguntam por que não funciona, quando não recebem filas de volta. A resposta é que funciona.
Se houver uma linha onde o valor de col é 1,2,3, eles receberão essa linha de volta. Mas linhas onde col =
1, não. A saber, col IN (1, 2, 3), é apenas um atalho sintático para col = 1 OU col = 2 OU col = 3 e col IN (@list) é o
mesmo que col = @list.

Felizmente não é tão comum como costumava ser, mas você ainda pode ver pessoas que respondem a
essas perguntas sugerindo SQL dinâmico. O que é uma solução tremendamente pobre para este problema.
Abre para injeção de SQL, torna o código mais complexo e também não é bom para o desempenho da
consulta.

A solução correta é usar uma função list-to-table, e há muitos deles por aí. A partir do SQL 2016, há até
uma função interna, string_split, para a tarefa, embora nem sempre seja a melhor escolha. Consulte ainda
meu artigo Matrizes e listas no SQL Server.

7.7 Enviando uma cláusula WHERE como um parâmetro


Isso é algo que já abordei amplamente na seção Outros buracos de injeção no capítulo sobre injeção de
SQL, mas gostaria de fazer uma breve recapitulação como um exemplo de abuso dinâmico de SQL.

Ocasionalmente vi usuários que desejam enviar uma parte da sintaxe SQL para um procedimento
armazenado, e não me refiro a uma pequena parte como uma lista de colunas para uma cláusula ORDER
BY, mas uma parte mais substancial como uma cláusula WHERE completa. Permitam-me que deixe isto
perfeitamente claro: enquanto estivermos a falar de código de aplicação simples, este é um não-não
absoluto.

Pode haver situações em que os requisitos são mais dinâmicos do que você pode encaixar na sintaxe T-
SQL bastante inflexível, e você prefere fazer o trabalho do lado do cliente. E tudo bem. Se você quiser
permitir que os usuários selecionem filtros, quais colunas retornar, o que agregar etc, acho que isso é
melhor resolvido do lado do cliente. Mas, em seguida, você deve fazer todo o trabalho no cliente para essa
função específica e não passar a sintaxe SQL para um procedimento armazenado. Não só esse
procedimento armazenado estaria aberto à injeção de SQL, mas você também criaria um acoplamento
muito estreito entre o cliente e o banco de dados, o que causaria um pesadelo de manutenção.

7.8 Trabalhando com uma tabela com colunas desconhecidas


De vez em quando, vejo esses cartazes nos fóruns que estão insatisfeitos porque a tabela temporária que
eles criaram dentro de seu SQL dinâmico não está mais lá quando o SQL dinâmico sai. Você diz a eles que
eles precisam criar a tabela temporária antes de executar o lote SQL dinâmico, pensando que eles usam
SELECT INTO apenas porque são preguiçosos. Mas, não, eles respondem que não podem fazer isso, porque
sua tabela temporária é dinâmica.

Eu tenho um grande problema em abordar sua falácia subjacente, porque apesar de ter visto esse padrão
muitas vezes ao longo dos anos, nunca fui capaz de entender completamente como eles acabaram nessa
situação miserável. Quando pedi a essas pessoas que descrevessem seu problema do início ao fim, na
melhor das hipóteses obtive uma resposta fragmentária, e o tópico morreu sem solução.

82 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

Eu tenho uma teoria, porém, que é que sua tabela de temp dinâmica é o resultado de um pivô dinâmico.
Como eu disse na seção sobre pivô dinâmico, esta é uma operação não relacional, e uma vez que você
tenha executado seu pivô dinâmico, só há uma coisa que você pode fazer com o resultado: retornar os
dados para o cliente. Você simplesmente não está mais no Kansas, Toto. Se você quiser executar alguma
filtragem, cálculos ou o que quer que seja em seus dados após o pivô dinâmico, você precisa voltar para a
prancheta e reorganizar sua ordem de computação e executar essas ações antes de fazer o pivô dinâmico.
O pivô dinâmico deve ser sempre a última coisa na parte SQL da sua solução. O trabalho restante terá que
ocorrer do lado do cliente.

Um cenário ao qual vi um ou dois pôsteres aludirem foi que eles estavam carregando arquivos, e os
arquivos tinham um layout diferente a cada vez. Isso por si só parece suspeito, mas se você realmente
tiver essa situação, não parece uma boa tarefa para uma solução totalmente SQL, mas isso deve ser
orquestrado a partir de uma linguagem do lado do cliente ou algo como SSIS (SQL Server Integration
Services).

Dito tudo isso, se você absolutamente quer uma tabela temporária com colunas dinâmicas, há uma
maneira de fazê-lo. Crie uma tabela de stub com suas colunas fixas. Em seguida, adicione as colunas
dinâmicas com ALTER TABLE ADD dentro do SQL dinâmico. Não estou dando exemplo, porque
definitivamente não é nada que eu recomendo. E certamente não é nada para usuários com pouca
experiência em SQL, já que você precisará saber como derivar os metadados da entrada da tabela
temporária, então este é um truque para usuários experientes. Estou tentado a dizer que você só entraria
nesse caminho, se realmente precisasse amar para se machucar.

Mais uma alternativa para lidar com essa situação é utilizar o CLR. Consulte a seção Usando o CLR em
meu artigo Como compartilhar dados entre procedimentos armazenados, onde apresento o procedimento
armazenado ExecAndInsert , que executa um lote SQL e cria uma tabela para cada conjunto de resultados,
com base nos metadados do conjunto de resultados. Eu usei este procedimento com sucesso em um
chicote de teste, mas não é nada que eu recomendo para o código do aplicativo.

7.9 BACKUP/RESTAURAÇÃO
Como eu disse que o SQL dinâmico é um jogo justo para tarefas de DBA, você pode se surpreender ao ver
BACKUP/RESTORE neste capítulo, mas há uma razão simples. Não é incomum ver códigos como este:

DECLARE @db sysname = 'Northwind',


@backuppath nvarchar(200) = 'C:\temp',
@sql nvarchar (MAX)

SELECIONE @sql = 'BANCO DE DADOS DE BACKUP' + quotename(@db) +


' PARA DISCO = ''' + @backuppath + '\' + @db + '-' +
substituir(convert(varchar(19), sysdatetime(), 126), ':', '') + '.bak''' PRINT @sql
EXEC(@sql)

Então, o que há de errado aqui? Porque você pode fazer isso de forma simples:
DECLARE @db sysname = 'NorthDynamic',
@backuppath nvarchar(200) = 'C:\temp', @filepath nvarchar(250)

SELECIONE @filepath = @backuppath + '\' + @db + '-' +


replace(convert(varchar(19), sysdatetime(), 126), ':', '') + '.bak' BACKUP DO BANCO DE DADOS @db
EM DISCO = @filepath

Ou seja, BACKUP e RESTORE aceitam variáveis para a maioria ou todos os seus argumentos. Aqui está um
exemplo funcional para RESTORE:
DECLARE @db sysname = 'BigDB',
@dmpname nvarchar(250) = 'S:\MSSQL\BigDB.bak',
@dev1 sysname = 'BigDB',

83 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

@mdfname nvarchar(250) = 'S:\MSSQL\SJUTTON\BigDB.mdf',


@dev2 sysname = 'BigDB_log',
@ldfname nvarchar(250) = 'S:\MSSQL\SJUTTON\BigDB.ldf',
@stats int = 10

RESTAURAR @db DO BANCO DE DADOS DO DISCO = @dmpname


COM MOVE @dev1 PARA @mdfname,
MOVER @dev2 PARA @ldfname,
ESTATÍSTICAS = @stats

(Altere o banco de dados e os nomes de arquivo para algo que você tenha no servidor para testar.)

Não verifiquei toda e qualquer parte da sintaxe dos comandos BACKUP e RESTORE. Os gráficos de
sintaxe nos Manuais Online indicam que as variáveis são suportadas na maioria dos lugares, mas também
há partes em que as variáveis não aparecem na sintaxe. Por exemplo, os Manuais Online não dizem que as
variáveis são suportadas com MOVE ou STATS, mas como você pode ver no exemplo acima, elas são
aceitas.

Dito isso, há situações em que você precisará usar SQL dinâmico para esses comandos. Se você quiser
torná-lo um parâmetro se quiser usar COMPRESSION, INIT, FORMAT ou qualquer outra palavra-chave do tipo,
você precisará criar uma cadeia de caracteres dinâmica - mas você ainda pode passar o banco de dados e o nome do
arquivo de backup como parâmetros para sp_executesql.

Outra situação em que você precisa usar o SQL dinâmico é se quiser usar um backup distribuído.
Contanto que o número de listras seja fixo, não há problema em fazê-lo estaticamente:
@db DO BANCO DE DADOS DE BACKUP EM DISCO = @stripe1, @stripe2 @stripe3

Mas isso é decomposto, se você quiser um número variável de listras, dependendo do tamanho do banco
de dados ou de outra coisa, já que a forma da cadeia de caracteres depende de quantas listras você deseja.
Você ainda pode usar sp_executesql para executar o comando, se você se estabelecer em um número
máximo de faixas, digamos 50, e sempre passar @stripe1, @stripe2 até @stripe50, mesmo que nem todas
essas variáveis @stripe apareçam no comando BACKUP que você gerar. Não, eu realmente não espero
que você faça isso, e nem eu mesmo faria isso, mas eu construiria um comando BACKUP com todas as
listras embutidas, e presumivelmente eu teria o nome das listras (ou alguma parte do nome) em uma
tabela de alguma forma. E é claro que eu tomaria precauções para evitar a injeção de SQL e envolver
todas as listras em quotename ou quotestring.

Um caso semelhante existe com RESTORE WITH MOVE. Se você quiser lidar com bancos de dados com
qualquer número de arquivos, precisará trabalhar com SQL dinâmico, já que não sabe de antemão quantas
cláusulas MOVE serão necessárias.

Mas para o uso diário desses comandos, não há necessidade de usar SQL dinâmico. Basta usar variáveis.

8. Conclusão
Você já leu um artigo sobre SQL dinâmico, e minha esperança é que você tenha aprendido que há coisas
boas e ruins que você pode fazer com SQL dinâmico. O SQL dinâmico pode ser uma bênção para
automatizar tarefas de DBA, e o SQL dinâmico usado no lugar certo também pode ser uma bênção no
código do aplicativo. Mas aplicado incorretamente, o SQL dinâmico pode ser uma maldição, produzindo
código que é quase impossível de ler e manter e que causa problemas de desempenho e abre
vulnerabilidades de segurança. E não é incomum que um desejo de usar SQL dinâmico seja devido a um
erro no início do design em que você violou uma ou mais das regras básicas para bancos de dados
relacionais.

Os pontos mais importantes deste artigo em resumo:


84 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

Use sempre instruções parametrizadas. Nunca embuta um valor que possa ser passado como
parâmetro!
Quando você cria cadeias de caracteres com nomes de objeto em variáveis: sempre use quotename.
Quando você precisar embutir valores de cadeia de caracteres, por exemplo, em instruções DDL, use
quotename com aspas simples como o segundo parâmetro, se você souber que o valor é <= 128
caracteres. Caso contrário, use dbo.quotestring ou dbo.quotestring_n.
Cuidado com a injeção de SQL!
Sempre inclua essa linha ao trabalhar com SQL dinâmico:
SE @debug = 1 IMPRIMA @sql
Sempre!

8.1 Reconhecimento e Feedback


Meus artigos não existem no vácuo, e recebo muitos comentários valiosos de colegas MVP, leitores de
meus artigos e outras pessoas. Para este artigo, gostaria de agradecer às seguintes pessoas por sua
contribuição: Anthony Faull, Chintak Chhapia, Jonathan Van Houtte, John Smart e John Barrow.

Se você tiver comentários sobre o artigo, você é mais do que bem-vindo para me enviar um e-mail para
esquel@sommarskog.se. Seu comentário pode ser sobre o conteúdo real, mas você também é mais do que
bem-vindo para apontar erros ortográficos e gramaticais. Ou apenas apontar passagens onde eu poderia
explicar melhor as coisas. Por outro lado, se você tem um problema específico com o qual está lutando, eu
o encorajo a fazer sua pergunta em um fórum público, pois é muito provável que você obtenha ajuda mais
rápido dessa forma, pois mais pessoas verão seu post. Se você quiser que eu veja a pergunta, o melhor
lugar é a tag sql-server-transct-sql no fórum Microsoft Q&A, mas devo avisá-lo que o tempo geralmente
não me permite ler todos os posts lá.

9. Revisões
2022-08-30
John Barrow teve a gentileza de fornecer uma revisão completa do artigo, e eu corrigi muitos erros
ortográficos e gramaticais, dos quais alguns tornaram o texto um tanto ininteligível em alguns
lugares.

2021-09-03
Adicionado um parágrafo final à seção Trabalhando com uma tabela com colunas desconhecidas.

2021-01-01
John Smart foi gentil em compartilhar suas funções SanitizeObjectName e SanitizeObjectNameEx,
que
são adaptados do que digo na seção Recebendo nomes de tabelas e outros objetos como
Parâmetros, e esta seção agora também inclui uma referência e um exemplo rápido de suas funções.

2020-12-24
Adicionado um último parágrafo na seção Fazendo algo em todos os bancos de dados sobre um
script searchalldatabases.sql que permite pesquisar um valor em todos os bancos de dados e tabelas
em sua instância.

2020-08-21
Aprimorou a discussão sobre as melhores práticas no uso da notação em duas partes no capítulo
sobre Desempenho. Um pouco vergonhosamente, descobri que os exemplos na seção sobre Pivô
Dinâmico não usavam notação de duas partes. Isso foi corrigido.

85 de 86 16/9/2023, 12:54
A Maldição e as Bênçãos do SQL Dinâmico https://www.sommarskog.se/dynamic_sql.html

2020-01-12
Jonathan Van Houtte teve a gentileza de sugerir uma dica para a seção The Need of Debug Prints
para mais uma maneira de exibir longas cadeias de caracteres SQL.

2020-01-11
Primeira versão. (Sim, havia um artigo com o mesmo título na mesma URL, mas este é
essencialmente um artigo inteiramente novo sobre o mesmo assunto.)

Voltar à minha página inicial.

86 de 86 16/9/2023, 12:54

Você também pode gostar