Requête PIVOT dynamique SQL Server?

203

J'ai été chargé de trouver un moyen de traduire les données suivantes:

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

dans ce qui suit:

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

Les espaces vides peuvent être des valeurs NULL ou des espaces vides, que ce soit bien, et les catégories devraient être dynamiques. Une autre mise en garde possible est que nous exécuterons la requête dans une capacité limitée, ce qui signifie que les tables temporaires sont épuisées. J'ai essayé de faire des recherches et j'ai atterri, PIVOTmais comme je ne l'ai jamais utilisé auparavant, je ne le comprends vraiment pas, malgré tous mes efforts pour le comprendre. Est-ce que quelqu'un peut-il me montrer la bonne direction?

Sean Cunningham
la source
3
Quelle version de SQL Server s'il vous plaît?
Aaron Bertrand
1
doublon possible de Write advanced SQL Select
RichardTheKiwi

Réponses:

251

PIVOT SQL dynamique:

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

Résultats:

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
Taryn
la source
Donc, \ @cols doit être concaténé en chaîne, non? Nous ne pouvons pas utiliser sp_executesql et la liaison de paramètres pour interpoler \ @cols là-dedans? Même si nous construisons \ @cols nous-mêmes, que se passe-t-il s'il contient en quelque sorte du SQL malveillant. Des mesures d'atténuation supplémentaires que je pourrais prendre avant de les concaténer et de les exécuter?
The Red Pea
Comment classeriez-vous les lignes et les colonnes à ce sujet?
Patrick Schomburg
@PatrickSchomburg Il existe différentes manières - si vous souhaitez trier le, @colsvous pouvez le supprimer DISTINCTet l'utiliser GROUP BYet ORDER BYlorsque vous obtenez la liste de @cols.
Taryn
Je vais essayer ça. Et les rangées? J'utilise aussi une date, et elle ne sort pas dans l'ordre.
Patrick Schomburg
1
Peu importe, je mettais la commande au mauvais endroit.
Patrick Schomburg
27

PIVOT SQL dynamique

Approche différente pour créer une chaîne de colonnes

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

Résultat

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
mkdave99
la source
13

Je sais que cette question est plus ancienne mais je cherchais à travers les réponses et pensais que je pourrais être en mesure d'étendre la partie "dynamique" du problème et éventuellement aider quelqu'un.

Tout d'abord, j'ai conçu cette solution pour résoudre un problème que deux collègues rencontraient avec des ensembles de données inconstants et volumineux devant être pivotés rapidement.

Cette solution nécessite la création d'une procédure stockée, donc si cela est hors de question pour vos besoins, veuillez arrêter de lire maintenant.

Cette procédure va prendre les variables clés d'une instruction pivot pour créer dynamiquement des instructions pivot pour différents tableaux, noms de colonnes et agrégats. La colonne statique est utilisée comme colonne de regroupement par / identité pour le pivot (cela peut être supprimé du code si ce n'est pas nécessaire mais est assez courant dans les instructions pivot et était nécessaire pour résoudre le problème d'origine), la colonne pivot est l'endroit où le les noms de colonne résultants finaux seront générés à partir de, et la colonne de valeur sera celle à laquelle l'agrégat sera appliqué. Le paramètre Table est le nom de la table, y compris le schéma (schema.tablename), cette partie du code pourrait utiliser un peu d'amour car elle n'est pas aussi propre que je le souhaiterais. Cela a fonctionné pour moi car mon utilisation n'était pas publique et l'injection SQL n'était pas un problème.

Commençons par le code pour créer la procédure stockée. Ce code devrait fonctionner dans toutes les versions de SSMS 2005 et supérieur mais je ne l'ai pas testé en 2005 ou 2016 mais je ne vois pas pourquoi cela ne fonctionnerait pas.

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

Ensuite, nous préparerons nos données pour l'exemple. J'ai pris l'exemple de données de la réponse acceptée avec l'ajout de quelques éléments de données à utiliser dans cette preuve de concept pour montrer les sorties variées du changement global.

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)

Les exemples suivants montrent les instructions d'exécution variées montrant les agrégats variés comme exemple simple. Je n'ai pas choisi de modifier les colonnes statique, pivot et valeur pour garder l'exemple simple. Vous devriez pouvoir simplement copier et coller le code pour commencer à jouer avec lui-même

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'

Cette exécution renvoie respectivement les ensembles de données suivants.

entrez la description de l'image ici

SFrejofsky
la source
Bon travail! Pouvez-vous s'il vous plaît faire une option de TVF au lieu de la procédure stockée. Serait pratique de choisir parmi ces TVF.
Przemyslaw Remin
3
Malheureusement non, à ma connaissance, car vous ne pouvez pas avoir une structure dynamique pour un TVF. Vous devez avoir un ensemble statique de colonnes dans un TVF.
SFrejofsky
8

Version mise à jour pour SQL Server 2017 à l'aide de la fonction STRING_AGG pour construire la liste des colonnes pivotantes:

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;
nvogel
la source
6

Vous pouvez y parvenir en utilisant TSQL dynamique (n'oubliez pas d'utiliser QUOTENAME pour éviter les attaques par injection SQL):

Pivots avec colonnes dynamiques dans SQL Server 2005

SQL Server - Table PIVOT dynamique - Injection SQL

Référence obligatoire à The Curse and Blessings of Dynamic SQL

davids
la source
11
FWIW QUOTENAMEn'aide les attaques par injection SQL que si vous acceptez @tableName comme paramètre d'un utilisateur et que vous l'ajoutez à une requête comme SET @sql = 'SELECT * FROM ' + @tableName;. Vous pouvez créer de nombreuses chaînes SQL dynamiques vulnérables et QUOTENAMEne faites rien pour vous aider.
Aaron Bertrand
2
@davids Veuillez vous référer à cette méta discussion . Si vous supprimez les hyperliens, votre réponse est incomplète.
Kermit
@Kermit, je suis d'accord que montrer le code est plus utile, mais dites-vous qu'il est nécessaire pour qu'il soit une réponse? Sans les liens, ma réponse est "Vous pouvez y parvenir en utilisant TSQL dynamique". La réponse sélectionnée suggère le même itinéraire, avec l'avantage supplémentaire si elle montre également comment le faire, c'est pourquoi elle a été sélectionnée comme réponse.
davids
2
J'ai voté pour la réponse sélectionnée (avant qu'elle ne soit sélectionnée) car elle avait un exemple et aiderait mieux quelqu'un de nouveau. Cependant, je pense que quelqu'un de nouveau devrait également lire les liens que j'ai fournis, c'est pourquoi je ne les ai pas supprimés.
davids
3

Il y a ma solution pour nettoyer les valeurs nulles inutiles

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);
m0rg4n
la source
2

Le code ci-dessous fournit les résultats qui remplacent NULL à zéro dans la sortie.

Création de table et insertion de données:

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)

Requête pour générer les résultats exacts qui remplacent également NULL par des zéros:

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

PRODUCTION :

entrez la description de l'image ici

Arockia Nirmal
la source