Consulta PIVOT dinâmica do SQL Server?

203

Fui encarregado de criar um meio de traduzir os seguintes dados:

date        category        amount
1/1/2012    ABC             1000.00
2/1/2012    DEF             500.00
2/1/2012    GHI             800.00
2/10/2012   DEF             700.00
3/1/2012    ABC             1100.00

no seguinte:

date        ABC             DEF             GHI
1/1/2012    1000.00
2/1/2012                    500.00
2/1/2012                                    800.00
2/10/2012                   700.00
3/1/2012    1100.00

Os pontos em branco podem ser NULLs ou espaços em branco, ou é bom, e as categorias precisariam ser dinâmicas. Outra ressalva possível é que executaremos a consulta em uma capacidade limitada, o que significa que as tabelas temporárias estão fora. Eu tentei pesquisar e consegui, PIVOTmas como nunca o usei antes, realmente não o entendo, apesar dos meus melhores esforços para descobrir. Alguém pode me apontar na direção certa?

Sean Cunningham
fonte
3
Qual versão do SQL Server, por favor?
Aaron Bertrand
1
possível duplicata de gravação avançado SQL Select
RichardTheKiwi

Respostas:

251

PIVOT SQL dinâmico:

create table temp
(
    date datetime,
    category varchar(3),
    amount money
)

insert into temp values ('1/1/2012', 'ABC', 1000.00)
insert into temp values ('2/1/2012', 'DEF', 500.00)
insert into temp values ('2/1/2012', 'GHI', 800.00)
insert into temp values ('2/10/2012', 'DEF', 700.00)
insert into temp values ('3/1/2012', 'ABC', 1100.00)


DECLARE @cols AS NVARCHAR(MAX),
    @query  AS NVARCHAR(MAX);

SET @cols = STUFF((SELECT distinct ',' + QUOTENAME(c.category) 
            FROM temp c
            FOR XML PATH(''), TYPE
            ).value('.', 'NVARCHAR(MAX)') 
        ,1,1,'')

set @query = 'SELECT date, ' + @cols + ' from 
            (
                select date
                    , amount
                    , category
                from temp
           ) x
            pivot 
            (
                 max(amount)
                for category in (' + @cols + ')
            ) p '


execute(@query)

drop table temp

Resultados:

Date                        ABC         DEF    GHI
2012-01-01 00:00:00.000     1000.00     NULL    NULL
2012-02-01 00:00:00.000     NULL        500.00  800.00
2012-02-10 00:00:00.000     NULL        700.00  NULL
2012-03-01 00:00:00.000     1100.00     NULL    NULL
Taryn
fonte
Então \ @cols deve ser concatenado por string, certo? Não podemos usar sp_executesql e ligação de parâmetro para interpolar \ @cols lá? Mesmo que construamos \ @cols a nós mesmos, e se de alguma forma contiver SQL malicioso. Quaisquer medidas mitigadoras adicionais que eu possa tomar antes de concatená-lo e executá-lo?
The Red Pea
Como você classificaria as linhas e colunas sobre isso?
Patrick Schomburg 29/11
@PatrickSchomburg Existem várias maneiras - se você quiser classificar as opções, @colspoderá remover DISTINCTe usar GROUP BYe ORDER BYquando receber a lista @cols.
Taryn
Eu vou tentar isso. E as linhas? Também estou usando um encontro, e ele não sai em ordem.
Patrick Schomburg 29/11
1
Não importa, eu estava colocando o pedido no lugar errado.
Patrick Schomburg 29/11
27

PIVOT SQL dinâmico

Abordagem diferente para criar string de colunas

create table #temp
(
    date datetime,
    category varchar(3),
    amount money
)

insert into #temp values ('1/1/2012', 'ABC', 1000.00)
insert into #temp values ('2/1/2012', 'DEF', 500.00)
insert into #temp values ('2/1/2012', 'GHI', 800.00)
insert into #temp values ('2/10/2012', 'DEF', 700.00)
insert into #temp values ('3/1/2012', 'ABC', 1100.00)

DECLARE @cols  AS NVARCHAR(MAX)='';
DECLARE @query AS NVARCHAR(MAX)='';

