Skip to Main Content
  • Questions
  • How to compare date duration of first row with second and so on.. and convert it to rows to column with indicators

Breadcrumb

Question and Answer

Chris Saxon

Thanks for the question, Shyam.

Asked: December 13, 2018 - 5:21 am UTC

Last updated: February 06, 2020 - 1:14 pm UTC

Version: 12.1.0

Viewed 10K+ times! This question is

You Asked

Hi Tom,
I have below table

CREATE TABLE PRDCT (ID NUMBER(10,0), 
PRDCT_CD VARCHAR2(1), 
START_DT DATE,
END_DT DATE);


With data in this like -
Insert Into PRDCT Values (1,'A',to_date('01-May-2017'),to_date('31-Jul-2017'))
Insert Into PRDCT Values (1,'B',to_date('01-May-2017'),to_date('31-Aug-2017'))
Insert Into PRDCT Values (1,'C',to_date('01-May-2017'),to_date('31-Dec-9999'))

SELECT * FROM PRDCT;

ID   PRDCT_CD   START_DT     END_DT
--------------------------------------
1    A          01-MAY-17    31-JUL-17
1    B          01-MAY-17    31-Aug-17
1    C          01-May-17    31-Dec-99


And I am looking for the below result:

ID  A_PRDCT_IND   B_PRDCT_IND   C_PRDCT_IND  START_DT   END_DT
-----------------------------------------------------------------
1   Y             Y             Y            01-MAY-17  31-JUL-17
1   N             Y             Y            01-Aug-17  31-Aug-17
1   N             N             Y            01-Sep-17  31-Dec-99


Since all 3 products are active till 31-Jul-17, the first row should show all indicator as 'Y'. And next row for product 'B' and 'C' should show indicator 'Y' with START_DT = END_DT+1 of 1st row till end date of product B, as both are active till 31-Aug-17 and so on...

I tried above solution using Pivot as below but no luck.

SELECT ID,
CASE WHEN PRDCT_A = 'A' THEN 'Y' ELSE 'N' END AS A_PRDCT_IND,
CASE WHEN PRDCT_B = 'B' THEN 'Y' ELSE 'N' END AS B_PRDCT_IND,
CASE WHEN PRDCT_C = 'C' THEN 'Y' ELSE 'N' END AS C_PRDCT_IND,
START_DT,
END_DT
FROM PRDCT
PIVOT(MAX(PRDCT_CD) FOR PRDCT_CD IN ('A' PRDCT_A, 'B' PRDCT_B, 'C' PRDCT_C))


Would be very appreciative if you provide a dynamic solution on this.

and Chris said...

If you want to show all the possible start/end periods and the codes active in each, you can:

- Unpivot the start/end dates to a single list of dates. Return the distinct dates
- Convert this back into start/end periods by returning the date and the next date (with lead)
- Outer join the products to this based on those that fall in each start/end period
- Pivot the rows based on the product codes you want to display. In the aggregate, return 'Y' if the code is non-null
- Map the null flags to N and do some date twiddling to get the ranges to be closed-closed intervals

with dates as ( 
  select distinct dt 
  from   prdct
  unpivot ( 
    dt for col in ( start_dt, end_dt )
  ) 
), ranges as (
  select dt start_dt, 
         lead ( dt ) over ( order by dt ) end_dt
  from   dates
), grps as (
  select p.prdct_cd, p.id, d.start_dt, d.end_dt
  from   ranges d
  left join prdct p
  on     p.start_dt <= d.start_dt
  and    p.end_dt >= d.end_dt
), rws as (
  select g.*, 
         row_number () over ( 
           partition by prdct_cd 
           order by start_dt 
         ) rn
  from   grps g
  where  end_dt is not null
)
  select id, start_dt + case when rn = 1 then 0 else 1 end start_dt, end_dt, 
         nvl ( a, 'N' ) a, 
         nvl ( b, 'N' ) b, 
         nvl ( c, 'N' ) c 
  from   rws 
  pivot (
    max ( case when prdct_cd is not null then 'Y' end )
    for  prdct_cd in ( 'A' a, 'B' b, 'C' c)
  )
  order  by start_dt;

ID   START_DT               END_DT                 A   B   C   
   1 01-MAY-2017 00:00:00   31-JUL-2017 00:00:00   Y   Y   Y   
   1 01-AUG-2017 00:00:00   31-AUG-2017 00:00:00   N   Y   Y   
   1 01-SEP-2017 00:00:00   31-DEC-9999 00:00:00   N   N   Y  

