Skip to Main Content

Breadcrumb

Question and Answer

Chris Saxon

Thanks for the question, Lisa.

Asked: October 04, 2016 - 3:24 pm UTC

Last updated: October 10, 2016 - 9:16 am UTC

Version: 11g

Viewed 1000+ times

You Asked

Hi Guys

Really hope you can help with an issue that I have. I use the code below to output the days of the current week (stored in a table called dates)

SELECT to_char(NEXT_DAY(SYSDATE,'MON')
+ case when to_char(SYSDATE,'D') in (6,7) then -1 else -8 end + rownum, 'DD-MM-YY') day
from  dual
connect by level <= 5;


I use the code below to populate my table

create table my_data
(apt_id int, 
app_date date, 
app_start_time varchar2(5),
app_end_time varchar2(5),
app_class varchar2(50)
)

insert into my_data values(636, date '03-10-2016', '09.00', '09.45', 'Maths');
insert into my_data values(637, date '04-10-2016', '10.00', '10.30', 'Biology');
insert into my_data values(649, date '05-10-2016', '10.45', '11.00', 'Computers');
insert into my_data values(1136, date '05-10-2016', '16.15', '17.00', 'French');
insert into my_data values(647, date '06-10-2016', '09.00', '09.30', 'German');
insert into my_data values(1131, date '06-10-2016', '12.30', '13.45', 'Chemistry');
insert into my_data values(641, date '07-10-2016', '16.00', '17.00', 'Physics');


I use row number to partition my data based on date and id eg someone could have numerous classes in one day the earliest class will always have the lowest id

with temp as (
select apt_id, app_date,app_start_time, app_end_time, app_class, ROW_NUMBER() OVER (partition by app_date order by apt_id) as R 
from my_data
)


I then use the code below to display my grid (date as rows and classes as columns)

select day, case when r = 1 Then app_start_time || ' - ' || app_class else ' ' End "Class 1", case when r = 2 Then app_start_time || ' - ' || app_class else ' ' End "Class 2"
from dates
left join temp on day = app_date
order by day;


This outputs

Day            Class 1             Class 2
03-10-2016     09.00 - Maths
04-10-2106     10.00 - Biology 
05-10-2016     10.45 - Computers   
05-10-2016                         16.15 - French


This code does not work correctly because on the 5th October there are two rows 1 for the 10.45 class and another for the 16.15 class.

I was hoping you could help me change this grid so that the dates become the column headers and the Class Numbers become the rows

eg

          03-10-2016        04-10-2016           05-10-2016
Class 1   09.00 - Maths     10.00 - Biology      10.45 - Computers
Class 2                                          16.15 - French  


Any help greatly appreciated

Thanks






and Chris said...

Sounds like you need to do the pivot!

You're taking the values from app_date and converting them to columns. Thus app_date goes in the for clause and the list of dates you want to appear in the output go in the in clause.

The values for the rows are the concatenation of start_time and class. You need to aggregate these (even if you only expect one row). So place these in a min(). This gives a pivot clause like:

pivot (min(app_start_time || ' - ' || app_class) 
  for app_date in (
    date'2016-10-03' as "03-10-2016", 
    date'2016-10-04' as "04-10-2016", 
    date'2016-10-05' as "05-10-2016")
)


To change the columns in your output add or remove dates from the in clause.

Oracle implicitly groups by any other columns in the source not in the pivot. So the results will be split by row_number() automatically!

This gives you:

create table my_data
(apt_id int, 
app_date date, 
app_start_time varchar2(5),
app_end_time varchar2(5),
app_class varchar2(50)
);

insert into my_data values(636, date '2016-10-03', '09.00', '09.45', 'Maths');
insert into my_data values(637, date '2016-10-04', '10.00', '10.30', 'Biology');
insert into my_data values(649, date '2016-10-05', '10.45', '11.00', 'Computers');
insert into my_data values(1136, date '2016-10-05', '16.15', '17.00', 'French');
insert into my_data values(647, date '2016-10-06', '09.00', '09.30', 'German');
insert into my_data values(1131, date '2016-10-06', '12.30', '13.45', 'Chemistry');
insert into my_data values(641, date '2016-10-07', '16.00', '17.00', 'Physics');

with rws as (
select app_date, app_start_time, app_class,
       row_number() over (partition by app_date order by app_start_time) rn
from   my_data
)
  select 'Class ' || rn, "03-10-2016", "04-10-2016", "05-10-2016"
  from   rws
  pivot (min(app_start_time || ' - ' || app_class) 
   for app_date in (
      date'2016-10-03' as "03-10-2016", 
      date'2016-10-04' as "04-10-2016", 
      date'2016-10-05' as "05-10-2016")
  );

