Skip to Main Content
  • Questions
  • SQL Query: how to break down a range of dates

Breadcrumb

Question and Answer

Connor McDonald

Thanks for the question, Livio.

Asked: September 02, 2015 - 2:01 pm UTC

Last updated: September 18, 2015 - 3:26 am UTC

Version: Oracle Database 11g Enterprise Edition Release 11.2.0.4.0 - 64bit Production

Viewed 1000+ times

You Asked

Hello,

I have the following table:

create table temp (
date_from date,
date_to date,
rate number);

with this data in it:

insert into temp values (date '1973-07-01', date '1992-12-31', 6.75);
insert into temp values (date '1993-01-01', date '2003-12-31', 8.25);
insert into temp values (date '2004-01-01', date '2004-06-30', 9.25);
insert into temp values (date '2004-07-01', date '2005-06-30', 9.75);
insert into temp values (date '2005-07-01', date '2008-06-30', 10.25);
insert into temp values (date '2008-07-01', date '2009-06-30', 10.9);
insert into temp values (date '2009-07-01', date '2010-06-30', 11.3);
insert into temp values (date '2010-07-01', date '2011-06-30', 11.6);
insert into temp values (date '2011-07-01', date '2012-06-30', 11);
insert into temp values (date '2012-07-01', date '2014-06-30', 10);
insert into temp values (date '2014-07-01', null, 10.1);
commit;

I am given a date parameter, p_date, and I want to break down the time spell this date falls in, in order to produce two new spells with the same rate. For instance, if p_date = date '2005-12-31', I do expect to replace the interval [date '2005-07-01', date '2008-06-30'] with the following two intervals
[date '2005-07-01', date '2005-12-31'] and [date '2006-01-01', date '2008-06-30'], both having a rate of 10.25.

Could you help me design a query like:

with q as (
/*REPLACE*/
)
select * from q
order by date_from;

such that, when I provide a replacement for /*REPLACE*/ containing p_date = date '2005-12-31', the above query returns the following result set:

DATE_FROM DATE_TO RATE
01/07/1973 31/12/1992 6.75
01/01/1993 31/12/2003 8.25
01/01/2004 30/06/2004 9.25
01/07/2004 30/06/2005 9.75
01/07/2005 31/12/2005 10.25
01/01/2006 30/06/2008 10.25
01/07/2008 30/06/2009 10.9
01/07/2009 30/06/2010 11.3
01/07/2010 30/06/2011 11.6
01/07/2011 30/06/2012 11
01/07/2012 30/06/2014 10
01/07/2014 10.1

Of course, the query is supposed to work also for dates after date '2014-07-01'.

Thanks in advance.

Livio




and Chris said...

Thanks for providing a complete script and examples.

It's not clear what should happen when you provide a date that is equal to one of the existing start or end dates. For the purposes of this answer I've assumed that the new range starts and ends on the same day. I've also guessed that by "works for dates after 2014-07-01" you mean it splits the last row at the provided date. The last row remains with a null end date.

If these assumptions are incorrect you'll need to update the query based on what these should be.

To split the range, we must first find the row where the provided date >= date_from and < date_to + 1 (or it's null):

select *
from   temp
where  to_date(:dt, 'dd/mm/yyyy') >= date_from 
and    (to_date(:dt, 'dd/mm/yyyy') < date_to + 1 or date_to is null)


Combine this with a two row table to double this up. Select the date_from, date_to or supplied date as appropriate:

select case 
          when r.r = 1 then date_from
          else least(to_date(:dt, 'dd/mm/yyyy') + 1, nvl(date_to, to_date(:dt, 'dd/mm/yyyy') + 1))
       end date_from, 
       case 
          when r.r = 1 then to_date(:dt, 'dd/mm/yyyy')
          else date_to
       end date_to, 
       rate
from   temp, (select rownum r from dual connect by level <= 2) r
where  to_date(:dt, 'dd/mm/yyyy') >= date_from 
and    (to_date(:dt, 'dd/mm/yyyy') < date_to + 1 or date_to is null)


To get the rest of the rows, union all this with those where the date is not in the range:

select date_from, date_to, rate
from   temp
where  not (to_date(:dt, 'dd/mm/yyyy') >= date_from and  
            (to_date(:dt, 'dd/mm/yyyy') < date_to + 1 or date_to is null)


Putting this all together gives:

var dt varchar2(10);
exec :dt := '31/12/2005';

with q as (
  select case 
            when r.r = 1 then date_from
            else least(to_date(:dt, 'dd/mm/yyyy') + 1, nvl(date_to, to_date(:dt, 'dd/mm/yyyy') + 1))
         end date_from, 
         case 
            when r.r = 1 then to_date(:dt, 'dd/mm/yyyy')
            else date_to
         end date_to, 
         rate
  from   temp, (select rownum r from dual connect by level <= 2) r
  where  to_date(:dt, 'dd/mm/yyyy') >= date_from 
  and    (to_date(:dt, 'dd/mm/yyyy') < date_to + 1 or date_to is null)
  union all
  select date_from, date_to, rate
  from   temp
  where  not (to_date(:dt, 'dd/mm/yyyy') >= date_from and  
              to_date(:dt, 'dd/mm/yyyy') < date_to + 1 or date_to is null)
)
select * from q
order by date_from, date_to;

DATE_FROM           DATE_TO                   RATE
------------------- ------------------- ----------
01/07/1973 00:00:00 31/12/1992 00:00:00       6.75
01/01/1993 00:00:00 31/12/2003 00:00:00       8.25
01/01/2004 00:00:00 30/06/2004 00:00:00       9.25
01/07/2004 00:00:00 30/06/2005 00:00:00       9.75
01/07/2005 00:00:00 31/12/2005 00:00:00      10.25
01/01/2006 00:00:00 30/06/2008 00:00:00      10.25
01/07/2008 00:00:00 30/06/2009 00:00:00       10.9
01/07/2009 00:00:00 30/06/2010 00:00:00       11.3
01/07/2010 00:00:00 30/06/2011 00:00:00       11.6
01/07/2011 00:00:00 30/06/2012 00:00:00         11
01/07/2012 00:00:00 30/06/2014 00:00:00         10
01/07/2014 00:00:00                           10.1

Rating

  (2 ratings)

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

Comments

Livio Curzola, September 02, 2015 - 4:48 pm UTC


How about table unnesting ?

Rajeshwaran, Jeyabal, September 03, 2015 - 7:58 am UTC

for each row

a) check if the input values lies in the range of date_from and date_to, then expand (duplicate) only those rows.
b) if input value does not lies in the range then no-expand the rows.


