Dynamiczne zapytanie PIVOT programu SQL Server?


202

Miałem zadanie wymyślić sposób na przetłumaczenie następujących danych:

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

w następujący sposób:

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

Puste miejsca mogą mieć wartości NULL lub puste, albo jest w porządku, a kategorie musiałyby być dynamiczne. Innym możliwym zastrzeżeniem jest to, że będziemy uruchamiać kwerendę z ograniczoną pojemnością, co oznacza, że ​​tabele tymczasowe są niedostępne. Próbowałem przeprowadzić badania i wylądowałem, PIVOTale ponieważ nigdy wcześniej tego nie używałem, naprawdę tego nie rozumiem, pomimo moich starań, aby to rozgryźć. Czy ktoś może skierować mnie we właściwym kierunku?


3
Jaką wersję programu SQL Server proszę?
Aaron Bertrand

Odpowiedzi:


250

Dynamiczna SQL PIVOT:

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

Wyniki:

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

Więc \ @cols muszą być połączone w łańcuch, prawda? Nie możemy używać sp_executesql i wiązania parametrów do interpolacji \ @cols? Nawet jeśli sami konstruujemy \ @cols, co jeśli w jakiś sposób zawiera złośliwy SQL. Jakieś dodatkowe kroki łagodzące, które mógłbym podjąć przed połączeniem i wykonaniem?
The Red Pea

Jak posortowałbyś na tym wiersze i kolumny?
Patrick Schomburg

@PatrickSchomburg Istnieje wiele sposobów - jeśli chcesz je posortować, @colsmożesz je usunąć DISTINCTi użyć, GROUP BYa ORDER BYkiedy otrzymasz listę @cols.
Taryn

Spróbuję tego. Co z rzędami? Używam też daty i nie wychodzi ona w porządku.
Patrick Schomburg

1
Nieważne, że składałem zamówienie w złym miejscu.
Patrick Schomburg

27

Dynamiczna SQL PIVOT

Inne podejście do tworzenia ciągu kolumn

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

Wynik

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

Wiem, że to pytanie jest starsze, ale szukałem odpowiedzi i pomyślałem, że mogę rozwinąć „dynamiczną” część problemu i być może komuś pomóc.

Przede wszystkim zbudowałem to rozwiązanie, aby rozwiązać problem, który miał kilku współpracowników z niestabilnymi i dużymi zestawami danych, które należy szybko przestawić.

To rozwiązanie wymaga utworzenia procedury składowanej, więc jeśli nie ma takiej potrzeby, proszę przestań czytać.

Ta procedura będzie obejmować kluczowe zmienne instrukcji przestawnej, aby dynamicznie tworzyć instrukcje przestawne dla różnych tabel, nazw kolumn i agregatów. Kolumna statyczna jest używana jako kolumna grupowania według / tożsamości dla elementu przestawnego (można go usunąć z kodu, jeśli nie jest to konieczne, ale jest dość powszechne w instrukcjach przestawnych i było konieczne do rozwiązania pierwotnego problemu), kolumna przestawna to miejsce, w którym końcowe kolumny wynikowe zostaną wygenerowane, a kolumna wartości jest tym, do czego agregat zostanie zastosowany. Parametr Table to nazwa tabeli zawierającej schemat (schema.tablename), w tej części kodu można by użyć trochę miłości, ponieważ nie jest ona tak czysta, jak bym tego chciał. Działa to dla mnie, ponieważ moje użycie nie było publicznie dostępne, a zastrzyk SQL nie był problemem.

Zacznijmy od kodu, aby utworzyć procedurę składowaną. Ten kod powinien działać we wszystkich wersjach SSMS 2005 i nowszych, ale nie testowałem go w 2005 ani 2016 roku, ale nie rozumiem, dlaczego to nie działa.

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

Następnie przygotujemy nasze dane do przykładu. Wziąłem przykład danych z zaakceptowanej odpowiedzi, dodając kilka elementów danych do wykorzystania w tym dowodzie koncepcji, aby pokazać różne wyniki łącznej zmiany.

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)

Poniższe przykłady pokazują zróżnicowane instrukcje wykonania, pokazujące różne agregaty jako prosty przykład. Nie zdecydowałem się zmienić kolumny statycznej, przestawnej i wartościowej, aby zachować prosty przykład. Powinieneś być w stanie po prostu skopiować i wkleić kod, aby samemu z nim zadzierać

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'

To wykonanie zwraca odpowiednio następujące zestawy danych.

wprowadź opis zdjęcia tutaj


Dobra robota! Czy możesz wybrać opcję TVF zamiast procedury składowanej. Byłoby wygodnie wybrać z takich TVF.
Przemysław Remin

3
Niestety nie, zgodnie z moją najlepszą wiedzą, ponieważ nie można mieć dynamicznej struktury dla TVF. Musisz mieć statyczny zestaw kolumn w TVF.
SFrejofsky

8

Zaktualizowana wersja dla SQL Server 2017 przy użyciu funkcji STRING_AGG do utworzenia listy kolumn przestawnych:

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

Możesz to osiągnąć za pomocą dynamicznego TSQL (pamiętaj, aby użyć QUOTENAME, aby uniknąć ataków typu SQL injection):

Pivots z dynamicznymi kolumnami w SQL Server 2005

SQL Server - Dynamiczna tabela PIVOT - SQL Injection

Obowiązkowe odniesienie do Klątwy i błogosławieństw dynamicznego SQL


11
FWIW QUOTENAMEpomaga w atakach SQL injection tylko wtedy, gdy akceptujesz @tableName jako parametr od użytkownika i dołączasz go do zapytania takiego jak SET @sql = 'SELECT * FROM ' + @tableName;. Możesz zbudować wiele wrażliwych dynamicznych ciągów SQL i QUOTENAMEnie zrobisz nic, by ci pomóc.
Aaron Bertrand

2
@davids Proszę odnieść się do tej meta dyskusji . Jeśli usuniesz hiperłącza, twoja odpowiedź będzie niepełna.
Kermit

@Kermit, zgadzam się, że pokazanie kodu jest bardziej pomocne, ale czy mówisz, że jest on wymagany, aby był odpowiedzią? Bez linków moja odpowiedź brzmi: „Możesz to osiągnąć za pomocą dynamicznego TSQL”. Wybrana odpowiedź sugeruje tę samą trasę, z dodatkową korzyścią, jeśli pokazuje także, jak to zrobić, dlatego została wybrana jako odpowiedź.
David

2
Głosowałem za wybraną odpowiedzią (zanim została wybrana), ponieważ miała ona przykład i lepiej pomoże komuś nowemu. Myślę jednak, że ktoś nowy powinien również przeczytać linki, które podałem, dlatego ich nie usunąłem.
David

3

Moje rozwiązanie usuwa niepotrzebne wartości zerowe

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

Poniższy kod przedstawia wyniki, które zastępują wartość NULL na zero na wyjściu.

Tworzenie tabeli i wstawianie danych:

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)

Zapytanie w celu wygenerowania dokładnych wyników, które również zastępują NULL zerami:

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

WYNIK :

wprowadź opis zdjęcia tutaj

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.