How to split record in multiple records from start/end date record - oracle

I'm trying to split record to multiple record from start/end date in Oracle
I have data like this
MachineID | start date | end date | running time |
WC01 | 2019/09/05 07:00 | 2019/09/07 09:00 | 26:00 |
and I want to split record to each day from 08:00 to 08:00
MachineID | running date | running time |
WC01 | 2019/09/05 | 1:00 |
WC01 | 2019/09/06 | 24:00 |
WC01 | 2019/09/07 | 1:00 |
Thank you for your help!

We can handle this via the help from a calendar table which contains all dates you expect to appear in your data set, along with a separate record for each minute:
WITH dates AS (
SELECT TIMESTAMP '2019-09-05 00:00:00' + NUMTODSINTERVAL(rownum, 'MINUTE') AS dt
FROM dual
CONNECT BY level <= 5000
)
SELECT
m.MachineID,
TRUNC(d.dt) AS running_date,
COUNT(t.MachineID) / 60 AS running_hours
FROM dates d
CROSS JOIN (SELECT DISTINCT MachineID FROM yourTable) m
LEFT JOIN yourTable t
ON d.dt >= t.start_date AND d.dt < t.end_date
WHERE
TO_CHAR(d.dt, 'HH24') >= '08' AND TO_CHAR(d.dt, 'HH24') < '21'
GROUP BY
m.MachineID,
TRUNC(d.dt)
ORDER BY
TRUNC(d.dt);
Demo

You can try below query:
SELECT
MACHINEID,
RUNNING_DATE,
DECODE(RUNNING_DATE, TRUNC(START_DATE), CASE
WHEN DIFF_START < 0 THEN 0
WHEN DIFF_START > 12 THEN 12
ELSE DIFF_START
END, TRUNC(END_DATE), CASE
WHEN DIFF_END < 0 THEN 0
WHEN DIFF_END > 12 THEN 12
ELSE DIFF_END
END, 24) AS RUNNING_HOURS
FROM
(
SELECT
MACHINEID,
RUNNING_DATE,
ROUND(24 *((TRUNC(START_DATE + LVL - 1) + 8 / 24) - START_DATE)) AS DIFF_START,
ROUND(24 *(END_DATE -(TRUNC(START_DATE + LVL - 1) + 8 / 24))) AS DIFF_END,
START_DATE,
END_DATE
FROM
(
SELECT
DISTINCT MACHINEID,
LEVEL AS LVL,
START_DATE,
END_DATE,
TRUNC(START_DATE + LEVEL - 1) AS RUNNING_DATE
FROM
YOURTABLE
CONNECT BY
LEVEL <= TRUNC(END_DATE) - TRUNC(START_DATE) + 1
)
);
db<>fiddle demo
Change the logic wherever it is not meeting your requirement. I have created the query taking sample data and expected output into consideration.
Cheers!!

Related

Oracle - Join 2 periods of time

I have 2 rows with 2 periods of time that intersect. For example:
---------------------------------------------
| START_DATE | END_DATE |
---------------------------------------------
| 01/01/2018 08:00:00 | 01/01/2018 09:30:00 |
| 01/01/2018 08:30:00 | 01/01/2018 10:00:00 |
---------------------------------------------
There are 30 minutes where both periods intersect. I want to avoid it. I would like to join both rows in one single column, taking the starting date as the older and the ending date as the newer:
---------------------------------------------
| START_DATE | END_DATE |
---------------------------------------------
| 01/01/2018 08:00:00 | 01/01/2018 10:00:00 |
---------------------------------------------
Have you any idea how can I get the solution I want with a SQL sentence?
For two rows just use greatest() and least(). But the problem is when you have many rows which may overlap in different ways. You can:
add row numbers to each row,
assign groups for overlapping periods using recursive query,
group data using this value and find min and max dates in each group.
dbfiddle demo
with
r(rn, start_date, end_date) as (
select row_number() over(order by start_date), start_date, end_date from t ),
c(rn, start_date, end_date, grp) as (
select rn, start_date, end_date, 1 from r where rn = 1
union all
select r.rn,
case when r.start_date <= c.end_date and c.start_date <= r.end_date
then least(r.start_date, c.start_date) else r.start_date end,
case when r.start_date <= c.end_date and c.start_date <= r.end_date
then greatest(r.end_date, c.end_date) else r.end_date end,
case when r.start_date <= c.end_date and c.start_date <= r.end_date
then grp else grp + 1 end
from c join r on r.rn = c.rn + 1)
select min(start_date), max(end_date) from c group by grp
If all you have is a set of date ranges, with no other correlating or constraining criteria, and you want to reduce that to a set of non overlapping ranges, you can do that with a recursive query like this one:
with recur(start_date, end_date) as (
select * from yourdata yd
where not exists (select 1 from yourdata cyd
where yd.start_Date between cyd.start_date and cyd.end_date
and (yd.start_date <> cyd.start_date or yd.end_date <> cyd.end_date))
union all
select r.start_date
, yd.end_date
from recur r
join yourdata yd
on r.start_date < yd.start_date
and yd.start_date <= r.end_date
and r.end_date < yd.end_date
)
select start_date, max(end_date) end_Date from recur group by start_Date;
In this query the anchor (the part before the union all) select all records whose start date is not contained in any other range.
The recursive part (the part after the union all) then select ranges that extend the current range. In both halves the original start date is returned while in the recursive part the new extended end date is returned. This results in a set of over lapping ranges with a common start date.
Finally the output query returns the start date and max end date grouped by start date.