Rating

  (9 ratings)

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

Comments

PL/SQL

Shyam Baviskar, December 14, 2018 - 7:20 am UTC

Thanks Chris!
But the first With clause of dates should be specific to ID.
It might be the case that set of dates will be different for other ID's, so we can't always rely on that.

Apologize for this but suppose If I have data like below:
CREATE TABLE PRDCT (ID NUMBER(10,0),
PRDCT_CD VARCHAR2(1),
LOB_CD VARCHAR2(2),
START_DT DATE,
END_DT DATE);

And data I have in this:

Insert Into PRDCT Values (1,'V',to_date('01-May-2017'),to_date('31-Jul-2017'),'DO')
Insert Into PRDCT Values (1,'D',to_date('01-May-2017'),to_date('31-Aug-2017'),'DO')
Insert Into PRDCT Values (1,'D',to_date('01-May-2017'),to_date('31-Dec-9999'),'DM')

Here start_date and end_date should be specific to the combination of ID and Consider LOB_CD only for 'D' and not for 'V'
And result should be like below

ID DO_IND DM_IND VS_IND START_DT END_DT
-------------------------------------------------
1 Y Y Y 01-MAY-17 31-JUL-17
1 N Y Y 01-Aug-17 31-Aug-17
1 N N Y 01-Sep-17 31-Dec-99

Chris Saxon
December 17, 2018 - 10:52 am UTC

if you need to consider ID values separately, take the distinct ID and date in the unpivot and partition by ID in the analytic functions.

PL/SQL

Shyam, December 17, 2018 - 6:46 am UTC

Hi Chris,
Thanks So Much! It is working as expected now.

I have another scenario like below
ID PRDCT_CD LOB_CD START_DT END_DT
---------------------------------------------
1 A DO 01-MAY-17 31-DEC-99
1 B DM 01-JUN-17 31-DEC-99

And expected result should be

ID DO_IND DM_IND VIS_IND START_DT END_DT
----------------------------------------------------
1 Y N N 01-MAY-17 31-JUL-17
1 Y Y N 01-AUG-17 31-DEC-99

Could you please help to solve this?
Chris Saxon
December 17, 2018 - 10:52 am UTC

What exactly have you tried? Why is this not working?

PL/SQL

Shyam, December 17, 2018 - 2:24 pm UTC

Hi Chris,
First scenario was working as expected but below scenario is not.
Scenario:
ID PRDCT_CD LOB_CD START_DT END_DT
---------------------------------------------
1 A DO 01-MAY-17 31-DEC-99
1 B DM 01-JUN-17 31-DEC-99

Expected result should be:

Scenario-1
ID A_IND B_IND C_IND START_DT END_DT
----------------------------------------------------
1 Y N N 01-MAY-17 31-JUL-17
1 Y Y N 01-AUG-17 31-DEC-99

Could you please help me to solve this? It's very urgent

PL/SQL

Shyam, December 17, 2018 - 4:22 pm UTC

Hi Chris,
For above scenario, I tried just static logic as below to achieve expected result.

ranges as (
select start_dt,
case when (extract day from end_dt)=1 then end_dt-1 else end_dt
from
(select dt start_dt,
lead ( dt ) over ( order by dt ) end_dt
from dates)
)

And then assigned rn = 1 when start_dt start from day 1 as below

rws as (
select gr.*,
case when extract (day from start_dt) = 1 then 1 else rn as rn
from
(select g.*,
row_number () over (
partition by prdct_cd
order by start_dt
) rn
from grps g
where end_dt is not null) gr
))

But this approach is static one and not correct as start_dt and end_dt can have any date in the table.
I tried with LAG/LEAD options as well but not sure how to achieve the expected result for scenario-2.

The logic should work for below scenarios
Scenario-1 : start_dt are same and end_dt are different
(This is working with the logic you provided)
Scenario-2 : start_dt are different and end_dt are same
Scenario-3 : start_dt and end_dt both are different.

It would be very appreciated if you help to solve for 2nd and 3rd scenarios as well within same query.
Chris Saxon
December 20, 2018 - 5:34 pm UTC

Does this work for you:

- Add the source column to the original unpivot
- Pass this through all the subqueries
- After the final pivot, adjust the start/end dates like so:

