Oracle, SQL, how to get intervals between dates - sql

I need help with a problem. Actually, I do not know if it will be possible to solve it directly in SQL.
I have a list of works. Each work has a start date and ending date, with this format
YYYY/MM/DD HH24:MI:SS
I need to calculate the cost of those jobs, the hour price depends on the time intervals in which the work has been done:
Nigth time: 22:00 to 6:00, for example: 20 €/h
Normal time: the rest 17 €/h
So, if I have a sample like this:
wo start end
21 2017/11/16 21:25:00 2017/11/16 22:55:00
22 2017/11/17 05:45:00 2017/11/17 07:05:00
23 2017/11/18 23:00:00 2017/11/19 1:10:00
24 2017/11/17 18:00:00 2017/11/17 19:00:00
I would need to calculate the intervals of the dates between the 22h and 6h and the rest to multiply them by their corresponding price
wo rest(minutes) night(minutes)
21 35 55
22 15 65
23 0 130
24 1 0
Thank for your help in advance.

Heh. If you really wish it :)
Fifth record (started at 2016-10-30) had been added for testing purposes.
SQL> with
2 src as (select timestamp '2017-11-16 21:25:00' b, timestamp '2017-11-16 22:55:00' f from dual union all
3 select timestamp '2017-11-17 05:45:00' b, timestamp '2017-11-17 07:05:00' f from dual union all
4 select timestamp '2017-11-18 23:00:00' b, timestamp '2017-11-19 1:10:00' f from dual union all
5 select timestamp '2017-11-17 18:00:00' b, timestamp '2017-11-17 19:00:00' f from dual union all
6 select timestamp '2016-10-30 00:00:00' b, timestamp '2016-11-03 23:00:00' f from dual),
7 srd as (select b, f, f - b t from src),
8 mmm as (select min(trunc(b)) b, max(trunc(f)) f from src),
9 rws as (select b + 6/24 + rownum - 1 b, b + 22/24 + rownum - 1 f from mmm connect by level <= f - b + 1),
10 mix as (select s.b, s.f, s.t, r.b rb, r.f rf from srd s, rws r where s.f >= r.b (+) and r.f (+) >= s.b),
11 clc as (select b, f, t, nvl(numtodsinterval(sum((least(f, rf) + 0) - (greatest(b, rb) + 0)), 'DAY'), interval '0' second) d from mix group by b, f, t)
12 select
13 to_char(b, 'dd.mm.yyyy hh24:mi') as "datetime begin",
14 to_char(f, 'dd.mm.yyyy hh24:mi') as "datetime finish",
15 cast(t as interval day to second(0)) as "total time",
16 cast(d as interval day to second(0)) as "daytime",
17 cast(t - d as interval day to second(0)) as "nighttime"
18 from
19 clc
20 order by
21 1, 2;
datetime begin datetime finish total time daytime nighttime
------------------ ------------------ -------------- -------------- --------------
16.11.2017 21:25 16.11.2017 22:55 +00 01:30:00 +00 00:35:00 +00 00:55:00
17.11.2017 05:45 17.11.2017 07:05 +00 01:20:00 +00 01:05:00 +00 00:15:00
17.11.2017 18:00 17.11.2017 19:00 +00 01:00:00 +00 01:00:00 +00 00:00:00
18.11.2017 23:00 19.11.2017 01:10 +00 02:10:00 +00 00:00:00 +00 02:10:00
30.10.2016 00:00 03.11.2016 23:00 +04 23:00:00 +03 08:00:00 +01 15:00:00