'CLASS'||RN  03-10-2016     04-10-2016       05-10-2016
Class 1      09.00 - Maths  10.00 - Biology  10.45 - Computers
Class 2                                      16.15 - French


Note you need to double quote your column names if you want them to start with numbers. Also - is the data type of dates.day a varchar2? If get rid of the table and recreate it with the values as dates! Otherwise you're going to run into all sorts of implicit conversion problems.

If you want to know more about pivoting read https://blogs.oracle.com/sql/entry/how_to_convert_rows_to

Rating

  (2 ratings)

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

Comments

Need more clarification

Lisa, October 05, 2016 - 12:27 pm UTC

Hi Chris

Thanks for the replying but having a few difficulties

The table my_data when its populated I put in the where condition

where to_char(apt_date, 'DD-MM-YYYY') in (Select to_char(next_day(Trunc(Sysdate) - 5, 'Monday') + (Level - 1), 'DD-MM-YYYY') Day From dual Connect By Level <= 5)


This ensures that my_data will only ever contain the weekdays of the current week. Eg if I populate my table today it will contain all the info from 03rd Oct 16 to 07 Oct 2016. If I populate my table anytime from 8th October to 14th October then it will contain info for 10-14 October.

As the app_date will change based on when the report is run Im not sure how to modify the code below.

pivot (min(app_start_time || ' - ' || app_class) 
  for app_date in (
    date'2016-10-03' as "03-10-2016", 
    date'2016-10-04' as "04-10-2016", 
    date'2016-10-05' as "05-10-2016")
)


I have tried this pivot where it looks at the day name but because of the aggregate function I am only getting 1 class out for each day

select x.*
from ( 
select trim(to_char(to_date(app_date,'DD-MM-YYYY'), 'Day')) day, app_start_time || ' - ' || trim(app_class) class
from my_data
)
pivot (min(class) for day in
('Monday' as Monday, 'Tuesday' as Tuesday, 'Wednesday' as Wednesday, 'Thursday' as Thursday, 'Friday' as Friday)
)x


Is there any way of grouping so that the output would be either the dates as the headers (mon-fri dates of current week) or days eg

03-10-2016       04-10-2016        05-10-2016       
09.00 - Maths    10.00 - Biology   10.45 - Computers
                                   16.15 - French


Monday           Tuesday           Wednesday
09.00 - Maths    10.00 - Biology   10.45 - Computers
                                   16.15 - French


Thanks in advance


Chris Saxon
October 05, 2016 - 4:01 pm UTC

If you want different dates to appear as columns you have to physically change the values in the "in". So for next week this would be:

pivot (min(app_start_time || ' - ' || app_class) 
  for app_date in (
    date'2016-10-10' as "10-10-2016", 
    date'2016-10-11' as "11-10-2016", 
    date'2016-10-12' as "12-10-2016", 
    date'2016-10-13' as "13-10-2016", 
    date'2016-10-14' as "14-10-2016")
)

Obviously this gets tricky to handle. You can pass the values dynamically. But to do so you need to return the results as XML!
with dates as (
with dates as (
  select rownum+date'2016-10-03' dt from dual
  connect by level <= 5
)
select x.*
from ( 
select app_date, 
       app_start_time || ' - ' || trim(app_class) class,
       row_number() over (partition by app_date order by app_start_time) rn
from   my_data
where  app_date between date'2016-10-03' and date'2016-10-07'
)
pivot xml (min(class) for app_date in (
  select dt from dates
)
)x;

        RN
----------
APP_DATE_XML
--------------------------------------------------------------------------------
         1
<PivotSet><item><column name = "APP_DATE">2016-10-04</column><column name = "MIN
(CLASS)">10.00 - Biology</column></item><item><column name = "APP_DATE">2016-10-
05</column><column name = "MIN(CLASS)">10.45 - Computers</column></item><item><c
olumn name = "APP_DATE">2016-10-06</column><column name = "MIN(CLASS)">09.00 - G
erman</column></item><item><column name = "APP_DATE">2016-10-07</column><column
name = "MIN(CLASS)">16.00 - Physics</column></item><item><column name = "APP_DAT
E">2016-10-08</column><column name = "MIN(CLASS)"></column></item></PivotSet>

         2
<PivotSet><item><column name = "APP_DATE">2016-10-04</column><column name = "MIN
(CLASS)"></column></item><item><column name = "APP_DATE">2016-10-05</column><col
umn name = "MIN(CLASS)">16.15 - French</column></item><item><column name = "APP_
DATE">2016-10-06</column><column name = "MIN(CLASS)">12.30 - Chemistry</column><
/item><item><column name = "APP_DATE">2016-10-07</column><column name = "MIN(CLA
SS)"></column></item><item><column name = "APP_DATE">2016-10-08</column><column
name = "MIN(CLASS)"></column></item></PivotSet>


