Treinamento SQL Server

Módulo 16 — PIVOT & UNPIVOT

Transformações de linhas em colunas (e vice-versa), relatórios de matriz e pivôs dinâmicos.

T-SQL Nível Intermediário/Avançado Hands-on Atualizado: 28 ago 2025

Objetivos

  • Entender a sintaxe e os casos de uso de PIVOT e UNPIVOT.
  • Construir pivôs estáticos e dinâmicos com dynamic SQL.
  • Comparar com alternativas (SUM(CASE...)) e avaliar desempenho.
  • Aplicar em relatórios por produto, mês, UF etc.

Pré-requisitos

  • Módulos 13–15 recomendados.
  • Agregações, GROUP BY e noções de dynamic SQL.
  • Acesso a um banco de testes.

1) Introdução & Quando usar

PIVOT transforma linhas em colunas, consolidando valores por um agregador (ex.: SUM/COUNT). UNPIVOT faz o inverso, útil para normalizar dados “wide” para “long”.

Agregador é obrigatório no PIVOT: SUM, AVG, COUNT, etc. Escolha conforme o contexto.

2) PIVOT (estático) — Sintaxe & Exemplo

-- Estrutura (resumo)
SELECT <colunas_linha>, [colunaPivot1], [colunaPivot2], ...
FROM (
  SELECT <linha>, <coluna_pivot>, <valor>
  FROM <fonte>
) AS src
PIVOT (
  <AGREGADOR>(<valor>)
  FOR <coluna_pivot> IN ([colunaPivot1], [colunaPivot2], ...)
) AS p;
-- Exemplo: total por Produto x Mês
-- Fonte "long" (Produto, Mes, Valor)
WITH Fonte AS (
  SELECT Produto, DATENAME(MONTH, DataVenda) AS Mes, Valor
  FROM dbo.VendasProdutos
)
SELECT Produto, [Janeiro], [Fevereiro], [Março], [Abril], [Maio], [Junho],
       [Julho], [Agosto], [Setembro], [Outubro], [Novembro], [Dezembro]
FROM Fonte
PIVOT (SUM(Valor) FOR Mes IN ([Janeiro],[Fevereiro],[Março],[Abril],[Maio],[Junho],
                              [Julho],[Agosto],[Setembro],[Outubro],[Novembro],[Dezembro])) AS p
ORDER BY Produto;
Dica: Use nomes de colunas entre colchetes. Se os valores de coluna tiverem espaços/caráteres especiais, use QUOTENAME() ao gerar dinamicamente.

3) PIVOT vs SUM(CASE...)

-- Equivalente com SUM(CASE WHEN ... THEN Valor ELSE 0 END)
SELECT
  Produto,
  SUM(CASE WHEN Mes = 'Janeiro'   THEN Valor ELSE 0 END) AS Janeiro,
  SUM(CASE WHEN Mes = 'Fevereiro' THEN Valor ELSE 0 END) AS Fevereiro,
  SUM(CASE WHEN Mes = 'Março'     THEN Valor ELSE 0 END) AS Março
FROM dbo.VendasProdutosPorMes
GROUP BY Produto;

4) UNPIVOT — Normalização

-- Exemplo: tabela "wide" com colunas por mês → normalizar para (Produto, Mes, Valor)
SELECT Produto, Mes, Valor
FROM (
  SELECT Produto, [Jan], [Fev], [Mar]
  FROM dbo.VendasProdutosWide
) AS w
UNPIVOT (
  Valor FOR Mes IN ([Jan], [Fev], [Mar])
) AS u;
Após o UNPIVOT: você pode traduzir rótulos (ex.: CASE Mes WHEN 'Jan' THEN 'Janeiro' ...) ou converter tipos conforme necessário.

5) PIVOT Dinâmico (STRING_AGG / QUOTENAME)

Quando a lista de colunas a pivotar é desconhecida (ex.: meses presentes nos dados), gere a consulta dinamicamente.

DECLARE @cols nvarchar(max);
DECLARE @sql  nvarchar(max);

-- 1) Extrair colunas dinamicamente (ex.: meses distintos)
SELECT @cols = STRING_AGG(QUOTENAME(Mes), ',')
FROM (SELECT DISTINCT DATENAME(MONTH, DataVenda) AS Mes FROM dbo.VendasProdutos) AS x;

