Jhonatan Pinheiro
Cargando página...
Jhonatan Pinheiro
Cargando página...
Jhonatan Pinheiro
Cargando página...
Qué es cada cosa, qué significa cada término y cómo se hace: NULL, collation, JOIN, CTE, funciones de ventana, Vista, Procedimiento, Función, índice, plan de ejecución, staging, carga incremental, RDL, esquema estrella, SCD, DAX y las particularidades de Dynamics AX y Navision — 70 respuestas con ejemplos listos para probar.
Este guia é feito de perguntas — as que aparecem de verdade quando alguém começa a trabalhar com SQL Server, T-SQL, SSIS, SSRS e BI. Cada resposta explica o que o termo significa, como a coisa funciona por dentro e como se faz na prática, com exemplo pronto para testar.
Não precisa ler na ordem. Use o índice, procure a dúvida e volte quando aparecer outra.
Os exemplos usam um banco imaginário de vendas com
Clientes,Pedidos,PedidoItens,ProdutoseVendedores. Todos rodam no SQL Server 2016 ou mais novo.
É um SGBD — Sistema Gerenciador de Banco de Dados — relacional, da Microsoft. Traduzindo: um programa que roda num servidor, guarda dados organizados em tabelas e responde a pedidos de leitura e escrita vindos de outros programas.
Ele faz quatro coisas que um arquivo comum não faz: garante que dois usuários gravando ao mesmo tempo não se atrapalhem, garante que uma operação incompleta não deixe lixo, controla quem pode ver o quê, e encontra uma linha entre milhões em milissegundos.
O que você instala não é só o banco. O pacote inclui o Database Engine (o banco em si), o SSIS (ferramenta de carga de dados), o SSRS (relatórios) e o SSAS (cubos analíticos). São produtos separados que costumam viver juntos.
SQL é a linguagem padrão para consultar bancos relacionais — SELECT, INSERT, UPDATE, DELETE. Funciona em qualquer banco, com pequenas variações.
T-SQL (Transact-SQL) é a versão da Microsoft: o SQL padrão mais tudo que permite programar dentro do banco — variáveis, IF, WHILE, tratamento de erro, funções próprias.
-- SQL padrão: roda em qualquer banco
SELECT Nome FROM Clientes WHERE UF = 'SP';
-- T-SQL: variável, condicional e função específica da Microsoft
DECLARE @Total INT;
SELECT @Total = COUNT(*) FROM Clientes WHERE UF = 'SP';
IF @Total > 100
PRINT 'Muitos clientes em SP: ' + CAST(@Total AS VARCHAR(10));Na prática, quem trabalha com SQL Server escreve T-SQL o tempo todo, mesmo sem perceber. TOP, ISNULL, GETDATE() e APPLY são T-SQL, não SQL padrão.
São três níveis de organização, do maior para o menor:
VendasDB, RH, Financeiro — cada um isolado dos outros.dbo.Clientes e rpt.Clientes são duas tabelas diferentes.-- Nome completo de um objeto: servidor.banco.schema.objeto
SELECT * FROM VendasDB.dbo.Clientes;
-- Criar um schema para separar o que é de relatório
CREATE SCHEMA rpt;
GOdbo que aparece antes do nome das tabelas?dbo significa database owner e é o schema padrão de todo banco SQL Server. Quando você cria uma tabela sem dizer o schema, ela nasce em dbo.
Escrever dbo.Clientes em vez de só Clientes não é preciosismo. Sem o schema, o SQL Server procura primeiro no schema do seu usuário e só depois em dbo — isso custa uma busca a mais e, pior, impede que o plano de execução seja reaproveitado entre usuários diferentes. Em consulta que roda milhares de vezes, faz diferença medível.
Tipo errado causa dois problemas: ocupa espaço à toa e produz resultado errado. Os casos que mais aparecem:
| Situação | Use | Não use | Por quê |
|---|---|---|---|
| Dinheiro | DECIMAL(18,2) | FLOAT | FLOAT é aproximado: 0,1 + 0,2 não dá exatamente 0,3 |
| Texto só com letras comuns | VARCHAR | NVARCHAR | NVARCHAR gasta o dobro de bytes |
| Texto com qualquer idioma | NVARCHAR | VARCHAR | VARCHAR perde caracteres fora da collation |
| Data e hora | DATETIME2(3) | DATETIME | DATETIME arredonda em blocos de 3,33 ms |
| Só a data | DATE | DATETIME | 3 bytes contra 8, e sem hora para atrapalhar comparação |
| Verdadeiro/falso | BIT | VARCHAR(1) | 1 bit contra 1 byte + risco de 'S'/'s'/'Y' |
CREATE TABLE dbo.Pedidos (
PedidoID INT IDENTITY PRIMARY KEY,
ClienteID INT NOT NULL,
DataPedido DATETIME2(3) NOT NULL,
ValorTotal DECIMAL(18,2) NOT NULL, -- dinheiro nunca é FLOAT
Observacao VARCHAR(500) NULL,
Ativo BIT NOT NULL DEFAULT 1
);NULL e por que ele se comporta de forma estranha?NULL não é zero nem texto vazio. Significa "não se sabe". E é por isso que ele quebra a lógica normal: qualquer comparação com o desconhecido resulta em desconhecido, não em verdadeiro ou falso.
SELECT 1 WHERE NULL = NULL; -- não retorna nada
SELECT 1 WHERE NULL <> 'texto'; -- também não retorna nadaPara testar nulo, use IS NULL / IS NOT NULL. Para substituir por um valor, ISNULL ou COALESCE:
SELECT Nome,
ISNULL(Telefone, 'não informado') AS Telefone,
COALESCE(Celular, Telefone, 'sem contato') AS MelhorContato
FROM dbo.Clientes
WHERE Email IS NOT NULL;ISNULL aceita dois argumentos; COALESCE aceita vários e devolve o primeiro que não for nulo.
O detalhe que mais derruba consulta: NOT IN com uma subconsulta que contenha um único NULL devolve vazio, silenciosamente. Use NOT EXISTS.
Você escreve numa ordem, o banco executa em outra. Entender isso explica metade dos erros de SQL:
1. FROM / JOIN -> monta o conjunto de linhas
2. WHERE -> filtra linha a linha
3. GROUP BY -> agrupa
4. HAVING -> filtra os grupos
5. SELECT -> escolhe e calcula as colunas
6. ORDER BY -> ordena
7. TOP / OFFSET -> cortaÉ por isso que você não pode usar no WHERE um apelido criado no SELECT — quando o WHERE roda, o SELECT ainda não aconteceu:
-- ERRO: 'Margem' ainda não existe quando o WHERE é avaliado
SELECT ValorTotal - Custo AS Margem FROM dbo.Pedidos WHERE Margem > 100;
-- Certo: repita a expressão, ou use uma subconsulta/CTE
SELECT ValorTotal - Custo AS Margem FROM dbo.Pedidos WHERE ValorTotal - Custo > 100;E é por isso também que o ORDER BY pode usar o apelido: ele roda depois do SELECT.
Collation é a regra que define como o banco compara e ordena texto: se diferencia maiúscula de minúscula, se ignora acento, qual a ordem alfabética.
O nome dela conta tudo: em SQL_Latin1_General_CP1_CI_AS, o CI é case insensitive (ignora maiúscula) e o AS é accent sensitive (diferencia acento).
-- Com CI: as duas linhas voltam
SELECT * FROM dbo.Clientes WHERE Nome = 'joão';
SELECT * FROM dbo.Clientes WHERE Nome = 'JOÃO';
-- Ver a collation de cada coluna
SELECT name, collation_name FROM sys.columns
WHERE object_id = OBJECT_ID('dbo.Clientes') AND collation_name IS NOT NULL;O problema aparece ao juntar tabelas de bancos com collations diferentes: o JOIN falha com "Cannot resolve the collation conflict". A saída é forçar uma das duas:
SELECT a.Nome
FROM BancoA.dbo.Clientes a
JOIN BancoB.dbo.Clientes b
ON a.Codigo = b.Codigo COLLATE SQL_Latin1_General_CP1_CI_AS;Chave primária (PK) é a coluna que identifica a linha de forma única. Não aceita nulo e não repete. Toda tabela deveria ter uma.
Chave estrangeira (FK) é a coluna que aponta para a chave primária de outra tabela. Ela é o que impede que exista pedido de um cliente que não existe.
CREATE TABLE dbo.Clientes (
ClienteID INT IDENTITY PRIMARY KEY, -- chave primária
Nome VARCHAR(200) NOT NULL
);
CREATE TABLE dbo.Pedidos (
PedidoID INT IDENTITY PRIMARY KEY,
ClienteID INT NOT NULL,
CONSTRAINT FK_Pedidos_Clientes
FOREIGN KEY (ClienteID) REFERENCES dbo.Clientes (ClienteID)
);Com essa FK no lugar, tentar inserir um pedido para o cliente 9999 (que não existe) dá erro. É o banco protegendo o dado de um bug da aplicação.
IDENTITY e o que é SEQUENCE?IDENTITY faz a coluna se numerar sozinha a cada INSERT. É o jeito mais comum de gerar id.
CREATE TABLE dbo.Clientes (ClienteID INT IDENTITY(1,1) PRIMARY KEY, Nome VARCHAR(200));
-- ^ começa em 1, soma 1 a cada linha
INSERT INTO dbo.Clientes (Nome) VALUES ('Maria');
SELECT SCOPE_IDENTITY(); -- id que acabou de ser gerado nesta sessãoUse SCOPE_IDENTITY() e não @@IDENTITY: o segundo devolve o id gerado por qualquer coisa, inclusive por uma trigger que rodou depois — e aí você grava o id errado.
SEQUENCE é um contador independente de tabela, útil quando duas tabelas precisam compartilhar a mesma numeração:
CREATE SEQUENCE dbo.NumeroDocumento AS INT START WITH 1000 INCREMENT BY 1;
SELECT NEXT VALUE FOR dbo.NumeroDocumento;Nos dois casos, buracos na numeração são normais: uma transação cancelada consome o número e não devolve.
WHERE e HAVING?WHERE filtra linhas, antes de agrupar. HAVING filtra grupos, depois de agrupar. É essa a única diferença, e ela decide qual usar.
SELECT ClienteID, SUM(ValorTotal) AS Total
FROM dbo.Pedidos
WHERE DataPedido >= '2026-01-01' -- descarta pedidos antigos ANTES de somar
GROUP BY ClienteID
HAVING SUM(ValorTotal) > 10000; -- descarta clientes pequenos DEPOIS de somarNão dá para trocar um pelo outro: WHERE SUM(...) > 10000 dá erro, porque no momento do WHERE a soma ainda não existe.
JOIN e quais são os tipos?JOIN junta linhas de duas tabelas usando uma condição. Os quatro que importam:
-- INNER: só quem existe nos dois lados
SELECT c.Nome, p.PedidoID
FROM dbo.Clientes c INNER JOIN dbo.Pedidos p ON p.ClienteID = c.ClienteID;
-- LEFT: todos os clientes; sem pedido, as colunas de pedido vêm NULL
SELECT c.Nome, p.PedidoID
FROM dbo.Clientes c LEFT JOIN dbo.Pedidos p ON p.ClienteID = c.ClienteID;
-- FULL: tudo dos dois lados
SELECT c.Nome, p.PedidoID
FROM dbo.Clientes c FULL OUTER JOIN dbo.Pedidos p ON p.ClienteID = c.ClienteID;
-- CROSS: cada linha de A com cada linha de B (produto cartesiano)
SELECT v.Nome, m.Mes FROM dbo.Vendedores v CROSS JOIN dbo.Meses m;RIGHT JOIN existe, mas na prática ninguém usa: é o LEFT com as tabelas trocadas, e fica mais difícil de ler.
Use LEFT JOIN quando a pergunta for "todos os X, com os Y que tiverem" — por exemplo, "todos os clientes e quanto cada um comprou, inclusive quem não comprou nada".
LEFT JOIN às vezes se comporta como INNER JOIN?Porque a condição da tabela da direita foi parar no WHERE. Como o WHERE roda depois do join, ele descarta as linhas em que aquela coluna veio nula — que são exatamente as que o LEFT JOIN tinha preservado.
-- Vira INNER sem avisar: cliente sem pedido tem Status NULL e é descartado
SELECT c.Nome, p.PedidoID
FROM dbo.Clientes c
LEFT JOIN dbo.Pedidos p ON p.ClienteID = c.ClienteID
WHERE p.Status = 'FATURADO';
-- Certo: a condição da direita vai no ON
SELECT c.Nome, p.PedidoID
FROM dbo.Clientes c
LEFT JOIN dbo.Pedidos p
ON p.ClienteID = c.ClienteID
AND p.Status = 'FATURADO';Regra prática: no LEFT JOIN, condição sobre a tabela da esquerda vai no WHERE; sobre a da direita, vai no ON.
UNION e UNION ALL?Os dois empilham o resultado de duas consultas. UNION remove duplicatas; UNION ALL não remove nada.
SELECT Email FROM dbo.Clientes
UNION ALL -- mantém repetidos, e é mais rápido
SELECT Email FROM dbo.Fornecedores;Remover duplicata custa uma ordenação do resultado inteiro. Se você sabe que não há repetição — ou se repetição não é problema — use sempre UNION ALL. A diferença em tabelas grandes é enorme.
EXISTS em vez de IN?IN compara com uma lista de valores. EXISTS só verifica se a subconsulta devolve alguma linha, e para na primeira que encontra.
-- IN: constrói a lista inteira
SELECT Nome FROM dbo.Clientes
WHERE ClienteID IN (SELECT ClienteID FROM dbo.Pedidos WHERE ValorTotal > 1000);
-- EXISTS: para no primeiro achado
SELECT Nome FROM dbo.Clientes c
WHERE EXISTS (SELECT 1 FROM dbo.Pedidos p
WHERE p.ClienteID = c.ClienteID AND p.ValorTotal > 1000);Em desempenho, o otimizador do SQL Server costuma tratar os dois de forma parecida. A diferença séria está na negação: NOT IN devolve resultado vazio se a subconsulta contiver um único NULL, porque "X não está em uma lista que contém o desconhecido" é indeterminado. NOT EXISTS não tem esse problema. Prefira NOT EXISTS sempre.
CTE (Common Table Expression) é um resultado temporário com nome, declarado com WITH antes da consulta. Serve para quebrar uma consulta grande em passos legíveis, lidos de cima para baixo.
WITH TotalPorCliente AS (
SELECT ClienteID, SUM(ValorTotal) AS Total
FROM dbo.Pedidos
GROUP BY ClienteID
)
SELECT c.Nome, t.Total
FROM TotalPorCliente t
JOIN dbo.Clientes c ON c.ClienteID = t.ClienteID
WHERE t.Total > 5000;Ela existe só durante aquela consulta. E não guarda dados: o otimizador expande o texto dela onde é usada. Se você referencia a mesma CTE três vezes, ela roda três vezes. Quando o resultado intermediário é caro e reutilizado, uma tabela temporária #tabela costuma sair mais barata.
É uma CTE que chama a si mesma, usada para percorrer hierarquias: organograma, categorias com subcategorias, estrutura de produto.
WITH Hierarquia AS (
SELECT FuncionarioID, Nome, GerenteID, 0 AS Nivel
FROM dbo.Funcionarios
WHERE GerenteID IS NULL -- âncora: onde começa
UNION ALL
SELECT f.FuncionarioID, f.Nome, f.GerenteID, h.Nivel + 1
FROM dbo.Funcionarios f
JOIN Hierarquia h ON h.FuncionarioID = f.GerenteID -- o passo recursivo
)
SELECT REPLICATE(' ', Nivel) + Nome AS Estrutura FROM Hierarquia
OPTION (MAXRECURSION 100);MAXRECURSION é a trava de segurança. Sem ela, um ciclo nos dados (A chefia B, B chefia A) roda para sempre.
É um cálculo feito sobre um conjunto de linhas sem juntar as linhas. Com GROUP BY, dez pedidos viram uma linha de total. Com função de janela, os dez pedidos continuam ali, cada um carregando o total ao lado.
A cláusula OVER define a janela: PARTITION BY diz como agrupar, ORDER BY diz em que ordem.
SELECT PedidoID,
ClienteID,
ValorTotal,
SUM(ValorTotal) OVER (PARTITION BY ClienteID) AS TotalDoCliente,
AVG(ValorTotal) OVER (PARTITION BY ClienteID) AS MediaDoCliente,
ValorTotal - LAG(ValorTotal) OVER (PARTITION BY ClienteID
ORDER BY DataPedido) AS DiferencaAnterior
FROM dbo.Pedidos;LAG pega o valor da linha anterior e LEAD, o da próxima — os dois resolvem "quanto variou em relação ao pedido passado" sem precisar de join da tabela com ela mesma.
ROW_NUMBER, RANK e DENSE_RANK?As três numeram linhas; a diferença está no empate:
| Valor | ROW_NUMBER | RANK | DENSE_RANK |
|---|---|---|---|
| 100 | 1 | 1 | 1 |
| 100 | 2 | 1 | 1 |
| 90 | 3 | 3 | 2 |
| 80 | 4 | 4 | 3 |
SELECT Nome, Total,
ROW_NUMBER() OVER (ORDER BY Total DESC) AS Linha,
RANK() OVER (ORDER BY Total DESC) AS Posicao,
DENSE_RANK() OVER (ORDER BY Total DESC) AS PosicaoDensa
FROM dbo.ResumoVendedores;Use ROW_NUMBER quando precisar de um número único (para deduplicar, por exemplo) e DENSE_RANK quando quiser "os três maiores valores", contando empates como uma posição só.
Duas formas boas. A primeira, com ROW_NUMBER:
WITH Numerados AS (
SELECT p.*,
ROW_NUMBER() OVER (PARTITION BY ClienteID ORDER BY DataPedido DESC) AS rn
FROM dbo.Pedidos p
)
SELECT * FROM Numerados WHERE rn = 1;A segunda, com OUTER APPLY — que costuma ser mais rápida quando há índice, porque busca uma linha por cliente em vez de numerar tudo:
SELECT c.Nome, ult.PedidoID, ult.DataPedido
FROM dbo.Clientes c
OUTER APPLY (
SELECT TOP (1) p.PedidoID, p.DataPedido
FROM dbo.Pedidos p
WHERE p.ClienteID = c.ClienteID
ORDER BY p.DataPedido DESC
) ult;CROSS APPLY?É um JOIN em que o lado direito enxerga cada linha do lado esquerdo. Um JOIN normal não consegue fazer isso — a subconsulta da direita é independente.
CROSS APPLY descarta a linha da esquerda quando o lado direito não devolve nada (como INNER JOIN); OUTER APPLY mantém, preenchendo com nulo (como LEFT JOIN).
Além do "último de cada grupo", ele serve para chamar uma função com o valor da linha:
SELECT c.Nome, itens.ProdutoID, itens.Quantidade
FROM dbo.Clientes c
CROSS APPLY dbo.tvf_ItensDoCliente(c.ClienteID) AS itens;Com OFFSET e FETCH NEXT, que exigem um ORDER BY:
DECLARE @Pagina INT = 3, @Tamanho INT = 20;
SELECT PedidoID, DataPedido, ValorTotal
FROM dbo.Pedidos
ORDER BY DataPedido DESC
OFFSET (@Pagina - 1) * @Tamanho ROWS
FETCH NEXT @Tamanho ROWS ONLY;Um aviso: OFFSET grande é lento, porque o banco precisa varrer e descartar todas as linhas puladas. A página 5.000 custa muito mais que a página 2. Em listas realmente grandes, o padrão melhor é a paginação por chave — em vez de "pule 100.000", peça "os 20 seguintes ao último que eu vi":
SELECT TOP (20) PedidoID, DataPedido
FROM dbo.Pedidos
WHERE DataPedido < @UltimaDataVista
ORDER BY DataPedido DESC;DELETE, TRUNCATE e DROP?| Comando | O que faz | Aceita WHERE | Volta o IDENTITY | Velocidade |
|---|---|---|---|---|
DELETE | Apaga linhas | Sim | Não | Lento (registra linha a linha) |
TRUNCATE | Esvazia a tabela | Não | Sim, para o início | Muito rápido |
DROP | Apaga a tabela inteira | Não | — | Rápido |
DELETE FROM dbo.Pedidos WHERE DataPedido < '2020-01-01'; -- apaga parte
TRUNCATE TABLE stg.Pedidos; -- esvazia staging
DROP TABLE stg.PedidosAntigo; -- some com a tabelaTRUNCATE é o certo para limpar tabela de staging antes de cada carga. Mas ele não funciona se a tabela for referenciada por uma foreign key, e não dispara triggers.
E os dois primeiros podem ser desfeitos se estiverem dentro de uma transação não confirmada — inclusive o TRUNCATE, ao contrário do que muita gente acredita.
Uma transação é um conjunto de comandos que vale tudo ou nada. Se um falhar, todos voltam atrás.
BEGIN TRANSACTION;
UPDATE dbo.Conta SET Saldo = Saldo - 100 WHERE ContaID = 1;
UPDATE dbo.Conta SET Saldo = Saldo + 100 WHERE ContaID = 2;
COMMIT; -- ou ROLLBACK para desfazerSem transação, uma falha entre os dois comandos faria o dinheiro sumir. ACID são as quatro garantias que o banco dá:
COMMIT, o dado sobrevive até a queda de energia.É uma consulta salva com nome, que você usa como se fosse tabela.
CREATE OR ALTER VIEW dbo.vw_PedidosFaturados AS
SELECT p.PedidoID, p.ClienteID, c.Nome AS ClienteNome, p.ValorTotal
FROM dbo.Pedidos p
JOIN dbo.Clientes c ON c.ClienteID = p.ClienteID
WHERE p.Status = 'FATURADO';
GO
SELECT * FROM dbo.vw_PedidosFaturados WHERE ValorTotal > 1000;Serve para três coisas: esconder complexidade (o join fica escrito uma vez), dar segurança (a pessoa acessa a view sem acessar a tabela, e você omite as colunas sensíveis) e criar um contrato estável (se a tabela mudar, você ajusta a view e quem consome não percebe).
Não. A view guarda só o texto da consulta. Toda vez que você a consulta, a consulta por trás roda de novo, no dado atual.
A exceção é a indexed view, que veremos a seguir.
INSERT ou UPDATE através de uma View?Sim, desde que a view seja simples: uma tabela só, sem agregação, sem DISTINCT, sem GROUP BY, e as colunas obrigatórias da tabela estejam nela.
CREATE OR ALTER VIEW dbo.vw_ClientesSP AS
SELECT ClienteID, Nome, UF FROM dbo.Clientes WHERE UF = 'SP';
GO
UPDATE dbo.vw_ClientesSP SET Nome = 'Maria Silva' WHERE ClienteID = 10;Um detalhe traiçoeiro: sem WITH CHECK OPTION, você consegue atualizar uma linha para que ela saia da view — mudar a UF para 'RJ' faz o registro desaparecer, e o comando é aceito. Com a opção, o banco recusa:
CREATE OR ALTER VIEW dbo.vw_ClientesSP AS
SELECT ClienteID, Nome, UF FROM dbo.Clientes WHERE UF = 'SP'
WITH CHECK OPTION;É a exceção da regra: uma view com índice clusterizado único, cujo resultado é gravado em disco e mantido atualizado automaticamente pelo SQL Server. Serve para agregação pesada que é consultada o tempo todo.
CREATE OR ALTER VIEW dbo.vw_VendasPorDia
WITH SCHEMABINDING AS -- obrigatório: amarra a view às tabelas
SELECT CAST(DataPedido AS date) AS Dia,
COUNT_BIG(*) AS Pedidos, -- COUNT_BIG, não COUNT
SUM(ValorTotal) AS Total
FROM dbo.Pedidos -- schema obrigatório
WHERE Status = 'FATURADO'
GROUP BY CAST(DataPedido AS date);
GO
CREATE UNIQUE CLUSTERED INDEX IX_vw_VendasPorDia ON dbo.vw_VendasPorDia (Dia);O preço é que todo INSERT/UPDATE na tabela base fica um pouco mais lento, porque precisa atualizar a view também. Vale quando a leitura é muito mais frequente que a escrita.
Tecnicamente muitas; na prática, duas. View sobre view sobre view parece organização, mas o otimizador precisa expandir tudo e acaba lendo tabelas que ninguém pediu naquela consulta. Quando você não consegue mais dizer de cabeça quais tabelas uma view lê, o desempenho já foi.
É um bloco de T-SQL guardado no banco, com nome e parâmetros, que você executa quando quer.
CREATE OR ALTER PROCEDURE dbo.usp_PedidosPorCliente
@ClienteID INT
AS
BEGIN
SET NOCOUNT ON;
SELECT PedidoID, DataPedido, ValorTotal
FROM dbo.Pedidos
WHERE ClienteID = @ClienteID
ORDER BY DataPedido DESC;
END
GO
EXEC dbo.usp_PedidosPorCliente @ClienteID = 42;Vale usar por quatro motivos: o plano de execução fica em cache e é reaproveitado; a regra vive num lugar só, em vez de repetida em cada tela; você dá permissão de executar a procedure sem dar acesso às tabelas; e a rede trafega uma chamada curta em vez de uma consulta inteira.
Três caminhos, com propósitos diferentes:
CREATE OR ALTER PROCEDURE dbo.usp_ContaPedidos
@ClienteID INT,
@Total INT OUTPUT -- 1) parâmetro de saída
AS
BEGIN
SET NOCOUNT ON;
SELECT @Total = COUNT(*) FROM dbo.Pedidos WHERE ClienteID = @ClienteID;
RETURN 0; -- 2) código de retorno: só INT, use para status
END
GO
DECLARE @Qtd INT;
EXEC dbo.usp_ContaPedidos @ClienteID = 42, @Total = @Qtd OUTPUT;
SELECT @Qtd;O terceiro caminho é simplesmente fazer um SELECT dentro da procedure — o resultado volta como conjunto de linhas. É o mais comum quando a procedure alimenta um relatório.
Use RETURN só para status (0 = ok, 1 = erro); ele aceita apenas inteiro.
SET NOCOUNT ON?Por padrão, cada comando devolve uma mensagem "(N rows affected)". Isso gasta rede e, pior, alguns clientes — inclusive o componente do SSIS — interpretam essa mensagem como um resultado a mais, o que quebra a leitura.
SET NOCOUNT ON desliga essas mensagens. Coloque como primeira linha de toda procedure. Não muda nada no resultado, só remove o ruído.
Com TRY...CATCH, e sempre junto de uma transação quando há escrita:
CREATE OR ALTER PROCEDURE dbo.usp_FaturarPedido
@PedidoID INT
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON; -- garante rollback em erro que não abortaria sozinho
BEGIN TRY
BEGIN TRANSACTION;
IF NOT EXISTS (SELECT 1 FROM dbo.Pedidos WHERE PedidoID = @PedidoID)
THROW 50001, 'Pedido não encontrado.', 1;
UPDATE dbo.Pedidos SET Status = 'FATURADO' WHERE PedidoID = @PedidoID;
COMMIT;
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 ROLLBACK;
INSERT INTO dbo.LogErro (Rotina, Mensagem, Numero, Linha)
VALUES ('usp_FaturarPedido', ERROR_MESSAGE(), ERROR_NUMBER(), ERROR_LINE());
THROW; -- repropaga o erro original para quem chamou
END CATCH
END
GOTHROW sem argumento repropaga o erro preservando número e mensagem. XACT_STATE() distingue transação ainda utilizável de transação já condenada — checar só @@TRANCOUNT não cobre esse caso.
É o SQL Server compilar o plano da procedure com base no primeiro valor de parâmetro que recebeu, e reutilizar esse plano para todos os valores seguintes.
Funciona bem quando os valores são parecidos. Quebra quando não são: se a primeira chamada foi para um cliente com 3 pedidos, o plano escolhido lê linha a linha; quando chega um cliente com 2 milhões de pedidos, esse mesmo plano é usado e a consulta trava.
É a explicação mais comum para "a procedure ficou lenta e ninguém mexeu em nada". A saída mais direta:
SELECT PedidoID, ValorTotal
FROM dbo.Pedidos
WHERE ClienteID = @ClienteID
OPTION (RECOMPILE); -- recompila só esta consulta, a cada execuçãoAlternativas: OPTION (OPTIMIZE FOR UNKNOWN), que usa a média das estatísticas, ou copiar o parâmetro para uma variável local.
| Procedure | Function | |
|---|---|---|
| Altera dados | Sim | Não |
Usada dentro de um SELECT | Não | Sim |
| Retorna | Conjuntos, OUTPUT, código | Um valor ou uma tabela |
| Transação | Pode controlar | Não pode |
TRY...CATCH | Pode | Não pode |
Regra simples: se faz alguma coisa, é procedure. Se calcula alguma coisa para ser usada numa consulta, é function.
CREATE TABLE #Temporaria (ID INT, Nome VARCHAR(100)); -- tabela temporária
DECLARE @Variavel TABLE (ID INT, Nome VARCHAR(100)); -- variável de tabela#tabela | @variavel | |
|---|---|---|
| Estatísticas | Tem | Não tem |
| Índice depois de criar | Pode | Não pode |
| Vive até | Fim da sessão | Fim do lote |
| Participa de transação | Sim | Não (não sofre rollback) |
O ponto que decide: estatísticas. Sem elas, o otimizador estima um número fixo de linhas para a variável de tabela e escolhe planos ruins quando o volume é grande. Para poucas linhas, @variavel é mais leve. Para muitas, use #tabela.
Cursor é o comando que percorre um resultado linha a linha, como um laço de programação.
DECLARE @ID INT;
DECLARE c CURSOR FOR SELECT PedidoID FROM dbo.Pedidos WHERE Status = 'ABERTO';
OPEN c;
FETCH NEXT FROM c INTO @ID;
WHILE @@FETCH_STATUS = 0
BEGIN
EXEC dbo.usp_FaturarPedido @PedidoID = @ID;
FETCH NEXT FROM c INTO @ID;
END
CLOSE c; DEALLOCATE c;Bancos relacionais são feitos para trabalhar com conjuntos, não com laços. O mesmo trabalho em um único UPDATE costuma ser dezenas de vezes mais rápido:
UPDATE dbo.Pedidos SET Status = 'FATURADO' WHERE Status = 'ABERTO';Cursor se justifica quando cada linha precisa mesmo de um tratamento diferente e indivisível — chamar um serviço externo, por exemplo. Fora isso, quase sempre existe uma versão em conjunto.
SQL injection é quando o texto digitado pelo usuário vira comando, em vez de ficar como dado. Acontece sempre que a consulta é montada com concatenação:
-- VULNERÁVEL: se @Nome for ' OR 1=1 -- a consulta devolve todos os clientes
DECLARE @Sql NVARCHAR(500) = 'SELECT * FROM dbo.Clientes WHERE Nome = ''' + @Nome + '''';
EXEC (@Sql);Com parâmetro, o valor viaja separado do texto do comando e nunca é interpretado como código:
-- SEGURO
SELECT * FROM dbo.Clientes WHERE Nome = @Nome;É por isso que procedure com parâmetro é segura por construção — desde que ela não concatene o parâmetro dentro de SQL dinâmico.
Use sp_executesql, que aceita parâmetros de verdade:
DECLARE @Sql NVARCHAR(MAX) = N'SELECT PedidoID, ValorTotal FROM dbo.Pedidos WHERE 1 = 1';
IF @ClienteID IS NOT NULL
SET @Sql += N' AND ClienteID = @ClienteID';
IF @DataInicio IS NOT NULL
SET @Sql += N' AND DataPedido >= @DataInicio';
EXEC sp_executesql @Sql,
N'@ClienteID INT, @DataInicio DATE', -- declaração dos parâmetros
@ClienteID = @ClienteID, -- valores
@DataInicio = @DataInicio;Repare que o valor nunca entra na string — só o nome do parâmetro. E se precisar montar nome de tabela ou coluna dinamicamente (que não pode ser parâmetro), passe por QUOTENAME(), que escapa colchetes e fecha a porta da injeção.
Três, e a escolha muda drasticamente o desempenho:
Escalar — devolve um valor único:
CREATE OR ALTER FUNCTION dbo.fn_Desconto (@Valor DECIMAL(18,2))
RETURNS DECIMAL(18,2)
AS
BEGIN
RETURN CASE WHEN @Valor > 1000 THEN @Valor * 0.10 ELSE 0 END;
ENDInline table-valued (iTVF) — devolve uma tabela, com um único RETURN:
CREATE OR ALTER FUNCTION dbo.tvf_PedidosDoCliente (@ClienteID INT)
RETURNS TABLE
AS
RETURN (SELECT PedidoID, DataPedido, ValorTotal
FROM dbo.Pedidos WHERE ClienteID = @ClienteID);Multi-statement table-valued (MSTVF) — devolve uma tabela montada em vários passos:
CREATE OR ALTER FUNCTION dbo.mstvf_Resumo (@Ano INT)
RETURNS @R TABLE (Mes INT, Total DECIMAL(18,2))
AS
BEGIN
INSERT INTO @R SELECT MONTH(DataPedido), SUM(ValorTotal)
FROM dbo.Pedidos WHERE YEAR(DataPedido) = @Ano GROUP BY MONTH(DataPedido);
RETURN;
ENDPorque ela é executada uma vez por linha. Numa consulta sobre um milhão de linhas, a função roda um milhão de vezes, e cada execução é uma troca de contexto dentro do motor.
Pior: até o SQL Server 2017, a presença de uma escalar impedia paralelismo na consulta inteira — mesmo nas partes que nada tinham a ver com ela.
O SQL Server 2019 introduziu o inlining automático, que reescreve algumas escalares como expressão e resolve o problema. Mas só algumas (nada de acesso a tabela, nada de WHILE), e nem sempre você controla a versão do servidor.
A saída é transformar a escalar em iTVF e chamar com APPLY:
CREATE OR ALTER FUNCTION dbo.tvf_Desconto (@Valor DECIMAL(18,2))
RETURNS TABLE
AS
RETURN (SELECT CASE WHEN @Valor > 1000 THEN @Valor * 0.10 ELSE 0 END AS Desconto);
GO
SELECT p.PedidoID, d.Desconto
FROM dbo.Pedidos p
CROSS APPLY dbo.tvf_Desconto(p.ValorTotal) d;A ausência de BEGIN/END. Uma iTVF tem um único RETURN (consulta) e nada mais. É essa forma que permite ao otimizador expandir a função dentro da consulta que a chama, como se fosse uma view com parâmetro — o custo vira praticamente zero.
A mesma lógica escrita com BEGIN ... RETURN ... END vira multi-statement, e o otimizador passa a tratá-la como caixa-preta: ele não enxerga o conteúdo e estima um número fixo de linhas (1 até o SQL Server 2012, 100 depois), o que produz planos ruins.
Regra prática: se cabe em uma consulta, faça iTVF. Se precisa de vários passos, faça uma procedure gravando em #tabela.
Não. Nenhum tipo de function pode fazer INSERT, UPDATE ou DELETE em tabela permanente, nem chamar procedure que altere dados, nem controlar transação.
Isso é proposital: como a function é usada dentro de um SELECT, o banco precisa poder executá-la quantas vezes quiser, em qualquer ordem, sem efeito colateral. Se você precisa alterar dados, use procedure.
É uma estrutura auxiliar que permite achar linhas sem ler a tabela inteira. Internamente é uma árvore B (B-tree): uma raiz que aponta para páginas intermediárias, que apontam para as folhas com os dados ordenados.
Para achar um valor entre um milhão de linhas, em vez de olhar um milhão de vezes, o banco desce uns 3 ou 4 níveis da árvore. É a mesma ideia do índice remissivo no fim de um livro: você não lê o livro inteiro procurando a palavra.
CREATE NONCLUSTERED INDEX IX_Pedidos_Cliente ON dbo.Pedidos (ClienteID);O preço: todo INSERT, UPDATE e DELETE precisa manter o índice atualizado. Índice demais deixa a escrita lenta; de menos, deixa a leitura lenta.
O clustered define a ordem física das linhas na tabela — os dados são as folhas do índice. Por isso só pode existir um por tabela. Quando você cria uma chave primária, o SQL Server cria um clustered nela por padrão.
O nonclustered é uma estrutura à parte, que guarda as colunas indexadas e um ponteiro para a linha. Podem existir vários.
A analogia que funciona: o clustered é a ordem dos capítulos do livro; o nonclustered é o índice remissivo no fim, que diz em que página está cada assunto.
INCLUDE num índice e o que é key lookup?Quando o índice acha a linha mas não tem todas as colunas pedidas, o banco precisa voltar à tabela para buscar o resto. Essa volta é o key lookup, e ela custa caro quando se repete milhares de vezes.
INCLUDE resolve: as colunas incluídas ficam guardadas nas folhas do índice, sem fazer parte da chave de busca.
CREATE NONCLUSTERED INDEX IX_Pedidos_Cliente_Data
ON dbo.Pedidos (ClienteID, DataPedido) -- chave: usada para filtrar e ordenar
INCLUDE (ValorTotal, Status); -- carona: só devolvidasQuando o índice tem tudo que a consulta precisa, dizemos que ele cobre a consulta. O key lookup desaparece do plano.
A ordem das colunas na chave importa: (ClienteID, DataPedido) serve para filtrar por cliente e por cliente + data. Não serve para filtrar só por data.
SARGable vem de Search ARGument able: quer dizer que o filtro pode aproveitar o índice. Ele deixa de ser SARGable quando você aplica função ou cálculo na coluna.
-- Não SARGable: precisa calcular YEAR() em cada linha
WHERE YEAR(DataPedido) = 2026
-- SARGable: o índice em DataPedido é usado
WHERE DataPedido >= '2026-01-01' AND DataPedido < '2027-01-01'Outros casos comuns: WHERE UPPER(Nome) = 'MARIA' (colação já resolve isso), WHERE Codigo + '' = '123' e WHERE ISNULL(Valor, 0) > 100. Em todos, o conserto é o mesmo — deixe a coluna sozinha de um lado da comparação.
Note também o intervalo com < no fim, em vez de BETWEEN '2026-01-01' AND '2026-12-31'. Com data e hora, BETWEEN perde tudo que aconteceu depois da meia-noite do último dia.
É o passo a passo que o SQL Server decidiu seguir para responder a consulta. No SSMS, Ctrl+M liga o plano real e Ctrl+L, o estimado.
O que procurar, em ordem de importância:
INCLUDE.Para medir sem depender do relógio da máquina:
SET STATISTICS IO, TIME ON;
-- sua consulta
SET STATISTICS IO, TIME OFF;STATISTICS IO mostra as leituras lógicas por tabela. É o número mais honesto: tempo varia com a carga do servidor, leitura não. Caiu de 400.000 para 300? Melhorou de verdade.
São os histogramas que o SQL Server mantém sobre a distribuição dos valores de cada coluna indexada. Ele usa isso para estimar quantas linhas um filtro vai devolver e, a partir da estimativa, escolher o plano.
Estatística desatualizada gera estimativa errada, que gera plano errado. É o que acontece depois de uma carga grande: o banco acha que a tabela tem mil linhas e ela tem dez milhões.
UPDATE STATISTICS dbo.Pedidos WITH FULLSCAN; -- uma tabela
EXEC sp_updatestats; -- o banco inteiroRodar UPDATE STATISTICS no fim de uma carga grande é uma das ações de maior efeito por menor esforço.
Com o tempo, inserções e exclusões deixam as páginas do índice fora de ordem e com espaço vazio. O banco passa a ler mais páginas para o mesmo dado.
-- Ver o quanto está fragmentado
SELECT OBJECT_NAME(ips.object_id) AS Tabela,
i.name AS Indice,
ips.avg_fragmentation_in_percent AS Fragmentacao
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'LIMITED') ips
JOIN sys.indexes i ON i.object_id = ips.object_id AND i.index_id = ips.index_id
WHERE ips.avg_fragmentation_in_percent > 10
ORDER BY ips.avg_fragmentation_in_percent DESC;A regra usual: até 5%, ignore; entre 5% e 30%, REORGANIZE; acima de 30%, REBUILD.
ALTER INDEX IX_Pedidos_Cliente ON dbo.Pedidos REORGANIZE;
ALTER INDEX IX_Pedidos_Cliente ON dbo.Pedidos REBUILD;REORGANIZE é leve e online. REBUILD é mais completo, e bloqueia a tabela na edição Standard — deixe para janela de manutenção.
Pelas DMVs (Dynamic Management Views), que expõem o que o motor está fazendo:
SELECT TOP (20)
qs.total_worker_time / qs.execution_count / 1000 AS CpuMedioMs,
qs.execution_count AS Execucoes,
qs.total_logical_reads / qs.execution_count AS LeiturasMedias,
SUBSTRING(st.text, (qs.statement_start_offset/2) + 1,
((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(st.text)
ELSE qs.statement_end_offset END - qs.statement_start_offset)/2) + 1) AS Consulta
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
ORDER BY qs.total_worker_time DESC;Ordene por total_worker_time para achar quem consome CPU e por total_logical_reads para achar quem lê demais. Uma consulta rápida que roda 100 mil vezes por hora costuma pesar mais que uma lenta que roda uma vez.
No SQL Server 2016 ou mais novo, ative o Query Store no banco: ele guarda o histórico de planos e permite forçar o plano bom quando um regride.
Agrupe pela coluna que deveria ser única e conte:
SELECT CPF, COUNT(*) AS Qtd
FROM dbo.Clientes
WHERE CPF IS NOT NULL
GROUP BY CPF
HAVING COUNT(*) > 1
ORDER BY Qtd DESC;Para apagar mantendo a mais recente, ROW_NUMBER é o caminho seguro — o DELETE sobre a CTE apaga na tabela base:
WITH Duplicadas AS (
SELECT ClienteID,
ROW_NUMBER() OVER (PARTITION BY CPF ORDER BY CriadoEm DESC) AS rn
FROM dbo.Clientes
WHERE CPF IS NOT NULL
)
DELETE FROM Duplicadas WHERE rn > 1;Antes de rodar em produção: troque o DELETE por SELECT *, confira a contagem e faça backup.
Órfão é o filho sem pai — o pedido cujo cliente não existe mais:
SELECT p.PedidoID, p.ClienteID
FROM dbo.Pedidos p
WHERE NOT EXISTS (SELECT 1 FROM dbo.Clientes c WHERE c.ClienteID = p.ClienteID);Se a foreign key existisse e estivesse ativa, isso não aconteceria. Órfão é sinal de FK faltando, ou de FK que foi desabilitada durante uma carga e nunca revalidada.
Compare os dois lados no mesmo nível de detalhe e olhe a diferença, em vez de olhar só os totais:
-- Cabeçalho contra a soma dos itens
SELECT p.PedidoID,
p.ValorTotal AS TotalCabecalho,
SUM(i.Quantidade * i.ValorUnit) AS TotalItens,
p.ValorTotal - SUM(i.Quantidade * i.ValorUnit) AS Diferenca
FROM dbo.Pedidos p
JOIN dbo.PedidoItens i ON i.PedidoID = p.PedidoID
GROUP BY p.PedidoID, p.ValorTotal
HAVING ABS(p.ValorTotal - SUM(i.Quantidade * i.ValorUnit)) > 0.01;As causas mais frequentes, em ordem:
INNER JOIN onde devia ser LEFT — registros somem sem aviso.BETWEEN — perde o último dia.NULL numa soma — SUM ignora nulos, + propaga.O > 0.01 em vez de <> 0 não é preguiça: com arredondamento em moeda, diferenças de centavo aparecem por representação numérica.
JOIN. Como achar?'ABC ' e 'ABC' parecem iguais na tela. LEN() ignora espaços à direita, DATALENGTH() não — quando os dois discordam, você achou:
SELECT ClienteID,
'[' + Codigo + ']' AS ComDelimitador,
LEN(Codigo) AS Tamanho,
DATALENGTH(Codigo) AS Bytes
FROM dbo.Clientes
WHERE Codigo <> LTRIM(RTRIM(Codigo));No SQL Server 2017 ou mais novo, TRIM() limpa os dois lados de uma vez. Para maiúsculas, a collation CI já resolve — se a sua for CS, normalize os dois lados com UPPER().
DBCC CHECKDB e o que é uma FK "não confiável"?DBCC CHECKDB verifica a integridade física do banco: páginas corrompidas, ponteiros quebrados, índices inconsistentes com os dados. É pesado — rode em janela de manutenção.
DBCC CHECKDB ('VendasDB') WITH NO_INFOMSGS, ALL_ERRORMSGS;Já a FK não confiável é um problema lógico. Ela acontece depois de um BULK INSERT ou de um ALTER TABLE ... NOCHECK CONSTRAINT: a constraint continua ali, mas o SQL Server sabe que dados entraram sem passar por ela.
SELECT name, is_not_trusted FROM sys.foreign_keys WHERE is_not_trusted = 1;Duas consequências: ela deixa de garantir o que promete, e o otimizador para de usá-la para simplificar planos. Para revalidar:
ALTER TABLE dbo.Pedidos WITH CHECK CHECK CONSTRAINT FK_Pedidos_Clientes;O WITH CHECK CHECK duplicado não é erro de digitação: o primeiro manda validar os dados existentes, o segundo religa a constraint.
Três tipos, que se combinam:
-- Completo: tudo, é a base de qualquer restauração
BACKUP DATABASE VendasDB TO DISK = 'D:\bkp\VendasDB_full.bak' WITH INIT, COMPRESSION;
-- Diferencial: só o que mudou desde o último completo
BACKUP DATABASE VendasDB TO DISK = 'D:\bkp\VendasDB_diff.bak' WITH DIFFERENTIAL;
-- Log: as transações desde o último backup de log (exige recovery model FULL)
BACKUP LOG VendasDB TO DISK = 'D:\bkp\VendasDB_log.trn';Para restaurar até um ponto no tempo, aplique na ordem: completo → diferencial mais recente → todos os logs seguintes. Use NORECOVERY em todos menos no último.
RESTORE DATABASE VendasDB FROM DISK = 'D:\bkp\VendasDB_full.bak' WITH NORECOVERY;
RESTORE DATABASE VendasDB FROM DISK = 'D:\bkp\VendasDB_diff.bak' WITH NORECOVERY;
RESTORE LOG VendasDB FROM DISK = 'D:\bkp\VendasDB_log.trn'
WITH STOPAT = '2026-03-19T14:30:00', RECOVERY;Uma verdade desconfortável: backup que nunca foi restaurado não é backup. Teste a restauração periodicamente, num servidor separado.
ETL significa Extract, Transform, Load — extrair de uma origem, transformar no formato que você precisa e carregar num destino. É o processo que leva o dado do sistema de origem até onde ele vai ser analisado.
SSIS (SQL Server Integration Services) é a ferramenta de ETL da Microsoft. Você desenha o fluxo no Visual Studio (com a extensão Integration Services Projects), gera um pacote e ele roda no servidor, normalmente agendado pelo SQL Server Agent.
Um pacote típico faz: lê um arquivo ou uma tabela de outro sistema, limpa e converte os dados, e grava numa tabela do seu banco — registrando o que deu certo e o que não deu.
São as duas superfícies de um pacote, e confundi-las é o erro mais comum:
A regra que economiza mais tempo: o que dá para fazer em SQL, faça em SQL. Uma transformação Sort do SSIS carrega tudo na memória do servidor de integração; um ORDER BY na origem usa o índice do banco. Deixe o Data Flow para mover dados e para o que o banco não faz bem — ler arquivo, chamar API, dividir para vários destinos.
Staging é uma área intermediária: tabelas onde o dado bruto é despejado sem transformação nenhuma, antes de virar o dado final.
Vale por três motivos: se algo der errado, você tem o material original para investigar; transformar com T-SQL sobre a staging é mais rápido e mais fácil de testar que fazer no componente gráfico; e a origem fica livre logo, em vez de ficar presa durante todo o processamento.
O padrão que resolve quase toda carga:
1. TRUNCATE da staging
2. Extrair da origem para a staging (sem transformar)
3. Transformar com T-SQL dentro da staging
4. Carregar da staging para o destino, dentro de transação
5. Validar e registrar o resultadoCarga full apaga tudo e recarrega. Simples e sempre correta, mas inviável quando a tabela tem milhões de linhas.
Carga incremental traz só o que mudou desde a última execução. Para saber o que mudou, você guarda uma marca d'água (watermark): a data/hora até onde já carregou.
CREATE TABLE dbo.ControleCarga (
Tabela SYSNAME NOT NULL PRIMARY KEY,
UltimaCarga DATETIME2(3) NOT NULL
);
-- Na origem, o pacote lê só o que mudou
DECLARE @Desde DATETIME2(3) =
(SELECT UltimaCarga FROM dbo.ControleCarga WHERE Tabela = 'Pedidos');
SELECT PedidoID, ClienteID, ValorTotal, AlteradoEm
FROM dbo.Pedidos
WHERE AlteradoEm > @Desde;Um detalhe que evita perder registro: grave como nova marca d'água o horário de início da carga, não o de fim. O que for alterado na origem enquanto o pacote roda ficaria num vão cego se você usasse o fim.
Todo componente de Data Flow tem uma saída de erro — a seta vermelha. Configure-a como Redirect Row e mande as linhas problemáticas para uma tabela de rejeitados:
CREATE TABLE stg.LinhasRejeitadas (
RejeitadaID INT IDENTITY PRIMARY KEY,
Pacote SYSNAME NOT NULL,
LinhaOriginal NVARCHAR(MAX) NOT NULL,
ErroCodigo INT,
OcorridoEm DATETIME2(3) DEFAULT SYSDATETIME()
);Uma nota com CNPJ inválido não pode impedir que as outras 50 mil entrem. Carregue o que é válido, separe o que não é, e mande a lista para quem cadastrou.
Depois da carga, valide o que entrou:
SELECT 'Pedidos sem cliente' AS Verificacao, COUNT(*) AS Qtd
FROM stg.Pedidos s
WHERE NOT EXISTS (SELECT 1 FROM dbo.Clientes c WHERE c.ClienteID = s.ClienteID)
UNION ALL
SELECT 'Valor negativo', COUNT(*) FROM stg.Pedidos WHERE ValorTotal < 0
UNION ALL
SELECT 'Data no futuro', COUNT(*) FROM stg.Pedidos WHERE DataPedido > SYSDATETIME();Carga que não valida é carga que mente. A validação é o que transforma "o pacote rodou" em "o dado está certo".
SSRS (SQL Server Reporting Services) é a ferramenta de relatórios da Microsoft. Você desenha no Report Builder ou no Visual Studio e publica num portal web, de onde as pessoas executam ou recebem por e-mail.
Relatório paginado é um relatório com layout fixo, pensado para caber em páginas — para imprimir ou virar PDF. Boleto, nota fiscal, extrato, relatório contábil de 300 páginas com o cabeçalho repetindo em cada uma. O nome vem daí: o conteúdo é organizado em páginas, não numa tela que rola.
O arquivo gerado é um RDL (Report Definition Language), um XML que descreve consulta, parâmetros e layout.
Resolvem problemas diferentes:
| SSRS | Power BI | |
|---|---|---|
| Formato | Página fixa, feita para imprimir | Tela interativa |
| Uso | Ler e arquivar | Explorar e filtrar |
| Entrega | PDF/Excel por e-mail agendado | Portal ou app |
| Exemplo | Nota fiscal, fechamento contábil | Painel de vendas |
Se o pedido é "preciso do relatório de fechamento em PDF todo dia 1º às 6h no e-mail da diretoria", é SSRS. Se é "quero entender por que a margem caiu no Sul", é Power BI. Confundir os dois gera meses de retrabalho.
CREATE OR ALTER PROCEDURE rpt.usp_VendasPorPeriodo
@DataInicio DATE,
@DataFim DATE,
@VendedorID INT = NULL -- NULL significa "todos"
AS
BEGIN
SET NOCOUNT ON;
SELECT v.Nome AS Vendedor, c.Nome AS Cliente, p.PedidoID, p.ValorTotal
FROM dbo.Pedidos p
JOIN dbo.Clientes c ON c.ClienteID = p.ClienteID
JOIN dbo.Vendedores v ON v.VendedorID = p.VendedorID
WHERE p.DataPedido >= @DataInicio
AND p.DataPedido < DATEADD(DAY, 1, @DataFim) -- inclui o dia inteiro
AND (@VendedorID IS NULL OR p.VendedorID = @VendedorID)
ORDER BY v.Nome, p.DataPedido;
ENDO DATEADD(DAY, 1, @DataFim) com < resolve o problema clássico do relatório que "esquece" os pedidos do último dia. E o (@VendedorID IS NULL OR ...) é o padrão de parâmetro opcional.
Parâmetro em cascata é quando um depende do outro — escolher o estado filtra as cidades. Você cria um dataset para cada lista, e o segundo recebe o valor do primeiro:
-- Dataset "Estados"
SELECT DISTINCT UF FROM dbo.Clientes ORDER BY UF;
-- Dataset "Cidades", que recebe @UF
SELECT DISTINCT Cidade FROM dbo.Clientes WHERE UF = @UF ORDER BY Cidade;Para parâmetro de múltipla escolha, o SSRS entrega uma lista separada por vírgula; do lado do T-SQL, use STRING_SPLIT:
WHERE c.UF IN (SELECT value FROM STRING_SPLIT(@Ufs, ','));Subscription é o agendamento: o relatório roda sozinho no horário marcado e entrega o resultado por e-mail ou numa pasta de rede. É o que atende "todo dia 1º às 6h no e-mail da diretoria" sem ninguém clicar em nada.
A data-driven subscription (edição Enterprise) vai além: uma consulta define quem recebe o quê. Uma execução gera 40 relatórios, um por gerente regional, cada um filtrado pela sua região.
Sobre os componentes de layout:
As expressões do SSRS usam sintaxe Visual Basic e começam com =:
=Sum(Fields!ValorTotal.Value)
=Format(Fields!ValorTotal.Value, "C2")
=IIf(RowNumber(Nothing) Mod 2 = 0, "#F5F5F5", "White")
="Página " & Globals!PageNumber & " de " & Globals!TotalPagesCuidado com IIf: diferente do if de uma linguagem normal, ele avalia os dois lados antes de escolher. Numa divisão, isso significa que o erro de divisão por zero acontece mesmo quando a condição diz para não dividir — proteja o divisor, não só a condição.
BI (Business Intelligence) é o conjunto de práticas que transforma dado bruto em informação para decidir: coletar, organizar, analisar e apresentar.
Data Warehouse é o banco feito para isso. Ele existe separado do banco de produção por três razões:
Essa diferença tem nome: OLTP (Online Transaction Processing) é o sistema do dia a dia, com muitas escritas pequenas. OLAP (Online Analytical Processing) é o analítico, com poucas leituras enormes.
Na modelagem dimensional, o desenho padrão é o star schema: uma tabela de fatos no centro, cercada de dimensões.
CREATE TABLE dim.Cliente (
ClienteSK INT IDENTITY PRIMARY KEY, -- surrogate key: chave própria do DW
ClienteBK INT NOT NULL, -- business key: o id do sistema de origem
Nome VARCHAR(200) NOT NULL,
Segmento VARCHAR(50) NULL,
ValidoDe DATE NOT NULL,
ValidoAte DATE NULL, -- NULL = versão vigente
Corrente BIT NOT NULL DEFAULT 1
);
CREATE TABLE fato.Vendas (
VendaSK BIGINT IDENTITY PRIMARY KEY,
DataSK INT NOT NULL,
ClienteSK INT NOT NULL,
ProdutoSK INT NOT NULL,
Quantidade INT NOT NULL,
ValorBruto DECIMAL(18,2) NOT NULL
);A surrogate key existe para o DW não depender do id da origem, que pode mudar, repetir entre empresas ou sumir numa migração.
A granularidade da tabela fato é o que cada linha representa — uma venda? um item de venda? um dia por produto? Definir isso antes de criar a tabela é a decisão mais importante do modelo, porque mudar depois significa refazer tudo.
SCD é Slowly Changing Dimension: dimensão que muda devagar. O tipo diz o que fazer quando um atributo muda.
O tipo 2 é o que garante que o faturamento do ano passado continue somando no segmento em que o cliente estava naquele momento, e não no atual:
-- 1) Encerra a versão vigente do que mudou
UPDATE d
SET d.ValidoAte = CAST(SYSDATETIME() AS DATE), d.Corrente = 0
FROM dim.Cliente d
JOIN stg.Cliente s ON s.ClienteID = d.ClienteBK
WHERE d.Corrente = 1 AND d.Segmento <> s.Segmento;
-- 2) Abre a nova versão
INSERT INTO dim.Cliente (ClienteBK, Nome, Segmento, ValidoDe, Corrente)
SELECT s.ClienteID, s.Nome, s.Segmento, CAST(SYSDATETIME() AS DATE), 1
FROM stg.Cliente s
WHERE NOT EXISTS (SELECT 1 FROM dim.Cliente d
WHERE d.ClienteBK = s.ClienteID AND d.Corrente = 1);Power BI é a ferramenta de visualização e análise da Microsoft. O que sustenta um relatório bom nela não são os gráficos, e sim o modelo: tabelas relacionadas em estrela, com uma dimensão calendário marcada como tabela de datas.
DAX (Data Analysis Expressions) é a linguagem de cálculo do Power BI. Ela se parece com fórmula de Excel, mas trabalha sobre o modelo inteiro:
Faturamento = SUM(fVendas[ValorLiquido])
Faturamento Ano Anterior =
CALCULATE([Faturamento], SAMEPERIODLASTYEAR(dCalendario[Data]))
Crescimento % =
DIVIDE([Faturamento] - [Faturamento Ano Anterior], [Faturamento Ano Anterior])Use DIVIDE() em vez do operador /: ele trata divisão por zero devolvendo vazio, em vez de um erro que contamina o visual inteiro.
Sobre o modo de conexão: Import traz os dados para dentro do arquivo (rápido, com atualização agendada) e DirectQuery consulta o banco a cada interação (sempre atual, mas transfere a lentidão da consulta para o usuário). Import é o padrão certo até existir um motivo concreto para o contrário.
QuickSight é o equivalente na AWS. Dois termos bastam: SPICE é o motor em memória, o análogo do modo Import; dataset e analysis separam a preparação do dado da montagem visual. Quem entende modelagem dimensional e sabe escrever SQL troca de ferramenta em dias.
ERP (Enterprise Resource Planning) é o sistema que integra a operação da empresa — vendas, estoque, financeiro, fiscal. Os da Microsoft têm particularidades que geram erro em quem chega sem saber:
No Dynamics AX, a coluna DATAAREAID separa as empresas dentro da mesma base. Toda consulta precisa filtrar por ela, senão você soma o faturamento do grupo inteiro:
SELECT SUM(LINEAMOUNT) AS Total
FROM dbo.SALESLINE
WHERE DATAAREAID = 'BR01' -- sem isso, o número vem errado
AND CREATEDDATETIME >= '2026-01-01';Ainda no AX: RECID é a chave real das tabelas, e muitos campos são enums numéricos — SALESSTATUS = 3 significa "faturado", e o significado está nos metadados do sistema, não no banco.
No Navision / Business Central, o nome da tabela inclui a empresa e usa $, o que exige colchetes:
SELECT No_, Name FROM [CRONUS Brasil Ltda$Customer] WHERE Blocked = 0;O _ no fim de No_ é como o Navision escapa palavras reservadas, e datas "vazias" costumam vir como 1753-01-01 — o mínimo do datetime — em vez de NULL.
A recomendação vale para qualquer ERP: não consulte o banco dele direto no relatório. Traga para uma staging, normalize os enums e os nomes, e construa em cima disso. Além de proteger o desempenho do sistema que a empresa usa para trabalhar, isso evita que a próxima atualização do ERP quebre vinte relatórios de uma vez.