rajesh@ORA11G> variable x varchar2(20)
rajesh@ORA11G> exec :x :='31-dec-2005';

PL/SQL procedure successfully completed.

rajesh@ORA11G> select case when dt between date_from and date_to and x = 2
  2      then greatest(dt,date_from) + 1 else date_from end as new_date_from,
  3      case when dt between date_from and date_to and x = 1
  4      then least(dt,date_to) else date_to end as new_date_to,rate
  5  from (
  6  select date_from,date_to,rate,
  7    to_date( substr( column_value,2),'yyyymmddhh24miss') dt,
  8    to_number(substr(column_value,1,1)) as x
  9  from temp ,
 10     table(cast( multiset(select level
 11            ||  to_char( to_date(:x,'dd-mon-yyyy'),'yyyymmddhh24miss')
 12     from dual
 13     connect by level <=
 14             case when to_date(:x,'dd-mon-yyyy') between
 15                     date_from and date_to then 2 else 1 end )
 16             as sys.odcinumberlist))
 17      )
 18  /

NEW_DATE_FROM           NEW_DATE_TO                   RATE
----------------------- ----------------------- ----------
01-JUL-1973 12:00:00 AM 31-DEC-1992 12:00:00 AM       6.75
01-JAN-1993 12:00:00 AM 31-DEC-2003 12:00:00 AM       8.25
01-JAN-2004 12:00:00 AM 30-JUN-2004 12:00:00 AM       9.25
01-JUL-2004 12:00:00 AM 30-JUN-2005 12:00:00 AM       9.75
01-JUL-2005 12:00:00 AM 31-DEC-2005 12:00:00 AM      10.25
01-JAN-2006 12:00:00 AM 30-JUN-2008 12:00:00 AM      10.25
01-JUL-2008 12:00:00 AM 30-JUN-2009 12:00:00 AM       10.9
01-JUL-2009 12:00:00 AM 30-JUN-2010 12:00:00 AM       11.3
01-JUL-2010 12:00:00 AM 30-JUN-2011 12:00:00 AM       11.6
01-JUL-2011 12:00:00 AM 30-JUN-2012 12:00:00 AM         11
01-JUL-2012 12:00:00 AM 30-JUN-2014 12:00:00 AM         10
01-JUL-2014 12:00:00 AM                               10.1

12 rows selected.

rajesh@ORA11G> set autotrace traceonly explain statistics
rajesh@ORA11G>
rajesh@ORA11G> select case when dt between date_from and date_to and x = 2
  2      then greatest(dt,date_from) + 1 else date_from end as new_date_from,
  3      case when dt between date_from and date_to and x = 1
  4      then least(dt,date_to) else date_to end as new_date_to,rate
  5  from (
  6  select date_from,date_to,rate,
  7    to_date( substr( column_value,2),'yyyymmddhh24miss') dt,
  8    to_number(substr(column_value,1,1)) as x
  9  from temp ,
 10     table(cast( multiset(select level
 11            ||  to_char( to_date(:x,'dd-mon-yyyy'),'yyyymmddhh24miss')
 12     from dual
 13     connect by level <=
 14             case when to_date(:x,'dd-mon-yyyy') between
 15                     date_from and date_to then 2 else 1 end )
 16             as sys.odcinumberlist))
 17      )
 18  /

12 rows selected.


Execution Plan
----------------------------------------------------------
Plan hash value: 1969555913