-- 2) Montar a query PIVOT
SET @sql = N'
SELECT Produto, ' + @cols + N'
FROM (
  SELECT Produto, DATENAME(MONTH, DataVenda) AS Mes, Valor
  FROM dbo.VendasProdutos
) AS src
PIVOT (SUM(Valor) FOR Mes IN (' + @cols + N')) AS p
ORDER BY Produto;';

-- 3) Executar
EXEC sp_executesql @sql;
Segurança: Sempre QUOTENAME() nos nomes de coluna e nunca concatene entradas do usuário sem validação → risco de SQL Injection em SQL dinâmico.

6) Modelagem & Desempenho

7) Laboratório Guiado

7.1) Setup (tabelas & dados)

-- Limpar e criar tabelas de exemplo
IF OBJECT_ID('dbo.VendasProdutos','U') IS NOT NULL DROP TABLE dbo.VendasProdutos;
IF OBJECT_ID('dbo.VendasProdutosWide','U') IS NOT NULL DROP TABLE dbo.VendasProdutosWide;
GO

CREATE TABLE dbo.VendasProdutos(
  IdVenda   int IDENTITY PRIMARY KEY,
  DataVenda date          NOT NULL,
  Produto   nvarchar(50)  NOT NULL,
  UF        char(2)       NULL,
  Valor     decimal(10,2) NOT NULL
);

-- Amostra
INSERT INTO dbo.VendasProdutos (DataVenda, Produto, UF, Valor) VALUES
('2025-01-10', N'Pneu A', 'SP', 1200.00),
('2025-01-21', N'Pneu B', 'SP',  750.00),
('2025-02-05', N'Pneu A', 'PR',  900.00),
('2025-02-18', N'Pneu C', 'SP',  500.00),
('2025-03-02', N'Pneu B', 'RJ',  650.00),
('2025-03-15', N'Pneu A', 'SP', 1100.00),
('2025-08-01', N'Pneu A', 'SP',  800.00),
('2025-08-10', N'Pneu B', 'SP',  999.90);

-- Tabela "wide" para UNPIVOT
CREATE TABLE dbo.VendasProdutosWide(
  Produto nvarchar(50) PRIMARY KEY,
  [Jan] decimal(10,2) NULL,
  [Fev] decimal(10,2) NULL,
  [Mar] decimal(10,2) NULL
);
INSERT INTO dbo.VendasProdutosWide (Produto, [Jan],[Fev],[Mar]) VALUES
(N'Pneu A', 1200.00, 900.00, 1100.00),
(N'Pneu B',  750.00, NULL,   650.00);
GO

7.2) PIVOT estático Produto × Mês

WITH Fonte AS (
  SELECT Produto, DATENAME(MONTH, DataVenda) AS Mes, Valor
  FROM dbo.VendasProdutos
)
SELECT Produto, [Janeiro], [Fevereiro], [Março], [Agosto]
FROM Fonte
PIVOT (SUM(Valor) FOR Mes IN ([Janeiro],[Fevereiro],[Março],[Agosto])) AS p
ORDER BY Produto;

7.3) PIVOT dinâmico (meses detectados)

DECLARE @cols nvarchar(max);
DECLARE @sql  nvarchar(max);

SELECT @cols = STRING_AGG(QUOTENAME(DATENAME(MONTH, DataVenda)), ',')
FROM (SELECT DISTINCT DataVenda FROM dbo.VendasProdutos) AS d;

SET @sql = N'
WITH Fonte AS (
  SELECT Produto, DATENAME(MONTH, DataVenda) AS Mes, Valor
  FROM dbo.VendasProdutos
)
SELECT Produto, ' + @cols + N'
FROM Fonte
PIVOT (SUM(Valor) FOR Mes IN (' + @cols + N')) AS p
ORDER BY Produto;';

EXEC sp_executesql @sql;

7.4) UNPIVOT para normalizar

SELECT Produto, Mes, Valor
FROM dbo.VendasProdutosWide
UNPIVOT (
  Valor FOR Mes IN ([Jan],[Fev],[Mar])
) AS u
ORDER BY Produto, Mes;

7.5) Alternativa com SUM(CASE)