Want to ROUND the Data according to DAY difference

Query :
select
TO_CHAR((to_date(IP_START_DATE,'DD-MM-YYYY HH24:MI:SS')+ (level-1)),'DD-MM-YYYY'),
TO_CHAR(to_date(IP_START_DATE,'DD-MM-YYYY HH24:MI:SS') + level,'DD-MM-YYYY') ,
to_number(regexp_substr(IP_PLAN_CONSUMPTION, '^\d+'))/(TO_DATE(IP_END_DATE, 'DD-MM-YYYY HH24:MI:SS') - TO_DATE(IP_START_DATE, 'DD-MM-YYYY HH24:MI:SS')) || regexp_substr(IP_PLAN_CONSUMPTION, '[A-Z]') as IP_PLAN_CONSUMPTION
FROM
dual
CONNECT BY
level <= to_date(IP_END_DATE,'DD-MM-YYYY HH24:MI:SS')-to_date(IP_START_DATE,'DD-MM-YYYY HH24:MI:SS')+1;
-> Data in Query :
select
TO_CHAR((to_date('16-07-2018 11:02','DD-MM-YYYY HH24:MI:SS')+ (level-1)),'DD-MM-YYYY'),
TO_CHAR(to_date('16-07-2018 11:02','DD-MM-YYYY HH24:MI:SS') + level,'DD-MM-YYYY'),
to_number(regexp_substr('4000 T', '^\d+'))/(TO_DATE('18-07-2018 00:00', 'DD-MM-YYYY HH24:MI:SS') - TO_DATE('16-07-2018 11:02', 'DD-MM-YYYY HH24:MI:SS')) || regexp_substr('4000 T', '[A-Z]') as IP_PLAN_CONSUMPTION
FROM
dual
CONNECT BY
level <= to_date('18-07-2018 00:00','DD-MM-YYYY HH24:MI:SS')-to_date('16-07-2018 11:02','DD-MM-YYYY HH24:MI:SS')+1;
Output will Be :
But its should be 2000 T
Not : If Start Date: 16-07-2018 00:00 & End Date : 19-07-2018 00:00 then Day Difference is 3 Days & Consumption is 4000 T then Inserted Consumption Should be 1333.333333333333 T ~ 1334 T in each date.
If you are storing dates, you should store them in your table as the DATE data type (and not as strings).
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE your_table( id, ip_start_date, ip_end_date, ip_plan_consumption ) AS
SELECT 1,
DATE '2018-07-16' + INTERVAL '11:02' HOUR TO MINUTE,
DATE '2018-07-18' + INTERVAL '00:00' HOUR TO MINUTE,
'4000 T'
FROM DUAL
UNION ALL
SELECT 2,
DATE '2018-07-16' + INTERVAL '11:02' HOUR TO MINUTE,
DATE '2018-07-16' + INTERVAL '23:08' HOUR TO MINUTE,
'3000 T'
FROM DUAL
UNION ALL
SELECT 3,
DATE '2018-07-10' + INTERVAL '00:00' HOUR TO MINUTE,
DATE '2018-07-13' + INTERVAL '23:59' HOUR TO MINUTE,
'15000 U'
FROM DUAL
;
Query 1:
WITH data ( id, start_dt, end_dt, consumption, units ) AS (
SELECT id,
TRUNC( IP_START_DATE ),
GREATEST( TRUNC( IP_START_DATE ) + 1, TRUNC( IP_END_DATE ) ),
TO_NUMBER( REGEXP_SUBSTR( IP_PLAN_CONSUMPTION, '^\d+' ) ),
REGEXP_SUBSTR( IP_PLAN_CONSUMPTION, '\S+$' )
FROM your_table
)
SELECT id,
t.column_value AS start_dt,
t.column_value + 1 AS end_dt,
consumption / ( end_dt - start_dt ) || units AS IP_PLAN_CONSUMPTION
FROM data d
CROSS JOIN
TABLE(
CAST(
MULTISET(
SELECT d.start_dt + LEVEL - 1
FROM DUAL
CONNECT BY d.start_dt + LEVEL - 1 < d.end_dt
)
AS SYS.ODCIDATELIST
)
) t
Results:
| ID | START_DT | END_DT | IP_PLAN_CONSUMPTION |
|----|----------------------|----------------------|---------------------|
| 1 | 2018-07-16T00:00:00Z | 2018-07-17T00:00:00Z | 2000T |
| 1 | 2018-07-17T00:00:00Z | 2018-07-18T00:00:00Z | 2000T |
| 2 | 2018-07-16T00:00:00Z | 2018-07-17T00:00:00Z | 3000T |
| 3 | 2018-07-10T00:00:00Z | 2018-07-11T00:00:00Z | 5000U |
| 3 | 2018-07-11T00:00:00Z | 2018-07-12T00:00:00Z | 5000U |
| 3 | 2018-07-12T00:00:00Z | 2018-07-13T00:00:00Z | 5000U |

