Dynamische PIVOT-Abfrage von SQL Server?


202

Ich wurde beauftragt, ein Mittel zur Übersetzung der folgenden Daten zu finden:

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

in die folgenden:

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

Die leeren Stellen können NULL oder Leerzeichen sein, entweder ist das in Ordnung, und die Kategorien müssten dynamisch sein. Eine weitere mögliche Einschränkung besteht darin, dass die Abfrage in einer begrenzten Kapazität ausgeführt wird, was bedeutet, dass temporäre Tabellen nicht verfügbar sind. Ich habe versucht zu recherchieren und bin darauf gelandet, PIVOTaber da ich das noch nie benutzt habe, verstehe ich es wirklich nicht, trotz meiner besten Bemühungen, es herauszufinden. Kann mich jemand in die richtige Richtung weisen?


3
Welche Version von SQL Server bitte?
Aaron Bertrand

1
Mögliches Duplikat von Write Advanced SQL Select
RichardTheKiwi

Antworten:


250

Dynamisches 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

Ergebnisse:

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

Also muss \ @cols string-verkettet sein, oder? Wir können sp_executesql und Parameterbindung nicht verwenden, um \ @cols dort zu interpolieren? Obwohl wir \ @cols selbst erstellen, was ist, wenn es irgendwie bösartiges SQL enthält? Gibt es zusätzliche mildernde Schritte, die ich unternehmen könnte, bevor ich es verkette und ausführe?
Die rote Erbse

Wie würden Sie die Zeilen und Spalten danach sortieren?
Patrick Schomburg

@PatrickSchomburg Es gibt verschiedene Möglichkeiten - wenn Sie die sortieren möchten, können @colsSie die entfernen DISTINCTund verwenden GROUP BYund ORDER BYwenn Sie die Liste von erhalten @cols.
Taryn

Ich werde das versuchen. Was ist mit den Zeilen? Ich verwende auch ein Datum und es kommt nicht in der richtigen Reihenfolge heraus.
Patrick Schomburg

1
Egal, ich habe die Bestellung an der falschen Stelle aufgegeben.
Patrick Schomburg

27

Dynamisches SQL PIVOT

Unterschiedlicher Ansatz zum Erstellen von Spaltenzeichenfolgen

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

Ergebnis

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

Ich weiß, dass diese Frage älter ist, aber ich habe die Antworten durchgesehen und dachte, ich könnte den "dynamischen" Teil des Problems erweitern und möglicherweise jemandem helfen.

In erster Linie habe ich diese Lösung entwickelt, um ein Problem zu lösen, das einige Mitarbeiter mit inkonstanten und großen Datenmengen hatten, die schnell geschwenkt werden mussten.

Diese Lösung erfordert die Erstellung einer gespeicherten Prozedur. Wenn dies für Ihre Anforderungen nicht in Frage kommt, hören Sie jetzt bitte auf zu lesen.

Bei dieser Prozedur werden die Schlüsselvariablen einer Pivot-Anweisung berücksichtigt, um dynamisch Pivot-Anweisungen für verschiedene Tabellen, Spaltennamen und Aggregate zu erstellen. Die Spalte "Statisch" wird als Spalte "Gruppieren nach / Identität" für den Pivot verwendet (diese kann bei Bedarf aus dem Code entfernt werden, ist jedoch in Pivot-Anweisungen häufig anzutreffen und war zur Lösung des ursprünglichen Problems erforderlich) Die resultierenden Spaltennamen am Ende werden generiert, und auf die Wertespalte wird das Aggregat angewendet. Der Table-Parameter ist der Name der Tabelle, einschließlich des Schemas (schema.tabellenname). Dieser Teil des Codes könnte etwas Liebe gebrauchen, da er nicht so sauber ist, wie ich es gerne hätte. Es hat bei mir funktioniert, weil meine Nutzung nicht öffentlich zugänglich war und die SQL-Injektion kein Problem darstellte.