A different approach is more brute force one, but it allows to distinct the interval configuration from the reporting.
It goes in three stept:
1) define the rate type for aech minute of the day (change the granularity if required)
create table day_config as
with helper as (
select
rownum -1 minute_id
from dual connect by level <= 24*60),
helper2 as (
select
minute_id,
trunc(minute_id/60) hour_no,
mod(minute_id,60) minute_no
from helper)
select
minute_id,hour_no, minute_no,
case when hour_no >= 22 or hour_no <= 5 then 0 else 1 end rate_id
from helper2;
select * from day_config order by minute_id;
MINUTE_ID HOUR_NO MINUTE_NO RATE_ID
---------- ---------- ---------- ----------
0 0 0 0
1 0 1 0
2 0 2 0
3 0 3 0
4 0 4 0
5 0 5 0
6 0 6 0
7 0 7 0
8 0 8 0
9 0 9 0
Here rate_id means nigth, rate_id 1 means a day.
Advantage is, that you can introduce as much rate types as required.
2) expand the configuration for the required interval e.g. to whole year.
So now we have for each minute of the year the configuration, which rate is to be applied.
create or replace view year_config as
select my_date + MINUTE_ID / (24*60) minute_ts , MINUTE_ID, HOUR_NO, MINUTE_NO, RATE_ID from day_config
cross join
(select DATE '2017-01-01' + rownum -1 as my_date from dual connect by level <= 365)
order by 1,2;
select * from (
select * from year_config
order by 1)
where rownum <= 5;
MINUTE_TS MINUTE_ID HOUR_NO MINUTE_NO RATE_ID
------------------- ---------- ---------- ---------- ----------
01-01-2017 00:00:00 0 0 0 0
01-01-2017 00:01:00 1 0 1 0
01-01-2017 00:02:00 2 0 2 0
01-01-2017 00:03:00 3 0 3 0
01-01-2017 00:04:00 4 0 4 0
3) the reporting is as easy as joining to our config table constraining the interval (half open) and grouping in the RATE.
select b, f,RATE_ID, count(*) minute_cnt
from tst join year_config c on c.MINUTE_TS >= tst.b and c.MINUTE_TS < tst.f
group by b, f,RATE_ID
order by b, f,RATE_ID;
B F RATE_ID MINUTE_CNT
------------------- ------------------- ---------- ----------
16-11-2017 21:25:00 16-11-2017 22:55:00 0 55
16-11-2017 21:25:00 16-11-2017 22:55:00 1 35
17-11-2017 05:45:00 17-11-2017 07:05:00 0 15
17-11-2017 05:45:00 17-11-2017 07:05:00 1 65
17-11-2017 18:00:00 17-11-2017 19:00:00 1 60
18-11-2017 23:00:00 19-11-2017 01:10:00 0 130

