Friday, September 30, 2011

Oracle's UNPIVOT Clause -- Going Further

Let's start with a nice little data set that has some un-pivot potential:

/* ===== code ===== */     
   
   WITH
     w_data  as
       ( select  'NY' st, 1 rgn, 13 q1cnt, 245.25 q1amt, 13 q2cnt, 245.25 q2amt, 13 q3cnt, 245.25 q3amt, 13 q4cnt, 245.25 q4amt  from dual union all
         select  'NY'   , 2    , 43      , 760.34      , 38      , 842.11      , 39      , 425.98      , 54      , 571.53        from dual union all
         select  'NY'   , 3    , 34      , 743.21      , 38      , 627.24      , null    , 463.74      , 41      , 154.96        from dual union all
         select  'HI'   , 1    , 14      , 256.13      , 11      , 532.48      , 10      , 246.37      , 18      , 261.37        from dual union all
         select  'OR'   , 1    , 73      , 758.14      , 71      , 132.84      , 63      , 623.74      , 82      , 851.92        from dual union all
         select  'OR'   , 2    , null    , null        , 62      , 275.11      , 73      , 846.32      , 70      , 253.19        from dual union all
         select  'UT'   , 1    , 27      , 156.38      , 32      , 637.19      , 31      , 256.48      , 39      , 146.75        from dual union all
         select  'UT'   , 2    , 24      , 264.82      , 20      , 423.61      , 21      , null        , 18      , 367.88        from dual union all
         select  'MT'   , 1    ,  3      , 374.28      ,  8      , 125.35      ,  5      , 423.66      ,  7      , 536.14        from dual 
       )
   select * from w_data ;
   

Here we have a couple of identifying columns (State and Region) that don't really have anything to do with the Un-Pivot -- they will just need to be associated with any measure that appears in their row. We also have eight columns of measures, two measures for each quarter of the year (Count and Amount). Un-Pivoting the data could mean that we take all these measures and un-pivot them into one single measures column, or it could mean that we un-pivot the four "Count" columns into one measures column and the four "Amount" columns into another measures column.





For the first example, let's start with that last one mentioned -- we'll un-pivot the data into two measure columns. We'll have a single "CNT" column and a seperate "AMT" column for the measures. We'll also create a new "QTR" colum that will contain an identifier that tells us which quarter the two measures came from.

Here is the solution. See the explanation and results below:

/* ===== code ===== */     
   
   WITH
     w_data  as
       ( select  'NY' st, 1 rgn, 13 q1cnt, 245.25 q1amt, 13 q2cnt, 245.25 q2amt, 13 q3cnt, 245.25 q3amt, 13 q4cnt, 245.25 q4amt  from dual union all
         select  'NY'   , 2    , 43      , 760.34      , 38      , 842.11      , 39      , 425.98      , 54      , 571.53        from dual union all
         select  'NY'   , 3    , 34      , 743.21      , 38      , 627.24      , null    , 463.74      , 41      , 154.96        from dual union all
         select  'HI'   , 1    , 14      , 256.13      , 11      , 532.48      , 10      , 246.37      , 18      , 261.37        from dual union all
         select  'OR'   , 1    , 73      , 758.14      , 71      , 132.84      , 63      , 623.74      , 82      , 851.92        from dual union all
         select  'OR'   , 2    , null    , null        , 62      , 275.11      , 73      , 846.32      , 70      , 253.19        from dual union all
         select  'UT'   , 1    , 27      , 156.38      , 32      , 637.19      , 31      , 256.48      , 39      , 146.75        from dual union all
         select  'UT'   , 2    , 24      , 264.82      , 20      , 423.61      , 21      , null        , 18      , 367.88        from dual union all
         select  'MT'   , 1    ,  3      , 374.28      ,  8      , 125.35      ,  5      , 423.66      ,  7      , 536.14        from dual 
       )
   select  st,  rgn
        ,  qtr
        ,  cnt,  amt
     from  w_data
  UNPIVOT  include nulls
             (   (   cnt,   amt ) FOR  qtr  IN
               ( ( q1cnt, q1amt )  as   1 
               , ( q2cnt, q2amt )  as   2 
               , ( q3cnt, q3amt )  as   3 
               , ( q4cnt, q4amt )  as   4 
               )
             )
  --where  nvl( cnt, 0 ) < 30
    order  by 1, 2, 3 ;


Explanation:

The "UNPIVOT" clause is placed immediately after the "FROM" clause. If you have a "WHERE" clause it will follow the "UNPIVOT" clause as shown.

The "Include Nulls" phrase will include an output row even if all the newly created measure columns are NULL. Take these two words out of the query and observe how the result-set changes.

Inside the "UNPIVOT" clause we start by creating the set of new measure columns (CNT and AMT). Notice that because we are creating a set (more than one) of new measures we put them in parentesis. These columns will contain all the un-pivoted measures. After the keyword "FOR" we create a new column (QTR) that will contain an identifier that tells us which quarter the measures came from (which original set of columns the specific count and amount measures came from).

