Converter linhas em colunas usando 'Pivot' no SQL Server


279

Eu li o material nas tabelas dinâmicas do MS e ainda estou tendo problemas para corrigir isso.

Eu tenho uma tabela temporária que está sendo criada, diremos que a coluna 1 é um número da loja e a coluna 2 é um número da semana e, por fim, a coluna 3 é um total de algum tipo. Além disso, os números da semana são dinâmicos, os números das lojas são estáticos.

Store      Week     xCount
-------    ----     ------
102        1        96
101        1        138
105        1        37
109        1        59
101        2        282
102        2        212
105        2        78
109        2        97
105        3        60
102        3        123
101        3        220
109        3        87

Gostaria que saia como uma tabela dinâmica, assim:

Store        1          2          3        4        5        6....
----- 
101        138        282        220
102         96        212        123
105         37        
109

Armazene números ao lado e semanas na parte superior.


1
possível duplicata da consulta dinâmica
RichardTheKiwi

Respostas:


356

Se você estiver usando o SQL Server 2005+, poderá usar a PIVOTfunção para transformar os dados de linhas em colunas.

Parece que você precisará usar o sql dinâmico se as semanas forem desconhecidas, mas é mais fácil ver o código correto usando uma versão codificada inicialmente.

Primeiro, aqui estão algumas definições e dados rápidos da tabela para uso:

CREATE TABLE #yt 
(
  [Store] int, 
  [Week] int, 
  [xCount] int
);

INSERT INTO #yt
(
  [Store], 
  [Week], [xCount]
)
VALUES
    (102, 1, 96),
    (101, 1, 138),
    (105, 1, 37),
    (109, 1, 59),
    (101, 2, 282),
    (102, 2, 212),
    (105, 2, 78),
    (109, 2, 97),
    (105, 3, 60),
    (102, 3, 123),
    (101, 3, 220),
    (109, 3, 87);

Se seus valores forem conhecidos, você codificará a consulta:

select *
from 
(
  select store, week, xCount
  from yt
) src
pivot
(
  sum(xcount)
  for week in ([1], [2], [3])
) piv;

Consulte Demonstração SQL

Então, se você precisar gerar o número da semana dinamicamente, seu código será:

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

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

set @query = 'SELECT store,' + @cols + ' from 
             (
                select store, week, xCount
                from yt
            ) x
            pivot 
            (
                sum(xCount)
                for week in (' + @cols + ')
            ) p '

execute(@query);

Consulte Demonstração SQL .

A versão dinâmica gera a lista de weeknúmeros que devem ser convertidos em colunas. Ambos dão o mesmo resultado:

| STORE |   1 |   2 |   3 |
---------------------------
|   101 | 138 | 282 | 220 |
|   102 |  96 | 212 | 123 |
|   105 |  37 |  78 |  60 |
|   109 |  59 |  97 |  87 |

4
Muito agradável! Mas como eliminar a coluna quando todos os valores dessa coluna são NULL?
ZooZ 19/01/2015

1
@ZooZ Veja a resposta abaixo . Ainda não tentei textualmente, mas o conceito é sólido.
ruffin

1
+1 "Parece que você precisará usar o sql dinâmico se as semanas forem desconhecidas, mas é mais fácil ver o código correto usando uma versão com hard-hard inicialmente." Diferente da função Qlikview Generic ( community.qlik.com/blogs/qlikviewdesignblog/2014/03/31/generic ), que permite não exigir que você nomeie explicitamente os diferentes "FOR ____ IN (...)"
The Red Pea

1
Se você estiver criando uma tabela dinâmica com um cte anteriormente. cte3 AS (select ... )então você tem a lógica acima definida com o @colse @query... há um erro. Nome do objeto inválido 'cte3'. Como você corrige isso. -
Elizabeth

3
Isso é fantástico - bom @bluefeet. Eu nunca tinha usado STUFF(...)antes (ou o XML PATHoutro). Para o benefício de outros leitores, tudo o que você está fazendo é unir os nomes das colunas e cortar a vírgula principal. Observe que o seguinte é um pouco mais simples: selecione @cols = (SELECT DISTINCT QUOTENAME (Week) + ',' da ordem yt por 1 FOR XML PATH ('')) set @cols = SUBSTRING (@cols, 1, LEN ( @cols) - 1) ... substituindo o group bypor distincte order by 1e cortar manualmente um sufixo vírgula!
DarthPablo

26

Isso é para o número dinâmico de semanas.

Exemplo completo aqui: SQL Dynamic Pivot

DECLARE @DynamicPivotQuery AS NVARCHAR(MAX)
DECLARE @ColumnName AS NVARCHAR(MAX)

