Oracle: splitting time range into days and calculation of duration - oracle

I'm developing code calculation service availability based on events, so I need to split events into daily "sub-events" and calculate duration of then.
So as input I have set of events like (EVENT_ID, START_TIME, END_TIME):
'event1';2021-05-01 12:30;2021-05-01 13:00
'event2';2021-05-03 10:55;2021-05-05 12:01
As output I'd like to get (EVENT_ID, DAY, DURATION_MINUTES):
'event1'; 2021-05-01; 30
'event2'; 2021-05-03; 785
'event2'; 2021-05-04; 1440
'event2'; 2021-05-05; 721
I can get it using procedures and cursor but this is not effective (the events database is quite big), so is there a way to do it using oracle sql query ? Any idea?

You appear to want a recursive query:
WITH days ( event_id, day, start_time, end_time ) AS (
SELECT event_id,
TRUNC( start_time ),
start_time,
end_time
FROM table_name
UNION ALL
SELECT event_id,
day + INTERVAL '1' DAY,
start_time,
end_time
FROM days
WHERE day + INTERVAL '1' DAY < end_time
)
SELECT event_id,
day,
ROUND(
(
LEAST(end_time, day + INTERVAL '1' DAY)
- GREATEST(start_time, day)
) * 24 * 60
) AS duration_minutes
FROM days
Which, for the sample data:
CREATE TABLE table_name ( event_id, start_time, end_time ) AS
SELECT 'event1', DATE '2021-05-01' + INTERVAL '12:30' HOUR TO MINUTE, DATE '2021-05-01' + INTERVAL '13:00' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT 'event2', DATE '2021-05-03' + INTERVAL '10:55' HOUR TO MINUTE, DATE '2021-05-05' + INTERVAL '12:01' HOUR TO MINUTE FROM DUAL;
Outputs:
EVENT_ID
DAY
DURATION_MINUTES
event1
2021-05-01
30
event2
2021-05-03
785
event2
2021-05-04
1440
event2
2021-05-05
721
db<>fiddle here

