FORMATING SELECT - sql

I have a select which shows me how much lasts a process, it looks like:
SELECT
info1,
info2,
ifno3...
(DATE,'DD.MM.YYYY')DAY,
to_char(DATE,'HH24:MI') hour_from,
to_char(DATE_UNTIL,''HH24:MI'')hour_until,
REGEXP_SUBSTR((date_until-date)DAY TO SECOND,'d{2}:\d{2}')lasts_time
.....
and if a process lasts for example from 09.06.2021 23:00 to 10.06.2021 07:00, then the output of query from above will be:
info1 info2 info3 DAY hour_from hour_until lasts_time
info1| info2 |info3 |09.06.2021 |23:00 | 07:00 | 08:00
and i want it to change in
if this process lasts over night, then the output should be something like this:
info1 info2 info3 DAY hour_from hour_until lasts_time
info1| info2 |info3 |09.06.2021 |23:00 | 00:00 | 01:00
info1| info2 |info3 |10.06.2021 |00:00 | 07:00 | 07:00
so it will count only until the day ends, and if the process is still on going in the new day the output will come in a new row,
I think I have here to deal with union all, but I'm not sure, if anyone has any idea would be great

Here's one option. Read comments within code.
SQL> with
2 test (id, info1, date_from, date_to) as
3 -- sample data
4 (select 1, 'info1', to_date('09.06.2021 23:00', 'dd.mm.yyyy hh24:mi'),
5 to_date('10.06.2021 07:00', 'dd.mm.yyyy hh24:mi')
6 from dual
7 ),
8 temp as
9 -- row generator, to generate all hours between DATE_FROM and DATE_TO
10 (select id, info1,
11 date_from + (level - 1) / 24 datum,
12 lead(date_from + (level - 1) / 24) over (order by date_from) next_datum
13 from test
14 connect by level <= (date_to - date_from) * 24 + 1
15 ),
16 temp2 as
17 -- find date boundaries (MINDAT and MAXDAT), as well as duration in between (LASTS_TIME)
18 (select id, info1, datum, next_datum,
19 min(datum) mindat,
20 max(case when to_char(next_datum, 'mi') = '00' then next_datum - 1/(24*60*60)
21 else datum
22 end
23 ) maxdat,
24 --
25 round((max(case when to_char(next_datum, 'mi') = '00' then next_datum - 1/(24*60*60)
26 else datum
27 end
28 ) -
29 min(datum)
30 ) * 24) lasts_time
31 from temp
32 group by id, info1, datum, next_datum
33 )
34 -- the final result
35 select id,
36 info1,
37 to_char(datum, 'dd.mm.yyyy') day,
38 min(to_char(datum, 'hh24:mi')) hour_from,
39 max(to_char(next_datum, 'hh24:mi')) hour_until,
40 sum(lasts_time) lasts_time
41 from temp2
42 group by id, info1,
43 to_char(datum, 'dd.mm.yyyy')
44 order by day;
The result is
ID INFO1 DAY HOUR_FROM HOUR_UNTIL LASTS_TIME
---------- ----- ---------- ---------- ---------- ----------
1 info1 09.06.2021 23:00 00:00 1
1 info1 10.06.2021 00:00 07:00 7
SQL>
For #astentx's comment: there's no endless loop in my case for values you suggested:
SQL> set timing on
SQL> with
2 test (id, info1, date_from, date_to) as
3 -- sample data
4 (select 1, 'info1', to_date('10.06.2021 07:00', 'dd.mm.yyyy hh24:mi'),
5 to_date('11.06.2021 07:00', 'dd.mm.yyyy hh24:mi')
6 from dual
7 ),
<snip>
ID INFO1 DAY HOUR_ HOUR_ LASTS_TIME
---------- ----- ---------- ----- ----- ----------
1 info1 10.06.2021 07:00 23:00 17
1 info1 11.06.2021 00:00 07:00 7
Elapsed: 00:00:00.04
SQL>

You can use recursive CTE to generate required amount of rows (which are equal to days distance between dates).
with a as (
select
'info1' as id
, to_date('2021-05-01 10:20:30') as dt_from
, to_date('2021-05-03 07:08:09') as dt_to
from dual
union all
select
'info2'
, to_date('2021-05-02 05:06:07')
, to_date('2021-05-02 10:20:30')
from dual
)
, b(id, dt_from, dt_to, dt) as (
select
id
, dt_from
, dt_to
, trunc(dt_from)
from a
union all
select
id
, dt_from
, dt_to
, dt + 1
from b
where dt < trunc(dt_to)
)
select
id
, dt
, numtodsinterval(
case
when trunc(dt_to) = dt
then dt_to
else dt + 1
end
-
greatest(dt_from, dt)
, 'DAY') as dur_interval
/*Or any rounding function you need*/
, trunc((case
when trunc(dt_to) = dt
then dt_to
else dt + 1
end
-
greatest(dt_from, dt))*24) as dur_hours
, case
when trunc(dt_from) = dt
then to_char(dt_from, 'hh24:mi')
else '00:00'
end as hour_from
, case
when trunc(dt_to) = dt
then to_char(dt_to, 'hh24:mi')
else '23:59'
end as hour_to
from b
order by
id
, dt
ID | DT | DUR_INTERVAL | DUR_HOURS | HOUR_FROM | HOUR_TO
:---- | :------------------ | :---------------------------- | --------: | :-------- | :------
info1 | 2021-05-01 00:00:00 | +000000000 13:39:30.000000000 | 13 | 10:20 | 23:59
info1 | 2021-05-02 00:00:00 | +000000001 00:00:00.000000000 | 24 | 00:00 | 23:59
info1 | 2021-05-03 00:00:00 | +000000000 07:08:09.000000000 | 7 | 00:00 | 07:08
info2 | 2021-05-02 00:00:00 | +000000000 05:14:23.000000000 | 5 | 05:06 | 10:20
db<>fiddle here