--Get distinct values of the PIVOT Column 
SELECT @ColumnName= ISNULL(@ColumnName + ',','') + QUOTENAME(Week)
FROM (SELECT DISTINCT Week FROM #StoreSales) AS Weeks

--Prepare the PIVOT query using the dynamic 
SET @DynamicPivotQuery = 
  N'SELECT Store, ' + @ColumnName + ' 
    FROM #StoreSales
    PIVOT(SUM(xCount) 
          FOR Week IN (' + @ColumnName + ')) AS PVTTable'
--Execute the Dynamic Pivot Query
EXEC sp_executesql @DynamicPivotQuery

Ei, eu tenho um violino onde preciso dinamizar tabelas dinamicamente, você acha que pode me ajudar com isso? dbfiddle.uk/...
parva Volley

@SillyVolley aqui é um, você não especificou o que queria girar. Também não sei se você pode fazer isso no Postgres, então fiz no SQL Server: dbfiddle.uk/…
Enkode

16

Já consegui a mesma coisa usando subconsultas. Portanto, se sua tabela original fosse chamada StoreCountsByWeek e você tivesse uma tabela separada que listasse os IDs da loja, seria assim:

SELECT StoreID, 
    Week1=(SELECT ISNULL(SUM(xCount),0) FROM StoreCountsByWeek WHERE StoreCountsByWeek.StoreID=Store.StoreID AND Week=1),
    Week2=(SELECT ISNULL(SUM(xCount),0) FROM StoreCountsByWeek WHERE StoreCountsByWeek.StoreID=Store.StoreID AND Week=2),
    Week3=(SELECT ISNULL(SUM(xCount),0) FROM StoreCountsByWeek WHERE StoreCountsByWeek.StoreID=Store.StoreID AND Week=3)
FROM Store
ORDER BY StoreID

Uma vantagem desse método é que a sintaxe é mais clara e facilita a junção a outras tabelas para atrair outros campos para os resultados.

Meus resultados anedóticos são que a execução dessa consulta em algumas milhares de linhas foi concluída em menos de um segundo e eu tinha 7 subconsultas. Mas, como observado nos comentários, é mais caro em termos de computação fazê-lo dessa maneira; portanto, tenha cuidado ao usar esse método se você espera que ele seja executado em grandes quantidades de dados.


8
é mais fácil, mas, como é uma operação muito cara, essas subconsultas precisam ser executadas uma vez para cada linha retornada da tabela.
Greg

11

Isto é o que você pode fazer:

SELECT * 
FROM yourTable
PIVOT (MAX(xCount) 
       FOR Week in ([1],[2],[3],[4],[5],[6],[7])) AS pvt

DEMO


5

Estou escrevendo um sp que pode ser útil para esse fim, basicamente esse sp gira qualquer tabela e retorna uma nova tabela dinâmica ou retorna apenas o conjunto de dados; é assim que se executa:

Exec dbo.rs_pivot_table @schema=dbo,@table=table_name,@column=column_to_pivot,@agg='sum([column_to_agg]),avg([another_column_to_agg]),',
        @sel_cols='column_to_select1,column_to_select2,column_to_select1',@new_table=returned_table_pivoted;

observe que no parâmetro @agg os nomes das colunas devem estar com '['e o parâmetro deve terminar com vírgula','

SP

Create Procedure [dbo].[rs_pivot_table]
    @schema sysname=dbo,
    @table sysname,
    @column sysname,
    @agg nvarchar(max),
    @sel_cols varchar(max),
    @new_table sysname,
    @add_to_col_name sysname=null
As
--Exec dbo.rs_pivot_table dbo,##TEMPORAL1,tip_liq,'sum([val_liq]),sum([can_liq]),','cod_emp,cod_con,tip_liq',##TEMPORAL1PVT,'hola';
Begin

    Declare @query varchar(max)='';
    Declare @aggDet varchar(100);
    Declare @opp_agg varchar(5);
    Declare @col_agg varchar(100);
    Declare @pivot_col sysname;
    Declare @query_col_pvt varchar(max)='';
    Declare @full_query_pivot varchar(max)='';
    Declare @ind_tmpTbl int; --Indicador de tabla temporal 1=tabla temporal global 0=Tabla fisica

    Create Table #pvt_column(
        pivot_col varchar(100)
    );

    Declare @column_agg table(
        opp_agg varchar(5),
        col_agg varchar(100)
    );

    IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(@table) AND type in (N'U'))
        Set @ind_tmpTbl=0;
    ELSE IF OBJECT_ID('tempdb..'+ltrim(rtrim(@table))) IS NOT NULL
        Set @ind_tmpTbl=1;

    IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(@new_table) AND type in (N'U')) OR 
        OBJECT_ID('tempdb..'+ltrim(rtrim(@new_table))) IS NOT NULL
    Begin
        Set @query='DROP TABLE '+@new_table+'';
        Exec (@query);
    End;

    Select @query='Select distinct '+@column+' From '+(case when @ind_tmpTbl=1 then 'tempdb.' else '' end)+@schema+'.'+@table+' where '+@column+' is not null;';
    Print @query;

    Insert into #pvt_column(pivot_col)
    Exec (@query)

    While charindex(',',@agg,1)>0
    Begin
        Select @aggDet=Substring(@agg,1,charindex(',',@agg,1)-1);

        Insert Into @column_agg(opp_agg,col_agg)
        Values(substring(@aggDet,1,charindex('(',@aggDet,1)-1),ltrim(rtrim(replace(substring(@aggDet,charindex('[',@aggDet,1),charindex(']',@aggDet,1)-4),')',''))));

        Set @agg=Substring(@agg,charindex(',',@agg,1)+1,len(@agg))

    End

    Declare cur_agg cursor read_only forward_only local static for
    Select 
        opp_agg,col_agg
    from @column_agg;

    Open cur_agg;

    Fetch Next From cur_agg
    Into @opp_agg,@col_agg;

    While @@fetch_status=0
    Begin

        Declare cur_col cursor read_only forward_only local static for
        Select 
            pivot_col 
        From #pvt_column;

        Open cur_col;

        Fetch Next From cur_col
        Into @pivot_col;

        While @@fetch_status=0
        Begin

            Select @query_col_pvt='isnull('+@opp_agg+'(case when '+@column+'='+quotename(@pivot_col,char(39))+' then '+@col_agg+
            ' else null end),0) as ['+lower(Replace(Replace(@opp_agg+'_'+convert(varchar(100),@pivot_col)+'_'+replace(replace(@col_agg,'[',''),']',''),' ',''),'&',''))+
                (case when @add_to_col_name is null then space(0) else '_'+isnull(ltrim(rtrim(@add_to_col_name)),'') end)+']'
            print @query_col_pvt
            Select @full_query_pivot=@full_query_pivot+@query_col_pvt+', '

            --print @full_query_pivot

            Fetch Next From cur_col
            Into @pivot_col;        

        End     

        Close cur_col;
        Deallocate cur_col;

        Fetch Next From cur_agg
        Into @opp_agg,@col_agg; 
    End

    Close cur_agg;
    Deallocate cur_agg;

    Select @full_query_pivot=substring(@full_query_pivot,1,len(@full_query_pivot)-1);

    Select @query='Select '+@sel_cols+','+@full_query_pivot+' into '+@new_table+' From '+(case when @ind_tmpTbl=1 then 'tempdb.' else '' end)+
    @schema+'.'+@table+' Group by '+@sel_cols+';';

    print @query;
    Exec (@query);