Round off time value to before midnight in oracle sql

I am writing a query that will allow me to workout dates for when particular jobs need to be completed.
The following is a list of my jobs and codes.
JOB_NO | CODES | RCVD_TIME
ABC1 | 1 | 07-JAN-17 09:44:07
DEF2 | 3 | 20-MAR-17 14:32:49
GHI3 | 3 | 27-MAR-17 10:00:03
JKL4 | 1 | 12-JAN-17 12:59:05
Now I have a few conditions in order to workout the end date/time for these jobs.
Code 1 - Add 1 day + anytime until 23:59:59
Code 3 - If rcvd time is before 1pm then job to be completed same day until 23:59:59, if rcvd time is 1pm or after then job to be completed before 1pm next day.
My simple query:
SELECT
JOB_NO, CODES, RCVD_TIME,
CASE
WHEN CODES = '1'
THEN RCVD_TIME + 1
ELSE RCVD_TIME
END AS TARGET
FROM...
This gives me (not sure how to write query for codes 3):
JOB_NO | CODES | RCVD_TIME | TARGET
ABC1 | 1 | 07-JAN-17 09:44:07 | 08-JAN-17 09:44:07
DEF2 | 3 | 20-MAR-17 14:32:49 | 20-MAR-17 14:32:49
GHI3 | 3 | 27-MAR-17 10:00:03 | 27-MAR-17 10:00:03
JKL4 | 1 | 12-JAN-17 12:59:05 | 13-JAN-17 12:59:05
This is what I would like:
JOB_NO | CODES | RCVD_TIME | TARGET
ABC1 | 1 | 07-JAN-17 09:44:07 | 08-JAN-17 23:59:59
DEF2 | 3 | 20-MAR-17 14:32:49 | 21-MAR-17 13:00:00
GHI3 | 3 | 27-MAR-17 10:00:03 | 27-MAR-17 23:59:59
JKL4 | 1 | 12-JAN-17 12:59:05 | 13-JAN-17 23:59:59
Would really appreciate if somebody could advise on how I can get my required answer.
Thanks in advance.
EDIT
bit frustrating but been told to add one more condition to consider working day of the week is between 1 and 6 so have to make sure the target time does not fall into a sunday (day 7). sorry guys for the double, just thought I would add it here in case it gets missed.
You can use a CASE statement in your SQL to handle your complex if-then logic.
-- TEST_DATA is not part of the solution
with test_data ( job_no, code, rcvd_time, target ) AS (
SELECT 'ABC1',1,to_date('07-JAN-17 09:44:07','DD-MON-YY HH24:MI:SS'),to_date('08-JAN-17 09:44:07','DD-MON-YY HH24:MI:SS') FROM DUAL UNION ALL
SELECT 'DEF2',3,to_date('20-MAR-17 14:32:49','DD-MON-YY HH24:MI:SS'),to_date('20-MAR-17 14:32:49','DD-MON-YY HH24:MI:SS') FROM DUAL UNION ALL
SELECT 'GHI3',3,to_date('27-MAR-17 10:00:03','DD-MON-YY HH24:MI:SS'),to_date('27-MAR-17 10:00:03','DD-MON-YY HH24:MI:SS') FROM DUAL UNION ALL
SELECT 'JKL4',1,to_date('12-JAN-17 12:59:05','DD-MON-YY HH24:MI:SS'),to_date('13-JAN-17 12:59:05','DD-MON-YY HH24:MI:SS') FROM DUAL)
-- Actual solution begins here...
SELECT job_no, code, rcvd_time,
case
when rcvd_time - trunc(rcvd_time) < 13/24 and code = 1 then
trunc(rcvd_time) + (86399/86400) + 1
when rcvd_time - trunc(rcvd_time) < 13/24 and code = 3 then
trunc(rcvd_time) + (86399/86400)
when rcvd_time - trunc(rcvd_time) >= 13/24 then
trunc(rcvd_time) + 1 + (13/24)
end target
from test_data;
You can use interval literals (so you do not need to sprinkle magic numbers throughout your code):
CASE
WHEN code = 1
THEN TRUNC( rcvd_time ) + INTERVAL '2' DAY - INTERVAL '1' SECOND
WHEN code = 3 AND TO_CHAR( rcvd_time, 'HH24' ) < '13'
THEN TRUNC( rcvd_time ) + INTERVAL '1' DAY - INTERVAL '1' SECOND
WHEN code = 3 -- AND TO_CHAR( rcvd_time, 'HH24' ) >= '13'
THEN TRUNC( rcvd_time ) + INTERVAL '1' DAY + INTERVAL '13' HOUR
END
To compare the time you can use various different techniques:
TO_CHAR( rcvd_time, 'HH24' ) < '13'
EXTRACT( HOUR FROM CAST( rcvd_time AS TIMESTAMP ) ) < 13
rcvd_time < TRUNC( rcvd_time ) + INTERVAL '13' HOUR
In the solution below, I assume that the "special" handling applies only when CODES is 3, otherwise the TARGET is the end of the next day. (This allows CODES values other than 1 and 3... you can adapt if you have more CODES values and additional rules.)
Since all TARGET date-times are offset from trunc(rcvd_time), I use a CASE expression just to determine the offset.
select job_no, codes, rcvd_time,
trunc(rcvd_time) +
case codes when 3 then
case when extract (hour from cast (rcvd_time as timestamp)) < 13
then interval '23:59:59' hour to second
else interval '1 13:00:00' day to second
end
else interval '1 23:59:59' day to second
end as target
from your_table;
NOTE: Thanks to MT0 for showing the ways to extract hour from a date; I had forgotten that extract(hour from...) works only for timestamps. I edited my answer accordingly.
Try this
select
t.*
,case when codes=3
and to_date(to_char(rcvd_time,'HH24:MI:SS'),'HH24:MI:SS')
>=to_date('13:00:00','HH24:MI:SS')
then trunc(rcvd_time)+1+(13/24) -- add 1 day and then add 13 hours
when codes=1
then trunc(rcvd_time)+2-(1/(24*60*60)) --add 2 days and subtract 1 second
else
trunc(rcvd_time)+1-(1/(24*60*60)) -- add 1 day and subtract 1 second
end as target
from tbl t;
Working sample
with tbl (JOB_NO,CODES,RCVD_TIME) as (
select 'ABC1',1,to_date('07-JAN-17 09:44:07','DD-MON-YY HH24:MI:SS') from dual union all
select 'DEF2',3,to_date('20-MAR-17 14:32:49','DD-MON-YY HH24:MI:SS') from dual union all
select 'GHI3',3,to_date('27-MAR-17 10:00:03','DD-MON-YY HH24:MI:SS') from dual union all
select 'JKL4',1,to_date('12-JAN-17 12:59:05','DD-MON-YY HH24:MI:SS') from dual)
select
t.*
,case when codes=3
and to_date(to_char(rcvd_time,'HH24:MI:SS'),'HH24:MI:SS')
>=to_date('13:00:00','HH24:MI:SS')
then trunc(rcvd_time)+1+(13/24) -- add 1 day and then add 13 hours
when codes=1
then trunc(rcvd_time)+2-(1/(24*60*60)) --add 2 days and subtract 1 second
else
trunc(rcvd_timeJOB_NO,CODES,RCVD_TIME)+1-(1/(24*60*60)) -- add 1 day and subtract 1 second
end as target
from tbl t;
Output
JOB_NO CODES RCVD_TIME TARGET
ABC1 1 07-JAN-2017 09:44:07 08-JAN-2017 23:59:59
DEF2 3 20-MAR-2017 14:32:49 21-MAR-2017 13:00:00
GHI3 3 27-MAR-2017 10:00:03 27-MAR-2017 23:59:59
JKL4 1 12-JAN-2017 12:59:05 13-JAN-2017 23:59:59