If your Oracle version is 12 or higher, you can use a lateral join (in any of several equivalent formulations/syntaxes) to make the query faster. For example (using the table set up in MT0's answer):
select event_id, day, round(1440 * duration_days) as duration_minutes
from table_name cross join lateral
( select trunc(start_time) + level - 1 as day,
case when level = 1 and connect_by_isleaf = 1
then end_time - start_time
when level = 1 then 1 - (start_time - trunc(start_time))
when connect_by_isleaf = 1 then end_time - trunc(end_time)
else 1 end as duration_days
from dual
connect by level <= 1 + trunc(end_time) - trunc(start_time)
)
where duration_days != 0
order by event_id, day
;
The where clause is used when the end_time is midnight (at the beginning of an otherwise "new" day); in that case, presumably, you don't want to include that "new day" in the output, with a duration of 0 minutes.
In the lateral view, level = 1 corresponds to the first date in the interval, while connect_by_isleaf = 1 is for the last date in the interval. A special calculation is made when the end_time and start_time are on the same date. The query computes the difference in days first, then converts to minutes. Note that date calculations aren't 100% precise; I used round so I don't get results like 33.9999999999938020 minutes. If the inputs are in hh24:mi, we know beforehand that the answer (in minutes) should be an integer, so round seems fine there.

Related

Time in oracle with crossing over 0:00

can you help me please.
I'm use:
round((24 * 60 * (to_date(EndPick, 'HH24:MI') - to_date(StartPick, 'HH24:MI'))), 2) as WorkTime
All time EndPick bigger then StartPick. But we have a chance, what Start bigger where
estimate time interval if there is a crossing over 00:00 into the next date?
In relult for example:
3
(minutes)
But we have a chance, what Start bigger where estimate time interval if there is a crossing over 00:00 into the next date?
Use a CASE expression and add a day if startpick > endpick:
SELECT round(
(24 * 60 * (to_date(EndPick, 'HH24:MI')
+ CASE
WHEN startpick > endpick
THEN INTERVAL '1' DAY
ELSE INTERVAL '0' DAY
END
- to_date(StartPick, 'HH24:MI')
)),
2
) as WorkTime
FROM table_name;
Which, for the sample data:
CREATE TABLE table_name (startpick, endpick) AS
SELECT '00:00', '01:00' FROM DUAL UNION ALL
SELECT '23:00', '00:00' FROM DUAL;
Outputs:
WORKTIME
60
60
fiddle

How to get last workday before holiday in Oracle [duplicate]

This question already has answers here:
How to get the previous working day from Oracle?
(4 answers)
Closed 1 year ago.
need help for some oracle stuff ..
I need to get Day-1 from sysdate, holiday and weekend will be excluded .
And for holiday, we need to get the range to get the last workday before holiday.
The start date and end date will coming from my holiday table.
ex :
Holiday Table
HolidayName
Start_date
End_Date
holiday1
5th Aug'21
6th Aug'21
condition :
this query run on 9th Aug 2021
expected result :
4th Aug'21
I've tried some query and function but I just can't get what I need.
Thanks a lot for help!
Here's one way to do it.
select max(d) as last_workday
from (select trunc(sysdate)-level as d from dual connect by level < 30) prior_month
where to_char(d, 'DY') not in ('SAT','SUN')
and not exists (select holidayname from holiday_table
where prior_month.d between start_date and end_date)
;
Without seeing your Holiday table, it's hard to say how many days back you would need to look to find the last workday. If you have a holiday that lasts for more than 30 days, you'll need to change the 30 to a larger number.
You can use a simple case expression to determine what day of the week the start of your holiday is, then subtract a number of days based on that.
WITH
holiday (holidayname, start_date, end_date)
AS
(SELECT 'holiday1', DATE '2021-8-5', DATE '2021-8-6' FROM DUAL
UNION ALL
SELECT 'Christmas', DATE '2021-12-25', DATE '2021-12-26' FROM DUAL
UNION ALL
SELECT 'July 4th', DATE '2021-7-4', DATE '2021-7-5' FROM DUAL)
SELECT holidayname,
start_date,
end_date,
start_date - CASE TO_CHAR (start_date, 'Dy') WHEN 'Mon' THEN 3 WHEN 'Sun' THEN 2 ELSE 1 END AS prior_business_day
FROM holiday;
HOLIDAYNAME START_DATE END_DATE PRIOR_BUSINESS_DAY
______________ _____________ ____________ _____________________
holiday1 05-AUG-21 06-AUG-21 04-AUG-21
Christmas 25-DEC-21 26-DEC-21 24-DEC-21
July 4th 04-JUL-21 05-JUL-21 02-JUL-21
You can use a recursive sub-query factoring clause from this answer:
WITH start_date (dt) AS (
SELECT DATE '2021-05-02' FROM DUAL
),
days ( dt, day, found ) AS (
SELECT dt,
TRUNC(dt) - TRUNC(dt, 'IW'),
0
FROM start_date
UNION ALL
SELECT dt - CASE day WHEN 0 THEN 3 WHEN 6 THEN 2 ELSE 1 END,
CASE WHEN day IN (0, 6, 5) THEN 4 ELSE day - 1 END,
CASE WHEN h.start_date IS NULL THEN 1 ELSE 0 END
FROM days d
LEFT OUTER JOIN holidays h
ON ( dt - CASE day WHEN 0 THEN 3 WHEN 6 THEN 2 ELSE 1 END
BETWEEN h.start_date AND h.end_date )
WHERE found = 0
)
SELECT dt
FROM days
WHERE found = 1;
Which, for the sample data:
CREATE TABLE holidays (HolidayName, Start_date, End_Date) AS
SELECT 'holiday1', DATE '2021-08-05', DATE '2021-08-06' FROM DUAL;
Outputs:
DT
2021-08-04 00:00:00
db<>fiddle here
Don't know if it's very efficient. Did it just for fun
create table holidays (
holiday_name varchar2(100) primary key,
start_date date not null,
end_date date not null
)
/
Table created
insert into holidays (holiday_name, start_date, end_date)
values ('holiday1', date '2021-08-05', date '2021-08-06');
1 row inserted
with days_before(day, wrk_day) as
(select trunc(sysdate - 1) d,
case
when h.holiday_name is not null then 0
when to_char(trunc(sysdate - 1), 'D') in ('6', '7') then 0
else 1
end work_day
from dual
left join holidays h
on trunc(sysdate - 1) between h.start_date and h.end_date
union all
select db.day - 1,
case
when h.holiday_name is not null then 0
when to_char(db.day - 1, 'D') in ('6', '7') then 0
else 1
end work_day
from days_before db
left join holidays h
on db.day - 1 between h.start_date and h.end_date
where db.wrk_day = 0) search depth first by day set order_no
select day from days_before where wrk_day = 1;
DAY
-----------
04.08.2021

Dynamically excluding first 5 minutes of data each hour of loading

Data gets fetched from the below "CLAIMANT" table to load into another table. This happens every hour (6 am, 7am, 8am, etc.)
Say I need to write a WHERE CLAUSE that excludes the first 5 minutes of data from being loaded, each time the above operation occurs. The where clause will be added to the overall script that handles the operation
I have tried this, but it excludes 0 records, so it's not detecting the minute by minute differences of the "Filed_Date"
where FILED_DATE not between SYSDATE
and ( TRUNC( SYSDATE, 'hh24' ) + interval '5' minute )
What is the correct way to write the where clause to exclude the data I need to exclude
Your original predicate has a start time and end time out of sync. If you run the query at 2:00 (sysdate), then the start time for your range is SYSDATE (2:00) and the end time is (TRUNC(SYSDATE,'HH24')+interval '5' minute), or 2:05. If you run the query after 14:05, then your start time will actually be after your end time.
select to_date('2021-03-03 14:00','YYYY-MM-DD HH24:MI') query_date,
to_date('2021-03-03 14:00','YYYY-MM-DD HH24:MI') start_time,
(trunc(to_date('2021-03-03 14:00','YYYY-MM-DD HH24:MI'),'HH24') + interval '5' minute) stop_time
from dual
union
select to_date('2021-03-03 14:10','YYYY-MM-DD HH24:MI') query_date,
to_date('2021-03-03 14:10','YYYY-MM-DD HH24:MI') start_time,
(trunc(to_date('2021-03-03 14:10','YYYY-MM-DD HH24:MI'),'HH24') + interval '5' minute) stop_time
from dual;
Results:
QUERY_DATE START_TIME STOP_TIME
-------------------- -------------------- --------------------
2021-03-03T14:00:00Z 2021-03-03T14:00:00Z 2021-03-03T14:05:00Z
2021-03-03T14:10:00Z 2021-03-03T14:10:00Z 2021-03-03T14:05:00Z
What you want is the data from 13:05 to 14:00, but the only data you are excluding is data you haven't received yet (after 14:00), so you get everything up to 14:00, or no data at all because of the mismatched start and stop times of your interval.
Try this, assuming the query runs on or after the top of the hour and you want to capture everything after the first five minutes of the previous hour up to the top of the current hour (i.e. the last 55 minutes of the previous hour):
where filed_date
between (trunc(sysdate,'HH24') - interval '55' minute) -- start time
and (trunc(sysdate,'HH24')) -- end time
Now if the job time (sysdate) is any time between 14:00 and 14:59, then start time for the predicate is always 13:05 and stop time is always 14:00.
select to_date('2021-03-03 14:00','YYYY-MM-DD HH24:MI') query_date,
(trunc(to_date('2021-03-03 14:00','YYYY-MM-DD HH24:MI'),'HH24') - interval '55' minute) start_time,
(trunc(to_date('2021-03-03 14:00','YYYY-MM-DD HH24:MI'),'HH24')) stop_time
from dual
union
select to_date('2021-03-03 14:10','YYYY-MM-DD HH24:MI') query_date,
(trunc(to_date('2021-03-03 14:10','YYYY-MM-DD HH24:MI'),'HH24') - interval '55' minute) start_time,
(trunc(to_date('2021-03-03 14:10','YYYY-MM-DD HH24:MI'),'HH24')) stop_time
from dual;
Results:
QUERY_DATE START_TIME STOP_TIME
-------------------- -------------------- --------------------
2021-03-03T14:00:00Z 2021-03-03T13:05:00Z 2021-03-03T14:00:00Z
2021-03-03T14:10:00Z 2021-03-03T13:05:00Z 2021-03-03T14:00:00Z
In general it is better to stay with positive assertions (i.e. BETWEEN); negative conditions (i.e. NOT BETWEEN) tend to discourage the CBO from using indexes, if they exist.
Alternatively, if you ever wanted a larger timeframe and always wanted to exclude the first five minutes of any hour that happened to fall in the range, you could do this:
where filed_date
between (sysdate - interval '3' hour) and sysdate
and to_number(to_char(filed_date,'MI'),'99') > 5
This would limit the minutes portion of filed_date to 6 or more, effectively filtering out the first five minutes of any hour in the range.

How to find the time difference with respect to date in Oracle?

I want to find the time difference between the value in the column of type Date and the fixed time of that particular date.
Consider,
value in column - 4/16/2011 4:00:19 PM
Fixed time as - 3:00:00 PM
I am expecting the answer as 1hr 0min 19sec ago. Whatever the type I'm ok with it.
Thanks
Since you don't care what data type is returned, I'd probably cast to a timestamp so that you can get an interval day to second returned.
SQL> select cast( sysdate as timestamp ) from dual
2 ;
CAST(SYSDATEASTIMESTAMP)
---------------------------------------------------------------------------
15-MAR-15 04.05.46.000000 PM
SQL> ed
Wrote file afiedt.buf
1 select cast( sysdate as timestamp ) -
2 cast( trunc(sysdate) + interval '15' hour as timestamp )
3* from dual
4 /
CAST(SYSDATEASTIMESTAMP)-CAST(TRUNC(SYSDATE)+INTERVAL'15'HOURASTIMESTAMP)
---------------------------------------------------------------------------
+000000000 01:06:18.000000
If you want to return a string rather than an interval, you can use extract to extract data from the interval
SQL> ed
Wrote file afiedt.buf
1 select extract( hour from delta ) || ' hours, ' ||
2 extract( minute from delta ) || ' minutes, ' ||
3 extract( second from delta ) || ' seconds ago'
4 from (
5 select cast( sysdate as timestamp ) -
6 cast( trunc(sysdate) + interval '15' hour as timestamp ) delta
7 from dual
8* )
SQL> /
EXTRACT(HOURFROMDELTA)||'HOURS,'||EXTRACT(MINUTEFROMDELTA)||'MINUTES,'||EXTRACT(
--------------------------------------------------------------------------------
1 hours, 10 minutes, 46 seconds ago

How to “group by” over a DATETIME range?

I'm trying to bulid up a datetime range based transactions report, for a business that can be open across two days, depending on the shift management.
The user can select a datetime range (monthly, daily, weekly, freely...), the query I implemented get the startDateTime and the EndDateTime, and will return all the transactions total grouped by day.
I.E.
DateTime Total Sales
---------------------------
10/15/2010 $2,300.38
10/16/2010 $1,780.00
10/17/2010 $4,200.22
10/20/2010 $900.66
My problem is that if the shift of the business is setted, for example, from 05.00 AM to 02.00 AM of the next day, all the transactions done from midnight to 02.00 AM will be grouped in the next day... and so on... the totals are corrupted.
When a business has a shift like this, it wants a report based on that shift, but without code patching (I'm using Java calling Oracle native queries), I'm unable to get the requested report.
I'm wondering if there is some smart manner to group by a datetime range these sets of transactions using nothing more than Oracle.
Here goes the query, for the the month of July:
SELECT Q1.dateFormat, NVL(Q1.sales, 0)
FROM (
SELECT to_date(to_char(tx.datetimeGMT +1/24 , 'mm-dd-yyyy'), 'mm-dd-yyyy') AS dateFormat
, NVL(SUM(tx.amount),0) AS sales
FROM Transaction tx
WHERE tx.datetimeGMT > to_date('20100801 08:59:59', 'yyyymmdd hh24:mi:ss') +1/24
AND tx.datetimeGMT < to_date('20100901 09:00:00', 'yyyymmdd hh24:mi:ss') + 1/24
GROUP BY to_date(to_char(tx.datetimeGMT +1/24 , 'mm-dd-yyyy'), 'mm-dd-yyyy')
) Q1
ORDER BY 1 DESC
Thank you all for your answers, by taking a look to them I could write down the query I was searching for:
SELECT CASE
WHEN EXTRACT(HOUR FROM TX.DATETIME) >= 5 THEN TO_CHAR(TX.DATETIME,'DD-MM-YYYY')
WHEN EXTRACT(HOUR FROM TX.DATETIME) BETWEEN 0 AND 2 THEN TO_CHAR(TX.DATETIME-1,'DD-MM-YYYY')
WHEN EXTRACT(hour from tx.datetime) between 2 and 5 THEN to_char(TX.DATETIME-1,'DD-MM-YYYY')
END AS age,
NVL(SUM(tx.amount),0) AS sales
FROM TRANSACTION TX
WHERE tx.datetime > to_date('20100801 08:59:59', 'yyyymmdd hh24:mi:ss')
AND TX.DATETIME < TO_DATE('20100901 09:00:00', 'yyyymmdd hh24:mi:ss')
GROUP BY CASE
WHEN EXTRACT(HOUR FROM TX.DATETIME) >= 5 THEN TO_CHAR(TX.DATETIME,'DD-MM-YYYY')
WHEN EXTRACT(HOUR FROM TX.DATETIME) BETWEEN 0 AND 2 THEN TO_CHAR(TX.DATETIME-1,'DD-MM-YYYY')
WHEN EXTRACT(hour from tx.datetime) between 2 and 5 THEN to_char(TX.DATETIME-1,'DD-MM-YYYY')
END
ORDER BY 1
To group by a date range, you'll have to have this range into a column value into a subquery, and group by it in your query. Obviously, this date range within this column value will be of VARCHAR type.
If the first shift of the day starts at 08:00, and the last shift of that same day ends 07:59 the next day, you can use something like this to group the transactions by the shift date.
select trunc(trans_date - interval '8' hour) as shift_date
,sum(amount)
from transactions
group
by trunc(trans_date - interval '8' hour)
order
by shift_date desc;
You can try this approach (just out of my head, not even sure if it runs):
select
trans_date,
trans_shift,
aggregates(whatever)
from (
select
-- we want to group by normalized transaction date,
-- not by real transaction date
normalized_trans_date,
-- get the shift to group by
case
when trans_date between trunc(normalized_trans_date) + shift_1_start_offset and
trunc(normalized_trans_date) + shift_1_end_offset then
1
when trans_date between trunc(normalized_trans_date) + shift_2_start_offset and
trunc(normalized_trans_date) + shift_2_end_offset then
2
...
when trans_date between trunc(normalized_trans_date) + shift_N_start_offset and
trunc(normalized_trans_date) + shift_N_end_offset then
N
end trans_shift,
whatever
from (
select
-- get a normalized transaction date: if date is before 1st shift
-- it belongs to the day before
case
when trans_date - trunc(trans_date) < shift_1_start_offset then
trans_date - 1
else
trans_date
end normalized_trans_date,
t.*
from
transactions t
)
)
group by trans_date, trans_shift
Ronnis solution with the trunc(trans_date - interval '8' hour) helped me in a similar query.
Did a Backup Report and had to summarize output-bytes from RC_BACKUP_SET_DETAILS. The backup task runs for more than 8 hours, there are several RC_BACKUP_SET_DETAILS rows for one job which starts at night time and end the next day.
select trunc(start_time - interval '12' hour) "Start Date",
to_char(sum(output_bytes)/(1024*1024*1024),'999,990.0') "Output GB"
from rc_backup_set_details
where db_key = 173916 and backup_type = 'I' and incremental_level = 0
group by trunc(start_time - interval '12' hour)
order by 1 asc;

Resources