End;
GO

Este é um exemplo de execução:

Exec dbo.rs_pivot_table @schema=dbo,@table=##TEMPORAL1,@column=tip_liq,@agg='sum([val_liq]),avg([can_liq]),',@sel_cols='cod_emp,cod_con,tip_liq',@new_table=##TEMPORAL1PVT;

então Select * From ##TEMPORAL1PVTretornaria:

insira a descrição da imagem aqui


2
select * from (select name, ID from Empoyee) Visits
    pivot(sum(ID) for name
    in ([Emp1],
    [Emp2],
    [Emp3]
    ) ) as pivottable;

2

Aqui está uma revisão da resposta do @Tayrn acima que pode ajudá-lo a entender o giro um pouco mais fácil:

Esta pode não ser a melhor maneira de fazer isso, mas foi isso que me ajudou a entender como girar as tabelas.

ID = linhas que você deseja dinamizar

MY_KEY = a coluna que você está selecionando na tabela original que contém os nomes das colunas que você deseja dinamizar.

VAL = o valor que você deseja retornar em cada coluna.

MAX (VAL) => Pode ser substituído por outras funções agregadas. SOMA (VAL), MIN (VAL), ETC ...

DECLARE @cols AS NVARCHAR(MAX),
@query  AS NVARCHAR(MAX)
select @cols = STUFF((SELECT ',' + QUOTENAME(MY_KEY) 
                from yt
                group by MY_KEY
                order by MY_KEY ASC
        FOR XML PATH(''), TYPE
        ).value('.', 'NVARCHAR(MAX)') 
    ,1,1,'')
set @query = 'SELECT ID,' + @cols + ' from 
         (
            select ID, MY_KEY, VAL 
            from yt
        ) x
        pivot 
        (
            sum(VAL)
            for MY_KEY in (' + @cols + ')
        ) p '

        execute(@query);

2

Apenas dê uma idéia de como outros bancos de dados resolvem esse problema. DolphinDBtambém possui suporte interno para rotação e o sql parece muito mais intuitivo e arrumado. É tão simples quanto especificar a coluna chave ( Store), a coluna dinâmica ( Week) e a métrica calculada ( sum(xCount)).

//prepare a 10-million-row table
n=10000000
t=table(rand(100, n) + 1 as Store, rand(54, n) + 1 as Week, rand(100, n) + 1 as xCount)

//use pivot clause to generate a pivoted table pivot_t
pivot_t = select sum(xCount) from t pivot by Store, Week

O DolphinDB é um banco de dados colunar de alto desempenho. O cálculo na demonstração custa tão baixo quanto 546 ms em um laptop dell xps (i7 cpu). Para obter mais detalhes, consulte o manual on-line do DolphinDB https://www.dolphindb.com/help/index.html?pivotby.html

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.