Related

PLSQL How to split each row based on begin and end date

Could you please assist me to split each row and create multiple rows for each date between begin_date and end_date.
ID CODE VIEW BEGIN_DATE END_DATE
-------------------------------------------
10400 null 2 17-FEB-20 17-FEB-20
10650 null 2 17-FEB-20 18-FEB-20
10900 null 2 19-FEB-20 21-FEB-20
10901 null 2 21-FEB-20 02-MAR-20
11650 2723 2 02-MAR-20 04-MAR-20
11650 1002 2 02-MAR-20 04-MAR-20
11650 1001 2 02-MAR-20 04-MAR-20
11650 1000 2 02-MAR-20 04-MAR-20
Currently I'm using below query but it doesn't seem to work
select
r.*
from rec r
connect by level <= end_date - begin_date + 1;
what i want is to some thing like this
ID CODE VIEW DATE
----------------------------------
11650 2723 2 02-MAR-20
11650 2723 2 03-MAR-20
11650 2723 2 04-MAR-20
.... continue
Here's one option:
Sample data:
SQL> with test (id, code, cview, begin_date, end_date) as
2 (select 10400, null, 2, date '2020-02-17', date '2020-02-17' from dual union all
3 select 10650, null, 2, date '2020-02-17', date '2020-02-18' from dual union all
4 select 11650, 2723, 2, date '2020-03-02', date '2020-03-04' from dual
5 )
Query begins here:
6 select id,
7 code,
8 cview,
9 begin_date + column_value - 1 as datum
10 from test cross join
11 table(cast(multiset(select level from dual
12 connect by level <= end_date - begin_date + 1
13 ) as sys.odcinumberlist))
14 order by id, datum;
ID CODE CVIEW DATUM
---------- ---------- ---------- ----------
10400 2 17.02.2020
10650 2 17.02.2020
10650 2 18.02.2020
11650 2723 2 02.03.2020
11650 2723 2 03.03.2020
11650 2723 2 04.03.2020
6 rows selected.
SQL>

Pivot two columns and keep the values same in sql

