Calculate Business Hours Between Two Dates without Creating Function or View - sql

I realize that this might be a somewhat redundant question BUT I have struggled to follow some of the examples that I did find and I thought I would ask again providing details on my specific scenario.
Here is why I am working with:
Oracle Database
The dates are in timestamp format
I cannot create any additional tables/views (due to permission issue)
I cannot create any custom functions (due to permission issue)
I have a 40 hour work week and business hours of 8 to 4:30 Monday through Friday. (I guess technically that leaves us with more than 40 hours to account for b/c I don't want to get SO FANCY TO worry about excluding lunch breaks)
I'm able to figure out to calculate hours but I don't know how to get in the business day component.

Starting with your example of 8AM Friday through 9AM Monday:
with dates as (
Select timestamp '2019-05-31 08:00:00' start_date
, timestamp '2019-06-03 09:00:00' end_date
from dual
)
We need to generate the days in between. We can do that with a recursive query:
, recur(start_date, calc_date, end_date) as (
-- Anchor Part
select start_date
, trunc(start_date)
, end_date
from dates
-- Recrusive Part
union all
select start_date
, calc_date+1
, end_date
from recur
where calc_date+1 < end_Date
)
From that we need to figure out a few things like, is the calc_day a weekday or a weekend, and what are the starting and ending times for the calc_day, we can then take those values and use a little date arithmetic to find the number of hours worked on that day (returned as day to second interval since we started with timestamps):
, days as (
select calc_date
, case when mod(to_number(to_char(calc_date,'d'))-1,6) != 0 then 1 end isWeekDay
, greatest(start_date, calc_date + interval '8' hour) start_time
, least(end_date, calc_date + interval '16:30' hour to minute) end_time
, least( ( least(end_date, calc_date + interval '16:30' hour to minute)
- greatest(start_date, calc_date + interval '8' hour)
) * case when mod(to_number(to_char(calc_date,'d'))-1,6) != 0 then 1 end
, interval '8' hour
) daily_hrs
from recur
where start_date < (calc_date + interval '16:30' hour to minute)
and (calc_date + interval '8' hour) < end_date
)
Note that in the above step, we've limited the daily hours to 8 hours a day, and the where clause guards against start or end days that are outside business hours. The final step is to sum the hours. Unfortunately Oracle doesn't have any native interval aggregate or analytic functions, but we can still manage by converting the intervals to seconds, summing them and then converting them back to an interval for output:
select calc_date
, daily_hrs
, numtodsinterval(sum( extract(hour from daily_hrs)*60*60
+ extract(minute from daily_hrs)*60
+ extract(second from daily_hrs)
) over (order by calc_date)
,'second') run_sum
from days;
I've done the sum above as an analytic function so we can see some of the intervening data, but if you just want the final output you can change the last part of the query to this:
select numtodsinterval(sum( extract(hour from daily_hrs)*60*60
+ extract(minute from daily_hrs)*60
+ extract(second from daily_hrs)
)
,'second') run_sum
Here's a db<>fiddle with the whole query in action. Note that in the fiddle, I've altered the DB session's NLS_TERRITORY setting to AMERICA to make the query work since the first day of the week is country specific. The second query in the fiddle replaces the territory specific function:
case when mod(to_number(to_char(calc_date,'d'))-1,6) != 0 then 1 end
with a location and language agnostic calculation:
case when (mod(mod(calc_date - next_day(date '2019-1-1',to_char(date '2019-01-06','day')),7),6)) != 0 then 1 end

Related

Is there a way to use SYSDATE with a weekly date?

So I've been trying to fetch some daily data with SYSDATE on a date type YYYYMMDD as following:
SELECT dates, trunc(calendar_date, 'DD') calendar_dates, weekday_nbr
FROM db.date
WHERE dates BETWEEN to_char(TRUNC(SYSDATE)-2, 'YYYYMMDD') AND to_char(TRUNC(SYSDATE)-1, 'YYYYMMDD')
But now I'm trying to use the same but on a YYYY+MM+Week date type with not much success
I tried using:
SELECT T time, period, fiscal_week
FROM db.time
WHERE time BETWEEN to_char(TRUNC(SYSDATE)-2, 'W') AND to_char(TRUNC(SYSDATE)-1,'W')
With time as a 7 digit number, and period and fiscal week as a 2 digit number
Knowing that there's no way I can truncate such date type, how can TRUNC SYSDATE YYYY+MM+Week to get the data on the last 2 weeks?
Also I was thinking about maybe getting the totals from a set day and then dropping all but the last 2 weeks, but on the long run maybe that would be time consuming.
Knowing that there's no way I can truncate such date type, how can TRUNC SYSDATE YYYY+MM+Week to get the data on the last 2 weeks?
Assuming that your fiscal weeks are from Monday-Sunday then you can truncate to the start of the ISO week (which is always Midnight on Monday) and use that for the basis of the comparison:
SELECT *
FROM db.time
WHERE dates >= TRUNC(SYSDATE, 'IW') - INTERVAL '14' DAY
AND dates < TRUNC(SYSDATE, 'IW')
If you have a column that is for weeks then you should still use a DATE data type and add a CHECK constraint (and can use virtual columns to generate the week and the year):
CREATE TABLE time (
dt DATE
CHECK (dt = TRUNC(dt, 'IW')),
year NUMBER(4,0)
GENERATED ALWAYS AS (EXTRACT(YEAR FROM dt)),
month NUMBER(2,0)
GENERATED ALWAYS AS (EXTRACT(MONTH FROM dt)),
week NUMBER(1,0)
GENERATED ALWAYS AS (FLOOR((dt - TRUNC(dt, 'MM'))/7) + 1),
time VARCHAR2(7)
GENERATED ALWAYS AS (
CAST(
TO_CHAR(dt, 'YYYYMM')
|| (FLOOR((dt - TRUNC(dt, 'MM'))/7) + 1)
AS VARCHAR2(7)
)
)
-- ...
);
fiddle
Then you can use the logic above on the date column.
If you do not have a DATE column then you will need to convert your YYYYMMW number into a DATE and then use the logic above.
For example, if the logic for your fiscal weeks (which you have not described) is that the first week of each month starts on the first Monday of the month then you can convert the YYYYMMW number to a DATE using:
SELECT NEXT_DAY(
TO_DATE(SUBSTR(time, 1, 6), 'YYYYMM') - INTERVAL '1' DAY,
'MONDAY'
) + INTERVAL '7' DAY * (SUBSTR(time, 7, 1) - 1) AS week_start
FROM db.time
and then could use it to filter the table using:
SELECT *
FROM (
SELECT t.*,
NEXT_DAY(
TO_DATE(SUBSTR(time, 1, 6), 'YYYYMM') - INTERVAL '1' DAY,
'MONDAY'
) + INTERVAL '7' DAY * (SUBSTR(time, 7, 1) - 1) AS week_start
FROM db.time t
)
WHERE week_start >= TRUNC(SYSDATE, 'IW') - INTERVAL '14' DAY
AND week_start < TRUNC(SYSDATE, 'IW')
If you have different logic for calculating when fiscal weeks start then you will need to apply that logic to the conversion.

Bring data from the last day of the previous month beyond

I'm using Oracle SQL Developer and I would like to make a SELECT that brings all the products sold from the last day of the previous month (but only products sold from 4 pm on the last day of the previous month) to the current day (only until 8 am of the current day).
For example, today is 7/21/2022. If I run this query today, it should bring data from:
06/30/2022 above 16:00hrs -> 07/21/2022 until 08:00hrs
You can use TRUNC(value, 'MM') to find midnight of the 1st day of the current month and then subtract 8 hours to find the start of the range and then use TRUNC(value) to find midnight of today and add 8 hours to find the end of the range:
SELECT *
FROM table_name
WHERE date_column >= TRUNC(SYSDATE, 'MM') - INTERVAL '8' HOUR
AND date_column <= TRUNC(SYSDATE) + INTERVAL '8' HOUR;
You can use TRUNC to get to the first day of a date's month. Then subtract one day and add sixteen hours. And it's again TRUNC that you use to get back to the beginning of a day (midnight) to which you can add eight hours.
select *
from mytable
where dt >= trunc(sysdate, 'mm') - interval '1' day + interval '16' hour
and dt < trunc(sysdate, 'dd') + interval '8' hour
order by dt;

Select Data From Multiple Days Between Certain Times (Spanning 2 days)

I need to know how many entries appear in my DB for the past 7 days with a timestamp between 23:00 & 01:00...
The Issue I have is the timestamp goes across 2 days and unsure if this is even possible in the one query.
So far I have come up with the below:
select trunc(timestamp) as DTE, extract(hour from timestamp) as HR, count(COLUMN) as Total
from TABLE
where trunc(timestamp) >= '12-NOV-19' and
extract(hour from timestamp) in ('23','00','01')
group by trunc(timestamp), extract(hour from timestamp)
order by 1,2 desc;
The result I am hoping for is something like this:
DTE | Total
20-NOV-19 5
19-NOV-19 4
18-NOV-19 4
17-NOV-19 6
Many thanks
Filter on the day first comparing it to TRUNC( SYSDATE ) - INTERVAL '7' DAY and then consider the hours by comparing the timestamp to itself truncated back to midnight with an offset of a number of hours.
select trunc(timestamp) as DTE,
extract(hour from timestamp) as HR,
count(COLUMN) as Total
from TABLE
WHERE timestamp >= TRUNC( SYSDATE ) - INTERVAL '7' DAY
AND ( timestamp <= TRUNC( timestamp ) + INTERVAL '01:00' HOUR TO MINUTE
OR timestamp >= TRUNC( timestamp ) + INTERVAL '23:00' HOUR TO MINUTE
)
group by trunc(timestamp), extract(hour from timestamp)
order by DTE, HR desc;
Subtract or add an hour to derive the date. I'm not sure what date you want to assign to each period, but the idea is:
select trunc(timestamp - interval '1' hour) as DTE,
count(*) as Total
from t
where trunc(timestamp - interval '1' hour) >= DATE '2019-11-12' and
extract(hour from timestamp) in (23, 0)
group by trunc(timestamp - interval '1' hour)
order by 1 desc;
Note: If you want times between 11:00 p.m. and 1:00 a.m., then you want the hour to be 23 or 0.

SQL statement dynamically using current time to choose a time frame in a field (Oracle)

All, I have something that is stumping me and I have seen a lot of examples, but nothing is helping solve this.
I have time frames like 03:30:00 to 11:29:59 that I work with (say shift times). I want to dynamically query data for the last shift based on the current shift.
Example: if it is currently between 11:30:00 AM and 7:29:59 PM, I want get the last shift that was between 03:30:00 AM and 11:30:00 AM.
This would look like an if statement in my mind:
If time between .... then
select time between....
elseif time between.... then
select time between...
I tried many combinations and can't figure this out. I think I would need a CASE and maybe a subquery? or maybe DECODE will work?
SELECT CAST(ccd.DATEc AS TIME) as time_occured,
FROM db.datatb ccd
WHERE ccd.DATE > SYSDATE - interval '1440' minute
AND (
((TO_CHAR(SYSDATE, 'hh24:mi:ss')BETWEEN '03:30:00' AND '11:29:59' IN (SELECT
ccd.DATEc FROM db.datatb WHERE (CAST(ccd.DATEc AS TIME)NOT BETWEEN '03:30:00
AM' AND '07:29:59 PM')))
OR (TO_CHAR(SYSDATE, 'hh24:mi:ss')BETWEEN '11:30:00' AND '19:29:59' IN
(SELECT ccd.DATEc FROM db.datatb WHERE (CAST(ccd.DATEc AS TIME) BETWEEN
'03:30:00 AM' AND '11:29:59 AM')))
OR (TO_CHAR(SYSDATE, 'hh24:mi:ss')NOT BETWEEN '03:30:00' AND '19:29:59' IN
(SELECT ccd.DATEc FROM db.datatb WHERE (CAST(ccd.DATEc AS TIME) BETWEEN
'11:30:00 AM' AND '07:29:59 PM')))
)
SELECT *
FROM db.datatb
CROSS JOIN
( SELECT TRUNC( SYSDATE - INTERVAL '210' MINUTE )
+ NUMTODSINTERVAL(
TRUNC(
( SYSDATE - INTERVAL '210' MINUTE
- TRUNC( SYSDATE - INTERVAL '210' MINUTE )
) * 3
) * 480
+ 210,
'MINUTE'
) AS current_shift_start
FROM DUAL
) css
WHERE DATEc >= css.current_shift_start - INTERVAL '8' HOUR
AND DATEc < css.current_shift_start;
Explanation:
The shifts are 8 hours each starting at 03:30 (or 210 minutes past midnight); so SYSDATE - INTERVAL '210' MINUTE will move offset the times so that after this offset they start at 00:00, 08:00 and 16:00 which is thirds of a day.
date_value - TRUNC( date_value ) calculates the fraction of a day (between 0 and 1) that the time component represents; so TRUNC( ( date_value - TRUNC( date_value ) ) * 3 ) maps that fraction of the day to 0, 1 or 2 corresponding to whether it is in the 1st, 2nd or 3rd 8 hour period of the day. Multiple that value by 480 minutes and then add the 210 minutes that the date was originally offset by and you have the minutes past the start of the day that the shift starts.

Oracle working with CONNECT BY and LEVEL with INTERVAL

Alex Poole posted a brilliant solution to another user's issue at Oracle formatting date intervals but
Didn't make it clear earlier that while I have questions on what parts of Alex' original query do, the deisred goal is a result set showing roster entries by individuals in 14 day intervals. Understanding exactly what all parts of the query do is key to always to getting a good result. So much emphasis was placed on certain parts of the query and not enough on where I wanted to arrive. :)
In this part of the query, what does the divisor of 48 have to do with 30 minute intervals? Jokingly, it could be the new Secret of the Universe (Hitchhiker's Guide to the Galaxy) -- it's not 42 anymore but 48. :)
A co worker and I figured it might be for a 6-day work week (48 hours -- 6 8 hour days). Here is Alex' query, note that the object tbl_stat is located at the top of the post, not part of Alex' query per se, it was part of the poster's original question:
with tmp_tab as (
select start_time + (level - 1)/48 as period_start,
start_time + level/48 - interval '1' second as period_end
from (
select to_date(:start_time, 'DD/MM/YYYY HH24:MI:SS') start_time,
to_date(:end_time, 'DD/MM/YYYY HH24:MI:SS') end_time
from dual
)
connect by start_time + (level - 1)/48 < end_time
)
select to_char(tt.period_start, 'DD/MM/YYYY HH24:MI') dt,
count(ts.track_datetime)
from tmp_tab tt
left join tbl_stat ts
on ts.track_datetime between tt.period_start and tt.period_end
group by tt.period_start
order by tt.period_start;
Tha'ts the whole query, but what does the interval " / 48" pertain to as far as setting up 30 minute intervals, etc:
select start_time + (level - 1)/48 as period_start,
start_time + level/48 - interval '1' second as period_end
Thanks, hope that's not too inane a question but I really don't see what's what with it.
The documentation on datetime/interval arithmetic explains:
You can use NUMBER constants in arithmetic operations on date and timestamp values, but not interval values. Oracle internally converts timestamp values to date values and interprets NUMBER constants in arithmetic datetime and interval expressions as numbers of days. For example, SYSDATE + 1 is tomorrow. SYSDATE - 7 is one week ago. SYSDATE + (10/1440) is ten minutes from now.
(That's perhaps slightly misleading; SYSDATE + 1 is the same time tomorrow...)
It's common to see 1/24 used to represent an hour in calculations, 1/(24*60) to represent a minute, and 1/(24*60*60) to represent a second. Some people prefer that format - or 1/86400 - while some prefer interval '1' second or numtodsinterval(1, 'SECOND'), but they all mean the same in the end.
As Peter Lang said, 1/48 represents half an hour, as a fraction of one day. 1/24 is an hour, so 1/(2*24) is half an hour; it's the same as (1/2)*(1/24) if that helps.
You can compare two dates to see the fractional difference:
select 1/48, to_date('12:30', 'HH24:MI') - to_date('12:00', 'HH24:MI') as diff
from dual
1/48 DIFF
---------- ----------
.020833333 .020833333
The query I need to get the intervals within the pay periods of an overtime roster is done and I'll share it.
Here is my Frankenstein, which now HAS life. A curious thing, though ... I took a post literally and originally had my WHERE predicate as:
WHERE TRIM(UPPER(TO_CHAR(TO_DATE(TT.PERIOD_END, 'DD/MM/YYYY' ), 'DAY'))) = 'SATURDAY'
AND MOD(TT.PAY_PERIOD,2) <> 0
This curiously picked MONDAY PERIOD_END dates. I had to set the = to 'THURSDAY' to get Saturday dates. Well, taking out the un-needed TO_DATE() resolved this but I found it strange that it wigged out the way it did, offsetting what I was looking for by 2 days.
WITH TMP_TAB AS (
SELECT
START_TIME + (LEVEL - 1) AS PERIOD_START
,START_TIME + LEVEL + INTERVAL '13' DAY AS PERIOD_END
,LEVEL AS PAY_PERIOD
FROM (
SELECT
TO_DATE
(
TO_CHAR(
(
SELECT
CASE WHEN MOD(TO_NUMBER(TO_CHAR((SELECT MIN(ENTERED_DT) FROM OVTR_LOG) + DT1,'J')),2) = 0
THEN TRUNC((SELECT MIN(ENTERED_DT) FROM OVTR_LOG) + DT1 + 7)
ELSE TRUNC((SELECT MIN(ENTERED_DT) FROM OVTR_LOG) + DT1) END AS FIRST_PPE_DT
FROM (SELECT 7 - TO_NUMBER(TO_CHAR(MIN(ENTERED_DT),'D')) AS DT1 FROM OVTR_LOG)
),'DD/MM/YYYY'
), 'DD/MM/YYYY'
) START_TIME
,TO_DATE
(
TO_CHAR(
(
SELECT
CASE WHEN MOD(TO_NUMBER(TO_CHAR((SELECT MAX(ENTERED_DT) FROM OVTR_LOG) + DT1,'J')),2) = 0
THEN TRUNC((SELECT MAX(ENTERED_DT) FROM OVTR_LOG) + DT1 + 7)
ELSE TRUNC((SELECT MAX(ENTERED_DT) FROM OVTR_LOG) + DT1) END AS MOST_RECENT_PPE_DT
FROM (SELECT 7 - TO_NUMBER(TO_CHAR(MAX(ENTERED_DT),'D')) AS DT1 FROM OVTR_LOG)
),'DD/MM/YYYY'
), 'DD/MM/YYYY'
) END_TIME
FROM DUAL
)
CONNECT BY START_TIME + (LEVEL -1) < END_TIME
)
SELECT
TO_CHAR(TT.PERIOD_START, 'DD/MM/YYYY') PERIOD_START
,TO_CHAR(TT.PERIOD_END, 'DD/MM/YYYY') PERIOD_END
FROM TMP_TAB TT
--LEFT JOIN TO THE OTR
--ON WHATEVER
WHERE TRIM(UPPER(TO_CHAR(TT.PERIOD_END, 'DAY'))) = 'SATURDAY'
AND MOD(TT.PAY_PERIOD,2) <> 0
GROUP BY
TT.PERIOD_START
,TT.PERIOD_END
ORDER BY TT.PERIOD_START
This gives the following result set. My apologies for not being "in complete form" here -- as Alex suggested the help centre should be reviewed and it will be.
The original question included I had was what does / 48 do? It divides LEVEL according to the context LEVEL is used in, straight and simple.
Here is what I originally wanted as a result set and with the great help of Alex' query (thanks to Peter's original question) I got there. This should explain where I was trying to arrive.
PERIOD_START PERIOD_END
23/04/2011 07/05/2011
07/05/2011 21/05/2011
21/05/2011 04/06/2011
04/06/2011 18/06/2011
18/06/2011 02/07/2011
02/07/2011 16/07/2011
16/07/2011 30/07/2011
30/07/2011 13/08/2011
13/08/2011 27/08/2011
27/08/2011 10/09/2011
10/09/2011 24/09/2011
24/09/2011 08/10/2011
08/10/2011 22/10/2011
22/10/2011 05/11/2011
05/11/2011 19/11/2011
19/11/2011 03/12/2011
03/12/2011 17/12/2011
17/12/2011 31/12/2011
31/12/2011 14/01/2012
14/01/2012 28/01/2012
28/01/2012 11/02/2012
11/02/2012 25/02/2012
25/02/2012 10/03/2012
10/03/2012 24/03/2012
24/03/2012 07/04/2012
07/04/2012 21/04/2012
21/04/2012 05/05/2012
05/05/2012 19/05/2012
19/05/2012 02/06/2012
02/06/2012 16/06/2012