How can I combine records on the basis of some conditions

I have following table,
id id_concept start_date end_date
----------------------------------------------
100 282 20/06/2016 24/06/2016
100 282 15/07/2016 18/07/2016
300 282 01/09/2016 02/09/2016
I need to combine the records which has same id,id_concept and the time between END_DATE of one record and START_DATE of the next is 30 days or less (<=30)
Also for the combined records, I need to take the start_date as the very first start_date of the record and end _date as the end_date of the last record
The o/p should be like,
id id_concept start_date end_date count
---------------------------------------------------
100 282 20/06/2016 18/07/2016 2
300 282 01/09/2016 02/09/2016 1
SQL Fiddle
PostgreSQL 9.3 Schema Setup:
CREATE TABLE table_name ( id, id_concept, start_date, end_date ) AS
SELECT 100, 282, DATE '2016-06-20', DATE '2016-06-24' UNION ALL
SELECT 100, 282, DATE '2016-07-15', DATE '2016-07-18' UNION ALL
SELECT 300, 282, DATE '2016-09-01', DATE '2016-09-02';
Query 1:
SELECT id,
id_concept,
MIN( start_date ) AS start_date,
MAX( end_date ) AS end_date,
COUNT(*) AS "count"
FROM (
SELECT id,
id_concept,
start_date,
end_date,
SUM( diff ) OVER (
PARTITION BY id, id_concept
ORDER BY start_date, end_date
) AS grp
FROM (
SELECT t.*,
CASE
WHEN LAG( end_date ) OVER (
PARTITION BY id, id_concept
ORDER BY start_date, end_date
) >= start_date - INTERVAL '30' DAY
THEN 0
ELSE 1
END AS diff
FROM table_name t
) t
) t
GROUP BY id, id_concept, grp
Results:
| id | id_concept | start_date | end_date | count |
|-----|------------|-----------------------------|-----------------------------|-------|
| 300 | 282 | September, 01 2016 00:00:00 | September, 02 2016 00:00:00 | 1 |
| 100 | 282 | June, 20 2016 00:00:00 | July, 18 2016 00:00:00 | 2 |