I have created a query to get different time types and hours
SELECT calc_time.hours measure,
calc_time.payroll_time_type elements,
calc_time.person_id,
calc_time.start_time
FROM hwm_tm_rep_work_hours_sum_v calc_time,
per_all_people_f papf
WHERE grp_type_id = 200
AND payroll_time_type IN ( 'Afternoon shift',
'TL',
'Evening shift',
'Regular Pay ',
'OT' )
AND (To_date(To_char(calc_time.start_time, 'YYYY-MM-DD') , 'YYYY-MM-DD') BETWEEN To_date(To_char(:From_Date, 'YYYY-MM-DD'), 'YYYY-MM-DD')
AND To_date( To_char(:To_Date, 'YYYY-MM-DD'), 'YYYY-MM-DD' ))
AND papf.person_id = calc_time.person_id
I get the output like -
Start_time person_id elements measure
01-Jan-2021 198 Regular Pay 10
01-Jan-2021 198 OT 2
01-jAN-2021 198 Afternoon shift 2
16-JAN-2021 198 Regular Pay 10
17-JAN-2021 198 OT 3
20-JAN-2021 198 EVENING SHIFT 8
08-JAN-2021 11 Regular Pay 8
09-JAN-2021 11 OT 1
08-JAN-2021 11 tl 2
10-JAN-2021 12 Evening shift 9
11-JAN-2021 12 Evening shift 9
I want this output to be dispplayed as follows WITHIN TWO DATES THAT I PASS AS PARAMETER - LIKE PARAMETER TO AND FROM DATE 01-JAN-2021 AND 31-JAN-2021
person_id Regular_pay OT OTHER_MEASURE OTHER_CODE
198 20 5 2 Afternoon shift
198 20 5 8 EVENING SHIFT
11 8 1 2 TL
12 18 Evening shift
So sum of Regular pay and OT IN separate columns and all others in other_measure and other_code
How can I tweak the main query to achieve this?
You can use:
SELECT *
FROM (
SELECT c.person_id,
SUM(CASE c.payroll_time_type WHEN 'Regular Pay' THEN SUM(c.hours) END)
OVER (PARTITION BY c.person_id) AS regular_pay,
SUM(CASE c.payroll_time_type WHEN 'OT' THEN SUM(c.hours) END)
OVER (PARTITION BY c.person_id) AS OT,
SUM(c.hours) AS other_measure,
c.payroll_time_type AS Other_code
FROM hwm_tm_rep_work_hours_sum_v c
INNER JOIN per_all_people_f p
ON (p.person_id = c.person_id)
WHERE grp_type_id = 200
AND payroll_time_type IN (
'Afternoon shift',
'TL',
'Evening shift',
'Regular Pay',
'OT'
)
AND c.start_time >= TRUNC(:from_date)
AND c.start_time < TRUNC(:to_date) + INTERVAL '1' DAY
GROUP BY
c.person_id,
c.payroll_time_type
)
WHERE other_code NOT IN ('Regular Pay', 'OT');
Which, for the sample data:
CREATE TABLE hwm_tm_rep_work_hours_sum_v (start_time, person_id, payroll_time_type, hours) AS
SELECT DATE '2021-01-01', 198, 'Regular Pay', 10 FROM DUAL UNION ALL
SELECT DATE '2021-01-01', 198, 'OT', 2 FROM DUAL UNION ALL
SELECT DATE '2021-01-01', 198, 'Afternoon shift', 2 FROM DUAL UNION ALL
SELECT DATE '2021-01-16', 198, 'Regular Pay', 10 FROM DUAL UNION ALL
SELECT DATE '2021-01-17', 198, 'OT', 3 FROM DUAL UNION ALL
SELECT DATE '2021-01-20', 198, 'Evening shift', 8 FROM DUAL UNION ALL
SELECT DATE '2021-01-08', 11, 'Regular Pay', 8 FROM DUAL UNION ALL
SELECT DATE '2021-01-09', 11, 'OT', 1 FROM DUAL UNION ALL
SELECT DATE '2021-01-08', 11, 'TL', 2 FROM DUAL UNION ALL
SELECT DATE '2021-01-10', 12, 'Evening shift', 9 FROM DUAL UNION ALL
SELECT DATE '2021-01-11', 12, 'Evening shift', 9 FROM DUAL;
CREATE TABLE per_all_people_f (person_id, grp_type_id) AS
SELECT 198, 200 FROM DUAL UNION ALL
SELECT 11, 200 FROM DUAL UNION ALL
SELECT 12, 200 FROM DUAL;
Outputs:
PERSON_ID
REGULAR_PAY
OT
OTHER_MEASURE
OTHER_CODE
11
8
1
2
TL
12
18
Evening shift
198
20
5
2
Afternoon shift
198
20
5
8
Evening shift
db<>fiddle here
You could try something like this - In your question, unfortunately, it is not clear in which table which columns/values ​​are available.
SELECT
calc_time.person_id,
(select sum(calc_time.start_time) FROM hwm_tm_rep_work_hours_sum_v calc_time where papf.person_id = calc_time.person_id and calc_time.payroll_time_type = 'Regular Pay') as Regular_Pay,
...
FROM hwm_tm_rep_work_hours_sum_v calc_time,
per_all_people_f papf
WHERE grp_type_id = 200
AND payroll_time_type IN ( 'Afternoon shift',
'TL',
'Evening shift',
'Regular Pay ',
'OT' )
AND (
To_date(To_char(calc_time.start_time, 'YYYY-MM-DD') , 'YYYY-MM-DD') BETWEEN To_date(To_char(:From_Date, 'YYYY-MM-DD'), 'YYYY-MM-DD')
AND To_date( To_char(:To_Date, 'YYYY-MM-DD'), 'YYYY-MM-DD' ) )
and papf.person_id = calc_time.person_id
-- use a group by
GROUP BY
calc_time.person_id
You may use aggregation and then apply model clause to calculate the required columns. Below is the code with comments, assuming you can manage filter by dates.
select *
from t
PERSON_ID | ELEMENTS | MEASURE
--------: | :-------------- | ------:
198 | Regular Pay | 1
198 | Regular Pay | 2
198 | Afternoon shift | 3
198 | Afternoon shift | 4
198 | OT | 5
198 | OT | 6
198 | EVENING SHIFT | 7
198 | EVENING SHIFT | 8
11 | Regular Pay | 11
11 | Regular Pay | 12
11 | TL | 13
11 | TL | 14
11 | EVENING SHIFT | 15
11 | EVENING SHIFT | 16
12 | TL | 21
12 | TL | 22
12 | EVENING SHIFT | 23
12 | EVENING SHIFT | 24
select
person_id,
ot,
regular_pay,
elements as other_code,
mes as other_measure
from (
/*First you need to aggregate all the measures by person_id and code*/
select
person_id,
elements,
sum(measure) as mes
from t
/*Date filter goes here*/
group by
person_id,
elements
)
model
/*RETURN UPDATED ROWS
will do the trick,
because we'll update only "other"
measures, so OT and Regular pay will no go
to the output*/
return updated rows
/*Where to break the calculation*/
partition by (person_id)
/*To be able to reference by code*/
dimension by (elements)
measures (
mes,
0 as ot,
0 as regular_pay
)
rules upsert (
ot[
elements not in ('OT', 'Regular Pay')
] = sum(mes)['OT'],
regular_pay[
elements not in ('OT', 'Regular Pay')
] = sum(mes)['Regular Pay']
)
PERSON_ID | OT | REGULAR_PAY | OTHER_CODE | OTHER_MEASURE
--------: | ---: | ----------: | :-------------- | ------------:
198 | 11 | 3 | EVENING SHIFT | 15
198 | 11 | 3 | Afternoon shift | 7
11 | null | 23 | TL | 27
11 | null | 23 | EVENING SHIFT | 31
12 | null | null | TL | 43
12 | null | null | EVENING SHIFT | 47
db<>fiddle here

SQLQuery for Time In and Time Out attendance in Oracle

