Converti righe in colonne usando "Pivot" in SQL Server


279

Ho letto le cose sulle tabelle pivot di MS e sto ancora riscontrando problemi a farlo correttamente.

Ho creato una tabella temporanea, diremo che la colonna 1 è un numero di negozio e la colonna 2 è un numero di settimana e infine la colonna 3 è un totale di un certo tipo. Anche i numeri delle settimane sono dinamici, i numeri dei negozi sono statici.

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

Vorrei che venisse fuori come una tabella pivot, in questo modo:

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

Memorizza i numeri in basso e le settimane in alto.


1
possibile duplicato della query PIVOT dinamica
RichardTheKiwi,

Risposte:


356

Se si utilizza SQL Server 2005+, è possibile utilizzare la PIVOTfunzione per trasformare i dati da righe in colonne.

Sembra che dovrai usare sql dinamico se le settimane sono sconosciute ma è più facile vedere il codice corretto usando inizialmente una versione hardcoded.

Innanzitutto, ecco alcune definizioni e dati rapidi per l'uso della tabella:

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 i tuoi valori sono noti, allora codificherai la query:

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

Vedi Demo SQL

Quindi, se è necessario generare dinamicamente il numero della settimana, il codice sarà:

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);

Vedi Demo SQL .

La versione dinamica, genera l'elenco di weeknumeri che dovrebbero essere convertiti in colonne. Entrambi danno lo stesso risultato:

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

4
Molto bella! Ma come eliminare la colonna quando tutti i valori di quella colonna sono NULL?
ZooZ

1
@ZooZ Vedi risposta sotto . Non l'ho provato alla lettera, ma il concetto è valido.
ruffin,

1
+1 "Sembra che dovrai usare sql dinamico se le settimane sono sconosciute ma è più facile vedere inizialmente il codice corretto usando inizialmente una versione rigida." A differenza della funzione generica di Qlikview ( community.qlik.com/blogs/qlikviewdesignblog/2014/03/31/generic ) che consente non richiede di nominare esplicitamente il diverso "FOR ____ IN (...)"
The Red Pea

1
Se si sta creando una tabella pivot con un cte in precedenza. cte3 AS (select ... )allora hai la logica sopra definita con @colse @query... c'è un errore. "Nome oggetto non valido" cte3 ". Come risolverlo. -
Elizabeth

3
Questo è fantastico - bello @bluefeet. Non avevo mai usato STUFF(...)prima (o l' XML PATHuno o l'altro ). A beneficio di altri lettori, tutto ciò che sta facendo è unire i nomi delle colonne e tagliare la virgola principale. Nota Penso che quanto segue sia leggermente più semplice: selezionare @cols = (SELECT DISTINCT QUOTENAME (Settimana) + ',' da yt ordine per 1 PER XML PATH ('')) set @cols = SUBSTRING (@cols, 1, LEN ( @cols) - 1) ... sostituendo il group bycon distincte order by 1tagliando manualmente una virgola con suffisso !
DarthPablo,

26

Questo è per il numero dinamico di settimane.

Esempio completo qui: 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

Ehi, ho un violino in cui ho bisogno di ruotare dinamicamente i tavoli, pensi di potermi aiutare? dbfiddle.uk/…
Silly Volley,

@SillyVolley qui è uno, non hai specificato su cosa volevi fare perno. Inoltre non so se è possibile farlo in Postgres, quindi l'ho fatto in SQL Server: dbfiddle.uk/…
Enkode

16

Ho ottenuto la stessa cosa in precedenza utilizzando le subquery. Quindi, se la tua tabella originale si chiamava StoreCountsByWeek e avessi una tabella separata in cui erano elencati gli ID negozio, sarebbe simile al seguente:

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

Un vantaggio di questo metodo è che la sintassi è più chiara e semplifica l'unione ad altre tabelle per estrarre anche altri campi nei risultati.

I miei risultati aneddotici sono che l'esecuzione di questa query su un paio di migliaia di righe è stata completata in meno di un secondo e in realtà avevo 7 subquery. Ma come notato nei commenti, è più costoso dal punto di vista computazionale farlo in questo modo, quindi fai attenzione a usare questo metodo se ti aspetti che venga eseguito su grandi quantità di dati.


8
è più semplice, ma è un'operazione molto costosa, quelle sottoquery devono essere eseguite una volta per ogni riga restituita dalla tabella.
Greg,

11

Questo è ciò che puoi fare:

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

DEMO


6

Sto scrivendo uno sp che potrebbe essere utile a questo scopo, sostanzialmente questo sp fa perno su qualsiasi tabella e restituisce una nuova tabella pivotata o restituisce solo il set di dati, questo è il modo di eseguirlo:

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;

si noti che nel parametro @agg i nomi delle colonne devono essere con '['e il parametro deve terminare con una virgola','

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

Questo è un esempio di esecuzione:

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;

poi Select * From ##TEMPORAL1PVTritornerebbe:

inserisci qui la descrizione dell'immagine


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

2

Ecco una revisione della risposta @Tayrn sopra che potrebbe aiutarti a capire un po 'più facilmente il pivot:

Questo potrebbe non essere il modo migliore per farlo, ma questo è ciò che mi ha aiutato a avvolgere la testa su come ruotare i tavoli.

ID = righe che desideri ruotare

MY_KEY = la colonna che stai selezionando dalla tabella originale che contiene i nomi delle colonne che vuoi ruotare.

VAL = il valore che vuoi restituire sotto ogni colonna.

MAX (VAL) => Può essere sostituito con altre funzioni aggregate. SOMMA (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

Ti do solo un'idea di come altri database risolvano questo problema. DolphinDBha anche il supporto integrato per il pivot e il sql sembra molto più intuitivo e pulito. È semplice come specificare la colonna chiave ( Store), la colonna pivotante ( Week) e la metrica calcolata ( 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

DolphinDB è un database colonnare ad alte prestazioni. Il calcolo nella demo costa fino a 546 ms su un laptop dell xps (i7 cpu). Per maggiori dettagli, consultare il manuale online di DolphinDB https://www.dolphindb.com/help/index.html?pivotby.html

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.