How to “group by” over a DATETIME range? - oracle

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;

Related

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

Oracle: splitting time range into days and calculation of duration

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.

Using "today" and "two days before" in date with Oracle

I have the following WHEREclause in a query:
SELECT ...
FROM ...
WHERE IMPORT_DATE between
to_date('2018-03-16 00:00:00', 'yyyy-mm-dd hh24:mi:ss') and
to_date('2018-03-16 23:59:59', 'yyyy-mm-dd hh24:mi:ss')
And I would like to write two new queries:
One with this same clause but using "today" instead of "2018-03-16".
Another with the same clause but using "the day before yesterday" (today - 2) instead of "2018-03-16".
How can I do this in Oracle?
As Oracle has no real DATE data type and always includes a time, it's usually better to not use between for conditions like that, but to use >= together with < compared to midnight the next day.
To find the rows from "today" use:
SELECT ...
FROM ...
WHERE import_date >= trunc(sysdate)
AND import_date < trunc(sysdate) + 1;
or:
SELECT ...
FROM ...
WHERE import_date >= trunc(sysdate) - 2
AND import_date < (trunc(sysdate) - 2) + 1;
The parentheses aren't really required in the second expression, they are just there to document that it's the same expression as the first one.
select trunc(sysdate) from dual;
Returns today's date without any time
select trunc(sysdate) - interval '2' day from dual;
Returns the day 2 days before today without time.
You can also use month, hour, year etc instead of day.

Calculate the number of days per month between two dates