I have a table in oracle with the below sample output.
EID | type | Date
24 | IN |03/25/2019 6:45 am
24 | OUT |03/25/2019 8:05 am
24 | IN |03/25/2019 8:06 am
24 | IN |03/25/2019 8:28 am
24 | OUT |03/25/2019 9:48 am
24 | IN |03/25/2019 9:52 am
24 | IN |03/25/2019 9:57 am
24 | IN |03/25/2019 10:44 am
24 | OUT |03/25/2019 12:16 pm
24 | OUT |03/25/2019 1:00 pm
24 | IN |03/25/2019 1:05 pm
24 | OUT |03/25/2019 2:21 pm
I want to build a query to achieve the below results:
EID | TIMEIN | TIMEOUT | DIIF_IN_MIN
24 | 03/25/2019 6:45 am | 03/25/2019 8:05 am | 1
24 | 03/25/2019 8:06 am | null | 0
24 | 03/25/2019 8:28 am | 03/25/2019 9:48 am | 4
24 | 03/25/2019 9:52 am | null | 0
24 | 03/25/2019 9:57 am | null | 0
24 | 03/25/2019 10:44 am | 03/25/2019 12:16 pm | 0
24 | null | 03/25/2019 1:00 pm | 5
24 | 03/25/2019 1:05 pm | 03/25/2019 2:21 pm | 0
You can use such a logic by the contribution of lead window analytic function
with tab(eid, type, dates ) as
(
select 24,'IN' ,timestamp'2019-03-25 06:45:00' from dual union all
select 24,'OUT',timestamp'2019-03-25 08:05:00' from dual union all
select 24,'IN' ,timestamp'2019-03-25 08:06:00' from dual union all
select 24,'IN' ,timestamp'2019-03-25 08:28:00' from dual union all
select 24,'OUT',timestamp'2019-03-25 09:48:00' from dual union all
select 24,'IN' ,timestamp'2019-03-25 09:52:00' from dual
)
select t1.eid, t1.dates as timein, t2.dates as timeout,
nvl(to_number(regexp_substr(to_char(t1.ld_dates - t2.dates),'[^:]+',1,2)),0)
as diff_in_minutes
from ( select lead(dates) over (order by dates) as ld_dates, t.*
from tab t
where type = 'IN' order by dates) t1
full join ( select * from tab where type = 'OUT' order by dates) t2
on t1.dates <= t2.dates and ld_dates > t2.dates
order by t1.dates;
EID TIMEIN TIMEOUT DIFF_IN_MINUTES
24 25.03.2019 06:45:00 25.03.2019 08:05:00 1
24 25.03.2019 08:06:00 NULL 0
24 25.03.2019 08:28:00 25.03.2019 09:48:00 4
24 25.03.2019 09:52:00 NULL 0
Demo
You can do this with the following logic.
You can get all the ins using a lead() query. Then you can get the unmatched outs using a lag():
select t.eid, date as timein,
(case when next_type = 'OUT' then next_date end) as timeout,
((case when next_type = 'OUT' then next_date end) - date) * (24 * 60) as diff_in_minutes
from (select t.*,
lead(type) over (partition by eid order by date) as next_type,
lead(type) over (partition by eid order by date) as next_date
from t
) t
where type = 'IN'
union all
select t.eid, null as timein,
date as timeout, null as diff_in_minutes
from (select t.*,
lag(type) over (partition by eid order by date) as prev_type,
lag(date) over (partition by eid order by date) as prev_date
from t
) t
where type = 'OUT' and (prev_type <> 'IN' or prev_type is null);
Here is a db<>fiddle with all your data, showing that it supports the multiple INs and OUTs.
Note this assumes that the date/time column is really a date. It only converts to a timestamp to show the time component in the result set.

Split week based on weightage