- If the source is END_DT, add one to the start date
- If the source for the next row is START_DT, subtract one from the end date

delete prdct;
Insert Into PRDCT Values (1,'A',to_date('01-May-2017'),to_date('31-dec-9999'));
Insert Into PRDCT Values (1,'B',to_date('01-Jun-2017'),to_date('31-dec-9999'));

with dates as ( 
  select distinct col, dt 
  from   prdct
  unpivot ( 
    dt for col in ( start_dt, end_dt )
  ) 
), ranges as (
  select col,
         dt start_dt, 
         lead ( dt ) over ( order by dt ) end_dt
  from   dates
), grps as (
  select distinct p.prdct_cd, p.id, d.start_dt, d.end_dt, col
  from   ranges d
  left join prdct p
  on     p.start_dt <= d.start_dt
  and    p.end_dt >= d.end_dt
), rws as (
  select g.*
  from   grps g
  where  end_dt is not null
), flags as (
  select col, id,
         start_dt, end_dt, 
         nvl ( a, 'N' ) a, 
         nvl ( b, 'N' ) b, 
         nvl ( c, 'N' ) c 
  from   rws 
  pivot (
    max ( case when prdct_cd is not null then 'Y' end )
    for  prdct_cd in ( 'A' a, 'B' b, 'C' c)
  )
)
  select col, id, 
         case 
           when col = 'END_DT' then start_dt + 1 
           else start_dt
         end start_dt, 
         case 
           when lead ( col ) over ( 
                  partition by id 
                  order by start_dt 
                ) = 'START_DT'
           then end_dt - 1
           else end_dt
         end end_dt, 
         a, b, c
  from   flags
  order  by start_dt;

COL        ID   START_DT               END_DT                 A   B   C   
START_DT      1 01-MAY-2017 00:00:00   31-MAY-2017 00:00:00   Y   N   N   
START_DT      1 01-JUN-2017 00:00:00   31-DEC-9999 00:00:00   Y   Y   N  

PL/SQL

Shyam, January 18, 2019 - 7:11 am UTC

Thanks so much Chris!
It's working as expected.
Sorry for delay in response.

PL/SQL

Shyam Baviskar, May 03, 2019 - 3:13 pm UTC

Hi Tom,
I am facing some performance issue and need your help to optimize below query:
I have audit table like below
CREATE AUDIT_TAB
(
HIST_ROW_ID NUMBER(10,0),
HIST_CREATE_DTM TIMESTAMP(3) DEFAULT SYSDATE,
HIST_IMAGE_CD VARCHAR2(1) NOT NULL,
PR_ID VARCHAR2(12) NOT NULL,
PR_LONG NUMBER(10,0) NOT NULL,
PR_LAT NUMBER(10,0) NOT NULL
)
And here are the index applied as below:
CREATE UNIQUE INDEX INDX_AUDIT_PRIM ON AUDIT_TAB (HIST_ROW_ID ASC)
CREATE NON UNIQUE INDEX INDX_HIST_CREATE_DTM ON AUDIT_TAB (HIST_CREATE_DTM ASC, HIST_IMAGE_CD ASC)

Insert Script:

INSERT INTO AUDIT_TAB VALUES (1, TO_TIMESTAMP('01-JAN-19 02.50.01 PM','DD-MON-RR HH.MI.SSXFF AM'),'B','PR1','112233','112234')
INSERT INTO AUDIT_TAB VALUES (2, TO_TIMESTAMP('02-JAN-19 03.20.31 PM','DD-MON-RR HH.MI.SSXFF AM'),'A','PR1','112235','112236')
INSERT INTO AUDIT_TAB VALUES (3, TO_TIMESTAMP('02-JAN-19 03.20.33 PM','DD-MON-RR HH.MI.SSXFF AM'),'B','PR1','112235','112236')
INSERT INTO AUDIT_TAB VALUES (4, TO_TIMESTAMP('03-JAN-19 01.15.10 PM','DD-MON-RR HH.MI.SSXFF AM'),'A','PR1','112233','112234')

Table Data:
SELECT * FROM AUDIT_TAB;

HIST_ROW_ID HIST_CREATE_DTM HIST_IMAGE_CD PR_ID PR_LONG PR_LAT
1 01-JAN-19 02.50.01 PM B PR1 112233 112234
2 02-JAN-19 03.20.31 PM A PR1 112235 112236
3 02-JAN-19 03.20.33 PM B PR1 112235 112236
4 03-JAN-19 01.15.10 PM A PR1 112233 112234

