Repeat a value over some rows - sql

I have a list of values and I have to display the mother value of the CHILDVALUE in the following way. The MOTHERVALUE should be same as CHILDVALUE at RNO = 1 , Otherwise the MOTHERVALUE is repeated for subsequent rows until a new sequence beginning from RNO = 1 is encountered. Below is what I wish to achieve
RNO ChildValue MotherValue SKIPNO CREATEDDATE
1 345dg 345dg 4 19/9/2018 2:49
2 342sds 345dg 4 19/9/2018 11:53
.
.
.
19 343dfd 345dg 4 6/11/2018 12:40
20 234dfs 345dg 4 6/11/2018 14:56
1 545ert 545ert 4 6/11/2018 15:17
2 543tye 545ert 4 7/11/2018 11:29
.
.
.
9 345cxv 545ert 4 16/11/2018 14:16
1 563mnj 563mnj 5 19/11/2018 2:12
The row numbers are dynamically produced according to the SKIPNO (not distinct) and CREATEDDATE. But I have a problem in displaying the correct MOTHERVALUE for some of the rows
I have tried the below query which best captures at least 50% of the requirement.
select RNO
, CHILDVALUE
, case RNO when 1 then CHILDVALUE
else lag(MOTHERVALUE) over (order by SKIPNO, CreatedDate ASC) end as MOTHERVALUE
, SKIPNO, CreatedDate
from( SELECT (ROW_NUMBER() OVER (PARTITION BY A.SKIPNO
ORDER BY A.SKIPNO, A.CreatedDate ASC) RNO
, A.*
from (select distinct CHILDVALUE
, CHILDVALUE as MOTHERVALUE
, SKIPNO
, CreatedDate
from values ) A
)
)
This query partially gives the expected output but is still far from the actual result as it shows the correct MOTHERVALUE only for the first two rows. The query does not help me to show the correctMOTHERVALUE` for more than second row.
Here is my table:
CREATE TABLE VALUES (
CHILDVALUE VARCHAR2(36),
SKIPNO VARCHAR2(36),
CREATEDDATE DATE)
Pls help.

N.B. I am assuming that your sample data had incorrect skipnos for the second and third groups. My answer uses updated values in its sample data.
You can use the first_value analytic function, rather than lag to achieve your aim, e.g.:
WITH vals AS (SELECT '345dg' childvalue, 4 skipno, to_date('19/09/2018 02:49', 'dd/mm/yyyy hh24:mi') createddate FROM dual UNION ALL
SELECT '342sds' childvalue, 4 skipno, to_date('19/09/2018 11:53', 'dd/mm/yyyy hh24:mi') createddate FROM dual UNION ALL
SELECT '343dfd' childvalue, 4 skipno, to_date('06/11/2018 12:40', 'dd/mm/yyyy hh24:mi') createddate FROM dual UNION ALL
SELECT '234dfs' childvalue, 4 skipno, to_date('06/11/2018 14:56', 'dd/mm/yyyy hh24:mi') createddate FROM dual UNION ALL
SELECT '545ert' childvalue, 5 skipno, to_date('06/11/2018 15:17', 'dd/mm/yyyy hh24:mi') createddate FROM dual UNION ALL
SELECT '543tye' childvalue, 5 skipno, to_date('07/11/2018 11:29', 'dd/mm/yyyy hh24:mi') createddate FROM dual UNION ALL
SELECT '345cxv' childvalue, 5 skipno, to_date('16/11/2018 14:16', 'dd/mm/yyyy hh24:mi') createddate FROM dual UNION ALL
SELECT '563mnj' childvalue, 6 skipno, to_date('19/11/2018 02:12', 'dd/mm/yyyy hh24:mi') createddate FROM dual)
SELECT row_number() OVER (PARTITION BY skipno ORDER BY createddate) rno,
childvalue,
first_value(childvalue) OVER (PARTITION BY skipno ORDER BY createddate) mothervlue,
skipno,
createddate
FROM vals;
RNO CHILDVALUE MOTHERVLUE SKIPNO CREATEDDATE
---------- ---------- ---------- ---------- -----------
1 345dg 345dg 4 19/09/2018
2 342sds 345dg 4 19/09/2018
3 343dfd 345dg 4 06/11/2018
4 234dfs 345dg 4 06/11/2018
1 545ert 545ert 5 06/11/2018
2 543tye 545ert 5 07/11/2018
3 345cxv 545ert 5 16/11/2018
1 563mnj 563mnj 6 19/11/2018

Related

Oracle SQL how many days has passed since previous order

I have a problem how to calculate the days how many days has passed since previous order.
My code:
select
order_id,
order_date
from
oe.orders
where customer_id = 838
order by
order_date desc
The order_id and order_date are like below:
order_id = 1920 & order_date= 25-MAR-19 15.45.38.000000000
order_id = 1618 & order_date= 08-FEB-19 12.51.39.000000000
order_id = 1592 & order_date= 04-FEB-19 07.35.46.000000000
...
I am new user of sql and no idea how to do it. Thank you for your help!
If you want the differences in days (just the date part) then:
WITH
tbl AS
(
Select 1 "ID", To_Date('25-MAR-19 15.45.38', 'dd-MON-yy hh24:mi:ss') "A_DATE" From Dual Union All
Select 2 "ID", To_Date('08-FEB-19 12.51.39', 'dd-MON-yy hh24:mi:ss') "A_DATE" From Dual Union All
Select 3 "ID", To_Date('04-FEB-19 07.35.46', 'dd-MON-yy hh24:mi:ss') "A_DATE" From Dual Union All
Select 4 "ID", To_Date('28-JAN-19 12.13.10', 'dd-MON-yy hh24:mi:ss') "A_DATE" From Dual
)
Select
ID "ID",
TRUNC(A_DATE, 'dd') - TRUNC(Nvl(First_Value(A_DATE) OVER (Order By ID Rows Between 1 Preceding And Current Row), A_DATE), 'dd') "DAYS_DIFF"
From
tbl
ID
DAYS_DIFF
1
0
2
-45
3
-4
4
-7
OR ...
Select
ID "ID",
TRUNC(A_DATE, 'dd') - TRUNC(Nvl(Last_Value(A_DATE) OVER (Order By ID Rows Between Current Row And 1 Following ), A_DATE), 'dd') "DAYS_DIFF"
From
tbl
Order By TRUNC(A_DATE, 'dd')
... result
ID
DAYS_DIFF
4
0
3
7
2
4
1
45
Regards
CREATE TABLE orders
(ORDER_ID, ORDER_DATE) AS
SELECT 3, TIMESTAMP'2022-10-31 09:54:48' FROM DUAL UNION ALL
SELECT 2, TIMESTAMP'2022-10-17 19:04:44' FROM DUAL UNION ALL
SELECT 1, TIMESTAMP'2022-10-08 14:44:23' FROM DUAL
SELECT order_id, order_date,
order_date - LAG(order_date) OVER (ORDER BY order_id) AS diff
FROM orders;
ORDER_ID ORDER_DATE DIFF
1 08-OCT-22 02.44.23.000000 PM -
2 17-OCT-22 07.04.44.000000 PM +000000009 04:20:21.000000000
3 31-OCT-22 09.54.48.000000 AM +000000013 14:50:04.000000000

SQL Query to fetch the most old start_date if there is no change in one column value

I have a table with the following columns per_all_assignments_m-
per_assignments
Person_id position_id system_person_type start_date END_DT
1 1 EMP 01-JAN-2019 20-JAN-2019
1 1 EMP 21-JAN-2019 31-DEC-4712
2 1 EMP 01-JAN-2019 03-JUL-2019
2 1 EMP 04-JUL-2019 08-SEP-2019
2 2 EMP 09-SEP-2019 31-DEC-2019
2 2 EMP 01-JAN-2020 31-DEC-4712
3 10 EMP 01-JAN-2019 20-JAN-2019
3 10 EMP 21-JAN-2019 08-SEP-2019
3 10 EMP 09-SEP-2019 20-JAN-2020
3 10 EMP 21-JAN-2020 31-DEC-4712
I have created the below query to fetch, if there is any value change in the column position_id. This query will fetch the date when the date was changed and the previous start date.
select person_id, prev_start_dt, effective_start_date current_start_dt,
case pos_new when pos_old then 1 else pos_old end pos_old
from (
select person_id, position_id pos_new, effective_start_date, effective_end_date,
lag(position_id) over (partition by person_id order by effective_start_date) pos_old,
lag(effective_start_date) over (partition by person_id order by effective_start_date) prev_start_dt,
case effective_start_date when 1 + lag(effective_end_date) over (partition by person_id order by effective_start_date)
then 1 end flag
from per_all_assignments_m
where person_id=1
and assignment_type = 'E')
where flag = 1 and (pos_new <> pos_old )
For the above table, this query will fetch 09-SEP-2019 for employee #2 as the current_start_dt and 04-JUL-2019 as prev_start_dt
Question-
Now, I want to add the condition where, if there is no change in the position_id values then the most old effective_start_date should be retrieved.
Example-
for EE#1 , current_start_dt should be 01-JAN-2019 and prev_start_dt should be the same for EE#3 , current_start_dt AND prev_start_dt should be
01-JAN-2019.
Any suggestions would be of great help!
Required output -
Person_id prev_start_dt current_start_dt pos_old
1 01-JAN-2019 01-JAN-2019 1
2 04-JUL-2019 09-SEP-2019 1
3 01-JAN-2019 01-JAN-2019 10
Here is one way to do it:
cte2 getr the person_id with changes, and the result is the person_id row and add the first occurence of person_id without changes
Fiddle
with cte as
(
select 1 as Person_id, 1 as position_id, 'EMP' as system_person_type, STR_TO_DATE('01-JAN-2019', "%d-%M-%Y") as start_date, STR_TO_DATE('20-JAN-2019', "%d-%M-%Y") as END_DT
union select 1, 1 , 'EMP' , STR_TO_DATE('21-JAN-2019', "%d-%M-%Y"), STR_TO_DATE('31-DEC-4712', "%d-%M-%Y")
union select 2 , 1 , 'EMP' , STR_TO_DATE('01-JAN-2019', "%d-%M-%Y") , STR_TO_DATE('03-JUL-2019', "%d-%M-%Y")
union select 2 , 1 , 'EMP' , STR_TO_DATE('04-JUL-2019', "%d-%M-%Y") , STR_TO_DATE('08-SEP-2019', "%d-%M-%Y")
union select 2 , 2 , 'EMP' , STR_TO_DATE('09-SEP-2019', "%d-%M-%Y") , STR_TO_DATE('31-DEC-2019' , "%d-%M-%Y")
union select 2 , 2 , 'EMP' , STR_TO_DATE('01-JAN-2020', "%d-%M-%Y") , STR_TO_DATE('31-DEC-4712', "%d-%M-%Y")
union select 3 , 10 , 'EMP' , STR_TO_DATE('01-JAN-2019', "%d-%M-%Y") , STR_TO_DATE('20-JAN-2019' , "%d-%M-%Y")
union select 3 , 10 , 'EMP' , STR_TO_DATE('21-JAN-2019', "%d-%M-%Y") , STR_TO_DATE('08-SEP-2019', "%d-%M-%Y")
union select 3 , 10 , 'EMP' , STR_TO_DATE('09-SEP-2019', "%d-%M-%Y") , STR_TO_DATE('20-JAN-2020', "%d-%M-%Y")
union select 3 , 10 , 'EMP' , STR_TO_DATE('21-JAN-2020', "%d-%M-%Y") ,STR_TO_DATE('31-DEC-4712', "%d-%M-%Y");
),
cte2 as
(
select a.Person_id, ( (b.start_date)) as prev_start_dt, (a.start_date) as current_start_dt, (b.position_id)
from cte a left join cte b on a.Person_id = b.Person_id and a.start_date > b.start_date and a.position_id <> b.position_id
and not exists(select 1 from cte c where a.Person_id = c.Person_id and a.start_date > c.start_date and c.start_date > b.start_date)
)
select Person_id, prev_start_dt, current_start_dt, position_id from cte2 where position_id is not null
union
select a.Person_id, a.start_date, a.start_date, a.position_id from cte a where not exists(select 1 from cte b where a.Person_id = b.Person_id and a.start_date > b.start_date)
and a.Person_id not in (select Person_id from cte2 where position_id is not null)
Output:
Person_id prev_start_dt current_start_dt position_id
1 2019-01-01 2019-01-01 1
2 2019-07-04 2019-09-09 1
3 2019-01-01 2019-01-01 10
If you want to track all the cases where there is a change in Position ID, something like below should work:
with cte as
(
select 1 as Person_id,1 as position_id,'EMP' as system_person_type,cast('01Jan2019' as date) as Start_date,cast('20Jan2019' as date) as end_dt from dual union
select 1 as Person_id,1 as position_id,'EMP' as system_person_type,cast('21Jan2019' as date) as Start_date,cast('31Dec4712' as date) as end_dt from dual union
select 2 as Person_id,1 as position_id,'EMP' as system_person_type,cast('01Jan2019' as date) as Start_date,cast('03Jul2019' as date) as end_dt from dual union
select 2 as Person_id,1 as position_id,'EMP' as system_person_type,cast('04Jul2019' as date) as Start_date,cast('08Sep2019' as date) as end_dt from dual union
select 2 as Person_id,2 as position_id,'EMP' as system_person_type,cast('09Sep2019' as date) as Start_date,cast('31Dec2019' as date) as end_dt from dual union
select 2 as Person_id,2 as position_id,'EMP' as system_person_type,cast('01Jan2020' as date) as Start_date,cast('31Dec4712' as date) as end_dt from dual union
select 3 as Person_id,10 as position_id,'EMP' as system_person_type,cast('01Jan2019' as date) as Start_date,cast('20Jan2019' as date) as end_dt from dual union
select 3 as Person_id,10 as position_id,'EMP' as system_person_type,cast('21Jan2019' as date) as Start_date,cast('08Sep2019' as date) as end_dt from dual union
select 3 as Person_id,10 as position_id,'EMP' as system_person_type,cast('09Sep2019' as date) as Start_date,cast('20Jan2020' as date) as end_dt from dual union
select 3 as Person_id,10 as position_id,'EMP' as system_person_type,cast('21Jan2020' as date) as Start_date,cast('31Dec4712' as date) as end_dt from dual
)
select x.Person_Id,
COALESCE(x.lag_start,x.min_date) as Prev_start_dt,
x.min_date as Current_Start_Dt,
COALESCE(x.lag_pos,x.position_id) as Pos_old
FROM
(select person_id,
position_id,
min(start_date) as min_date,
max(start_date) as max_date,
lag(max(start_date)) Over(Partition by Person_Id Order by max(start_date)) as lag_start,
lag(position_id) Over(Partition by Person_Id Order by min(start_date)) as lag_pos
from cte
group by person_id, position_id) x
inner join
(select person_id,
count(distinct position_id) as cnt
from cte
group by person_id) y
ON x.person_id=y.person_id
Where x.Position_id<>x.lag_pos or y.cnt=1
The idea is to group the information at a person and position level and then use the lag function to determine if there is a change. Cnt=1 filter is applied to get the cases where there is no position change. Hope this helps.

Oracle SQL: Count a time period when the value was over the threshold

My table MEASUREMENTS (Oracle SQL 12) has 3 columns: DT - measurement timestamp, MEASUREMENT - value, THRESHOLD - upper limit.
Sometimes measurements are above the threshold. Trying to calculate time periods when the measurement value was higher than the threshold.
DT | MEASUREMENT | THRESHOLD
---------------+-------------+--------------------
04.08.16 01:10 | 60,5 | 70,0
04.08.16 01:20 | 65,5 | 70,0
04.08.16 01:30 | 68,1 | 70,0
04.08.16 01:40 | 70,1* | 70,0 //period start
04.08.16 01:50 | 70,1* | 70,0
04.08.16 02:00 | 70,75* | 70,0 //period end
04.08.16 02:10 | 53,5 | 70,0
04.08.16 02:20 | 50,15 | 70,0
04.08.16 02:30 | 52,15 | 70,0
04.08.16 02:40 | 53,15 | 70,0
Expected result (02:00-01:40=00:20):
DURATION | START | END
---------+----------------+---------------
00:20 | 04.08.16 01:40 | 04.08.16 02:00
You can use row_number() to identify the periods. This is a gaps-and-islands problem. The following returns each period where the measurement exceeds the threshold:
select max(dt) - min(dt) as duration, min(dt), max(dt)
from (select t.*,
row_number() over (order by dt) as seqnum,
row_number() over (partition by (case when measurement > threshold then 1 else 2 end), order by dt) as seqnum_t
from t
) t
where measurement > threshold
group by (seqnum - seqnum_t)
You can use the MATCH_RECOGNIZE clause (plus some extra info):
WITH t (DT, MEASUREMENT, THRESHOLD) AS (
SELECT TO_DATE('01:10', 'hh24:mi'), 60.5 , 70 FROM dual UNION ALL
SELECT TO_DATE('01:20', 'hh24:mi'), 65.5 , 70 FROM dual UNION ALL
SELECT TO_DATE('01:30', 'hh24:mi'), 68.1 , 70 FROM dual UNION ALL
SELECT TO_DATE('01:40', 'hh24:mi'), 70.1 , 70 FROM dual UNION ALL
SELECT TO_DATE('01:50', 'hh24:mi'), 70.1 , 70 FROM dual UNION ALL
SELECT TO_DATE('02:00', 'hh24:mi'), 70.75 , 70 FROM dual UNION ALL
SELECT TO_DATE('02:10', 'hh24:mi'), 53.5 , 70 FROM dual UNION ALL
SELECT TO_DATE('02:20', 'hh24:mi'), 50.15 , 70 FROM dual UNION ALL
SELECT TO_DATE('02:30', 'hh24:mi'), 52.15 , 70 FROM dual UNION ALL
SELECT TO_DATE('02:40', 'hh24:mi'), 53.15 , 70 FROM dual)
SELECT MEASUREMENT_MAX, match_num, FIRST_DT, LAST_DT, (LAST_DT-FIRST_DT)*24*60 AS DURATION
FROM t
MATCH_RECOGNIZE (
ORDER BY DT
MEASURES
FINAL MAX(MEASUREMENT) AS MEASUREMENT_MAX,
MATCH_NUMBER() AS match_num,
FINAL LAST(DT) AS LAST_DT,
FINAL FIRST(DT) AS FIRST_DT
PATTERN (a+)
DEFINE
a AS MEASUREMENT > THRESHOLD);
MEASUREMENT_MAX match_num FIRST_DT LAST_DT DURATION
70.75 3 01.06.2018 01:40:00 01.06.2018 02:00:00 20
You don't need to use two row_numbers, you can directly use it via cumulative approach :
select max(dt) - min(dt) as duration, min(dt), max(dt)
from (select *, row_number() over (order by dt) as seq,
sum(case when measurement > threshold then 1 else 0 end) over(order by dt) as grp
from table
) t
where measurement > threshold
group by (seq - grp);
Using row_number analytical function:
SQL> WITH measurements (DT, MEASUREMENT, THRESHOLD) AS (
2 select to_date('04.08.16 01:10', 'DD.MM.YY HH24:MI'), 60.5, 70.0 from dual union all
3 select to_date('04.08.16 01:20', 'DD.MM.YY HH24:MI'), 65.5, 70.0 from dual union all
4 select to_date('04.08.16 01:30', 'DD.MM.YY HH24:MI'), 68.1, 70.0 from dual union all
5 select to_date('04.08.16 01:40', 'DD.MM.YY HH24:MI'), 70.1, 70.0 from dual union all
6 select to_date('04.08.16 01:50', 'DD.MM.YY HH24:MI'), 70.1, 70.0 from dual union all
7 select to_date('04.08.16 02:00', 'DD.MM.YY HH24:MI'), 70.75, 70.0 from dual union all
8 select to_date('04.08.16 02:10', 'DD.MM.YY HH24:MI'), 53.5, 70.0 from dual union all
9 select to_date('04.08.16 02:20', 'DD.MM.YY HH24:MI'), 50.15, 70.0 from dual union all
10 select to_date('04.08.16 02:30', 'DD.MM.YY HH24:MI'), 52.15, 70.0 from dual union all
11 select to_date('04.08.16 02:40', 'DD.MM.YY HH24:MI'), 53.15, 70.0 from dual),
12 ---------------------
13 ---- end of data preparation
14 ---------------------
15 calculated_values AS (
16 SELECT DT,
17 MEASUREMENT,
18 THRESHOLD,
19 row_number() OVER (ORDER BY dt) - row_number() OVER (PARTITION BY CASE WHEN MEASUREMENT > THRESHOLD THEN 1 ELSE 0 END ORDER BY dt) rn,
20 CASE WHEN MEASUREMENT > THRESHOLD THEN 1 ELSE 0 END threshold_flag
21 FROM measurements)
22 SELECT cast(numtodsinterval(MAX(dt)-MIN(dt), 'DAY') AS INTERVAL DAY(0) TO SECOND(0)) AS duration,
23 MIN(dt) AS "START",
24 MAX(dt) AS "END"
25 FROM calculated_values
26 WHERE threshold_flag > 0
27 GROUP BY rn;
OUTPUT:
DURATION START END
----------- -------------------- --------------------
+0 00:20:00 8/4/2016 1:40:00 8/4/2016 2:00:00
Your query will be:
WITH calculated_values AS (
SELECT DT,
MEASUREMENT,
THRESHOLD,
row_number() OVER (ORDER BY dt) - row_number() OVER (PARTITION BY CASE WHEN MEASUREMENT > THRESHOLD THEN 1 ELSE 0 END ORDER BY dt) rn,
CASE WHEN MEASUREMENT > THRESHOLD THEN 1 ELSE 0 END threshold_flag
FROM measurements)
SELECT cast(numtodsinterval(MAX(dt)-MIN(dt), 'DAY') AS INTERVAL DAY(0) TO SECOND(0)) AS duration,
MIN(dt) AS "START",
MAX(dt) AS "END"
FROM calculated_values
WHERE threshold_flag > 0
GROUP BY rn;

SQL , Analytical Functions , rownumber

I need to get same rownumber or numeric value in SQL to group values that match conditions like the following example:
If we have same Agent name and the time variance between current row and preceding row value is less than 06:00 hours after applying partition by name and ordering by time
then add same rownumber else increase it.
example for row data and output of rownumber:
person date_time rownumber
A 01/04/2018 10:00 1
A 01/04/2018 13:00 1
A 01/04/2018 14:00 1
A 01/04/2018 15:00 1
A 01/04/2018 23:00 2
A 02/04/2018 03:00 2
A 02/04/2018 12:00 3
A 02/04/2018 16:00 3
B 01/04/2018 17:00 4
B 01/04/2018 20:30 4
C 01/04/2018 18:00 5
C 01/04/2018 22:00 5
You can do this with a combination of LAG and SUM analytic functions, like so:
WITH your_table AS (SELECT 'A' person, to_date('01/04/2018 10', 'dd/mm/yyyy hh24') date_time FROM dual UNION ALL
SELECT 'A' person, to_date('01/04/2018 13', 'dd/mm/yyyy hh24') date_time FROM dual UNION ALL
SELECT 'A' person, to_date('01/04/2018 14', 'dd/mm/yyyy hh24') date_time FROM dual UNION ALL
SELECT 'A' person, to_date('01/04/2018 15', 'dd/mm/yyyy hh24') date_time FROM dual UNION ALL
SELECT 'A' person, to_date('01/04/2018 23', 'dd/mm/yyyy hh24') date_time FROM dual UNION ALL
SELECT 'A' person, to_date('02/04/2018 03', 'dd/mm/yyyy hh24') date_time FROM dual UNION ALL
SELECT 'A' person, to_date('02/04/2018 12', 'dd/mm/yyyy hh24') date_time FROM dual UNION ALL
SELECT 'A' person, to_date('02/04/2018 16', 'dd/mm/yyyy hh24') date_time FROM dual UNION ALL
SELECT 'B' person, to_date('01/04/2018 17', 'dd/mm/yyyy hh24') date_time FROM dual UNION ALL
SELECT 'B' person, to_date('01/04/2018 20', 'dd/mm/yyyy hh24') date_time FROM dual UNION ALL
SELECT 'C' person, to_date('01/04/2018 18', 'dd/mm/yyyy hh24') date_time FROM dual UNION ALL
SELECT 'C' person, to_date('01/04/2018 22', 'dd/mm/yyyy hh24') date_time FROM dual)
SELECT person,
date_time,
SUM(period_change) OVER (ORDER BY person, date_time) rownumber
FROM (SELECT person,
date_time,
CASE WHEN date_time - LAG(date_time, 1, date_time - 7/24) OVER (PARTITION BY person ORDER BY date_time) > 6/24 THEN 1 ELSE 0 END period_change
FROM your_table);
PERSON DATE_TIME ROWNUMBER
------ ----------- ----------
A 01/04/2018 1
A 01/04/2018 1
A 01/04/2018 1
A 01/04/2018 1
A 01/04/2018 2
A 02/04/2018 2
A 02/04/2018 3
A 02/04/2018 3
B 01/04/2018 4
B 01/04/2018 4
C 01/04/2018 5
C 01/04/2018 5
This works by putting 1 in the additional column whenever a new group is triggered.
Once you have that, then you can do a running sum on that column. That means that after every group change, subsequent rows will be assigned the same number, up until the next group change.
N.B. As suggested by Peter Lang in the comments below, you might prefer to change the case statement generating the "period_change" column to:
CASE WHEN date_time - LAG(date_time) OVER (PARTITION BY person ORDER BY date_time) < 6/24 THEN 0 ELSE 1 END

PL/SQL: How to convert multiple rows with continuous time frames into one single row convering the whole time frame?

Let's say I have the following database table:
id | from | to
1 | 01-JAN-2015 | 03-MAR-2015
1 | 04-MAR-2015 | 31-AUG-2015
1 | 01-SEP-2015 | 31-DEC-2015
2 | 01-JAN-2015 | 30-JUN-2015
2 | 01-NOV-2015 | 31-DEC-2015
And I want to summarise the records with the same id that are continuous in time into one single row covering the full time frame, as follows:
id | from | to
1 | 01-JAN-2015 | 31-DEC-2015
2 | 01-JAN-2015 | 30-JUN-2015
2 | 01-NOV-2015 | 31-DEC-2015
So, because the time frames are sequential and have no gaps between them, the 3 rows for id 1 could be converted into 1 single row with the minimum from date and the maximum to date. The 2 rows for id 2 would remain the same as the time frames are not continuous.
I'm thinking on doing this using a loop through a cursor, but I might be complicating things.
Any better ideas? perhaps with SQL queries only?
You can do it using hierarchical queries, something like this:
select id, min(root_dt_from) dt_from, dt_to
from (select id, dt_from, dt_to, level, connect_by_isleaf, connect_by_root(dt_from) root_dt_from
from t
where connect_by_isleaf = 1
connect by prior id = id and prior (dt_to + 1) = dt_from
)
group by id, dt_to;
Sample execution:
SQL> with t as (
2 select 1 id, to_date('01-JAN-2015', 'DD-MON-YYYY') dt_from, to_date('03-MAR-2015', 'DD-MON-YYYY') dt_to from dual union all
3 select 1 id, to_date('04-MAR-2015', 'DD-MON-YYYY') dt_from, to_date('31-AUG-2015', 'DD-MON-YYYY') dt_to from dual union all
4 select 1 id, to_date('01-SEP-2015', 'DD-MON-YYYY') dt_from, to_date('31-DEC-2015', 'DD-MON-YYYY') dt_to from dual union all
5 select 2 id, to_date('01-JAN-2015', 'DD-MON-YYYY') dt_from, to_date('30-JUN-2015', 'DD-MON-YYYY') dt_to from dual union all
6 select 2 id, to_date('01-NOV-2015', 'DD-MON-YYYY') dt_from, to_date('31-DEC-2015', 'DD-MON-YYYY') dt_to from dual
7 ) -- end of sample data
8 select id, min(root_dt_from) dt_from, dt_to
9 from (select id, dt_from, dt_to, level, connect_by_isleaf, connect_by_root(dt_from) root_dt_from
10 from t
11 where connect_by_isleaf = 1
12 connect by prior id = id and prior (dt_to + 1) = dt_from
13 )
14 group by id, dt_to;
ID DT_FROM DT_TO
---------- ----------- -----------
1 01-JAN-2015 31-DEC-2015
2 01-NOV-2015 31-DEC-2015
2 01-JAN-2015 30-JUN-2015
You can do this is stages with a few analytic and aggregate functions:
with t1(id, from_dt, to_dt) as (
select 1, to_date('01-JAN-2015', 'dd-mon-rrrr'), to_date('03-MAR-2015', 'dd-mon-rrrr') from dual union all
select 1, to_date('04-MAR-2015', 'dd-mon-rrrr'), to_date('31-AUG-2015', 'dd-mon-rrrr') from dual union all
select 1, to_date('01-SEP-2015', 'dd-mon-rrrr'), to_date('31-DEC-2015', 'dd-mon-rrrr') from dual union all
select 2, to_date('01-JAN-2015', 'dd-mon-rrrr'), to_date('30-JUN-2015', 'dd-mon-rrrr') from dual union all
select 2, to_date('01-NOV-2015', 'dd-mon-rrrr'), to_date('31-DEC-2015', 'dd-mon-rrrr') from dual
), t2 as (
select id
, from_dt
, to_dt
, from_dt-lag(to_dt,1,from_dt-1) over (partition by id order by to_dt) dst
, row_number() over (partition by id order by to_dt) rn
from t1
), t3 as (
select id
, from_dt
, to_dt
, sum(dst) over (partition by id order by rn) - rn grp
from t2
)
select id
, min(from_dt) from_dt
, max(to_dt) to_dt
from t3
group by id, grp;
The first stage T1 is just recreating your data. In T2 I subtract the lag of to_dt from from_dt to find the distance (dst) between consecutive records and generate row_number for each record (rn). In T3 I subtract rn from the running sum of dst to generate a group id (grp). Finally in the output stage I take the min and max of from_dt and to_dt respectively grouping by ID and grp columns.
You can try here some analytical functions which can really simplify
the scenario. Hope this below snippet helps. Let me know for any
issues.
SELECT B.ID,
MIN(B.FRM_DT) FRM_DT,
MAX(B.TO_DT) TO_DT
FROM
(SELECT A.ID,
A.FRM_DT,
A.TO_DT,
NVL(LAG(A.TO_DT+1) OVER(PARTITION BY A.ID ORDER BY A.TO_DT),A.FRM_DT) nxt_dt,
CASE
WHEN NULLIF(A.FRM_DT,NVL(LAG(A.TO_DT+1) OVER(PARTITION BY A.ID ORDER BY A.TO_DT),A.FRM_DT)) IS NULL
THEN 'True'
ELSE 'False'
END COND
FROM
(SELECT 1 AS ID,
TO_DATE('01/01/2015') FRM_DT,
TO_DATE('03/03/2015') TO_DT
FROM DUAL
UNION
SELECT 1 AS ID,
TO_DATE('03/04/2015') FRM_DT,
TO_DATE('07/31/2015') TO_DT
FROM DUAL
UNION
SELECT 1 AS ID,
TO_DATE('08/01/2015') FRM_DT,
TO_DATE('12/31/2015') TO_DT
FROM DUAL
UNION
SELECT 2 AS ID,
TO_DATE('01/01/2015') FRM_DT,
TO_DATE('06/30/2015') TO_DT
FROM DUAL
UNION
SELECT 2 AS ID,
TO_DATE('11/01/2015') FRM_DT,
TO_DATE('12/31/2015') TO_DT
FROM DUAL
UNION
SELECT 3 AS ID,
TO_DATE('01/01/2015') FRM_DT,
TO_DATE('03/14/2015') TO_DT
FROM DUAL
UNION
SELECT 3 AS ID,
TO_DATE('03/15/2015') FRM_DT,
TO_DATE('11/30/2015') TO_DT
FROM DUAL
UNION
SELECT 3 AS ID,
TO_DATE('12/01/2015') FRM_DT,
TO_DATE('12/31/2015') TO_DT
FROM DUAL
UNION
SELECT 4 AS ID,
TO_DATE('02/01/2015') FRM_DT,
TO_DATE('05/30/2015') TO_DT
FROM DUAL
UNION
SELECT 4 AS ID,
TO_DATE('06/01/2015') FRM_DT,
TO_DATE('12/31/2015') TO_DT
FROM DUAL
)A
)B
GROUP BY B.ID,
B.COND;
-----------------------------------OUTPUT------------------------------------------
ID FRM_DT TO_DT
4 02/01/2015 05/30/2015
4 06/01/2015 12/31/2015
1 01/01/2015 12/31/2015
2 01/01/2015 06/30/2015
2 11/01/2015 12/31/2015
3 01/01/2015 12/31/2015
-----------------------------------OUTPUT------------------------------------------