PL/SQL - Calculate distinct days between overlapping time periods

Imagine this scenario (YYYY/MM/DD):
Start date: 2015/01/01 End date: 2015/08/10
Start date: 2014/10/03 End date: 2015/07/06
Start date: 2015/09/30 End date: 2016/04/28
Using PL/SQL can I calculate the distinct days between these overlapping dates?
Edit: My table has 2 DATE columns, Start_Date and End_Date. The result I'm expecting is 515 days ((2015/08/10 - 2014/10/03) + (2016/04/28 -2015/09/30))
You can do also with pure SQL (no need for PL/SQL):
with
minmax as (select min(start_date) min_dt, max(end_date) max_dt from myTable ),
dates as (
SELECT min_dt + rownum-1 dt1
FROM minmax CONNECT BY ROWNUM <= (max_dt - min_dt +1)
)
select count(*) from dates
where exists(
select 1 from MyTable T2
where dates.dt1 between T2.start_date and T2.end_date )
NOTE: an idea, written from head, not tested. Adapt generated dates as needed, with start date and needed length.
Hope it helps.
EDIT: Using actual table dates
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE DATES ( start_date, end_date ) AS
SELECT DATE '2015-01-01', DATE '2015-08-10' FROM DUAL
UNION ALL SELECT DATE '2014-10-03', DATE '2015-07-06' FROM DUAL
UNION ALL SELECT DATE '2015-09-30', DATE '2016-04-28' FROM DUAL
Query 1:
SELECT COUNT( DISTINCT COLUMN_VALUE ) AS number_of_days
FROM DATES d,
TABLE(
CAST(
MULTISET(
SELECT d.START_DATE + LEVEL - 1
FROM DUAL
CONNECT BY d.START_DATE + LEVEL - 1 < d.END_DATE
)
AS SYS.ODCIDATELIST
)
)
ORDER BY 1
Results:
| NUMBER_OF_DAYS |
|----------------|
| 522 |
Query 2 - Check:
SELECT DATE '2015-08-10' - DATE '2014-10-03'
+ DATE '2016-04-28' - DATE '2015-09-30'
FROM DUAL
Results:
| DATE'2015-08-10'-DATE'2014-10-03'+DATE'2016-04-28'-DATE'2015-09-30' |
|---------------------------------------------------------------------|
| 522 |

Resources