I have a weights defined like below in a table.
DayNum | Day | Weight | Cumulative Weight
1 | MON | 0.3 | 0.3
2 | TUE | 0.15 | 0.45 (Sum of Mon and Tues)
3 | WED | 0.1 | 0.55 (Sum of Mon and Tues and Wed)
4 | THU | 0.1 | 0.65
5 | FRI | 0.15 | 0.8
6 | SAT | 0.2 | 1
And I have amounts in another table defined at weekly level (Mon - Sun) like below.
Item | Date | Amount
A | 30-May-16 | 10 ---- Week in May and June
A | 6-Jun-16 | 20
A | 13-Jun-16 | 30 and so on
A | 27-Jun-16 | 60 ---- Week in Jun and July
Now I want to insert into another table at a daily level, for the weeks which are overlapping between 2 different months (in above example - 30 May to 5 Jun).
Can anyone explain how I can achieve this in Oracle.
Output should be like below.
Item | Date | Amount
A | 30-May-16 | 4.5 (2 days from May which are Mon and Tues - so calculation is 10 * 0.45)
A | 1-Jun-16 | 5.5 (5 days from May which is the rest of the week - 10 minus 4.5)
A | 6-Jun-16 | 20 and so on
A | 27-Jun-16 | 39 (4 days from June which are Mon till Thurs - so calculation is 60 * 0.65)
A | 1-Jul-16 | 21 (3 days from July which is the rest of the week - 60 minus 39)
Try:
WITH some_data AS(
select a.*,
trunc( trunc( add_months( "DATE", 1 ), 'MM' ) - "DATE" )
As days_to_end_of_month,
trunc( add_months( "DATE", 1 ), 'MM' )
As start_of_next_month
from amounts a
), some_other_data AS (
SELECT some_data.*,
CASE WHEN days_to_end_of_month >= 6 THEN Amount
ELSE ( SELECT some_data.amount * "Cumulative Weight" FROM Weights w
WHERE some_data.days_to_end_of_month = DayNum )
END as new_Amount
FROM some_data
)
SELECT Item, "DATE", New_Amount as amount
FROM some_other_data
UNION ALL
SELECT Item, start_of_next_month, amount-new_amount
FROM some_other_data
WHERE days_to_end_of_month < 6
ORDER BY "DATE"
;
Oracle Setup:
CREATE TABLE Weights ( DayNum, Day, Weight ) AS
SELECT 1, 'MON', 0.3 FROM DUAL UNION ALL
SELECT 2, 'TUE', 0.15 FROM DUAL UNION ALL
SELECT 3, 'WED', 0.1 FROM DUAL UNION ALL
SELECT 4, 'THU', 0.1 FROM DUAL UNION ALL
SELECT 5, 'FRI', 0.15 FROM DUAL UNION ALL
SELECT 6, 'SAT', 0.2 FROM DUAL;
CREATE TABLE weekly_levels ( Item, "Date", Amount ) AS
SELECT 'A', DATE '2016-05-30', 10 FROM DUAL UNION ALL
SELECT 'A', DATE '2016-06-06', 20 FROM DUAL UNION ALL
SELECT 'A', DATE '2016-06-13', 30 FROM DUAL UNION ALL
SELECT 'A', DATE '2016-06-27', 60 FROM DUAL;
Query:
SELECT item,
start_date,
SUM( amount * weight ) AS amount
FROM (
SELECT item,
"Date" AS start_date,
LEAST( "Date" + INTERVAL '6' DAY, LAST_DAY( "Date" ) ) AS end_date,
amount
FROM weekly_levels
UNION
SELECT item,
GREATEST( "Date", TRUNC( "Date" + INTERVAL '6' DAY, 'MM' ) ) AS start_date,
"Date" + INTERVAL '6' DAY AS end_date,
amount
FROM weekly_levels
) d
INNER JOIN
Weights w
ON ( w.DayNum BETWEEN TO_CHAR( start_date, 'D' )
AND TO_CHAR( end_date, 'D' ) )
GROUP BY item, start_date
ORDER BY item, start_date;
Output:
ITEM START_DATE AMOUNT
---- ------------------- ----------
A 2016-05-30 00:00:00 4.5
A 2016-06-01 00:00:00 5.5
A 2016-06-06 00:00:00 20
A 2016-06-13 00:00:00 30
A 2016-06-27 00:00:00 39
A 2016-07-01 00:00:00 21

Oracle Event Count Query