WITH Fonte AS (
  SELECT Produto, DATENAME(MONTH, DataVenda) AS Mes, Valor
  FROM dbo.VendasProdutos
)
SELECT
  Produto,
  SUM(CASE WHEN Mes='Janeiro'   THEN Valor ELSE 0 END) AS Janeiro,
  SUM(CASE WHEN Mes='Fevereiro' THEN Valor ELSE 0 END) AS Fevereiro,
  SUM(CASE WHEN Mes='Março'     THEN Valor ELSE 0 END) AS Março,
  SUM(CASE WHEN Mes='Agosto'    THEN Valor ELSE 0 END) AS Agosto
FROM Fonte
GROUP BY Produto
ORDER BY Produto;

8) Exercícios

  1. PIVOT Produto × UF: Mostre total por Produto nas colunas por UF (SP, RJ, PR).
  2. PIVOT por Trimestre: Crie colunas T1..T4 usando DATEPART(QUARTER).
  3. PIVOT dinâmico: Gire por todos os meses presentes na tabela, usando STRING_AGG + QUOTENAME.
  4. UNPIVOT: Converta VendasProdutosWide em formato normalizado e some por mês.
  5. CASE: Replique o exercício 1 usando SUM(CASE) em vez de PIVOT.

9) Gabarito (sugestões)

-- 1) Produto × UF
WITH F AS (
  SELECT Produto, UF, Valor FROM dbo.VendasProdutos
)
SELECT Produto, [SP], [RJ], [PR]
FROM F
PIVOT (SUM(Valor) FOR UF IN ([SP],[RJ],[PR])) AS p
ORDER BY Produto;

-- 2) Por Trimestre
WITH F AS (
  SELECT Produto, DATEPART(QUARTER, DataVenda) AS T, Valor
  FROM dbo.VendasProdutos
)
SELECT Produto, [1] AS T1, [2] AS T2, [3] AS T3, [4] AS T4
FROM F
PIVOT (SUM(Valor) FOR T IN ([1],[2],[3],[4])) AS p
ORDER BY Produto;

-- 3) PIVOT dinâmico por mês
DECLARE @cols nvarchar(max), @sql nvarchar(max);
SELECT @cols = STRING_AGG(QUOTENAME(DATENAME(MONTH, DataVenda)), ',')
FROM (SELECT DISTINCT DataVenda FROM dbo.VendasProdutos) AS d;
SET @sql = N'
WITH F AS (
  SELECT Produto, DATENAME(MONTH, DataVenda) AS Mes, Valor FROM dbo.VendasProdutos
)
SELECT Produto, ' + @cols + N' FROM F
PIVOT (SUM(Valor) FOR Mes IN (' + @cols + N')) AS p
ORDER BY Produto;';
EXEC sp_executesql @sql;

-- 4) UNPIVOT + soma por mês
WITH N AS (
  SELECT Produto, Mes, Valor
  FROM dbo.VendasProdutosWide
  UNPIVOT (Valor FOR Mes IN ([Jan],[Fev],[Mar])) AS u
)
SELECT Mes, SUM(Valor) AS TotalMes
FROM N
GROUP BY Mes;

-- 5) Produto × UF com SUM(CASE)
SELECT
  Produto,
  SUM(CASE WHEN UF='SP' THEN Valor ELSE 0 END) AS SP,
  SUM(CASE WHEN UF='RJ' THEN Valor ELSE 0 END) AS RJ,
  SUM(CASE WHEN UF='PR' THEN Valor ELSE 0 END) AS PR
FROM dbo.VendasProdutos
GROUP BY Produto
ORDER BY Produto;

10) Erros Comuns & Checklist

Erros Comuns

  • Esquecer o agregador no PIVOT (obrigatório).
  • Não proteger nomes dinâmicos com QUOTENAME().
  • Esperar que PIVOT preencha NULL com 0 (use ISNULL()).
  • Pivotar modelos “wide” como armazenamento — dificulta manutenção.

Checklist Rápido

  • Fonte está em formato “longo” (linha=chave; coluna_pivot=dimensão; valor=medida)?
  • Lista de colunas é conhecida (estático) ou dinâmica?
  • Protegiu colunas dinâmicas com QUOTENAME e validou entradas?
  • Avaliou SUM(CASE) como alternativa?

11) Próximos Passos