SELECT @cols = @cols + QUOTENAME(category) + ',' FROM (select distinct category from #temp ) as tmp
select @cols = substring(@cols, 0, len(@cols)) --trim "," at end

set @query = 
'SELECT * from 
(
    select date, amount, category from #temp
) src
pivot 
(
    max(amount) for category in (' + @cols + ')
) piv'

execute(@query)
drop table #temp

Resultado

date                    ABC     DEF     GHI
2012-01-01 00:00:00.000 1000.00 NULL    NULL
2012-02-01 00:00:00.000 NULL    500.00  800.00
2012-02-10 00:00:00.000 NULL    700.00  NULL
2012-03-01 00:00:00.000 1100.00 NULL    NULL
mkdave99
fonte
13

Sei que essa pergunta é mais antiga, mas estava procurando as respostas e pensei que poderia expandir a parte "dinâmica" do problema e possivelmente ajudar alguém.

Antes de mais, criei esta solução para resolver um problema que alguns colegas de trabalho estavam tendo com conjuntos de dados grandes e inconstantes que precisavam ser rapidamente dinamizados.

Esta solução requer a criação de um procedimento armazenado; portanto, se isso estiver fora de questão para suas necessidades, pare de ler agora.

Este procedimento incluirá as variáveis-chave de uma instrução dinâmica para criar dinamicamente instruções dinâmicas para tabelas, nomes de colunas e agregados variados. A coluna Estática é usada como a coluna agrupar por / identidade para o pivô (isso pode ser retirado do código, se não for necessário, mas é bastante comum nas instruções de pivô e era necessário para resolver o problema original), a coluna pivô é onde o os nomes das colunas resultantes finais serão gerados e a coluna de valor é à qual o agregado será aplicado. O parâmetro Table é o nome da tabela, incluindo o esquema (schema.tablename). Essa parte do código pode usar um pouco de amor, porque não é tão limpa quanto eu gostaria que fosse. Funcionou para mim porque meu uso não era público e a injeção de sql não era uma preocupação.

Vamos começar com o código para criar o procedimento armazenado. Este código deve funcionar em todas as versões do SSMS 2005 e acima, mas não o testei em 2005 ou 2016, mas não consigo ver por que não funcionaria.

create PROCEDURE [dbo].[USP_DYNAMIC_PIVOT]
    (
        @STATIC_COLUMN VARCHAR(255),
        @PIVOT_COLUMN VARCHAR(255),
        @VALUE_COLUMN VARCHAR(255),
        @TABLE VARCHAR(255),
        @AGGREGATE VARCHAR(20) = null
    )

AS


BEGIN

SET NOCOUNT ON;
declare @AVAIABLE_TO_PIVOT NVARCHAR(MAX),
        @SQLSTRING NVARCHAR(MAX),
        @PIVOT_SQL_STRING NVARCHAR(MAX),
        @TEMPVARCOLUMNS NVARCHAR(MAX),
        @TABLESQL NVARCHAR(MAX)

if isnull(@AGGREGATE,'') = '' 
    begin
        SET @AGGREGATE = 'MAX'
    end


 SET @PIVOT_SQL_STRING =    'SELECT top 1 STUFF((SELECT distinct '', '' + CAST(''[''+CONVERT(VARCHAR,'+ @PIVOT_COLUMN+')+'']''  AS VARCHAR(50)) [text()]
                            FROM '+@TABLE+'
                            WHERE ISNULL('+@PIVOT_COLUMN+','''') <> ''''
                            FOR XML PATH(''''), TYPE)
                            .value(''.'',''NVARCHAR(MAX)''),1,2,'' '') as PIVOT_VALUES
                            from '+@TABLE+' ma
                            ORDER BY ' + @PIVOT_COLUMN + ''

declare @TAB AS TABLE(COL NVARCHAR(MAX) )

INSERT INTO @TAB EXEC SP_EXECUTESQL  @PIVOT_SQL_STRING, @AVAIABLE_TO_PIVOT 

SET @AVAIABLE_TO_PIVOT = (SELECT * FROM @TAB)


SET @TEMPVARCOLUMNS = (SELECT replace(@AVAIABLE_TO_PIVOT,',',' nvarchar(255) null,') + ' nvarchar(255) null')


SET @SQLSTRING = 'DECLARE @RETURN_TABLE TABLE ('+@STATIC_COLUMN+' NVARCHAR(255) NULL,'+@TEMPVARCOLUMNS+')  
                    INSERT INTO @RETURN_TABLE('+@STATIC_COLUMN+','+@AVAIABLE_TO_PIVOT+')

                    select * from (
                    SELECT ' + @STATIC_COLUMN + ' , ' + @PIVOT_COLUMN + ', ' + @VALUE_COLUMN + ' FROM '+@TABLE+' ) a

                    PIVOT
                    (
                    '+@AGGREGATE+'('+@VALUE_COLUMN+')
                    FOR '+@PIVOT_COLUMN+' IN ('+@AVAIABLE_TO_PIVOT+')
                    ) piv

                    SELECT * FROM @RETURN_TABLE'



EXEC SP_EXECUTESQL @SQLSTRING

END

Em seguida, prepararemos nossos dados para o exemplo. Peguei o exemplo de dados da resposta aceita com a adição de alguns elementos de dados para usar nessa prova de conceito para mostrar os resultados variados da alteração agregada.

create table temp
(
    date datetime,
    category varchar(3),
    amount money
)

insert into temp values ('1/1/2012', 'ABC', 1000.00)
insert into temp values ('1/1/2012', 'ABC', 2000.00) -- added
insert into temp values ('2/1/2012', 'DEF', 500.00)
insert into temp values ('2/1/2012', 'DEF', 1500.00) -- added
insert into temp values ('2/1/2012', 'GHI', 800.00)
insert into temp values ('2/10/2012', 'DEF', 700.00)
insert into temp values ('2/10/2012', 'DEF', 800.00) -- addded
insert into temp values ('3/1/2012', 'ABC', 1100.00)

Os exemplos a seguir mostram as instruções de execução variadas, mostrando os agregados variados como um exemplo simples. Não optei por alterar as colunas estática, dinâmica e de valor para manter o exemplo simples. Você deve apenas copiar e colar o código para começar a mexer nele sozinho

exec [dbo].[USP_DYNAMIC_PIVOT] 'date','category','amount','dbo.temp','sum'
exec [dbo].[USP_DYNAMIC_PIVOT] 'date','category','amount','dbo.temp','max'
exec [dbo].[USP_DYNAMIC_PIVOT] 'date','category','amount','dbo.temp','avg'
exec [dbo].[USP_DYNAMIC_PIVOT] 'date','category','amount','dbo.temp','min'

Esta execução retorna os seguintes conjuntos de dados, respectivamente.

insira a descrição da imagem aqui

SFrejofsky
fonte
Bom trabalho! Você pode optar por TVF em vez de procedimento armazenado. Seria conveniente selecionar um desses TVF.
Przemyslaw Remin
3
Infelizmente não, pelo que sei, porque você não pode ter uma estrutura dinâmica para um TVF. Você precisa ter um conjunto estático de colunas em um TVF.
SFrejofsky
8

Versão atualizada para o SQL Server 2017 usando a função STRING_AGG para construir a lista de colunas dinâmicas:

create table temp
(
    date datetime,
    category varchar(3),
    amount money
);

insert into temp values ('20120101', 'ABC', 1000.00);
insert into temp values ('20120201', 'DEF', 500.00);
insert into temp values ('20120201', 'GHI', 800.00);
insert into temp values ('20120210', 'DEF', 700.00);
insert into temp values ('20120301', 'ABC', 1100.00);


DECLARE @cols AS NVARCHAR(MAX),
    @query  AS NVARCHAR(MAX);

SET @cols = (SELECT STRING_AGG(category,',') FROM (SELECT DISTINCT category FROM temp WHERE category IS NOT NULL)t);

set @query = 'SELECT date, ' + @cols + ' from 
            (
                select date
                    , amount
                    , category
                from temp
           ) x
            pivot 
            (
                 max(amount)
                for category in (' + @cols + ')
            ) p ';

execute(@query);

drop table temp;
nvogel
fonte
6

Você pode conseguir isso usando o TSQL dinâmico (lembre-se de usar QUOTENAME para evitar ataques de injeção de SQL):

Pivôs com colunas dinâmicas no SQL Server 2005

SQL Server - Tabela dinâmica PIVOT - Injeção de SQL

Referência obrigatória à maldição e bênçãos do SQL dinâmico

Davids
fonte
11
O FWIW QUOTENAMEsomente ajuda a ataques de injeção SQL se você estiver aceitando @tableName como parâmetro de um usuário e anexando-o a uma consulta como SET @sql = 'SELECT * FROM ' + @tableName;. Você pode criar várias seqüências de caracteres dinâmicas e vulneráveis ​​em SQL e QUOTENAMEnão fará uma lambida para ajudá-lo.
Aaron Bertrand
2
@ Davids Por favor, consulte esta meta discussão . Se você remover os hiperlinks, sua resposta estará incompleta.
Caco
@Kermit, eu concordo que mostrar o código é mais útil, mas você está dizendo que é necessário para que seja uma resposta? Sem os links, minha resposta é "Você pode conseguir isso usando o TSQL dinâmico". A resposta selecionada sugere a mesma rota, com o benefício adicional se também mostrar como fazê-lo, razão pela qual foi selecionada como resposta.
Davids
2
Votei na resposta selecionada antes de ser selecionada, porque ela tinha um exemplo e ajudará melhor alguém novo. No entanto, acho que alguém novo também deve ler os links que forneci, e é por isso que não os removi.
Davids
3

Minha solução está limpando os valores nulos desnecessários

DECLARE @cols AS NVARCHAR(MAX),
@maxcols AS NVARCHAR(MAX),
@query  AS NVARCHAR(MAX)

select @cols = STUFF((SELECT ',' + QUOTENAME(CodigoFormaPago) 
                from PO_FormasPago
                order by CodigoFormaPago
        FOR XML PATH(''), TYPE
        ).value('.', 'NVARCHAR(MAX)') 
    ,1,1,'')

select @maxcols = STUFF((SELECT ',MAX(' + QUOTENAME(CodigoFormaPago) + ') as ' + QUOTENAME(CodigoFormaPago)
                from PO_FormasPago
                order by CodigoFormaPago
        FOR XML PATH(''), TYPE
        ).value('.', 'NVARCHAR(MAX)')
    ,1,1,'')

set @query = 'SELECT CodigoProducto, DenominacionProducto, ' + @maxcols + '
            FROM
            (
                SELECT 
                CodigoProducto, DenominacionProducto,
                ' + @cols + ' from 
                 (
                    SELECT 
                        p.CodigoProducto as CodigoProducto,
                        p.DenominacionProducto as DenominacionProducto,
                        fpp.CantidadCuotas as CantidadCuotas,
                        fpp.IdFormaPago as IdFormaPago,
                        fp.CodigoFormaPago as CodigoFormaPago
                    FROM
                        PR_Producto p
                        LEFT JOIN PR_FormasPagoProducto fpp
                            ON fpp.IdProducto = p.IdProducto
                        LEFT JOIN PO_FormasPago fp
                            ON fpp.IdFormaPago = fp.IdFormaPago
                ) xp
                pivot 
                (
                    MAX(CantidadCuotas)
                    for CodigoFormaPago in (' + @cols + ')
                ) p 
            )  xx 
            GROUP BY CodigoProducto, DenominacionProducto'

t @query;

execute(@query);
m0rg4n
fonte
2

O código abaixo fornece os resultados que substituem NULL a zero na saída.

Criação de tabela e inserção de dados:

create table test_table
 (
 date nvarchar(10),
 category char(3),
 amount money
 )

 insert into test_table values ('1/1/2012','ABC',1000.00)
 insert into test_table values ('2/1/2012','DEF',500.00)
 insert into test_table values ('2/1/2012','GHI',800.00)
 insert into test_table values ('2/10/2012','DEF',700.00)
 insert into test_table values ('3/1/2012','ABC',1100.00)

Consulta para gerar os resultados exatos que também substituem NULL por zeros:

DECLARE @DynamicPivotQuery AS NVARCHAR(MAX),
@PivotColumnNames AS NVARCHAR(MAX),
@PivotSelectColumnNames AS NVARCHAR(MAX)

--Get distinct values of the PIVOT Column
SELECT @PivotColumnNames= ISNULL(@PivotColumnNames + ',','')
+ QUOTENAME(category)
FROM (SELECT DISTINCT category FROM test_table) AS cat

--Get distinct values of the PIVOT Column with isnull
SELECT @PivotSelectColumnNames 
= ISNULL(@PivotSelectColumnNames + ',','')
+ 'ISNULL(' + QUOTENAME(category) + ', 0) AS '
+ QUOTENAME(category)
FROM (SELECT DISTINCT category FROM test_table) AS cat

--Prepare the PIVOT query using the dynamic 
SET @DynamicPivotQuery = 
N'SELECT date, ' + @PivotSelectColumnNames + '
FROM test_table
pivot(sum(amount) for category in (' + @PivotColumnNames + ')) as pvt';

--Execute the Dynamic Pivot Query
EXEC sp_executesql @DynamicPivotQuery

RESULTADO :

insira a descrição da imagem aqui

Arockia Nirmal
fonte