Query PIVOT dinamica di SQL Server?


203

Mi è stato assegnato il compito di trovare un modo per tradurre i seguenti dati:

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

nel seguente:

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

I punti vuoti possono essere NULL o vuoti, o va bene, e le categorie dovrebbero essere dinamiche. Un altro possibile avvertimento è che eseguiremo la query a capacità limitata, il che significa che le tabelle temporanee non sono disponibili. Ho provato a fare ricerche e sono atterrato, PIVOTma dato che non l'ho mai usato prima, non lo capisco davvero, nonostante i miei migliori sforzi per capirlo. Qualcuno può indicarmi la giusta direzione?


3
Quale versione di SQL Server, per favore?
Aaron Bertrand,

1

Risposte:


251

PIVOT SQL dinamico:

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

risultati:

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

Quindi \ @cols deve essere concatenato con le stringhe, giusto? Non possiamo usare sp_executesql e il parametro-binding per interpolare \ @cols lì dentro? Anche se costruiamo noi stessi \ @cols, e se in qualche modo contenesse SQL dannoso. Eventuali ulteriori misure attenuanti che potrei prendere prima di concatenarlo ed eseguirlo?
The Red Pea,

Come ordineresti le righe e le colonne su questo?
Patrick Schomburg,

@PatrickSchomburg Esistono diversi modi: se si desidera ordinare i file, @colsè possibile rimuovere DISTINCTe utilizzare GROUP BYe ORDER BYquando si ottiene l'elenco di @cols.
Taryn

Ci proverò. E le file? Sto usando anche una data e non esce in ordine.
Patrick Schomburg,

1
Non importa, stavo mettendo l'ordine nel posto sbagliato.
Patrick Schomburg,

27

PIVOT SQL dinamico

Approccio diverso per la creazione di stringhe di colonne

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

Risultato

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

13

So che questa domanda è più vecchia, ma stavo cercando le risposte e ho pensato che avrei potuto espandere la parte "dinamica" del problema e forse aiutare qualcuno.

Innanzitutto ho creato questa soluzione per risolvere un problema che un paio di colleghi stavano riscontrando con insistenti e grandi set di dati che dovevano essere girati rapidamente.

Questa soluzione richiede la creazione di una procedura memorizzata, quindi se è fuori questione per le tue esigenze, ti preghiamo di smettere di leggere ora.

Questa procedura prenderà in considerazione le variabili chiave di un'istruzione pivot per creare in modo dinamico istruzioni pivot per diverse tabelle, nomi di colonne e aggregati. La colonna Statica viene utilizzata come colonna gruppo / identità per il pivot (questo può essere rimosso dal codice se non necessario ma è abbastanza comune nelle istruzioni pivot ed era necessario per risolvere il problema originale), la colonna pivot è dove i nomi delle colonne risultanti dalla fine verranno generati e la colonna del valore è ciò a cui verrà applicato l'aggregato. Il parametro Table è il nome della tabella incluso lo schema (schema.tablename) questa parte del codice potrebbe usare un po 'd'amore perché non è così pulito come vorrei che fosse. Ha funzionato per me perché il mio utilizzo non era rivolto pubblicamente e l'iniezione sql non era un problema.

Iniziamo con il codice per creare la procedura memorizzata. Questo codice dovrebbe funzionare in tutte le versioni di SSMS 2005 e successive, ma non l'ho testato nel 2005 o nel 2016, ma non riesco a capire perché non funzionerebbe.

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

Successivamente prepareremo i nostri dati per l'esempio. Ho preso l'esempio di dati dalla risposta accettata con l'aggiunta di un paio di elementi di dati da utilizzare in questa dimostrazione del concetto per mostrare i vari risultati della modifica aggregata.

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)

I seguenti esempi mostrano le varie istruzioni di esecuzione che mostrano i vari aggregati come semplice esempio. Non ho scelto di cambiare le colonne statica, pivot e value per mantenere semplice l'esempio. Dovresti essere in grado di copiare e incollare il codice per iniziare a scherzare da solo

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'

Questa esecuzione restituisce rispettivamente i seguenti set di dati.

inserisci qui la descrizione dell'immagine


Buon lavoro! Potete per favore fare un'opzione di TVF invece della procedura memorizzata. Sarebbe conveniente selezionare da tale TVF.
Przemyslaw Remin,

3
Purtroppo no, per quanto ne so, perché non è possibile avere una struttura dinamica per un TVF. Devi avere un set statico di colonne in un TVF.
SFrejofsky,

8

Versione aggiornata per SQL Server 2017 utilizzando la funzione STRING_AGG per costruire l'elenco di colonne pivot:

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;

6

Puoi farlo usando TSQL dinamico (ricorda di usare QUOTENAME per evitare attacchi di iniezione SQL):

Pivot con colonne dinamiche in SQL Server 2005

SQL Server - Tabella dinamica PIVOT - Iniezione SQL

Riferimento obbligatorio a The Curse and Blessings of Dynamic SQL


11
FWIW QUOTENAMEaiuta gli attacchi di iniezione SQL solo se si accetta @tableName come parametro da un utente e lo si aggiunge a una query come SET @sql = 'SELECT * FROM ' + @tableName;. Puoi creare molte stringhe SQL dinamiche vulnerabili e QUOTENAMEnon ti leccare per aiutarti.
Aaron Bertrand,

2
@davids Fare riferimento a questa meta discussione . Se rimuovi i collegamenti ipertestuali, la tua risposta è incompleta.
Kermit

@Kermit, sono d'accordo che mostrare il codice sia più utile, ma stai dicendo che è necessario per essere una risposta? Senza i collegamenti, la mia risposta è "È possibile ottenere ciò utilizzando TSQL dinamico". La risposta selezionata suggerisce lo stesso percorso, con l'ulteriore vantaggio se mostra anche come farlo, motivo per cui è stata selezionata come risposta.
davids

2
Ho votato a favore della risposta selezionata (prima che fosse selezionata) perché aveva un esempio e aiuterà meglio qualcuno di nuovo. Tuttavia, penso che qualcuno di nuovo dovrebbe anche leggere i link che ho fornito, motivo per cui non li ho rimossi.
davids

3

C'è la mia soluzione per ripulire i valori nulli inutili

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

2

Il codice seguente fornisce i risultati che sostituiscono NULL a zero nell'output.

Creazione tabella e inserimento dati:

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)

Interrogazione per generare i risultati esatti che sostituisce anche NULL con zeri:

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

PRODUZIONE :

inserisci qui la descrizione dell'immagine

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.