--------------------------------------------------------------------------------------------
| Id  | Operation                           | Name | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                    |      | 89848 |  1842K|   305   (1)| 00:00:04 |
|   1 |  NESTED LOOPS                       |      | 89848 |  1842K|   305   (1)| 00:00:04 |
|   2 |   TABLE ACCESS FULL                 | TEMP |    11 |   209 |     3   (0)| 00:00:01 |
|   3 |   COLLECTION ITERATOR SUBQUERY FETCH|      |  8168 | 16336 |    27   (0)| 00:00:01 |
|*  4 |    CONNECT BY WITHOUT FILTERING     |      |       |       |            |          |
|   5 |     FAST DUAL                       |      |     1 |       |     2   (0)| 00:00:01 |
--------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   4 - filter(LEVEL<=CASE  WHEN ((TO_DATE(:X,'dd-mon-yyyy')>=:B1) AND
              (TO_DATE(:X,'dd-mon-yyyy')<=:B2)) THEN 2 ELSE 1 END )


Statistics
----------------------------------------------------------
          0  recursive calls
          0  db block gets
          7  consistent gets
          0  physical reads
          0  redo size
        995  bytes sent via SQL*Net to client
        500  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
         11  sorts (memory)
          0  sorts (disk)
         12  rows processed

rajesh@ORA11G>
rajesh@ORA11G> var dt varchar2(10);
rajesh@ORA11G> exec :dt := '31/12/2005';

PL/SQL procedure successfully completed.

rajesh@ORA11G>
rajesh@ORA11G> with q as (
  2    select case
  3              when r.r = 1 then date_from
  4              else least(to_date(:dt, 'dd/mm/yyyy') + 1, nvl(date_to, to_date(:dt,
  5  'dd/mm/yyyy') + 1))
  6           end date_from,
  7           case
  8              when r.r = 1 then to_date(:dt, 'dd/mm/yyyy')
  9              else date_to
 10           end date_to,
 11           rate
 12    from   temp, (select rownum r from dual connect by level <= 2) r
 13    where  to_date(:dt, 'dd/mm/yyyy') >= date_from
 14    and    (to_date(:dt, 'dd/mm/yyyy') < date_to + 1 or date_to is null)
 15    union all
 16    select date_from, date_to, rate
 17    from   temp
 18    where  not (to_date(:dt, 'dd/mm/yyyy') >= date_from and
 19                to_date(:dt, 'dd/mm/yyyy') < date_to + 1 or date_to is null)
 20  )
 21  select * from q
 22  order by date_from, date_to;

11 rows selected.


Execution Plan
----------------------------------------------------------
Plan hash value: 2340467245

--------------------------------------------------------------------------------------------
| Id  | Operation                           | Name | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                    |      |     3 |    93 |     9  (12)| 00:00:01 |
|   1 |  SORT ORDER BY                      |      |     3 |    93 |     9  (12)| 00:00:01 |
|   2 |   VIEW                              |      |     3 |    93 |     8   (0)| 00:00:01 |
|   3 |    UNION-ALL                        |      |       |       |            |          |
|   4 |     MERGE JOIN CARTESIAN            |      |     1 |    32 |     5   (0)| 00:00:01 |
|*  5 |      TABLE ACCESS FULL              | TEMP |     1 |    19 |     3   (0)| 00:00:01 |
|   6 |      BUFFER SORT                    |      |     1 |    13 |     2   (0)| 00:00:01 |
|   7 |       VIEW                          |      |     1 |    13 |     2   (0)| 00:00:01 |
|   8 |        COUNT                        |      |       |       |            |          |
|*  9 |         CONNECT BY WITHOUT FILTERING|      |       |       |            |          |
|  10 |          FAST DUAL                  |      |     1 |       |     2   (0)| 00:00:01 |
|* 11 |     TABLE ACCESS FULL               | TEMP |     2 |    38 |     3   (0)| 00:00:01 |
--------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   5 - filter("DATE_FROM"<=TO_DATE(:DT,'dd/mm/yyyy') AND ("DATE_TO" IS NULL OR
              INTERNAL_FUNCTION("DATE_TO")+1>TO_DATE(:DT,'dd/mm/yyyy')))
   9 - filter(LEVEL<=2)
  11 - filter("DATE_TO" IS NOT NULL AND ("DATE_FROM">TO_DATE(:DT,'dd/mm/yyyy') OR
              INTERNAL_FUNCTION("DATE_TO")+1<=TO_DATE(:DT,'dd/mm/yyyy')))


Statistics
----------------------------------------------------------
          0  recursive calls
          0  db block gets
         12  consistent gets
          0  physical reads
          0  redo size
        969  bytes sent via SQL*Net to client
        500  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          3  sorts (memory)
          0  sorts (disk)
         11  rows processed
   
rajesh@ORA11G>
rajesh@ORA11G> set autotrace off
rajesh@ORA11G>
rajesh@ORA11G>
rajesh@ORA11G>

Connor McDonald
September 18, 2015 - 3:26 am UTC

Yup, lots of ways this can be achieved. Thanks for your input