web-dev-qa-db-fra.com

Comment faire pivoter sur plusieurs colonnes dans SQL Server?

Quelle est la meilleure façon d'aplatir les tableaux sur une seule ligne?

Par exemple, avec le tableau suivant:

+-----+-------+-------------+------------------+
| Id  | hProp | iDayOfMonth | dblTargetPercent |
+-----+-------+-------------+------------------+
| 117 |    10 |           5 |           0.1400 |
| 118 |    10 |          10 |           0.0500 |
| 119 |    10 |          15 |           0.0100 |
| 120 |    10 |          20 |           0.0100 |
+-----+-------+-------------+------------------+

Je voudrais produire le tableau suivant:

+-------+--------------+-------------------+--------------+-------------------+--------------+-------------------+--------------+-------------------+
| hProp | iDateTarget1 | dblPercentTarget1 | iDateTarget2 | dblPercentTarget2 | iDateTarget3 | dblPercentTarget3 | iDateTarget4 | dblPercentTarget4 |
+-------+--------------+-------------------+--------------+-------------------+--------------+-------------------+--------------+-------------------+
|    10 |            5 |              0.14 |           10 |              0.05 |           15 |              0.01 |           20 |              0.01 |
+-------+--------------+-------------------+--------------+-------------------+--------------+-------------------+--------------+-------------------+

J'ai réussi à le faire en utilisant un pivot et en rejoignant ensuite la table d'origine plusieurs fois, mais je suis assez sûr qu'il existe une meilleure façon. Cela fonctionne comme prévu:

select
X0.hProp,
X0.iDateTarget1,
X1.dblTargetPercent [dblPercentTarget1],
X0.iDateTarget2,
X2.dblTargetPercent [dblPercentTarget2],
X0.iDateTarget3,
X3.dblTargetPercent [dblPercentTarget3],
X0.iDateTarget4,
X4.dblTargetPercent [dblPercentTarget4]
from (
    select
        hProp,
        max([1]) [iDateTarget1],
        max([2]) [iDateTarget2],
        max([3]) [iDateTarget3],
        max([4]) [iDateTarget4]
    from (
        select
            *,
            rank() over (partition by hProp order by iWeek) rank#
        from [Table X]
    ) T
    pivot (max(iWeek) for rank# in ([1],[2],[3], [4])) pv
    group by hProp
) X0
left join [Table X] X1 on X1.hprop = X0.hProp and X1.iWeek = X0.iDateTarget1
left join [Table X] X2 on X2.hprop = X0.hProp and X2.iWeek = X0.iDateTarget2
left join [Table X] X3 on X3.hprop = X0.hProp and X3.iWeek = X0.iDateTarget3
left join [Table X] X4 on X4.hprop = X0.hProp and X4.iWeek = X0.iDateTarget4
8
Zach Smith

Voici une façon d'obtenir le jeu de résultats souhaité sans effectuer les jointures multiples. Il prend un peu plus de configuration et utilise deux opérations de pivot au lieu d'une, mais évite les jointures multiples.

J'avoue que je devais le chercher, mais Ken O'Bonn avait un excellent article. https://blogs.msdn.Microsoft.com/kenobonn/2009/03/22/pivot-on-two-or-more-fields-in-sql-server/

/** Build up a Table to work with. **/
DECLARE @T TABLE
    (
    ID INT NOT NULL PRIMARY KEY
    , hProp INT NOT NULL
    , iDayOfMonth INT NOT NULL
    , dblTargetPercent DECIMAL(6,4) NOT NULL
    )

INSERT INTO @T
(ID, hProp, iDayOfMonth, dblTargetPercent)
VALUES (117,10,5,0.1400)
        , (118, 10, 10, 0.0500) 
        , (119, 10, 15, 0.0100)
        , (120, 10, 20, 0.0100)

/** Create a CTE and give us predictable names to work with for
    date and percentage
    **/
;WITH CTE_Rank AS
    (
    SELECT ID
        , hProp
        , iDayOfMonth 
        , dblTargetPercent 
        , sDateName = 'iDateTarget' + CAST(DENSE_RANK() OVER (PARTITION BY hPRop ORDER BY iDayOfMonth) AS VARCHAR(10))
        , sPercentName = 'dblPercentTarget' + CAST(DENSE_RANK() OVER (PARTITION BY hPRop ORDER BY iDayOfMonth) AS VARCHAR(10))
    FROM @T
    )
SELECT hProp 
    , iDateTarget1 = MAX(iDateTarget1)
    , dblPercentTarget1 = MAX(dblPercentTarget1)
    , iDateTarget2 = MAX(iDateTarget2)
    , dblPercentTarget2 = MAX(dblPercentTarget2)
    , iDateTarget3 = MAX(iDateTarget3)
    , dblPercentTarget3 = MAX(dblPercentTarget3)
    , iDateTarget4 = MAX(iDateTarget4)
    , dblPercentTarget4 = MAX(dblPercentTarget4)
FROM CTE_Rank AS R
    PIVOT(MAX(iDayOfMonth) FOR sDateName IN ([iDateTarget1], [iDateTarget2], [iDateTarget3], [iDateTarget4])) AS DayOfMonthName 
    PIVOT(MAX(dblTargetPercent) FOR sPercentName IN (dblPercentTarget1, dblPercentTarget2, dblPercentTarget3, dblPercentTarget4)) AS TargetPercentName
GROUP BY hProp
10
Jonathan Fite

Donné:

DECLARE @T table
(
    ID integer NOT NULL PRIMARY KEY,
    hProp integer NOT NULL,
    iDayOfMonth integer NOT NULL,
    dblTargetPercent decimal(6,4) NOT NULL
);

INSERT @T
    (ID, hProp, iDayOfMonth, dblTargetPercent)
VALUES 
    (117, 10, 05, 0.1400),
    (118, 10, 10, 0.0500),
    (119, 10, 15, 0.0100),
    (120, 10, 20, 0.0100);

Vous pouvez obtenir le résultat décrit avec un pivot manuel:

WITH Ranked AS
(
    SELECT
        T.*,
        rn = ROW_NUMBER() OVER (
            PARTITION BY T.hProp 
            ORDER BY T.iDayOfMonth)
    FROM @T AS T
)
SELECT
    R.hProp,
    iDateTarget1 =      MAX(CASE WHEN R.rn = 1 THEN R.iDayOfMonth END),
    dblPercentTarget1 = MAX(CASE WHEN R.rn = 1 THEN R.dblTargetPercent END),
    iDateTarget2 =      MAX(CASE WHEN R.rn = 2 THEN R.iDayOfMonth END),
    dblPercentTarget1 = MAX(CASE WHEN R.rn = 2 THEN R.dblTargetPercent END),
    iDateTarget3 =      MAX(CASE WHEN R.rn = 3 THEN R.iDayOfMonth END),
    dblPercentTarget3 = MAX(CASE WHEN R.rn = 3 THEN R.dblTargetPercent END),
    iDateTarget4 =      MAX(CASE WHEN R.rn = 4 THEN R.iDayOfMonth END),
    dblPercentTarget4 = MAX(CASE WHEN R.rn = 4 THEN R.dblTargetPercent END)
FROM Ranked AS R
GROUP BY
    R.hProp;

db <> violon ici

8
Paul White 9

Je préfère annuler le pivotement en utilisant l'application croisée puis utiliser un seul pivot. Il y a un problème avec cette technique car les deux valeurs finiront par être mappées sur la même colonne (type), ce qui doit être traité à la sortie pour être refondu au type approprié. Cependant, d'après mon expérience, cette méthode fonctionne très bien avec des ensembles de données plus volumineux. Notez qu'il n'y a pas de regroupement.

En utilisant les mêmes données source:

;with src (hProp, iDayOfMonth, dblTargetPercent, rw) as (
select hProp, iDayOfMonth, dblTargetPercent, 
    ROW_NUMBER() over (partition by hProp order by iDayOfMonth) 
from @T
)
,unpvt(hProp, typ, val) as (
select hprop, ca.typ + ltrim(rw), ca.val from src
cross apply (values (iDayOfMonth, 'iDayOfMonth'),(dblTargetPercent, 'dblTargetPercent')) ca (val, typ)
)

select *
from unpvt
pivot (max(val) for typ in ([iDayOfMonth1],[dblTargetPercent1],[iDayOfMonth2],[dblTargetPercent2],
    [iDayOfMonth3],[dblTargetPercent3],[iDayOfMonth4],[dblTargetPercent4]))p

J'ai testé ma solution avec 4 millions de lignes en utilisant le code suivant pour générer des données de test:

if object_id(N'tempdb..#T',N'U') is not null drop table #T
create table #T(ID int identity(1,1) primary key clustered,
    hProp int not null, iDayOfMonth int not null, dblTargetPercent decimal(6,4) not null)

;with src(hProp) as (
select 1 union all 
select hProp+1 from src where hProp+1 <= 1000000
)
,dta(iDayOfMonth, dblTargetPercent) as (
select 5,  0.1400 union all 
select 10, 0.0500 union all 
select 15, 0.0100 union all 
select 20, 0.0100
)
insert #T(hProp, iDayOfMonth, dblTargetPercent)
select hProp, iDayOfMonth, dblTargetPercent
from src, dta
option(maxrecursion 0)

La solution de Paul White est la plus rapide, suivie de la mienne. La vieille école gagne!

2
Keith Gresham