Requirement : Capture quarterly data from audit table and get the count for how many times Longitude (PR_LONG) and Latitude (PR_LAT) change against each PR_ID based on before and after.

Expected Result : For above example the count should written 2

For this I have written below query and it is working as expected, but it is taking 25-30 min to execute.
Note: This table has millions of data.

SELECT SUM(CASE WHEN PR_LONG = LEAD_LONG AND PR_LAT = LEAD_LAT
THEN 0
ELSE 1
END) - COUNT(DISTINCT PR_ID) CHG_CNT
FROM
(SELECT PR_ID,
PR_LONG,
PR_LAT,
HIST_IMAGE_CD,
NVL(LEAD(PR_LONG) OVER (PARTITION BY PR_ID ORDER BY HIST_ROW_ID),99999) LEAD_LONG,
NVL(LEAD(PR_LAT) OVER (PARTITION BY PR_ID ORDER BY HIST_ROW_ID),99999) LEAD_LAT
FROM AUDIT_TAB
WHERE HIST_CREATE_DTM BETWEEN ADD_MONTHS(TRUNC(SYSDATE, 'Q'),-3) AND TRUNC(SYSDATE, 'Q') -1 / 24*60*60);

Appreciate if you help to optimize above query.
Chris Saxon
May 10, 2019 - 8:28 am UTC

To help with performance questions we need to see the query's execution plan.

To get this run:

set serveroutput off
alter session set statistics_level = all;

<your query>

select * 
from   table(dbms_xplan.display_cursor(null, null, 'ALLSTATS LAST'));


And submit a new question with the output of this.

If you want to know more about getting execution plans, see:

https://blogs.oracle.com/sql/how-to-create-an-execution-plan

with analytics

Rajeshwaran, Jeyabal, May 13, 2019 - 3:29 pm UTC

was reading the initial response.

- Unpivot the start/end dates to a single list of dates. Return the distinct dates
- Convert this back into start/end periods by returning the date and the next date (with lead)
- Outer join the products to this based on those that fall in each start/end period
- Pivot the rows based on the product codes you want to display. In the aggregate, return 'Y' if the code is non-null
- Map the null flags to N and do some date twiddling to get the ranges to be closed-closed intervals


rather than all these, how about doing a cross row comparision using analytics? something like this. Kindly advice.

demo@PDB1> select id,
  2      case when end_dt <= flag_a then 'Y' else 'N' end a_prod_ind,
  3      case when end_dt <= flag_b then 'Y' else 'N' end b_prod_ind,
  4      case when end_dt <= flag_c then 'Y' else 'N' end c_prod_ind,
  5      start_dt,end_dt
  6  from (
  7  select id,start_dt,end_dt,
  8      greatest( last_value( a_new_start_dt ignore nulls) over(order by end_dt rows between unbounded preceding and unbounded following) ,
  9      last_value( a_new_end_dt ignore nulls) over(order by end_dt rows between unbounded preceding and unbounded following) ) as flag_a ,
 10      greatest( last_value( b_new_start_dt ignore nulls) over(order by end_dt rows between unbounded preceding and unbounded following)  ,
 11      last_value( b_new_end_dt ignore nulls) over(order by end_dt rows between unbounded preceding and unbounded following) ) as flag_b ,
 12      greatest( last_value( c_new_start_dt ignore nulls) over(order by end_dt rows between unbounded preceding and unbounded following) ,
 13      last_value( c_new_end_dt ignore nulls) over(order by end_dt rows between unbounded preceding and unbounded following) ) as flag_c
 14  from (
 15  select *
 16  from (
 17  select t.*, start_dt x1,end_dt x2 from t
 18      )
 19  pivot(
 20      max(x1) as new_start_dt,
 21      max(x2) as new_end_dt
 22      for prdct_cd in ('A' as a,'B' as b,'C' as c) )
 23        )
 24        )
 25  /

        ID A B C START_DT    END_DT
---------- - - - ----------- -----------
         1 Y Y Y 01-MAY-2017 31-JUL-2017
         1 N Y Y 01-MAY-2017 31-AUG-2017
         1 N N Y 01-MAY-2017 31-DEC-9999

