Skip to Main Content
  • Questions
  • Need to create dynamic columns in the report

Breadcrumb

Question and Answer

Connor McDonald

Thanks for the question, Kavyashree.

Asked: July 26, 2018 - 10:24 am UTC

Last updated: August 20, 2018 - 4:20 am UTC

Version: Oracle 11g

Viewed 10K+ times! This question is

You Asked

Hi Tom,

I am new to SQL and PLSQL, I have a requirement to create a report, which includes dynamic column for month based on number of months from Program duration.

I actually tried to create scripts in LiveSQL, but right now it is not accessible, hence I am giving all the sample script ready to run.

Create table program_det (program_name varchar2(50), program_date NUMBER);
/

Insert into program_det values('PROGRAM_1', 201801);
Insert into program_det values('PROGRAM_1', 201807);
COMMIT;
/


---------------------------------------------------------------------------------------------------
What I exactly meant is

I have columns <Program_Name>, <Program_Date>

Suppose I have one Program duration of 6 months, say from 201801 to 201806, the the total number of month is 6. In this case I need to dynamically generate 6 columns in the report as below.

Program_Name Program_Date Month_1 Month_2 Month_3 Month_4 Month_5 Month_6
Program_1 12018  0 0 0 0 0
Program_1 62018  0 0 0 0 0


---------------------------------------------------------------------------------------------------
I have tried with below approach, here I am able to find the number of months but I need to help to create dynamic columns based on this number of months.

select program_name, 
      months_between(TO_DATE(max(program_date),'YYYYMM'),TO_DATE(min(program_date),'YYYYMM')) 
       number_months
from program_det
where program_name ='PROGRAM_1'
group by program_name;

---------------------------------------------------------------------------------------------------

I am not sure, if this may be very silly and simple, but I am unable to find a solution.

Please help!

Regards,
Kavya

and Connor said...

The challenge here is that you need to do a *dynamic* pivot, ie, the number of columns is unknown until you look at the data. Stew Ashton wrote up a nice blog post on how to do that here

https://stewashton.wordpress.com/2018/05/28/generic-pivot-function/

which I'll use to help with this question. I've also used DATE datatypes because NEVER EVER use numbers for data.

First I can generate the months I need

SQL> Create table program_det (program_name varchar2(50), program_date date);

Table created.

SQL> Insert into program_det values('PROGRAM_1', date '2018-01-01');

1 row created.

SQL> Insert into program_det values('PROGRAM_1', date '2018-07-01');

1 row created.

SQL> COMMIT;

Commit complete.

SQL>
SQL>
SQL> with pgm as
  2  ( select program_name,
  3                 min(program_date) mind,
  4                 max(program_date) maxd
  5          from program_det
  6          group by program_name )
  7  select *
  8  from  pgm p,
  9        table(cast(multiset(
 10            select add_months(p.mind,rownum-1)
 11            from   pgm
 12            where  program_name = p.program_name
 13            connect by add_months(mind,rownum-1) <= p.maxd
 14            ) as sys.odcidatelist));

PROGRAM_NAME                                       MIND      MAXD      COLUMN_VA
-------------------------------------------------- --------- --------- ---------
PROGRAM_1                                          01-JAN-18 01-JUL-18 01-JAN-18
PROGRAM_1                                          01-JAN-18 01-JUL-18 01-FEB-18
PROGRAM_1                                          01-JAN-18 01-JUL-18 01-MAR-18
PROGRAM_1                                          01-JAN-18 01-JUL-18 01-APR-18
PROGRAM_1                                          01-JAN-18 01-JUL-18 01-MAY-18
PROGRAM_1                                          01-JAN-18 01-JUL-18 01-JUN-18
PROGRAM_1                                          01-JAN-18 01-JUL-18 01-JUL-18

7 rows selected.


Now just for ease of readability I'll create a view on that

SQL> create or replace view v as
  2  with pgm as
  3  ( select program_name,
  4                 min(program_date) mind,
  5                 max(program_date) maxd
  6          from program_det
  7          group by program_name )
  8  select program_name,
  9         to_char(column_value,'YYYYMM') mth
 10  from  pgm p,
 11        table(cast(multiset(
 12            select add_months(p.mind,rownum-1)
 13            from   pgm
 14            where  program_name = p.program_name
 15            connect by add_months(mind,rownum-1) <= p.maxd
 16            ) as sys.odcidatelist));

View created.


and then I'll pass it into the generic pivot

SQL> set serveroutput off
SQL> var rc refcursor
SQL> begin
  2        :rc := generic_pivot(
  3          '(select program_name, mth, 0 val from v)',
  4          'MTH',
  5          'SUM(VAL)'
  6        );
  7      end;
  8  /

PL/SQL procedure successfully completed.

SQL> print rc

PROGRAM_NAME                                           201801     201802     201803     201804     201805     201806     201807
-------------------------------------------------- ---------- ---------- ---------- ---------- ---------- ---------- ----------
PROGRAM_1


I've hard-coded zero in there, but you would join or similar to get the numeric data you want to present.

Rating

  (7 ratings)

Is this answer out of date? If it is, please let us know via a Comment

Comments

Listagg limit