The easiest way is probably to get all minutes worked in a recursive WITH clause and then see in which time range the minutes fall. As Oracle doesn't have a TIME datatype unfortunately, we'll have to work with times strings ('00'00' till '23:59').
with shifts as
(
select 'night' as shift, '00:00' as starttime, '05:59' as endtime, 20 as cost from dual
union all
select 'normal' as shift, '06:00' as starttime, '21:59' as endtime, 17 as cost from dual
union all
select 'night' as shift, '22:00' as starttime, '23:59' as endtime, 20 as cost from dual
)
, workminutes(wo, workminute, thetime, endtime) as
(
select wo, to_char(starttime, 'hh24:mi') as workminute, starttime as thetime, endtime
from mytable
union all
select
wo,
to_char(thetime + interval '1' minute, 'hh24:mi') as workminute,
thetime + interval '1' minute as thetime,
endtime
from workminutes
where thetime + interval '1' minute < endtime
)
select
wo,
count(case when s.shift = 'normal' then 1 end) as normal_time,
coalesce(sum(case when m.workminute between '06:00' and '21:59' then s.cost end), 0)
as normal_cost,
count(case when s.shift = 'night' then 1 end) as night_time,
coalesce(sum(case when m.workminute not between '06:00' and '21:59' then s.cost end), 0)
as night_cost,
count(*) as total_time,
coalesce(sum(s.cost), 0)
as total_cost
from workminutes m
join shifts s on m.workminute between s.starttime and s.endtime
group by wo
order by wo;
Output:
WO NORMAL_TIME NORMAL_COST NIGHT_TIME NIGHT_COST TOTAL_TIME TOTAL_COST
21 35 595 55 1100 90 1695
22 65 1105 15 300 80 1405
23 0 0 130 2600 130 2600
24 60 1020 0 0 60 1020
25 4800 81600 2340 46800 7140 128400
(This query looks a lot nicer of course, if you have a real shifts table and don't have to make one up on-the-fly. Also, you may not need all those seven columns I have in my result.)

Related

(bigquery) how number of hours event is happening within multiple dates

So my data looks like this:
DATE TEMPERATURE
2012-01-13 23:15:00 UTC 0
2012-01-14 01:35:00 UTC 5
2012-01-14 02:15:00 UTC 6
2012-01-14 03:15:00 UTC 8
2012-01-14 04:15:00 UTC 0
2012-01-14 04:55:00 UTC 0
2012-01-14 05:15:00 UTC -2
2012-01-14 05:35:00 UTC 0
I am trying to calculate the amount of time a zip code temperature will drop to 0 or below on any given day. On the 13th, it only happens for a very short amount of time so we don't really care. I want to know how to calculate the number of minutes this happens on the 14th, since it looks like a significantly (and consistently) cold day.
I want the query to add two more columns.
The first column added would be the time difference between the rows on a given date. So row 3- row 2=40 mins and row 4-row3=60 mins.
The second column would total the amount of minutes for a whole day the minutes the temperature has dropped to 0 or below. Here row 2-4 would be ignored. From row 5-8, total time that the temperature was 0 or below would be about 90 mins
It should end up looking like this:
DATE TEMPERATURE MINUTES_DIFFERENCE TOTAL_MINUTES
2012-01-13 23:15:00 UTC 0 0 0
2012-01-14 01:35:00 UTC 5 140 0
2012-01-14 02:15:00 UTC 6 40 0
2012-01-14 03:15:00 UTC 8 60 0
2012-01-14 04:15:00 UTC 0 60 60
2012-01-14 04:55:00 UTC 0 30 90
2012-01-14 05:15:00 UTC-2 20 110
2012-01-14 05:35:00 UTC 0 20 130
Use below
select *,
sum(minutes_difference) over(order by date) total_minutes
from (
select *,
ifnull(timestamp_diff(timestamp(date), lag(timestamp(date)) over(order by date), minute), 0) as minutes_difference
from your_table
)
if applied to sample data in your question - output is
Update to answer updated question
select * except(new_grp, grp),
sum(if(temperature > 0, 0, minutes_difference)) over(partition by grp order by date) total_minutes
from (
select *, countif(new_grp) over(order by date) as grp
from (
select *,
ifnull(timestamp_diff(timestamp(date), lag(timestamp(date)) over(order by date), minute), 0) as minutes_difference,
ifnull(((temperature <= 0) and (lag(temperature) over(order by date) > 0)) or
((temperature > 0) and (lag(temperature) over(order by date) <= 0)), true) as new_grp
from your_table
)
)
with output

How to group data weekly in column and hourly in row

I have data like following
ID SalesTime Qty Unit Price Item
1 01/01/2021 08:10:00 10 10 A
2 01/01/2021 11:30:00 2 9 B
3 01/01/2021 11:59:50 1 8 C
4 01/02/2021 13:00:00 5 15 D
5 01/03/2021 10:00:00 4 10 A
6 01/03/2021 12:00:00 5 9 B
7 01/03/2021 12:50:00 6 15 D
8 01/04/2021 10:50:00 5 8 C
9 01/04/2021 11:10:00 2 10 A
10 ............
I wanna summarize the total into the form,
for example:
Mon Tue Wed Thu Fri Sat Sun
08:00~09:59 20 21 50 100 60 70 210
10:00~11:59 60 25 60 90 75 80 200
12:00~13:59 100 10 50 60 70 50 150
How to do that in MS SQL, thanks a lot.
You can extract the hour and divide by two for the rows. And then use conditional aggregation for the columns. Assuming you want the total of the price times quantity:
select convert(time, dateadd(hour, 2 * (datepart(hour, salestime) / 2), 0)) as hh,
sum(case when datename(weekday, salestime) = 'Monday' then qty * unit_price end) as mon,
sum(case when datename(weekday, salestime) = 'Tuesday' then qty * unit_price end) as tue,
. . .
from t
group by datepart(hour, salestime) / 2
order by min(salestime);
Note: This just returns the beginning of the time period, rather than the full range.

How to fill missing values in certain time interval

I have table in below format
user timestamp count total_count
xyz 01-01-2020 00:12:00 45 45
xyz 01-01-2020 00:27:00 12 57
xyz 01-01-2020 00:29:00 11 68
xyz 01-01-2020 00:53:00 32 100
I want the data into 5 min interval like below (Expected Output)
user timestamp count total_count
xyz 01-01-2020 00:05:00 0 0
xyz 01-01-2020 00:10:00 0 0
xyz 01-01-2020 00:15:00 45 45
xyz 01-01-2020 00:20:00 0 45
xyz 01-01-2020 00:25:00 0 45
xyz 01-01-2020 00:30:00 23 68
xyz 01-01-2020 00:35:00 0 68
xyz 01-01-2020 00:40:00 0 68
xyz 01-01-2020 00:45:00 0 68
xyz 01-01-2020 00:50:00 0 68
xyz 01-01-2020 00:55:00 32 100
I tried
SELECT
TIMESTAMP_SECONDS(5*60 * DIV(UNIX_SECONDS(timestamp), 5*60)) timekey,
SUM(count) AS count,
MAX(total_count) as total_count
FROM db.table
WHERE
timestamp BETWEEN {{ start_date }}
AND {{ end_date }}
AND user = {{ user_id }}
GROUP BY
timekey
ORDER BY
timekey
Result of above query:
user timestamp count total_count
xyz 01-01-2020 00:15:00 45 45
xyz 01-01-2020 00:30:00 23 68
xyz 01-01-2020 00:55:00 32 100
How can I fill those missing timestamps in above query and fill values of count(with zeros) and total_count(previous non null value)?
Use generate_timestamp_array() to fill in the missing values:
SELECT ts,
SUM(t.count) AS count,
MAX(t.total_count) as total_count
FROM UNNEST(GENERATE_TIMESTAMP_ARRAY( {{start_date}}, {{end_date}}, INTERVAL 5 minute)) ts LEFT JOIN
db.table t
ON t.timestamp >= ts AND
t.timestamp < TIMESTAMP_ADD(ts, INTERVAL 5 minute) AND
t.user = {{ user_id }}
GROUP BY ts
ORDER BY ts;
If you need to partition by the table, you can slightly modify the query:
SELECT ts,
SUM(t.count) AS count,
MAX(t.total_count) as total_count
FROM UNNEST(GENERATE_TIMESTAMP_ARRAY( {{start_date}}, {{end_date}}, INTERVAL 5 minute)) ts LEFT JOIN
(SELECT t.*
FROM db.table t
WHERE timestamp BETWEEN {{ start_date }} AND {{ end_date }}
) t
ON t.timestamp >= ts AND
t.timestamp < TIMESTAMP_ADD(ts, INTERVAL 5 minute) AND
t.user = {{ user_id }}
GROUP BY ts
ORDER BY ts;

My SQL Query is working on one date, but I want start date to end date

I am using SQL Server 2005
I have two tables:
CheckInOut
TR BadgeNum USERID Dated Time CHECKTYPE
------- --------- ------ ----------------------- ----------------------- ----------
2337334 4 1 2018-04-01 00:00:00.000 2018-04-14 10:10:58.000 I
2337334 4 1 2018-04-01 00:00:00.000 2018-04-14 18:10:00.000 O
2337334 4 1 2018-04-02 00:00:00.000 2018-04-14 10:00:10.000 I
2337335 4 1 2018-04-02 00:00:00.000 2018-04-14 18:14:27.000 O
2337336 4 1 2018-04-03 00:00:00.000 2018-04-14 10:22:10.000 I
2337334 4 1 2018-04-03 00:00:00.000 2018-04-14 18:03:11.000 O
2337337 44 5 2018-04-01 00:00:00.000 2018-04-14 09:27:03.000 I
2337337 44 5 2018-04-01 00:00:00.000 2018-04-14 18:27:42.000 O
2337337 44 5 2018-04-02 00:00:00.000 2018-04-14 10:00:50.000 I
2337337 44 5 2018-04-02 00:00:00.000 2018-04-14 18:02:25.000 O
2337337 44 5 2018-04-03 00:00:00.000 2018-04-14 08:58:36.000 I
2337337 44 5 2018-04-03 00:00:00.000 2018-04-14 18:12:18.000 O
UserInfo
Tr UserID BadgeNumber Name
----- ------- ----------- --------------
13652 44 5 SAMIA NAZ
13653 4 1 Waqar Yousufzai
I need to calculate presence hours for each day for each user. My below query is working fine for given day. But I need to calculate for a given range. How do I get expected result?
Select isnull(max(ch.userid), 0)As 'ID'
,isnull(max(ch.badgenum), 0)as 'Badge#'
,isnull(max(convert(Char(10), ch.dated, 103)), '00:00')as 'Date'
,isnull(max(ui.name),'Empty')as 'Name'
,isnull(min(convert(VARCHAR(26), ch.time, 108)), '00:00') as 'Time In'
,case when min(ch.time) = max(ch.time) then '' else isnull(max(convert(VARCHAR(26), ch.time, 108)), '00:00') end as 'TimeOut'
,case when min(ch.time) = max(ch.time) then 'Absent' else 'Present' end as 'Status'
,isnull(CONVERT(varchar(3),DATEDIFF(minute,min(ch.time), max(ch.time))/60) + ' hrs and ' +
RIGHT('0' + CONVERT(varchar(2),DATEDIFF(minute,min(ch.time),max(ch.time))%60),2) + 'Min' , 0) as 'Total Hrs'
From CHECKINOUT ch left Join userinfo ui on ch.badgenum = ui.badgenumber
Where ch.Dated between '2018-04-01' and '2018-04-03' GROUP BY ch.badgenum
Query result
ID Badge# Date Name Time In TimeOut Status Total Hrs
--- ------ ---------- --------------- -------- ---------- -------- -----------------
4 1 03/04/2018 Waqar Yousufzai 11:33:34 18:24:23 Present 30 hrs and 14Min
82 3 03/04/2018 TANVEER ANSARI 09:37:14 19:18:22 Present 32 hrs and 37Min
13 4 03/04/2018 07:19:26 09:30:17 Present 21 hrs and 49Min
44 5 03/04/2018 SAMIA NAZ 08:53:15 18:25:21 Present 33 hrs and 24Min
28 7 03/04/2018 Anees Ahmad 08:34:57 22:00:38 Present 61 hrs and 25Min
46 8 03/04/2018 Shazia - OT 08:10:41 16:15:05 Present 32 hrs and 01Min
Expected result
ID Badge# Date Name Time In TimeOut Status Total Hrs
--- ------ ---------- --------------- -------- ---------- -------- -----------------
4 1 01/04/2018 Waqar Yousufzai 10:30:00 18:00:00 Present 7 hrs and 30Min
4 1 02/04/2018 Waqar Yousufzai 10:30:00 18:00:00 Present 7 hrs and 30Min
4 1 03/04/2018 Waqar Yousufzai 10:00:00 18:00:00 Present 8 hrs and 00Min
44 5 01/04/2018 SAMIA 08:00:00 18:00:00 Present 10 hrs and 00Min
44 5 02/04/2018 SAMIA 08:30:00 18:00:00 Present 9 hrs and 30Min
44 5 03/04/2018 SAMIA 08:00:00 18:00:00 Present 10 hrs and 00Min
You shouldn't do aggregation on date value, it must be part of grouping. Get time out and time in using conditional aggregation. And count total hours worked. Your query should be something like:
select
BadgeNum, USERID, Dated, Name
, right('0' + cast(datediff(mi, [in], [out]) / 60 as varchar(10)), 2) + ':'
+ right('0' + cast(datediff(mi, [in], [out]) % 60 as varchar(10)), 2)
from (
select
ch.BadgeNum, ch.USERID, dated = cast(ch.Dated as date), ui.Name
, [in] = min(case when ch.CHECKTYPE = 'I' then ch.Time end)
, [out] = min(case when ch.CHECKTYPE = 'O' then ch.Time end)
from
CheckInOut ch
left join UserInfo ui on ch.USERID = ui.badgenumber
where
ch.Dated >= '20180401'
and ch.Dated < '20180404'
group by ch.BadgeNum, ch.USERID, cast(ch.Dated as date), ui.Name
) t

Difference in Start and End times grouping by hour

My table has info similar to below
Emp Date START_TIME END_TIME Code Minutes
--- -------- ------------------- ------------------- ---- -------
E1 11/1/2012 11/1/2012 6:55:00 AM 11/1/2012 7:01:00 AM C1 6
E1 11/1/2012 11/1/2012 6:57:00 AM 11/1/2012 8:01:00 AM C2 64
E2 11/1/2012 11/1/2012 6:57:00 AM 11/1/2012 8:00:00 AM C2 63
E1 11/2/2012 11/2/2012 7:35:00 AM 11/2/2012 8:01:00 AM C1 26
Expected Output is
Date Code Range Minutes
--------- ---- ----------------------- -------
11/1/2012 C1 6:30:00 AM-7:00:00 AM 5
11/1/2012 C1 7:00:00 AM-7:30:00 AM 1
11/1/2012 C2 6:30:00 AM-7:00:00 AM 6
11/1/2012 C2 7:00:00 AM-7:30:00 AM 60
11/1/2012 C2 7:30:00 AM-8:00:00 AM 60
11/1/2012 C2 8:00:00 AM-8:30:00 AM 1
11/2/2012 C1 7:30:00 AM-8:00:00 AM 25
11/2/2012 C1 8:00:00 AM-8:30:00 AM 1
Leaving out Emp field, I want to group by date, and code with total time spent in a span of 30 minutes each. And the limitation I have is to achieve this using select statements i.e. only through SQL queries coz PL/SQL is not allowed. Thanks in advance!
a solution involving the model clause.
first lets compute the amount of 30 minute blocks we need per entry.
SQL> select emp, start_time, end_time, code,
2 trunc(start_time, 'mi')
3 - (mod(to_char(trunc(start_time, 'mi'), 'mi'), 30) / 1440) start_block,
4 ceil(2*24*(end_time-(trunc(start_time, 'mi')
5 - (mod(to_char(trunc(start_time, 'mi'), 'mi'), 30) / 1440)))) blocks
6 from tab f
7 /
EM START_TIME END_TIME CO START_BLOCK BLOCKS
-- ---------------------- ---------------------- -- ---------------------- ----------
E1 11/01/2012 06:55:00 am 11/01/2012 07:01:00 am C1 11/01/2012 06:30:00 am 2
E1 11/01/2012 06:57:00 am 11/01/2012 08:01:00 am C2 11/01/2012 06:30:00 am 4
E2 11/01/2012 06:57:00 am 11/01/2012 08:00:00 am C2 11/01/2012 06:30:00 am 3
E1 11/02/2012 07:35:00 am 11/02/2012 08:01:00 am C1 11/02/2012 07:30:00 am 2
now, we generate rows to break this into 30 minute periods using the model clause.
SQL> with foo as (select rownum id, emp, start_time, end_time, code,
2 trunc(start_time, 'mi')
3 - (mod(to_char(trunc(start_time, 'mi'), 'mi'), 30) / 1440) start_block,
4 ceil(2*24*(end_time-(trunc(start_time, 'mi')
5 - (mod(to_char(trunc(start_time, 'mi'), 'mi'), 30) / 1440)))) blocks
6 from tab f)
7 select trunc(start_time) thedate, code, emp, range, minutes
8 from foo
9 model partition by(id)
10 dimension by(0 as f)
11 measures(code, emp, start_time, end_time, start_block, blocks,
12 sysdate as start_range,
13 sysdate as end_range,
14 cast(0 as number) minutes,
15 cast('' as varchar2(50)) range)
16 rules (start_range [for f from 0 to blocks[0]-1 increment 1] = start_block[0] + (30*cv(f)/1440),
17 end_range[any] = start_range[cv()] + (30/1440),
18 code[any] = code[0],
19 emp[any] = emp[0],
20 start_time[any] = start_time[0],
21 end_time[any] = end_time[0],
22 range [any] = to_char(start_range[cv()], 'dd/mm/yyyy hh:mi:ss am') || ' - ' || to_char(end_range[cv()], 'dd/mm/yyyy hh24:mi:ss am'),
23 minutes [any] = case
24 when start_time[0] between start_range[cv()] and end_range[cv()]
25 then 1440 *(end_range[cv()] - start_time[0])
26 when end_time[0] between start_range[cv()] and end_range[cv()]
27 then 1440 *(end_time[0] - start_range[cv()])
28 else 1440 * (end_range[cv()] - start_range[cv()])
29 end );
CO EM RANGE MINUTES
-- -- -------------------------------------------------- ----------
C2 E2 11/01/2012 06:30:00 am - 11/01/2012 07:00:00 am 3
C2 E2 11/01/2012 07:00:00 am - 11/01/2012 07:30:00 am 30
C2 E2 11/01/2012 07:30:00 am - 11/01/2012 08:00:00 am 30
C1 E1 11/01/2012 06:30:00 am - 11/01/2012 07:00:00 am 5
C1 E1 11/01/2012 07:00:00 am - 11/01/2012 07:30:00 am 1
C1 E1 11/02/2012 07:30:00 am - 11/02/2012 08:00:00 am 25
C1 E1 11/02/2012 08:00:00 am - 11/02/2012 08:30:00 am 1
C2 E1 11/01/2012 06:30:00 am - 11/01/2012 07:00:00 am 3
C2 E1 11/01/2012 07:00:00 am - 11/01/2012 07:30:00 am 30
C2 E1 11/01/2012 07:30:00 am - 11/01/2012 08:00:00 am 30
C2 E1 11/01/2012 08:00:00 am - 11/01/2012 08:30:00 am 1
11 rows selected.
so we are partitioning by:
partition by(id)
ie by a unique reference. then we are going to generate rows with our dimension
dimension by(0 as f)
in conjuction with part of the rules:
for f from 0 to blocks[0]-1 increment 1
so the start_range column is generated with
start_range [for f from 0 to blocks[0]-1 increment 1] = start_block[0] + (30*cv(f)/1440),
start_block[0] is in the first query, eg:
EM START_TIME END_TIME CO START_BLOCK BLOCKS
-- ---------------------- ---------------------- -- ---------------------- ----------
E1 11/01/2012 06:55:00 am 11/01/2012 07:01:00 am C1 11/01/2012 06:30:00 am 2
so for this row, it evaluates to
start_range[0 to 1] = 11/01/2012 06:30:00 am + (30minutes * the value of f)
i.e.
start_range[0] = 11/01/2012 06:30:00 am + (30min*0) = 11/01/2012 06:30:00 am
start_range[1] = 11/01/2012 06:30:00 am + (30min*1) = 11/01/2012 07:00:00 am
the rest is pretty straight forward:
end_range[any] = start_range[cv()] + (30/1440),
means that for end-range on the current row, we take start_range and add 30 minutes.
the range column is a concatenation of start_range and end_range:
range [any] = to_char(start_range[cv()], 'dd/mm/yyyy hh:mi:ss am') || ' - ' || to_char(end_range[cv()], 'dd/mm/yyyy hh24:mi:ss am'),
finally in order to calc minutes in that range:
minutes [any] = case
when start_time[0] between start_range[cv()] and end_range[cv()]
then 1440 *(end_range[cv()] - start_time[0])
when end_time[0] between start_range[cv()] and end_range[cv()]
then 1440 *(end_time[0] - start_range[cv()])
else 1440 * (end_range[cv()] - start_range[cv()])
end );
if start_time sits in the range, take the end of the range - start
time
if end_time sits in the range, take the end_time - start of the range
otherwise its end_range - start_range.
1440 just gets the answer as minutes.
now we can just group that all up:
SQL> with foo as (select rownum id, emp, start_time, end_time, code,
2 trunc(start_time, 'mi')
3 - (mod(to_char(trunc(start_time, 'mi'), 'mi'), 30) / 1440) start_block,
4 ceil(2*24*(end_time-(trunc(start_time, 'mi')
5 - (mod(to_char(trunc(start_time, 'mi'), 'mi'), 30) / 1440)))) blocks
6 from tab f)
7 select thedate, code, range, sum(minutes) minutes
8 from (select trunc(start_time) thedate, code, emp, range, minutes
9 from foo
10 model partition by(id)
11 dimension by(0 as f)
12 measures(code, emp, start_time, end_time, start_block, blocks,
13 sysdate as start_range,
14 sysdate as end_range,
15 cast(0 as number) minutes,
16 cast('' as varchar2(50)) range)
17 rules (start_range [for f from 0 to blocks[0]-1 increment 1] = start_block[0] + (30*cv(f)/1440),
18 code[any] = code[0],
19 emp[any] = emp[0],
20 end_range[any] = start_range[cv()] + (30/1440),
21 start_time[any] = start_time[0],
22 end_time[any] = end_time[0],
23 range [any] = to_char(start_range[cv()], 'dd/mm/yyyy hh:mi:ss am') || ' - ' || to_char(end_range[cv()], 'dd/mm/yyyy hh24:mi:ss am'),
24 minutes [any] = case
25 when start_time[0] between start_range[cv()] and end_range[cv()]
26 then 1440 *(end_range[cv()] - start_time[0])
27 when end_time[0] between start_range[cv()] and end_range[cv()]
28 then 1440 *(end_time[0] - start_range[cv()])
29 else 1440 * (end_range[cv()] - start_range[cv()])
30 end ))
31 group by thedate, code, range
32 order by thedate, code, range;
THEDATE CO RANGE MINUTES
---------- -- -------------------------------------------------- ----------
11/01/2012 C1 11/01/2012 06:30:00 am - 11/01/2012 07:00:00 am 5
11/01/2012 C1 11/01/2012 07:00:00 am - 11/01/2012 07:30:00 am 1
11/01/2012 C2 11/01/2012 06:30:00 am - 11/01/2012 07:00:00 am 6
11/01/2012 C2 11/01/2012 07:00:00 am - 11/01/2012 07:30:00 am 60
11/01/2012 C2 11/01/2012 07:30:00 am - 11/01/2012 08:00:00 am 60
11/01/2012 C2 11/01/2012 08:00:00 am - 11/01/2012 08:30:00 am 1
11/02/2012 C1 11/02/2012 07:30:00 am - 11/02/2012 08:00:00 am 25
11/02/2012 C1 11/02/2012 08:00:00 am - 11/02/2012 08:30:00 am 1
I am pretty sure this can be cleaned up, and made more legible and more efficient as Oracle is not one of my strong suits, but it works and should give an idea of how to accomplish the task.
The key here is joining to a list of numbers to break up your records into half hour periods.
SELECT "Date",
"Code",
"RangeStart" + ((r - 1) / 48.0) AS "RangeStart",
"RangeStart" + (r / 48.0) AS "RangeEnd",
SUM(CASE WHEN r = 1 THEN "StartMinutes"
WHEN "END_TIME" >= "RangeStart" + ((r - 1) / 48.0) AND "END_TIME" < "RangeStart" + (r / 48.0) THEN "EndMinutes"
ELSE 30
END) AS "TotalMinutes"
FROM ( SELECT "Emp",
"Date",
"START_TIME",
"END_TIME",
"Code",
CASE WHEN EXTRACT(MINUTE from "START_TIME") > 30 THEN 60 ELSE 30 END - EXTRACT(MINUTE from "START_TIME") AS "StartMinutes",
EXTRACT(MINUTE from END_TIME) - CASE WHEN EXTRACT(MINUTE from "END_TIME") > 30 THEN 30 ELSE 0 END AS "EndMinutes",
"START_TIME" - (EXTRACT(MINUTE from "START_TIME") - CASE WHEN EXTRACT(MINUTE from "START_TIME") > 30 THEN 30 ELSE 0 END) / (60 * 24.0) AS "RangeStart"
FROM T
) T
INNER JOIN
( SELECT Rownum r
FROM dual
CONNECT BY Rownum <= 100
) r
ON "END_TIME" > ("RangeStart" + ((r - 1) / 48.0))
GROUP BY "Date", "Code", "RangeStart" + ((r - 1) / 48.0), "RangeStart" + (r / 48.0)
ORDER BY "Code", "Date", "RangeStart";
EXAMPLE ON SQL FIDDLE
Here's another solution (it's not very elegant and uses hard-coded date literals to obtain the boundaries for the buckets - should probably be replaced by a sub-query to obtain them):
with v_data as (
select 1 pk, 'E1' emp, to_date('2012-11-01', 'YYYY-MM-DD') as date1, to_date('2012-11-01 06:55:00', 'YYYY-MM-DD hh24:mi:ss') as start_time, to_date('2012-11-01 07:01:00', 'YYYY-MM-DD hh24:mi:ss') as end_time, 'C1' as code, 6 as minutes from dual union all
select 2 pk, 'E1' emp, to_date('2012-11-01', 'YYYY-MM-DD') as date1, to_date('2012-11-01 06:57:00', 'YYYY-MM-DD hh24:mi:ss') as start_time, to_date('2012-11-01 08:01:00', 'YYYY-MM-DD hh24:mi:ss') as end_time, 'C2' as code, 64 as minutes from dual union all
select 3 pk, 'E2' emp, to_date('2012-11-01', 'YYYY-MM-DD') as date1, to_date('2012-11-01 06:57:00', 'YYYY-MM-DD hh24:mi:ss') as start_time, to_date('2012-11-01 08:00:00', 'YYYY-MM-DD hh24:mi:ss') as end_time, 'C2' as code, 63 as minutes from dual union all
select 4 pk, 'E1' emp, to_date('2012-11-02', 'YYYY-MM-DD') as date1, to_date('2012-11-02 07:35:00', 'YYYY-MM-DD hh24:mi:ss') as start_time, to_date('2012-11-02 08:01:00', 'YYYY-MM-DD hh24:mi:ss') as end_time, 'C1' as code, 26 as minutes from dual),
v_buckets as (
select
to_date('2012-11-01', 'YYYY-MM-DD') + (rownum-1)/48 as bucket_start,
to_date('2012-11-01', 'YYYY-MM-DD') + rownum/48 as bucket_end
from dual
connect by rownum <96
)
select v3.date1, v3.bucket_start, v3.bucket_end, v3.code, sum(v3.time_spent_in_bucket) as minutes
from (
select v2.*, (least(end_time, bucket_end) - greatest(start_time, bucket_start))*1440 as time_spent_in_bucket from
(
select buck.*,
v1.*
from v_buckets buck
join v_data v1
on (
-- time slot completely contained in one bucket
(v1.start_time >= buck.bucket_start and v1.start_time < buck.bucket_end and
v1.end_time >= buck.bucket_start and v1.end_time < buck.bucket_end)
-- time slot starts in bucket, expands to next bucket
or (v1.start_time >= buck.bucket_start and v1.start_time < buck.bucket_end and
v1.end_time >= buck.bucket_end)
-- time slot started in previous bucket, ends in this bucket)
or (v1.start_time < buck.bucket_start and v1.end_time > buck.bucket_start and
v1.end_time <= buck.bucket_end)
-- time slot began in previous bucket, expands to next bucket
or (v1.start_time < buck.bucket_start and v1.end_time >= buck.bucket_end)
)
) v2
) v3
where start_time is not null
group by date1, bucket_start, bucket_end, code
order by bucket_start, code
This is my try:
select trunc(trunc_start) as datetime, code, range , sum(duration) minutes
from (
select code, end_time, start_time, TRUNC_START ,
to_char(trunc_start,'hh:mi:ss AM')||'-'||to_char(trunc_start+1/48,'hh:mi:ss AM') as range,
case
when end_time-trunc_start between 0 and 1/48 then (end_time-trunc_start)*1440
when start_time-trunc_start between 0 and 1/48 then (trunc_start-start_time)*1440+30
else 30
end as duration
from(
select s.*, n ,
trunc(start_time) + trunc((start_time-trunc(start_time))*48)/48 + (n-1)/48 as trunc_start
from s
join (select level n from dual connect by level <=48) a
on n-2 <= (end_time-start_time)*100
)b
)
where trunc_start < end_time --eliminating fake intervals
group by code, trunc(trunc_start), range
order by 1, 3
;
sorry for the where :)
SQLFIDDLE
Here's some general example of ranges for you:
SELECT job
, sum(decode(greatest(sal,2999), least(sal,6000), 1, 0)) "Range 3000-6000"
, sum(decode(greatest(sal,1000), least(sal,2999), 1, 0)) "Range 1000-3000"
, sum(decode(greatest(sal,0), least(sal,999), 1, 0)) "Range 0-1000"
FROM scott.emp
GROUP BY job
/