demo@PDB1>
demo@PDB1> select * from t;

        ID P START_DT    END_DT
---------- - ----------- -----------
         1 A 01-MAY-2017 31-JUL-2017
         1 B 01-MAY-2017 31-AUG-2017
         1 C 01-MAY-2017 31-DEC-9999

demo@PDB1>
demo@PDB1>

Chris Saxon
May 14, 2019 - 1:35 pm UTC

This doesn't generate all the date combinations. For example, if you move the start_date of B:

update t
set    start_dt = date'2017-06-01'
where  prdct_cd = 'B';

select id,
    case when end_dt <= flag_a then 'Y' else 'N' end a_prod_ind,
    case when end_dt <= flag_b then 'Y' else 'N' end b_prod_ind,
    case when end_dt <= flag_c then 'Y' else 'N' end c_prod_ind,
    start_dt,end_dt
from (
select id,start_dt,end_dt,
    greatest( last_value( a_new_start_dt ignore nulls) over(order by end_dt rows between unbounded preceding and unbounded following) ,
    last_value( a_new_end_dt ignore nulls) over(order by end_dt rows between unbounded preceding and unbounded following) ) as flag_a ,
    greatest( last_value( b_new_start_dt ignore nulls) over(order by end_dt rows between unbounded preceding and unbounded following)  ,
    last_value( b_new_end_dt ignore nulls) over(order by end_dt rows between unbounded preceding and unbounded following) ) as flag_b ,
    greatest( last_value( c_new_start_dt ignore nulls) over(order by end_dt rows between unbounded preceding and unbounded following) ,
    last_value( c_new_end_dt ignore nulls) over(order by end_dt rows between unbounded preceding and unbounded following) ) as flag_c
from (
select *
from (
select t.*, start_dt x1,end_dt x2 from t
    )
pivot(
    max(x1) as new_start_dt,
    max(x2) as new_end_dt
    for prdct_cd in ('A' as a,'B' as b,'C' as c) )
      )
      )
/

ID A_PROD_IND  B_PROD_IND  C_PROD_IND   START_DT        END_DT                 
 1 Y           Y           Y            01-MAY-2017     31-JUL-2017 00:00:00    
 1 N           Y           Y            01-JUN-2017     31-AUG-2017 00:00:00    
 1 N           N           Y            01-MAY-2017     31-DEC-9999 00:00:00  


There should be four rows in the output, covering the following date ranges:

1 May - 31 May
1 Jun - 31 Jul
1 Aug - 31 Aug
1 Sep - 31 Dec

PL/SQL

Shyam Baviskar, February 05, 2020 - 1:22 pm UTC

Hi Chris,
Thanks a lot for your continuous support!

Need one more help to get below result.

Q : How to adjust the string (after the specific patter of the original string) to 8 characters.
Ex.1. Input String = 'MFT123456' --9 Characters
Output String = 'MF123456' --restricted to 8 Characters after removing 'T' from original string.

Ex.2. Input String = 'MFT1234567' --9 Characters
Output String = 'M1234567' --restricted to 8 Characters after removing 'FT' from original string.

and so on...

Appreciate if you could help on this.
Chris Saxon
February 05, 2020 - 3:05 pm UTC

You'll need some combination of substr, replace, and/or regexp_substr. I'm not clear what your replacement rules are. So post a new question explaining what you're trying to do if you're stuck.

PL/SQL

Shyam Baviskar, February 06, 2020 - 7:31 am UTC

Hi Chris,
Apologize for the confusion.

Here are few examples. I want to restrict the output string to 8 characters by adjusting all the digits (after 'MFT' in below example) from Input string.

Ex.1. Input String is 'MFT123456'
Expected output String = 'MF123456'

Ex.2. Input String is 'LCC1234567'
Expected output String = 'L1234567'

I tried to achieve this using below logic. But it's not working for 2nd example and looking for dynamic solution for this.

SELECT CASE WHEN LENGTH('MFT123456') > 8
THEN CASE WHEN SUBSTR('MFT123456', 1,3) = 'MFT' THEN SUBSTR('MFT123456',1,2) || SUBSTR('MFT123456',4)
END
END AS PIN;
Chris Saxon
February 06, 2020 - 1:14 pm UTC

I'm still not clear on how you decide which characters to keep.

But in any case, this is unrelated to the original question. Please post this as a NEW question (not a review) when questions are open again.

More to Explore

Analytics

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