A reader, July 27, 2018 - 11:27 am UTC

Hi connor
Could you pls rewrite Stew function generic_pivot
So it can manage string over 50000 character?
Please do not suggest standalone tip.
Just need a new generic_pivot holding such requirement.

@Kavyashree: Rewrite

Stew Ashton, July 27, 2018 - 12:50 pm UTC

Hello,

In my opinion, it would have been more appropriate to ask me to rewrite the function, rather than Connor, since I wrote the original function.

Connor gave you a link to my blog post. If you click on that link, you can read the post AND you can post a comment. If you ask nicely in the comment I will think about maybe fulfilling your requirement.

In your comment, please post CREATE TABLE and INSERT statements with test data and indicate in detail what output you expect.

That's right, you have to do some of the work, we should not have to do everything.

Best regards,
Stew Ashton

P.S. If something good comes out of this, I will report back.

Correction

Stew Ashton, July 27, 2018 - 12:52 pm UTC

I assumed that Kavyashree wrote the followup, but maybe he didn't. Sorry if I was wrong.

Best,
Stew

Problem with original answer

Stew Ashton, July 27, 2018 - 1:00 pm UTC

This is for Connor.

The output from your demonstration returns nulls, not 0.

I have reproduced the problem, but I don't understand why this is so. If I create a table based on the SELECT ... FROM V then everything works as expected.

I have tried and failed to find something wrong with my GENERIC_PIVOT function. I'm stumped...

Best regards,
Stew


Connor McDonald
July 28, 2018 - 6:57 am UTC

Looks like a bug to me. A workaround is to use:

with x as (select /*+ materialize */ program_name, mth, 0 val from v) select * from x

as the query, but of course, that should not be needed.

Not answered

An asktom site follower, July 28, 2018 - 9:13 am UTC

Hi connor 
Could you pls rewrite Stew function generic_pivot 
So it can manage string over 50000 character? 
Please do not suggest standalone tip. 
Just need a new generic_pivot holding such requirement. 

Thanks ask Tom team for feesback
Connor McDonald
July 29, 2018 - 11:37 am UTC

We hope that AskTOM is seen as a service that builds community, build developers...not a authoring service.

That was very helpful solution

Kavyashree, July 30, 2018 - 3:59 am UTC

That was really helpful solution for my problem, I am thankful to this.

Just have one last query with this. I tried the same approach using the actual tables which I am suppose to form a query. But I face some issue in the last section.

SQL> set serveroutput off
SQL> var rc refcursor
SQL> begin
2 :rc := generic_pivot(
3 '(select program_name, mth, 0 val from v)',
4 'MTH',
5 'SUM(VAL)'
6 );
7 end;
8 /

Here, I need to understand the significance of generic_pivot. I tried to run same commands as you have given, it throws me error that 'generic_pivot' is an invalid identifier.

Is it some kind of a variable/function I need to declare before proceed with assignment part?

Please help!
Connor McDonald
August 01, 2018 - 12:33 pm UTC

Read the entire answer - there is a link to where to get the function.

Minor Tweak

AndyP, August 16, 2018 - 6:13 am UTC

Whilst acknowledging that this part of the answer isn't the focus of the suggested approach, can I suggest that the date generator doesn't quite work?

If you add another value for program:
insert into program_det values('PROGRAM_2',date '2017-03-01');
insert into program_det values('PROGRAM_2',date '2018-05-01');

the existing query breaks (for me anyway, on 11.2 and 12.1) with ORA-30009: Not enough memory for CONNECT BY operation

This slight alteration, using the generator to produce numbers rather than dates, does work for the general case

(NB also dropped the "where program_name = p.program_name" predicate, as that was redundant for the single program case and didn't help in the multi-program case)

with pgm as
(
select program_name
      ,min(program_date) mind
      ,months_between(max(program_date),min(program_date)) months
  from program_det
 group by program_name
)
select program_name
      ,add_months(mind,rws.column_value-1) newmonth
  from pgm,table(cast(multiset
(
select level
  from dual
 connect by level <= months
) as sys.odcinumberlist)) rws
order by 1,2
;

PROGRAM_NAME         NEWMONTH
-------------------- -----------
PROGRAM_1            01-Jan-2018
PROGRAM_1            01-Feb-2018
PROGRAM_1            01-Mar-2018
PROGRAM_1            01-Apr-2018
PROGRAM_1            01-May-2018
PROGRAM_1            01-Jun-2018
PROGRAM_2            01-Mar-2017
PROGRAM_2            01-Apr-2017
PROGRAM_2            01-May-2017
PROGRAM_2            01-Jun-2017
PROGRAM_2            01-Jul-2017
PROGRAM_2            01-Aug-2017
PROGRAM_2            01-Sep-2017
PROGRAM_2            01-Oct-2017
PROGRAM_2            01-Nov-2017
PROGRAM_2            01-Dec-2017
PROGRAM_2            01-Jan-2018
PROGRAM_2            01-Feb-2018
PROGRAM_2            01-Mar-2018
PROGRAM_2            01-Apr-2018

Connor McDonald
August 20, 2018 - 4:20 am UTC

nice input

More to Explore

SQL

The Oracle documentation contains a complete SQL reference.