Beginnen wir mit dem Code zum Erstellen der gespeicherten Prozedur. Dieser Code sollte in allen Versionen von SSMS 2005 und höher funktionieren, aber ich habe ihn 2005 oder 2016 nicht getestet, kann aber nicht erkennen, warum er nicht funktionieren würde.

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

Als nächstes bereiten wir unsere Daten für das Beispiel vor. Ich habe das Datenbeispiel der akzeptierten Antwort mit einigen Datenelementen entnommen, die in diesem Proof of Concept verwendet werden sollen, um die unterschiedlichen Ergebnisse der aggregierten Änderung zu zeigen.

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)

Die folgenden Beispiele zeigen die verschiedenen Ausführungsanweisungen, die die verschiedenen Aggregate als einfaches Beispiel zeigen. Ich habe mich nicht dafür entschieden, die statischen, Pivot- und Wertespalten zu ändern, um das Beispiel einfach zu halten. Sie sollten in der Lage sein, den Code einfach zu kopieren und einzufügen, um selbst damit herumzuspielen

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'

Diese Ausführung gibt jeweils die folgenden Datensätze zurück.

Geben Sie hier die Bildbeschreibung ein


Gut gemacht! Können Sie bitte eine Option für TVF anstelle einer gespeicherten Prozedur festlegen? Wäre bequem aus einem solchen TVF auszuwählen.
Przemyslaw Remin

3
Leider nicht nach meinem besten Wissen, da Sie keine dynamische Struktur für eine TVF haben können. Sie müssen einen statischen Satz von Spalten in einer TVF haben.
SFrejofsky

8

Aktualisierte Version für SQL Server 2017 mit der Funktion STRING_AGG zum Erstellen der Pivot-Spaltenliste:

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

Sie können dies mit dynamischem TSQL erreichen (denken Sie daran, QUOTENAME zu verwenden, um SQL-Injection-Angriffe zu vermeiden):

Pivots mit dynamischen Spalten in SQL Server 2005

SQL Server - Dynamische PIVOT-Tabelle - SQL Injection

Obligatorischer Verweis auf den Fluch und den Segen von Dynamic SQL


11
FWIW QUOTENAMEhilft SQL-Injection-Angriffen nur, wenn Sie @tableName als Parameter von einem Benutzer akzeptieren und an eine Abfrage wie anhängen SET @sql = 'SELECT * FROM ' + @tableName;. Sie können viele anfällige dynamische SQL-Zeichenfolgen erstellen und QUOTENAMEwerden nicht lecken, um Ihnen zu helfen.
Aaron Bertrand

2
@davids Bitte beziehen Sie sich auf diese Meta-Diskussion . Wenn Sie die Hyperlinks entfernen, ist Ihre Antwort unvollständig.
Kermit

@Kermit, ich stimme zu, dass das Anzeigen des Codes hilfreicher ist, aber sagen Sie, dass es erforderlich ist, damit es eine Antwort ist? Ohne die Links lautet meine Antwort "Sie können dies mit dynamischem TSQL erreichen". Die ausgewählte Antwort schlägt dieselbe Route vor, mit dem zusätzlichen Vorteil, dass auch gezeigt wird, wie dies zu tun ist, weshalb sie als Antwort ausgewählt wurde.
David

2
Ich habe die ausgewählte Antwort (bevor sie ausgewählt wurde) hochgestimmt, weil sie ein Beispiel hatte und besser jemandem hilft, der neu ist. Ich denke jedoch, dass jemand, der neu ist, auch die von mir bereitgestellten Links lesen sollte, weshalb ich sie nicht entfernt habe.
David

3

Es gibt meine Lösung, die die unnötigen Nullwerte bereinigt

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

Der folgende Code liefert die Ergebnisse, die NULL in der Ausgabe durch Null ersetzen .

Tabellenerstellung und Dateneinfügung:

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)

Abfrage, um die genauen Ergebnisse zu generieren, die auch NULL durch Nullen ersetzen:

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

AUSGABE :

Geben Sie hier die Bildbeschreibung ein

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.