After the "IN" keyword we include a parenthesis enclosed list that associates each set of original measure columns with an identifying value. For example, the original columns (Q1CNT and Q1AMT) are assigned the identifier of "1" -- this is the value that will appear in the new QTR column allong side the measures from those two columns.

And, one last item, if you un-comment (remove the "--" from the beginning of) the where clause, then the result-set will filter out any records that have a CNT of 30 or greater.





Notice that in the following example, all eight measure columns are un-pivoted into one single measure column (VAL). The new "VAL_ID" column will contain a text identifier that not only tells which quarter the measure comes from, but also tells us whether it is a "Count" or an "Amount" value.

/* ===== code ===== */     
   
   WITH
     w_data  as
       ( select  'NY' st, 1 rgn, 13 q1cnt, 245.25 q1amt, 13 q2cnt, 245.25 q2amt, 13 q3cnt, 245.25 q3amt, 13 q4cnt, 245.25 q4amt  from dual union all
         select  'NY'   , 2    , 43      , 760.34      , 38      , 842.11      , 39      , 425.98      , 54      , 571.53        from dual union all
         select  'NY'   , 3    , 34      , 743.21      , 38      , 627.24      , null    , 463.74      , 41      , 154.96        from dual union all
         select  'HI'   , 1    , 14      , 256.13      , 11      , 532.48      , 10      , 246.37      , 18      , 261.37        from dual union all
         select  'OR'   , 1    , 73      , 758.14      , 71      , 132.84      , 63      , 623.74      , 82      , 851.92        from dual union all
         select  'OR'   , 2    , null    , null        , 62      , 275.11      , 73      , 846.32      , 70      , 253.19        from dual union all
         select  'UT'   , 1    , 27      , 156.38      , 32      , 637.19      , 31      , 256.48      , 39      , 146.75        from dual union all
         select  'UT'   , 2    , 24      , 264.82      , 20      , 423.61      , 21      , null        , 18      , 367.88        from dual union all
         select  'MT'   , 1    ,  3      , 374.28      ,  8      , 125.35      ,  5      , 423.66      ,  7      , 536.14        from dual 
       )
   select  st,  rgn
        ,  val_id,  val
     from  w_data
  UNPIVOT  include nulls
             (   val    FOR   val_id        IN
               ( q1cnt   as  'Qtr-1 Count'
               , q2cnt   as  'Qtr-2 Count'
               , q3cnt   as  'Qtr-3 Count'
               , q4cnt   as  'Qtr-4 Count'
               , q1amt   as  'Qtr-1 Amount'
               , q2amt   as  'Qtr-2 Amount'
               , q3amt   as  'Qtr-3 Amount'
               , q4amt   as  'Qtr-4 Amount'
               )
             )
    order  by 1, 2, 3 ;


/* --- Partial Results --- */

     --  ST  RGN  VAL_ID        VAL
     --  --  ---  ------------  -------
     --  HI  1    Qtr-1 Amount  256.13
     --  HI  1    Qtr-1 Count   14
     --  HI  1    Qtr-2 Amount  532.48
     --  HI  1    Qtr-2 Count   11
     --  HI  1    Qtr-3 Amount  246.37
     --  HI  1    Qtr-3 Count   10
     --  HI  1    Qtr-4 Amount  261.37
     --  HI  1    Qtr-4 Count   18
     --  MT  1    Qtr-1 Amount  374.28
     --  MT  1    Qtr-1 Count   3
     --  ...




This next example is very similar to the last one. In this query, there are two identifying columns created (VAL_QTR and VAL_TYP) -- the identifying information here is split between these two columns. The first tells us which quarter the measure came from, and the second column tells us whether it was a "Count" or an "Amount" value.

/* ===== code ===== */     
   
   WITH
     w_data  as
       ( select  'NY' st, 1 rgn, 13 q1cnt, 245.25 q1amt, 13 q2cnt, 245.25 q2amt, 13 q3cnt, 245.25 q3amt, 13 q4cnt, 245.25 q4amt  from dual union all
         select  'NY'   , 2    , 43      , 760.34      , 38      , 842.11      , 39      , 425.98      , 54      , 571.53        from dual union all
         select  'NY'   , 3    , 34      , 743.21      , 38      , 627.24      , null    , 463.74      , 41      , 154.96        from dual union all
         select  'HI'   , 1    , 14      , 256.13      , 11      , 532.48      , 10      , 246.37      , 18      , 261.37        from dual union all
         select  'OR'   , 1    , 73      , 758.14      , 71      , 132.84      , 63      , 623.74      , 82      , 851.92        from dual union all
         select  'OR'   , 2    , null    , null        , 62      , 275.11      , 73      , 846.32      , 70      , 253.19        from dual union all
         select  'UT'   , 1    , 27      , 156.38      , 32      , 637.19      , 31      , 256.48      , 39      , 146.75        from dual union all
         select  'UT'   , 2    , 24      , 264.82      , 20      , 423.61      , 21      , null        , 18      , 367.88        from dual union all
         select  'MT'   , 1    ,  3      , 374.28      ,  8      , 125.35      ,  5      , 423.66      ,  7      , 536.14        from dual 
       )
   select  st,  rgn
        ,  val_qtr, val_typ, val
     from  w_data
  UNPIVOT  include nulls
             (   val    FOR  ( val_qtr, val_typ  ) IN
               ( q1cnt   as  ( 'Qtr-1', 'Count'  )
               , q2cnt   as  ( 'Qtr-2', 'Count'  )
               , q3cnt   as  ( 'Qtr-3', 'Count'  )
               , q4cnt   as  ( 'Qtr-4', 'Count'  )
               , q1amt   as  ( 'Qtr-1', 'Amount' )
               , q2amt   as  ( 'Qtr-2', 'Amount' )
               , q3amt   as  ( 'Qtr-3', 'Amount' )
               , q4amt   as  ( 'Qtr-4', 'Amount' )
               )
             )
    order  by 1, 2, 3, 4 ;