My SAMPLE table has the following five columns:
sample_id (PK) (NUMBER)
sampled_on (DATE)
received_on (DATE)
completed_on (DATE)
authorized_on (DATE)
I would like a query with one row per hour (constrained by a given date range) and five columns:
The hour YYYY-MM-DD HH24
Number of samples sampled during that hour
Number of samples received during that hour
Number of samples completed during that hour
Number of samples authorized during that hour
Please provide a query or at least a point in the right direction.
Reopened with bounty:
+300 reputation for the first person to incorporate Rob van Wijk's answer (single access to sample) into a view where I can efficiently query by date range (start_date/end_date or start_date/num_days).
Try:
CREATE OR REPLACE VIEW my_view AS
WITH date_bookends AS (
SELECT LEAST(MIN(t.sampled_on), MIN(t.received_on), MIN(t.completed_on), MIN(t.authorized_on)) 'min_date'
GREATEST(MAX(t.sampled_on), MAX(t.received_on), MAX(t.completed_on), MAX(t.authorized_on)) 'max_date'
FROM SAMPLE t),
all_hours AS (
SELECT t.min_date + numtodsinterval(LEVEL - 1,'hour') date_by_hour
FROM date_bookends t
CONNECT BY LEVEL <= ( t.max_date - t.min_date + 1) * 24)
SELECT h.date_by_hour,
COUNT(CASE WHEN h.hour = TRUNC(s.sampled_on,'hh24') THEN 1 END) sampled#
COUNT(CASE WHEN h.hour = TRUNC(s.received_on,'hh24') THEN 1 END) received#
COUNT(CASE WHEN h.hour = TRUNC(s.completed_on,'hh24') THEN 1 END) completed#
COUNT(CASE WHEN h.hour = TRUNC(s.authorized_on,'hh24') THEN 1 END) authorized#
FROM all_hours h
CROSS JOIN sample s
GROUP BY h.hour
Without using Subquery Factoring:
CREATE OR REPLACE VIEW my_view AS
SELECT h.date_by_hour,
COUNT(CASE WHEN h.hour = TRUNC(s.sampled_on,'hh24') THEN 1 END) sampled#
COUNT(CASE WHEN h.hour = TRUNC(s.received_on,'hh24') THEN 1 END) received#
COUNT(CASE WHEN h.hour = TRUNC(s.completed_on,'hh24') THEN 1 END) completed#
COUNT(CASE WHEN h.hour = TRUNC(s.authorized_on,'hh24') THEN 1 END) authorized#
FROM (SELECT t.min_date + numtodsinterval(LEVEL - 1,'hour') date_by_hour
FROM (SELECT LEAST(MIN(t.sampled_on), MIN(t.received_on), MIN(t.completed_on), MIN(t.authorized_on)) 'min_date'
GREATEST(MAX(t.sampled_on), MAX(t.received_on), MAX(t.completed_on), MAX(t.authorized_on)) 'max_date'
FROM SAMPLE t) t
CONNECT BY LEVEL <= ( t.max_date - t.min_date + 1) * 24) h
CROSS JOIN sample s
GROUP BY h.hour
The query accesses the SAMPLES table twice - the first time to get the earliest & latest date to frame the construction of the date_by_hour value.
This may not be the prettiest or most optimal solution, but it seems to work. Explanation: first convert all the dates to YYYY-MM-DD HH24 format, next gather number sampled/received/completed/authorized by date+HH24, finally join together.
with sample_hour as
(select sample_id,
to_char(sampled_on, 'YYYY-MM-DD HH24') sampled_on,
to_char(received_on, 'YYYY-MM-DD HH24') received_on,
to_char(completed_on, 'YYYY-MM-DD HH24') completed_on,
to_char(authorized_on, 'YYYY-MM-DD HH24') authorized_on
from sample),
s as
(select sampled_on thedate, count(*) num_sampled
from sample_hour
group by sampled_on),
r as
(select received_on thedate, count(*) num_received
from sample_hour
group by received_on),
c as
(select completed_on thedate, count(*) num_completed
from sample_hour
group by completed_on),
a as
(select authorized_on thedate, count(*) num_authorized
from sample_hour
group by authorized_on)
select s.thedate, num_sampled, num_received, num_completed, num_authorized
from s
left join r on s.thedate = r.thedate
left join c on s.thedate = c.thedate
left join a on s.thedate = a.thedate
;
This assumes a table "sample" created something like this:
create table sample
(sample_id number not null primary key,
sampled_on date,
received_on date,
completed_on date,
authorized_on date);
Here is an example. First create the table and insert some random data.
SQL> create table sample
2 ( sample_id number primary key
3 , sampled_on date
4 , received_on date
5 , completed_on date
6 , authorized_on date
7 )
8 /
Tabel is aangemaakt.
SQL> insert into sample
2 select level
3 , trunc(sysdate) + dbms_random.value(0,2)
4 , trunc(sysdate) + dbms_random.value(0,2)
5 , trunc(sysdate) + dbms_random.value(0,2)
6 , trunc(sysdate) + dbms_random.value(0,2)
7 from dual
8 connect by level <= 1000
9 /
1000 rijen zijn aangemaakt.
Then introduce the variables for your given date range and fill them.
SQL> var DATE_RANGE_START varchar2(10)
SQL> var DATE_RANGE_END varchar2(10)
SQL> exec :DATE_RANGE_START := '2009-10-23'
PL/SQL-procedure is geslaagd.
SQL> exec :DATE_RANGE_END := '2009-10-24'
PL/SQL-procedure is geslaagd.
First you'll have to generate all hours in your given date range. This makes sure that in case you have an hour where no dates are present, you'll still have a record with 4 zeros. The implementation is in the all_hours query. The rest of the query (with only one table access to your sample table!) can then be quite simple like this.
SQL> with all_hours as
2 ( select to_date(:DATE_RANGE_START,'yyyy-mm-dd') + numtodsinterval(level-1,'hour') hour
3 from dual
4 connect by level <=
5 ( to_date(:DATE_RANGE_END,'yyyy-mm-dd')
6 - to_date(:DATE_RANGE_START,'yyyy-mm-dd')
7 + 1
8 ) * 24
9 )
10 select h.hour
11 , count(case when h.hour = trunc(s.sampled_on,'hh24') then 1 end) sampled#
12 , count(case when h.hour = trunc(s.received_on,'hh24') then 1 end) received#
13 , count(case when h.hour = trunc(s.completed_on,'hh24') then 1 end) completed#
14 , count(case when h.hour = trunc(s.authorized_on,'hh24') then 1 end) authorized#
15 from all_hours h
16 cross join sample s
17 group by h.hour
18 /
HOUR SAMPLED# RECEIVED# COMPLETED# AUTHORIZED#
------------------- ---------- ---------- ---------- -----------
23-10-2009 00:00:00 18 25 20 20
23-10-2009 01:00:00 26 24 16 13
23-10-2009 02:00:00 16 26 17 15
23-10-2009 03:00:00 19 18 27 13
23-10-2009 04:00:00 28 20 18 23
23-10-2009 05:00:00 17 13 19 21
23-10-2009 06:00:00 18 23 16 15
23-10-2009 07:00:00 19 24 14 22
23-10-2009 08:00:00 21 19 23 22
23-10-2009 09:00:00 25 20 23 24
23-10-2009 10:00:00 16 21 25 18
23-10-2009 11:00:00 21 29 21 18
23-10-2009 12:00:00 33 28 24 20
23-10-2009 13:00:00 24 19 15 15
23-10-2009 14:00:00 20 27 16 25
23-10-2009 15:00:00 15 25 27 13
23-10-2009 16:00:00 19 14 27 18
23-10-2009 17:00:00 22 22 15 27
23-10-2009 18:00:00 20 19 29 23
23-10-2009 19:00:00 20 18 17 23
23-10-2009 20:00:00 11 18 20 27
23-10-2009 21:00:00 13 25 24 19
23-10-2009 22:00:00 22 13 22 29
23-10-2009 23:00:00 20 20 19 24
24-10-2009 00:00:00 18 17 18 29
24-10-2009 01:00:00 23 30 26 21
24-10-2009 02:00:00 28 19 28 25
24-10-2009 03:00:00 21 21 11 23
24-10-2009 04:00:00 23 20 21 17
24-10-2009 05:00:00 24 16 23 23
24-10-2009 06:00:00 23 26 22 30
24-10-2009 07:00:00 25 26 18 12
24-10-2009 08:00:00 24 20 23 17
24-10-2009 09:00:00 18 26 15 19
24-10-2009 10:00:00 20 19 25 18
24-10-2009 11:00:00 19 27 17 20
24-10-2009 12:00:00 23 16 18 20
24-10-2009 13:00:00 15 15 22 19
24-10-2009 14:00:00 23 23 16 29
24-10-2009 15:00:00 18 31 32 28
24-10-2009 16:00:00 22 15 18 13
24-10-2009 17:00:00 25 17 20 26
24-10-2009 18:00:00 19 20 21 16
24-10-2009 19:00:00 22 13 28 29
24-10-2009 20:00:00 23 17 23 14
24-10-2009 21:00:00 18 18 21 22
24-10-2009 22:00:00 22 20 18 21
24-10-2009 23:00:00 21 18 22 22
48 rijen zijn geselecteerd.
Hope this helps.
Regards,
Rob.
I'd do are 4 queries like this (one for each date):
SELECT <date to hour>, count(*) FROM sample GROUP BY <date to hour>
And then put the data together in the application. If you really want a single query, you can join the individual queries on hour.
Try this...
WITH src_data AS
( SELECT sample_id
, TRUNC( sampled_on, 'HH24' ) sampled_on
, TRUNC( received_on, 'HH24' ) received_on
, TRUNC( completed_on, 'HH24' ) completed_on
, TRUNC( authorized_on, 'HH24' ) authorized_on
FROM sample
)
, src_hours AS
( SELECT sampled_on the_date
FROM src_data
WHERE sampled_on IS NOT NULL
UNION
SELECT received_on the_date
FROM src_data
WHERE received_on IS NOT NULL
UNION
SELECT completed_on the_date
FROM src_data
WHERE completed_on IS NOT NULL
UNION
SELECT authorized_on the_date
FROM src_data
WHERE authorized_on IS NOT NULL
)
SELECT h.the_date
, ( SELECT COUNT(*)
FROM src_data s
WHERE s.sampled_on = h.the_date ) num_sampled_on
, ( SELECT COUNT(*)
FROM src_data r
WHERE r.received_on = h.the_date ) num_received_on
, ( SELECT COUNT(*)
FROM src_data c
WHERE c.completed_on = h.the_date ) num_completed_on
, ( SELECT COUNT(*)
FROM src_data a
WHERE a.authorized_on = h.the_date ) num_authorized_on
FROM src_hours h
Maybe somthing like creating this view:
create view hours as
select hour, max(cnt_sample) cnt_sample, max(cnt_received) cnt_received, max(cnt_completed) cnt_completed, max(cnt_authorized) cnt_authorized
from (
select to_char(sampled_on , 'yyyymmddhh24') hour,
count(sample_id) over (partition by to_char(sampled_on ,'yyyymmddhh24')) cnt_sample,
0 cnt_received,
0 cnt_completed,
0 cnt_authorized from sample union all
select to_char(received_on , 'yyyymmddhh24') hour,
0 cnt_sample,
count(sample_id) over (partition by to_char(received_on ,'yyyymmddhh24')) cnt_received,
0 cnt_completed,
0 cnt_authorized from sample union all
select to_char(completed_on , 'yyyymmddhh24') hour,
0 cnt_sample,
0 cnt_received,
count(sample_id) over (partition by to_char(completed_on ,'yyyymmddhh24')) cnt_completed,
0 cnt_authorized from sample union all
select to_char(authorized_on, 'yyyymmddhh24') hour,
0 cnt_sample,
0 cnt_received,
0 cnt_completed,
count(sample_id) over (partition by to_char(authorized_on ,'yyyymmddhh24')) cnt_authorized from sample
)
group by hour
;
and then selecting from the view:
select * from hours where hour >= '2001010102' and hour <= '2001010105'
order by hour;
I now propose:
create view hours_ as
with four as (
select 1 as n from dual union all
select 2 as n from dual union all
select 3 as n from dual union all
select 4 as n from dual )
select
case when four.n = 1 then trunc(sampled_on , 'hh24')
when four.n = 2 then trunc(received_on , 'hh24')
when four.n = 3 then trunc(completed_on , 'hh24')
when four.n = 4 then trunc(authorized_on, 'hh24')
end hour_,
sum ( case when four.n = 1 then 1
else 0
end ) sample_,
sum ( case when four.n = 2 then 1
else 0
end ) receive_,
sum ( case when four.n = 3 then 1
else 0
end ) complete_,
sum ( case when four.n = 4 then 1
else 0
end ) authorize_
from
four cross join sample
group by
case when four.n = 1 then trunc(sampled_on , 'hh24')
when four.n = 2 then trunc(received_on , 'hh24')
when four.n = 3 then trunc(completed_on , 'hh24')
when four.n = 4 then trunc(authorized_on, 'hh24')
end ;
In order to see if the view is indeed accessed only once:
explain plan for select * from hours_
where hour_ between sysdate -1 and sysdate;
select * from table (dbms_xplan.display);
Which results in:
--------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 61 | 16 (7)| 00:00:01 |
| 1 | VIEW | HOURS_ | 1 | 61 | 16 (7)| 00:00:01 |
| 2 | HASH GROUP BY | | 1 | 39 | 16 (7)| 00:00:01 |
|* 3 | FILTER | | | | | |
| 4 | NESTED LOOPS | | 1 | 39 | 15 (0)| 00:00:01 |
| 5 | VIEW | | 4 | 12 | 8 (0)| 00:00:01 |
| 6 | UNION-ALL | | | | | |
| 7 | FAST DUAL | | 1 | | 2 (0)| 00:00:01 |
| 8 | FAST DUAL | | 1 | | 2 (0)| 00:00:01 |
| 9 | FAST DUAL | | 1 | | 2 (0)| 00:00:01 |
| 10 | FAST DUAL | | 1 | | 2 (0)| 00:00:01 |
|* 11 | TABLE ACCESS FULL| SAMPLE | 1 | 36 | 2 (0)| 00:00:01 |
--------------------------------------------------------------------------------
Here's what I'm thinking, but I'm not sure it's optimal enough for a view.
select
the_date,
sum(decode(the_type,'S',the_count,0)) samples,
sum(decode(the_type,'R',the_count,0)) receipts,
sum(decode(the_type,'C',the_count,0)) completions,
sum(decode(the_type,'A',the_count,0)) authorizations
from(
select
trunc(sampled_on,'HH24') the_date,
'S' the_type,
count(1) the_count
FROM sample
group by trunc(sampled_on,'HH24')
union all
select
trunc(received_on,'HH24'),
'R',
count(1)
FROM sample
group by trunc(received_on,'HH24')
union all
select
trunc(completed_on,'HH24'),
'C',
count(1)
FROM sample
group by trunc(completed_on,'HH24')
union all
select
trunc(authorized_on,'HH24'),
'A',
count(1)
FROM sample
group by trunc(authorized_on,'HH24')
)
group by the_date
Then, to query, you could just query with normal date contructs:
select * from magic_view where the_date > sysdate-1;
EDIT
Okay, so I created a sample table and did some metrics:
create table sample (
sample_id number primary key,
sampled_on date,
received_on date,
completed_on date,
authorized_on date
);
insert into sample (
select
level,
trunc(sysdate) + dbms_random.value(0,2),
trunc(sysdate) + dbms_random.value(0,2),
trunc(sysdate) + dbms_random.value(0,2),
trunc(sysdate) + dbms_random.value(0,2),
from dual
connect by level <= 1000
);
The explain plan is:
---------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)|
---------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 4000 | 97K| 25 (20)|
| 1 | HASH GROUP BY | | 4000 | 97K| 25 (20)|
| 2 | VIEW | | 4000 | 97K| 24 (17)|
| 3 | UNION-ALL | | | | |
| 4 | HASH GROUP BY | | 1000 | 9000 | 6 (17)|
| 5 | TABLE ACCESS FULL| SAMPLE | 1000 | 9000 | 5 (0)|
| 6 | HASH GROUP BY | | 1000 | 9000 | 6 (17)|
| 7 | TABLE ACCESS FULL| SAMPLE | 1000 | 9000 | 5 (0)|
| 8 | HASH GROUP BY | | 1000 | 9000 | 6 (17)|
| 9 | TABLE ACCESS FULL| SAMPLE | 1000 | 9000 | 5 (0)|
| 10 | HASH GROUP BY | | 1000 | 9000 | 6 (17)|
| 11 | TABLE ACCESS FULL| SAMPLE | 1000 | 9000 | 5 (0)|
---------------------------------------------------------------------
On my machine, the a query against this view for the past 24 hours completes in 23ms. Not bad, but it's only 1,000 rows. Before you discount the 4 separate queries, you'll need to do performance analysis of the individual solutions.
Similar to René Nyffenegger's idea. Filter by each type of date field, and then amalgamate the counts.
Note, that it's not possible to do this query in one Select, because you need to both Group and Order By each date field, this is impossible without splitting into separate sub-queries.
I have coded a date range of '2009-11-04' to '2009-11-04 23:59:59' for this example:
SELECT
DateHour,
SUM(sampled) total_sampled,
SUM(received) total_received,
SUM(completed) total_completed,
SUM(authorized) total_authorized
FROM
(SELECT
TO_CHAR(CREATED_DATE, 'YYYY-MM-DD HH24') DateHour,
1 sampled,
0 received,
0 completed,
0 authorized
FROM
SAMPLE
WHERE
sampled_on >= TO_DATE('2009-11-04', 'YYYY-MM-DD')
AND sampled_on <= TO_DATE('2009-11-04 23:59:59', 'YYYY-MM-DD HH24:MI:SS')
UNION ALL
SELECT
TO_CHAR(CREATED_DATE, 'YYYY-MM-DD HH24') DateHour,
0 sampled,
1 received,
0 completed,
0 authorized
FROM
SAMPLE
WHERE
received_on >= TO_DATE('2009-11-04', 'YYYY-MM-DD')
AND received_on <= TO_DATE('2009-11-04 23:59:59', 'YYYY-MM-DD HH24:MI:SS')
UNION ALL
SELECT
TO_CHAR(CREATED_DATE, 'YYYY-MM-DD HH24') DateHour,
0 sampled,
0 received,
1 completed,
0 authorized
FROM
SAMPLE
WHERE
completed_on >= TO_DATE('2009-11-04', 'YYYY-MM-DD')
AND completed_on <= TO_DATE('2009-11-04 23:59:59', 'YYYY-MM-DD HH24:MI:SS')
UNION ALL
SELECT
TO_CHAR(CREATED_DATE, 'YYYY-MM-DD HH24') DateHour,
0 sampled,
0 received,
0 completed,
1 authorized
FROM
SAMPLE
WHERE
authorized_on >= TO_DATE('2009-11-04', 'YYYY-MM-DD')
AND authorized_on <= TO_DATE('2009-11-04 23:59:59', 'YYYY-MM-DD HH24:MI:SS'))
GROUP BY
DateHour
ORDER BY
DateHour