If you're happy with showing Mon-Fri as column names there's a better solution though:

Filter the dates you want in the initial subquery. Then pivot the results by day:

select x.*
from ( 
select trim(to_char(app_date, 'Dy')) day, 
       app_start_time || ' - ' || trim(app_class) class,
       row_number() over (partition by app_date order by app_start_time) rn
from   my_data
where  app_date between date'2016-10-03' and date'2016-10-07'
)
pivot (min(class) for day in
('Mon' as Monday, 'Tue' as Tuesday, 'Wed' as Wednesday, 'Thu' as Thursday, 'Fri' as Friday)
)x;

RN  MONDAY         TUESDAY          WEDNESDAY          THURSDAY           FRIDAY
1   09.00 - Maths  10.00 - Biology  10.45 - Computers  09.00 - German     16.00 - Physics
2                                   16.15 - French     12.30 - Chemistry

Or you could look into Anton Scheffer's solution using custom types:

https://technology.amis.nl/2006/05/24/dynamic-sql-pivoting-stealing-antons-thunder/

Beware, this does a lot of parsing!

Great solution but would love to try a few more things

Lisa, October 06, 2016 - 3:09 pm UTC

Hi Chris

Thanks again for replying the code works perfect. I was just wondering a few additional things if possible.

select x.*
from ( 
select trim(to_char(app_date, 'Dy')) day, 
       app_start_time || chr(13) || chr(10)|| trim(app_class) class,
       row_number() over (partition by app_date order by app_start_time) rn
from   my_data
where  app_date between date'2016-10-03' and date'2016-10-07'
)
pivot (min(class) for day in
('Mon' as Monday, 'Tue' as Tuesday, 'Wed' as Wednesday, 'Thu' as Thursday, 'Fri' as Friday)
)x;


I wanted to add a newline between the start time and the class but the code above doesn't seem to be working and when I research online this seems to be the way to do it?

Also I have been looking at the pivot xml using the code below

with y as (
select x.*
from ( 
select app_date,
       app_start_time || ' - ' || app_end_time || ' ' || trim(app_reason) class,
       row_number() over (partition by app_date order by app_start_time) rn
from   my_data
where  app_date between date'2016-10-03' and date'2016-10-07'
)
pivot xml (min(class) for app_date in (select app_date from appoint)
)x
)

select  extractvalue(app_date_xml,'/PivotSet/item[1]/column[2]') Monday,
        extractvalue(app_date_xml,'/PivotSet/item[2]/column[2]') Tuesday,
        extractvalue(app_date_xml,'/PivotSet/item[3]/column[2]') Wednesday,
        extractvalue(app_date_xml,'/PivotSet/item[4]/column[2]') Thursday,
        extractvalue(app_date_xml,'/PivotSet/item[5]/column[2]') Friday
from y


This code also works but I thought using pivot xml you can change the column headings to whatever you wanted. eg after the line of code extractvalue(app_date_xml,'/PivotSet/item[1]/column[2]') you still need to enter the column name ie Monday. Instead of Monday can you not extract a value from your XML and use that instead?

Again thank you so much for the help


Chris Saxon
October 10, 2016 - 9:16 am UTC

What's the issue with the newlines? Your code looks fine to me:

SQL> select x.*
  2  from (
  3  select trim(to_char(app_date, 'Dy')) day,
  4         app_start_time || chr(13) || chr(10)|| trim(app_class) class,
  5         row_number() over (partition by app_date order by app_start_time) rn
  6  from   my_data
  7  where  app_date between date'2016-10-03' and date'2016-10-07'
  8  )
  9  pivot (min(class) for day in
 10  ('Mon' as Monday, 'Tue' as Tuesday, 'Wed' as Wednesday, 'Thu' as Thursday, 'Fri' as Friday)
 11  )x;

        RN MONDAY     TUESDAY    WEDNESDAY  THURSDAY   FRIDAY
---------- ---------- ---------- ---------- ---------- ----------
         1 09.00      10.00      10.45      09.00      16.00
           Maths      Biology    Computers  German     Physics

         2                       16.15      12.30
                                 French     Chemistry


Instead of Monday can you not extract a value from your XML and use that instead?

You still need to explicitly specify the column aliases. There may be a way to do some XML magic to extract the values as column headings, but I'm not aware of one.

More to Explore

Analytics

Analytic SQL got you confused? Check out Connor McDonald's complete video course.