/* --- Partial Results --- */

     --  ST  RGN  VAL_QTR  VAL_TYP  VAL
     --  --  ---  -------  -------  -------
     --  HI  1    Qtr-1    Amount   256.13
     --  HI  1    Qtr-1    Count    14
     --  HI  1    Qtr-2    Amount   532.48
     --  HI  1    Qtr-2    Count    11
     --  HI  1    Qtr-3    Amount   246.37
     --  HI  1    Qtr-3    Count    10
     --  HI  1    Qtr-4    Amount   261.37
     --  HI  1    Qtr-4    Count    18
     --  MT  1    Qtr-1    Amount   374.28
     --  MT  1    Qtr-1    Count    3
     --  ...




This final example shows us that we can create as many identifying columns as we wish to include; and that we can define their values to just about anything we would need.

/* ===== code ===== */     
   
   WITH
     w_data  as
       ( select  'NY' st, 1 rgn, 13 q1cnt, 245.25 q1amt, 13 q2cnt, 245.25 q2amt, 13 q3cnt, 245.25 q3amt, 13 q4cnt, 245.25 q4amt  from dual union all
         select  'NY'   , 2    , 43      , 760.34      , 38      , 842.11      , 39      , 425.98      , 54      , 571.53        from dual union all
         select  'NY'   , 3    , 34      , 743.21      , 38      , 627.24      , null    , 463.74      , 41      , 154.96        from dual union all
         select  'HI'   , 1    , 14      , 256.13      , 11      , 532.48      , 10      , 246.37      , 18      , 261.37        from dual union all
         select  'OR'   , 1    , 73      , 758.14      , 71      , 132.84      , 63      , 623.74      , 82      , 851.92        from dual union all
         select  'OR'   , 2    , null    , null        , 62      , 275.11      , 73      , 846.32      , 70      , 253.19        from dual union all
         select  'UT'   , 1    , 27      , 156.38      , 32      , 637.19      , 31      , 256.48      , 39      , 146.75        from dual union all
         select  'UT'   , 2    , 24      , 264.82      , 20      , 423.61      , 21      , null        , 18      , 367.88        from dual union all
         select  'MT'   , 1    ,  3      , 374.28      ,  8      , 125.35      ,  5      , 423.66      ,  7      , 536.14        from dual 
       )
   select  st,   rgn
        ,  qtr,  qnm,  qrn,  qwrd,  qna,  qnb
        ,  cnt,  amt
     from  w_data
  UNPIVOT  include nulls
             (   (   cnt,   amt )  FOR  ( qtr ,        qnm  ,  qrn  ,   qwrd  ,  qna ,  qnb ) IN
               ( ( q1cnt, q1amt )   as  (   1 , 'Quarter-1' ,   'I' ,   'One' , 0.00 , 0.25 )
               , ( q2cnt, q2amt )   as  (   2 , 'Quarter-2' ,  'II' ,   'Two' , 0.25 , 0.50 )
               , ( q3cnt, q3amt )   as  (   3 , 'Quarter-3' , 'III' , 'Three' , 0.50 , 0.75 )
               , ( q4cnt, q4amt )   as  (   4 , 'Quarter-4' ,  'IV' ,  'Four' , 0.75 , 1.00 )
               )
             )
    order  by 1, 2, 3 ;


/* --- Partial Results --- */

     --  ST  RGN  QTR  QNM        QRN  QWRD   QNA    QNB    CNT  AMT
     --  --  ---  ---  ---------  ---  -----  -----  -----  ---  -------
     --  HI  1    1    Quarter-1  I    One    0      0.25    14  256.13
     --  HI  1    2    Quarter-2  II   Two    0.25   0.5     11  532.48
     --  HI  1    3    Quarter-3  III  Three  0.5    0.75    10  246.37
     --  HI  1    4    Quarter-4  IV   Four   0.75   1       18  261.37
     --  MT  1    1    Quarter-1  I    One    0      0.25     3  374.28
     --  MT  1    2    Quarter-2  II   Two    0.25   0.5      8  125.35
     --  ...


No comments:

Post a Comment