Using Oracle 12c, I need to run a script on an existing summary table of projects. The summary table has a project, a start date, and an end date. I need to break this data out into the number of days per month for each project.
An example is Project A has a start date of 2/10/2016 and an end date of 3/10/2016. My ending result for this example should be:
Project A, February, 19
Project A, March, 10
This was an easier one as some dates may span 2 or 3 months. This doesn't seem too difficult but for some reason I'm having trouble wrapping my head around it and overthinking it. Does someone have an quick and easy solution to this? I would like to run this as a SQL statement but a PL/SQL script would also work.
In this solution we don't assume any prior knowledge of the time period covered. Also, this solution does not use joins (which may be important for performance).
with
-- begin test data (this section can be deleted)
inputs ( project, start_date, end_date ) as (
select 'A', date '2014-10-03', date '2014-12-15' from dual union all
select 'B', date '2015-03-01', date '2015-03-31' from dual union all
select 'C', date '2015-11-30', date '2016-03-01' from dual
),
-- end test data; solution begins here (it includes the word "with" from the first line)
prep ( project, end_date, dt ) as (
select project, end_date, start_date from inputs union all
select project, end_date, end_date + 1 from inputs union all
select project, end_date, add_months( trunc(start_date, 'mm'), level )
from inputs
connect by add_months (trunc(start_date, 'mm'), level) <= end_date
and prior project = project
and prior sys_guid() is not null
),
computations ( project, dt, month, day_count ) as (
select project, dt, to_char(dt, 'Mon-yyyy'),
lead(dt) over (partition by project order by dt) - dt
from prep
where dt <= end_date + 1
)
select project, month, day_count
from computations
where day_count > 0
order by project, dt
;
OUTPUT:
PROJECT MONTH DAY_COUNT
------- -------- ---------
A Oct-2014 29
A Nov-2014 30
A Dec-2014 15
B Mar-2015 31
C Nov-2015 1
C Dec-2015 31
C Jan-2016 31
C Feb-2016 29
C Mar-2016 1
9 rows selected
If you can do an assumption on the maximum number of days for a project (1000 in my example), you can use the following:
with yourTable(project, startDate, endDate) as
(
select 'Project a' as project,
date '2016-02-10' as startDate,
date '2016-03-10' as endDate
from dual
UNION ALL
select 'Project XX',
date '2016-01-01',
date '2016-01-10'
from dual
)
select project, to_char(startDate + n, 'MONTH'), count(1)
from yourTable
inner join (
select level n
from dual
connect by level <= 1000
)
on (startDate + n <= endDate)
group by project, to_char(startDate + n, 'MONTH')
The part with the CONNECT BY is used as a date generator, assuming that every project is at maximum 1000 days long; the external query uses the date generator to split the row of a project in many rows, one for each day between start and end date, and then aggregates by month and project to build the output.
A slightly different way, based on months and not days, could be:
select project, to_char(add_months(startDate, n ), 'MONTH'),
case
when trunc(add_months(startDate, n ), 'MONTH') = trunc(add_months(endDate, n ), 'MONTH')
then endDate - startDate +1
when trunc(add_months(startDate, n ), 'MONTH') <= startDate
then last_day(add_months(startDate, n)) - startDate
when last_day(add_months(startDate, n )) >= endDate
then endDate - trunc(add_months(startDate, n ), 'MONTH') +1
else
last_day(add_months(startDate, n )) - trunc(last_day(add_months(startDate, n )), 'MONTH')
end as numOfDays
from yourTable
inner join (
select level -1 n
from dual
connect by level <= 1000
)
on trunc(add_months(startDate, n ), 'MONTH') <= trunc(endDate, 'MONTH')
This is a bit more complicated, to handle the different cases, but more efficient, given that it works at month level, not day level
I think you're after something like:
WITH sample_data AS (SELECT 'A' PROJECT, to_date('10/02/2016', 'dd/mm/yyyy') start_date, to_date('10/03/2016', 'dd/mm/yyyy') end_date FROM dual UNION ALL
SELECT 'B' PROJECT, to_date('10/02/2016', 'dd/mm/yyyy') start_date, to_date('10/06/2016', 'dd/mm/yyyy') end_date FROM dual UNION ALL
SELECT 'C' PROJECT, to_date('10/02/2016', 'dd/mm/yyyy') start_date, to_date('18/02/2016', 'dd/mm/yyyy') end_date FROM dual)
SELECT PROJECT,
to_char(add_months(trunc(start_date, 'mm'), LEVEL -1), 'fmMonth yyyy', 'nls_date_language=english') mnth,
CASE WHEN trunc(end_date, 'mm') = add_months(trunc(start_date, 'mm'), LEVEL -1)
THEN end_date
ELSE add_months(trunc(start_date, 'mm'), LEVEL) -1
END - CASE WHEN trunc(start_date, 'mm') = add_months(trunc(start_date, 'mm'), LEVEL -1)
THEN start_date + 1
ELSE add_months(trunc(start_date, 'mm'), LEVEL -1)
END + 1 num_days
FROM sample_data
CONNECT BY PRIOR PROJECT = PROJECT
AND PRIOR sys_guid() IS NOT NULL
AND add_months(trunc(start_date, 'mm'), LEVEL -1) <= TRUNC(end_date, 'mm');
PROJECT MNTH NUM_DAYS
------- -------------- ----------
A February 2016 19
A March 2016 10
B February 2016 19
B March 2016 31
B April 2016 30
B May 2016 31
B June 2016 10
C February 2016 8
This uses the multi-row connect-by-level technique (the presence of the and prior sys_guid() is not null enables the connect by to loop through each row separately) to loop through each project row in the sample_data table (you presumably have the project information in a table already, so you wouldn't need to have the sample_data subquery at all; you could just reference your table directly in the main SQL).
We then compare the month of the start date with the month of the row being generated by the connect by, and if it's the same month, then we know we need to use the start date, otherwise we use the first of the month of the generated row; we do similarly for the end date.
That way, we can now subtract one from the other and make adjustments to make the calculation correct. You may need to tweak this yourself if you need a start and end date of the same day to count as 1 day, rather than 0 - it'll probably need an extra case statement to take account of when the start and end date are in the same month.
Using this approach won't limit your project length; it could be as long as you liked.
ETA: Looks like Mathguy posted an answer whilst I was typing out my answer, and whilst our basic methods are the same, mine doesn't use an analytic function to determine the difference in the number of days. You may or may not find their answer more performant than mine - you should test both to see which one works best with your data.

Finding a count of rows in an arbitrary date range using Oracle

The question I need to answer is this "What is the maximum number of page requests we have ever received in a 60 minute period?"
I have a table that looks similar to this:
date_page_requested date;
page varchar(80);
I'm looking for the MAX count of rows in any 60 minute timeslice.
I thought analytic functions might get me there but so far I'm drawing a blank.
I would love a pointer in the right direction.
You have some options in the answer that will work, here is one that uses Oracle's "Windowing Functions with Logical Offset" feature instead of joins or correlated subqueries.
First the test table:
Wrote file afiedt.buf
1 create table t pctfree 0 nologging as
2 select date '2011-09-15' + level / (24 * 4) as date_page_requested
3 from dual
4* connect by level <= (24 * 4)
SQL> /
Table created.
SQL> insert into t values (to_date('2011-09-15 11:11:11', 'YYYY-MM-DD HH24:Mi:SS'));
1 row created.
SQL> commit;
Commit complete.
T now contains a row every quarter hour for a day with one additional row at 11:11:11 AM. The query preceeds in three steps. Step 1 is to, for every row, get the number of rows that come within the next hour after the time of the row:
1 with x as (select date_page_requested
2 , count(*) over (order by date_page_requested
3 range between current row
4 and interval '1' hour following) as hour_count
5 from t)
Then assign the ordering by hour_count:
6 , y as (select date_page_requested
7 , hour_count
8 , row_number() over (order by hour_count desc, date_page_requested asc) as rn
9 from x)
And finally select the earliest row that has the greatest number of following rows.
10 select to_char(date_page_requested, 'YYYY-MM-DD HH24:Mi:SS')
11 , hour_count
12 from y
13* where rn = 1
If multiple 60 minute windows tie in hour count, the above will only give you the first window.
This should give you what you need, the first row returned should have
the hour with the highest number of pages.
select number_of_pages
,hour_requested
from (select to_char(date_page_requested,'dd/mm/yyyy hh') hour_requested
,count(*) number_of_pages
from pages
group by to_char(date_page_requested,'dd/mm/yyyy hh')) p
order by number_of_pages
How about something like this?
SELECT TOP 1
ranges.date_start,
COUNT(data.page) AS Tally
FROM (SELECT DISTINCT
date_page_requested AS date_start,
DATEADD(HOUR,1,date_page_requested) AS date_end
FROM #Table) ranges
JOIN #Table data
ON data.date_page_requested >= ranges.date_start
AND data.date_page_requested < ranges.date_end
GROUP BY ranges.date_start
ORDER BY Tally DESC
For PostgreSQL, I'd first probably write something like this for a "window" aligned on the minute. You don't need OLAP windowing functions for this.
select w.ts,
date_trunc('minute', w.ts) as hour_start,
date_trunc('minute', w.ts) + interval '1' hour as hour_end,
(select count(*)
from weblog
where ts between date_trunc('minute', w.ts) and
(date_trunc('minute', w.ts) + interval '1' hour) ) as num_pages
from weblog w
group by ts, hour_start, hour_end
order by num_pages desc
Oracle also has a trunc() function, but I'm not sure of the format. I'll either look it up in a minute, or leave to see a friend's burlesque show.
WITH ranges AS
( SELECT
date_page_requested AS StartDate,
date_page_requested + (1/24) AS EndDate,
ROWNUMBER() OVER(ORDER BY date_page_requested) AS RowNo
FROM
#Table
)
SELECT
a.StartDate AS StartDate,
MAX(b.RowNo) - a.RowNo + 1 AS Tally
FROM
ranges a
JOIN
ranges b
ON a.StartDate <= b.StartDate
AND b.StartDate < a.EndDate
GROUP BY a.StartDate
, a.RowNo
ORDER BY Tally DESC
or:
WITH ranges AS
( SELECT
date_page_requested AS StartDate,
date_page_requested + (1/24) AS EndDate,
ROWNUMBER() OVER(ORDER BY date_page_requested) AS RowNo
FROM
#Table
)
SELECT
a.StartDate AS StartDate,
( SELECT MIN(b.RowNo) - a.RowNo
FROM ranges b
WHERE b.StartDate > a.EndDate
) AS Tally
FROM
ranges a
ORDER BY Tally DESC

Resources