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
Related
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
How to round up time interval to next day in Oracle SQL?
select apppackage
, numtodsinterval(
sum( trunc(extract (day from (periods)) * 86400
+ extract (hour from (periods)) *3600
+ extract (minute from (periods))*60
+ extract (second from (periods)))
), 'SECOND') as retention_period
, count(apppackage) as users
from retentions
where apppackage = 'com.Freesoul.Rotter'
group by apppackage;
The output of this is
'com.Freesoul.Rotter' '+2969 04:32:47.000000' '3'
and desired output is
'com.Freesoul.Rotter' '2970' '3'
but if the output of query is
'com.Freesoul.Rotter' '+2969 00:00:00.000000' '3'
then desired output is
'com.Freesoul.Rotter' '2969' '3'
column period is of INTERVAL DAY(9) TO SECOND(6) type
and i won't mind if the retention_period is changed to number datatype.
I'll be grateful if anyone can suggest change in my query to attain the desired output.
The result of your sum is in seconds, so you don't really need to convert it to an interval at all. Just divide by 60*60*24 to get the answer in days, and round it up with ceil():
select apppackage
, ceil(sum( trunc(extract (day from (periods)) * 86400
+ extract (hour from (periods)) *3600
+ extract (minute from (periods))*60
+ extract (second from (periods)))
) / 86400) as retention_period
, count(apppackage) as users
from retentions
where apppackage = 'com.Freesoul.Rotter'
group by apppackage;
Demo with artificial data in a CTE just to mimic your expected results, for both scenarios:
-- CTE for sample data
with retentions (apppackage, periods) as (
select 'com.Freesoul.Rotter', interval '+2967 04:32:47.000000' day(9) to second(6) from dual
union all
select 'com.Freesoul.Rotter', interval '1' day from dual
union all
select 'com.Freesoul.Rotter', interval '1' day from dual
union all
select 'com.Freesoul.XYZ', interval '+2967 00:00:00.000000' day(9) to second(6) from dual
union all
select 'com.Freesoul.XYZ', interval '1' day from dual
union all
select 'com.Freesoul.XYZ', interval '1' day from dual
)
-- actual query
select apppackage
, ceil(sum( trunc(extract (day from (periods)) * 86400
+ extract (hour from (periods)) *3600
+ extract (minute from (periods))*60
+ extract (second from (periods)))
) / 86400) as retention_period
, count(apppackage) as users
from retentions
where apppackage = 'com.Freesoul.Rotter'
-- extra clause for dummy data
or apppackage = 'com.Freesoul.XYZ'
group by apppackage;
APPPACKAGE RETENTION_PERIOD USERS
------------------- ---------------- ----------
com.Freesoul.XYZ 2969 3
com.Freesoul.Rotter 2970 3
Your expected output shows a plain number. If you actually want it as an interval, but as the whole number of days, just pass ceil'd number into numtodsinterval or more simply (and usually faster for some reason) multiply by interval '1' day.
With the same dummy data:
select apppackage
, ceil(sum( trunc(extract (day from (periods)) * 86400
+ extract (hour from (periods)) *3600
+ extract (minute from (periods))*60
+ extract (second from (periods)))
) / 86400) * interval '1' day as retention_period
, count(apppackage) as users
...
APPPACKAGE RETENTION_PERIOD USERS
------------------- --------------------- ----------
com.Freesoul.XYZ +2969 00:00:00.000000 3
com.Freesoul.Rotter +2970 00:00:00.000000 3
As #mathguy pointed out, you probably don't need or want the trunc() call in there; that is removing the fractional seconds from each period before they are summed, which sounds insignificant but could easily affect the result you get.
How about this?
CASE WHEN numtodsinterval(extract (day from periods), 'DAY') = periods THEN
extract (day from periods)
ELSE
extract (day from periods) + 1
END
Oracle SQL:
I been trying to get this some snippet of query working. When you run it, it prompts you for an hour, and displays no records. I don’t want the query to have a static hour as it will need to be run 4 times a day.
So from the ‘&date’ input, I want it to show data for the past 24 hours. Is that possible?
dt_time = timestamp(6) field
select distinct to_char(dt_ time,'dd/mm/yyyy hh24'), fault_description
from order
where to_char(dt_time,'hh24') <= '&date' -24
order by to_char(dt_ time,'dd/mm/yyyy hh24');
Example, if you enter 10 (when query executed) it will show the data from 10(:00) through to 10(:00)next day
[Hope there is enough info for someone to answer, please]
Use a combination of between and interval:
where dt_time between
to_date(&date, 'ddmmyyyy hh24') and
to_date(&date, 'ddmmyyyy hh24') + interval '24' hour
You'll need to pass/parse the input date as a full date and not only the hour part, in order to prevent unexpected results.
You can use NUMTODSINTERVAL in a CTE to get the hour as user input.
WITH t_hour( h ) AS
( SELECT NUMTODSINTERVAL(&d,'HOUR' ) h FROM DUAL
)
SELECT DISTINCT TO_CHAR(dt_time,'dd/mm/yyyy hh24'),
fault_description
FROM ORDER
CROSS JOIN t_hour
WHERE dt_time BETWEEN TRUNC(SYSDATE) + h AND TRUNC(SYSDATE) + 1 + h
ORDER BY 1;
You need to make use of SYSDATE as you are fetching last one day data.
Also if you add or subtract number with a date column the offset will be number of days, not number of hours. So you need to use &date/24
Try this:
select distinct to_char(dt_time,'dd/mm/yyyy hh24'), fault_description
from order
where dt_time - &date / 24 between TRUNC(sysdate-1) and TRUNC(sysdate)
order by to_char(dt_ time,'dd/mm/yyyy hh24');
Please comment.
WHERE DATEDIFF(SYSDATE,DT_TIME) * 24 < &DATE AND DT_TIME < SYSDATE
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.
My question is similar to following question:
http://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:14582643282111
The difference is my inner query returns two records and I have outer query.
I need to write inner query something like this which will give me a list of dates between two date ranges (I am trying this query whcih does not execute).
Select * from outerTable where my_date in
(
select to_date(r.REQ_DATE) + rownum -1
from all_objects, (MY_INNER_QUERY Where ID =100) r
where rownum <= to_date(r.DUE_DATE,'dd-mon-yyyy')-to_date(r.REQ_DATE,'dd-mon-yyyy')+1;
)
My inner query returns following 2 rows:
Select * from innerTable Where ID =100
Start date end date
3/19/2013 3/21/2013
3/8/2013 3/8/2013
So I am need inner query which will return following dates to outer query:
3/19/2013
3/20/2013
3/21/2013
3/8/2013
Great question - this one really drew me in! The answer was more or less buried further down on Tom's post. Here's the short version, using a table called TestDR to define the ranges. First the TestDR contents:
SELECT * FROM TestDR;
STARTDATE ENDDATE
--------- ---------
19-MAR-13 21-MAR-13
08-MAR-13 08-MAR-13
Now for the query to create one row for each date in the range:
WITH NUMS AS (
SELECT LEVEL-1 DaysToAdd
FROM DUAL
CONNECT BY LEVEL <= 60
)
SELECT StartDate + DaysToAdd TheDate
FROM TestDR
CROSS JOIN NUMS
WHERE TestDR.EndDate - TestDR.StartDate + 1 > DaysToAdd
ORDER BY 1
THEDATE
---------
08-MAR-13
19-MAR-13
20-MAR-13
21-MAR-13
With the query adapted from Tom's posting, you have to know the maximum range going in to "seed" the NUMS query. He used 60 in his example so that's what I used above. If you don't think any row from your subquery will ever have a range of more than 60 days then this will do the trick. If you think the maximum could be as much as 1000 days (about three years) then change the 60 to 1000. I tried this and queried a 2 1/2 year range and the results were instantaneous.
If you want to specify the exact "seed" count you can calculate it if you're willing to make the query a bit more complicated. Here's how I can do it with my TestDR table:
WITH NUMS AS (
SELECT LEVEL-1 DaysToAdd
FROM DUAL
CONNECT BY LEVEL <= (
SELECT MAX(EndDate - StartDate + 1)
FROM TestDR)
)
SELECT ... and the rest of the query as above
For your problem, you don't need to enumerate the dates. A simple JOIN suffices.
SELECT o.*
FROM outerTable o
INNER JOIN innerTable i
ON i.ID = 100
AND o.my_date BETWEEN i.REQ_DT and i.DUE_DT
From your code, I can tell that you must be a OO programmer and not familiar with SQL. It does a lot for you, so don't try to control it. It will hinder it's optimization features.
Don't take this in the wrong way, I had the same mindset (believing that I am smarter than the machine).
In your outer query use an OR statement, which allows your date to be equal to either the return Start_Date or End_Date
AND (date = subQuery.Start_Date
OR date = subQuery.End_Date)
Using your dates:
SELECT smth... FROM some_tab
WHERE your_date IN
( -- remove unnecessary columns, leave only what you select in outer query
-- or select *
SELECT start_date
, TRUNC(start_date, 'iw') wk_starts
, TRUNC(start_date, 'iw') + 7 - 1/86400 wk_ends
, TO_NUMBER (TO_CHAR (start_date, 'IW')) ISO_wk#
FROM
(
SELECT (start_date-1) + LEVEL AS start_date
FROM
( -- replace this part with selecting your start and end dates from your table --
SELECT to_date('03/21/2013', 'MM/DD/YYYY') end_date
, to_date('03/19/2013', 'MM/DD/YYYY') start_date
FROM dual
)
CONNECT BY LEVEL <= (end_date - start_date)
)
) -- your outer query ends --
/
START_DATE WK_STARTS WK_ENDS ISO_WK#
----------------------------------------------------------
3/19/2013 3/18/2013 3/24/2013 11:59:59 PM 12
3/20/2013 3/18/2013 3/24/2013 11:59:59 PM 12
Annual table of dates and ISO weeks etc... Use any dates for start and end dates. The connect by and number of days between is used to generate table on the fly. You may use between operator if using hard structures...:
SELECT start_date
, TRUNC(start_date, 'iw') wk_starts
, TRUNC(start_date, 'iw') + 7 - 1/86400 wk_ends
, TO_NUMBER (TO_CHAR (start_date, 'IW')) ISO_wk#
FROM
(-- This part simplifies above formatting and optional --
SELECT (start_date-1) + LEVEL AS start_date
FROM
(-- Replace start/end dated with any dates --
SELECT TRUNC(ADD_MONTHS (SYSDATE, 12), 'Y')-1 end_date
, TRUNC(SYSDATE, 'YEAR') start_date
FROM dual
)
CONNECT BY LEVEL <= (end_date - start_date) -- number of days between dates
)
/
START_DATE WK_STARTS WK_ENDS ISO_WK#
-----------------------------------------------------------
1/1/2013 12/31/2012 1/6/2013 11:59:59 PM 1
1/2/2013 12/31/2012 1/6/2013 11:59:59 PM 1
1/3/2013 12/31/2012 1/6/2013 11:59:59 PM 1
...
12/28/2013 12/23/2013 12/29/2013 11:59:59 PM 52
12/29/2013 12/23/2013 12/29/2013 11:59:59 PM 52
12/30/2013 12/30/2013 1/5/2014 11:59:59 PM 1