Compute average of data between time period in Oracle sql - sql

I have the following table named IMETERDATA:
DEVNAME VARCHAR2(25)
DEVID VARCHAR2(8)
USEDATE TIMESTAMP(6)
INSTANTPOWER NUMBER(3,0)
TOTALENERGY NUMBER(7,4)
ROWNUMBER NUMBER(4,0)
I want to compute and show average of Totalenergy on 2-hour interval. For instance, if I have a data for specific date (e.g. Nov 22, 2016), I want to calculate average of Totalenergy for period of: 12am-2am, 2am-4am .... 10pm-12pm. I want to calculate the average for all dates with specified interval. What I have done so far:
select to_number(to_char(USEDATE, 'HH24')) as "HOUR",
avg(TOTALENERGY) as "AVERAGE", TRUNC(USEDATE, 'DD') as "DATE"
from IMETERDATA
WHERE TRUNC(USEDATE) in (select DISTINCT(TRUNC(USEDATE, 'DD'))
from IMETERDATA) and (to_number(to_char(USEDATE, 'HH24')) >= 12 and to_number(to_char(USEDATE, 'HH24')) < 14 )
group by to_number(to_char(USEDATE, 'HH24')), TRUNC(USEDATE, 'DD');
This query gives only average from 12pm to 2pm.How can I calculate for 24 hours? I want result from 12am to 11.59pm with interval of 2 hours:
12AM - 2AM ---> 58.50
2AM - 4AM ----> 60.35
...
10PM - 11.59PM --> 40.35

Hmm. Can I do it without creating another table?
Sure. Use a query like below (this example generates 2-hour periods for 2 days):
ALTER SESSION SET NLS_DATE_FORMAT = 'yyyy-mm-dd hh24:mi'
;
SELECT date '2016-11-22' + NUMTODSINTERVAL( 2 * (level - 1), 'HOUR' ) as period_start
FROM dual
CONNECT BY LEVEL <= 2 * 12 ; -- 2 days of 12 "two hours" periods
PERIOD_START
----------------
2016-11-22 00:00
2016-11-22 02:00
2016-11-22 04:00
2016-11-22 06:00
2016-11-22 08:00
2016-11-22 10:00
2016-11-22 12:00
2016-11-22 14:00
2016-11-22 16:00
2016-11-22 18:00
2016-11-22 20:00
2016-11-22 22:00
2016-11-23 00:00
2016-11-23 02:00
2016-11-23 04:00
2016-11-23 06:00
2016-11-23 08:00
2016-11-23 10:00
2016-11-23 12:00
2016-11-23 14:00
2016-11-23 16:00
2016-11-23 18:00
2016-11-23 20:00
2016-11-23 22:00
24 rows selected
And then join a result of the above query to your table and calculate averages
SELECT x.PERIOD_START,
AVG( i.TOTALENERGY )
FROM (
the_above_query
) x
JOIN IMETERDATA i
ON i.USEDATE >= x.PERIOD_START AND i.USEDATE < x.PERIOD_START + interval '2' hour
GROUP BY x.PERIOD_START

Have a look at Analytic Functions: windowing_clause, there you can do it straight forward.
select USEDATE,
AVG(TOTALENERGY) OVER (ORDER BY USEDATE RANGE BETWEEN INTERVAL '1' HOUR PRECEDING AND INTERVAL '1' HOUR FOLLOWING) as AVERAGE,
MIN(USEDATE) OVER (ORDER BY USEDATE RANGE BETWEEN INTERVAL '1' HOUR PRECEDING AND INTERVAL '1' HOUR FOLLOWING) as INTERVAL_START,
MAXUSEDATE) OVER (ORDER BY USEDATE RANGE BETWEEN INTERVAL '1' HOUR PRECEDING AND INTERVAL '1' HOUR FOLLOWING) as INTERVAL_END
from IMETERDATA;
This query gives you all averages for each time +/- 1 hour (i.e. 2 hours).
If you need just the times of given hours you can use
with t as
(select USEDATE,
AVG(TOTALENERGY) OVER (ORDER BY USEDATE RANGE BETWEEN INTERVAL '1' HOUR PRECEDING AND INTERVAL '1' HOUR FOLLOWING) as AVERAGE,
MIN(USEDATE) OVER (ORDER BY USEDATE RANGE BETWEEN INTERVAL '1' HOUR PRECEDING AND INTERVAL '1' HOUR FOLLOWING) as INTERVAL_START,
MAXUSEDATE) OVER (ORDER BY USEDATE RANGE BETWEEN INTERVAL '1' HOUR PRECEDING AND INTERVAL '1' HOUR FOLLOWING) as INTERVAL_END
from IMETERDATA)
select *
from t
where USEDATE = TRUNC(USEDATE, 'HH')
AND EXTRACT(HOUR FROM USEDATE) IN (1,3,5,7,...);
Instead of EXTRACT(HOUR FROM USEDATE) IN (1,3,5,7,...) you could also use MOD(EXTRACT(HOUR FROM USEDATE), 1) = 1
Maybe this is not 100% what you are looking for (your question is not so clear in that regards) but I assume you get an idea how to use it.

Related

Split row data base on timestamp SQL Oracle

Good day everyone. I have a table as below. Duration is the time from current state to next state.
Timestamp
State
Duration(minutes)
10/9/2022 8:50:00 AM
A
35
10/9/2022 9:25:00 AM
B
10
10/9/2022 9:35:00 AM
C
...
How do I split data at 9:00 AM of each day like below:
Timestamp
State
Duration(minutes)
10/9/2022 8:50:00 AM
A
10
10/9/2022 9:00:00 AM
A
25
10/9/2022 9:25:00 AM
B
10
10/9/2022 9:35:00 AM
C
...
Thank you.
Use a row-generator function to generate extra rows when the timestamp is before 09:00 and the next timestamp is after 09:00 (and calculate the diff value rather than storing it in the table):
SELECT l.ts AS timestamp,
t.state,
ROUND((l.next_ts - l.ts) * 24 * 60, 2) As diff
FROM (
SELECT timestamp,
LEAD(timestamp) OVER (ORDER BY timestamp) AS next_timestamp,
state
FROM table_name
) t
CROSS APPLY (
SELECT GREATEST(
t.timestamp,
TRUNC(t.timestamp - INTERVAL '9' HOUR) + INTERVAL '9' HOUR + LEVEL - 1
) AS ts,
LEAST(
t.next_timestamp,
TRUNC(t.timestamp - INTERVAL '9' HOUR) + INTERVAL '9' HOUR + LEVEL
) AS next_ts
FROM DUAL
CONNECT BY
TRUNC(t.timestamp - INTERVAL '9' HOUR) + INTERVAL '9' HOUR + LEVEL - 1 < t.next_timestamp
) l;
Which, for your sample data:
CREATE TABLE table_name (Timestamp, State) AS
SELECT DATE '2022-10-09' + INTERVAL '08:50' HOUR TO MINUTE, 'A' FROM DUAL UNION ALL
SELECT DATE '2022-10-09' + INTERVAL '09:25' HOUR TO MINUTE, 'B' FROM DUAL UNION ALL
SELECT DATE '2022-10-09' + INTERVAL '09:35' HOUR TO MINUTE, 'C' FROM DUAL UNION ALL
SELECT DATE '2022-10-12' + INTERVAL '09:35' HOUR TO MINUTE, 'D' FROM DUAL;
Outputs:
TIMESTAMP
STATE
DIFF
2022-10-09 08:50:00
A
10
2022-10-09 09:00:00
A
25
2022-10-09 09:25:00
B
10
2022-10-09 09:35:00
C
1405
2022-10-10 09:00:00
C
1440
2022-10-11 09:00:00
C
1440
2022-10-12 09:00:00
C
35
2022-10-12 09:35:00
D
null
fiddle

Selecting Dates from Monday to Saturday and Hours Between Morning 8:00:00 AM To Next Day Morning 7:00:00 AM

I am trying to work on a query where there is date selection in the where clause i.e. if sysdate is Monday I have to get the dates from Monday to Saturday and Hours Between Morning 08:00:00 AM to Next Day Morning 07:00:00 AM. I am hardcoding the dates and Hours in the where clause, When I run the query data does not show.
Query:
SELECT TO_CHAR(sysdate, 'HH24:MI:SS'), REPLACE(TO_CHAR(sysdate, 'DAY'), ' ')
FROM dual
WHERE TO_CHAR(sysdate, 'HH24:MI:SS') BETWEEN '08:01:00' AND '08:00:00'
AND TO_CHAR(sysdate, 'DAY') >= 'MONDAY'
AND TO_CHAR(sysdate, 'DAY') <= 'SATURDAY';
You need to filter the wider range first (from 8 AM at Monday till the end of a Saturday, if I understood correctly) and then exclude time from 7 AM till 8 AM.
In the below code iw format element stands for ISO week, that starts on the Monday.
with a as (
select
date '2022-04-17'
+ interval '01:30:00' hour to second
+ interval '2' hour * level
as dt
from dual
connect by level < 90
)
select
to_char(dt, 'yyyymmdd') as day_
, listagg(to_char(dt, 'hh24:mi'), ',')
within group (order by dt asc) as hours
from a
where 1 = 1
/*From Mon 08 AM*/
and dt > trunc(dt, 'iw') +
interval '8' hour
/*Till Sat end of the day*/
and dt < trunc(dt, 'iw') + 6
/*and except minutes between 7 and 8 AM*/
and not (
to_char(dt, 'hh24mi') < '0800'
and to_char(dt, 'hh24mi') > '0700'
)
group by to_char(dt, 'yyyymmdd')
DAY_ | HOURS
:------- | :----------------------------------------------------------------
20220418 | 09:30,11:30,13:30,15:30,17:30,19:30,21:30,23:30
20220419 | 01:30,03:30,05:30,09:30,11:30,13:30,15:30,17:30,19:30,21:30,23:30
20220420 | 01:30,03:30,05:30,09:30,11:30,13:30,15:30,17:30,19:30,21:30,23:30
20220421 | 01:30,03:30,05:30,09:30,11:30,13:30,15:30,17:30,19:30,21:30,23:30
20220422 | 01:30,03:30,05:30,09:30,11:30,13:30,15:30,17:30,19:30,21:30,23:30
20220423 | 01:30,03:30,05:30,09:30,11:30,13:30,15:30,17:30,19:30,21:30,23:30
db<>fiddle here
(And what if sysdate isn't Monday?)
Therefore, could you explain a little bit better what is the input (dates? One date? SYSDATE?) and what is desired output (related to that input).
Basically, I don't understand what you want. Meanwhile, errors you made (if it'll help).
Format model is wrong; this is what you did:
SQL> select to_char(sysdate, 'DAY') day, length(to_char(sysdate, 'DAY')) len from dual;
DAY LEN
--------- ----------
FRIDAY 9
"FRIDAY" doesn't have 9 characters; it has 6 of them --> use the fm format modifier (it'll truncate trailing spaces):
SQL> select to_char(sysdate, 'fmDAY') day, length(to_char(sysdate, 'fmDAY')) len from dual;
DAY LEN
--------- ----------
FRIDAY 6
SQL>
Today (22.04.2022) is Friday. Your query searches for data whose day is between "MONDAY" and "SATURDAY". As you're comparing strings and alphabet goes as [A, B, ..., F, G, ..., M, N, ..., S, T], "F(riday)" is NEVER between M(onday) and S(aturday) so there's zero chance that it'll work.
As of hours: which time exactly is between 08:01 and 08:00? Time doesn't go backwards (unless you meant "08:01 today and 08:00 tomorrow").
if sysdate is Monday I have to get the dates from Monday to Saturday and Hours Between Morning 08:00:00 AM to Next Day Morning 07:00:00 AM.
You can find whether SYSDATE is Monday by comparing the day to the start of the ISO week (which will always be midnight on Monday):
SELECT *
FROM DUAL
WHERE SYSDATE - TRUNC(SYSDATE, 'IW') < 1
You can find out whether the hours are between 08:00 and 07:00 the next day by subtracting 8 hours and finding out whether the time is between 00:00 and 23:00:
SELECT *
FROM DUAL
WHERE (SYSDATE - INTERVAL '8' HOUR) - TRUNC(SYSDATE - INTERVAL '8' HOUR)) DAY TO SECOND
<= INTERVAL '23' HOUR;
You can combine the two to find out if the day is between Monday and Saturday and the time is between 08:00 and 07:00 on the next day (so for Saturday, it would include 7 hours of Sunday) using:
SELECT *
FROM DUAL
WHERE (SYSDATE - INTERVAL '8' HOUR) - TRUNC(SYSDATE - INTERVAL '8' HOUR), 'IW') < 6
AND (SYSDATE - INTERVAL '8' HOUR) - TRUNC(SYSDATE - INTERVAL '8' HOUR)) DAY TO SECOND
<= INTERVAL '23' HOUR;
Note: This does not use TO_CHAR so it is unaffected by any changes to the NLS_TERRITORY or NLS_DATE_LANGUAGE session parameters so it will always give the same answer (independent of the settings of the user who runs the query).
You can use such a combination
SELECT TO_CHAR(dt,'HH24:MI:SS','NLS_DATE_LANGUAGE=English') AS Hour,
TO_CHAR(dt,'Day','NLS_DATE_LANGUAGE=English') AS day
FROM t
WHERE TO_CHAR(dt,'Dy','NLS_DATE_LANGUAGE=English') IN ('Tue','Wed','Thu','Fri')
AND TO_CHAR(dt, 'HH24:MI:SS') NOT BETWEEN '07:00:01' AND '08:00:00'
OR TO_CHAR(dt,'Dy','NLS_DATE_LANGUAGE=English') = 'Mon'
AND TO_CHAR(dt, 'HH24:MI:SS')>= '07:00:00'
OR TO_CHAR(dt,'Dy','NLS_DATE_LANGUAGE=English') = 'Sat'
AND TO_CHAR(dt, 'HH24:MI:SS')<= '08:00:00'
where needs to consider restricting the periods for the bound dates individually
Demo

Alternative to this query to run under MariaDb 10.1

This query works as expected under Mysql 8, but MariaDB 10.1 is used on my server. Do you know if an alternative exists to this ? And how to achieve it ?
SELECT * FROM (
SELECT
*,
SEC_TO_TIME(SUM(TIME_TO_SEC(TIMEDIFF(hs.`ending_hour`, hs.`starting_hour`))) OVER (ORDER BY hs.starting_hour RANGE BETWEEN INTERVAL '12' HOUR PRECEDING AND INTERVAL '12' HOUR following)) AS tot
FROM
time_table hs
WHERE hs.`starting_hour` > DATE_SUB(NOW(), INTERVAL 50 DAY) AND hs.`ending_hour` <= NOW()
ORDER BY hs.`starting_hour` ASC
) t1
HAVING tot >= '14:00:00'
;
fiddle
The problem is RANGE BETWEEN INTERVAL on OVER window function doesn't exists under MariaDB at this moment.
Thank you
Sample data:
id starting_hour ending_hour
------ ------------------- ---------------------
1 2018-09-02 06:00:00 2018-09-02 08:30:00
2 2018-09-02 08:30:00 2018-09-02 10:00:00
4 2018-09-03 11:00:00 2018-09-03 15:00:00
5 2018-09-04 15:30:00 2018-09-04 16:00:00
6 2018-09-04 16:15:00 2018-09-04 17:00:00
7 2018-09-19 00:00:00 2018-09-19 03:00:00
8 2018-09-19 04:00:00 2018-09-19 15:00:00
9 2018-09-20 00:00:00 2018-09-20 22:01:00
10 2018-10-21 12:00:00 2018-10-21 11:00:00
11 2018-10-29 09:09:00 2018-10-29 10:10:00
12 2018-10-09 02:10:00 2018-10-09 14:00:00
In my use case id 7, 8 and 9 are the results.
RE-EDIT
Thanks to #Gordon Linoff answer's, this is the corrected query.
But finally doesn't work as expected. Increasing INTERVAL 50 DAY return non wanted rows that MySQL window function doesn't.
SELECT hs.*,
(
SELECT SEC_TO_TIME(SUM(TIME_TO_SEC(TIMEDIFF(hs2.ending_hour, hs2.starting_hour))))
FROM hours_sailor hs2
WHERE hs2.starting_hour >= DATE_SUB(hs.starting_hour, INTERVAL 12 HOUR) AND hs2.starting_hour <= DATE_SUB(NOW(), INTERVAL 12 HOUR)
) AS duration
FROM `time_table` hs
WHERE hs.`starting_hour` > DATE_SUB(NOW(), INTERVAL 50 DAY) AND hs.`ending_hour` <= NOW()
HAVING duration >= '14:00:00'
ORDER BY hs.starting_hour ASC;
You can express this using a correlated subquery. I think this is the equivalent logic:
SELECT hs.*,
(SELECT SEC_TO_TIME(SUM(TIME_TO_SEC(TIMEDIFF(hs2.ending_hour, hs2.starting_hour)))
FROM time_table hs2
WHERE hs2.starting_hour >= hs.starting_hour - INTERVAL '12' HOUR AND
hs2.starting_hour <= hs.starting_hour + INTERVAL '12' HOUR
) AS tot
FROM time_table hs
WHERE hs.starting_hour > DATE_SUB(NOW(), INTERVAL 50 DAY) AND
hs.ending_hour <= NOW()
HAVING tot >= '14:00:00'
ORDER BY hs.starting_hour ASC;
EDIT:
If you also want the timing restriction for the "range" calculation, you need to include it in the subquery. This filtering is built into the window function, but it is more often a hinderance than feature:
SELECT hs.*,
(SELECT SEC_TO_TIME(SUM(TIME_TO_SEC(TIMEDIFF(hs2.ending_hour, hs2.starting_hour)))
FROM time_table hs2
WHERE hs2.starting_hour >= hs.starting_hour - INTERVAL '12' HOUR AND
hs2.starting_hour <= hs.starting_hour + INTERVAL '12' HOUR AND
hs2.starting_hour > DATE_SUB(NOW(), INTERVAL 50 DAY) AND
hs2.ending_hour <= NOW()
) AS tot
FROM time_table hs
WHERE hs.starting_hour > DATE_SUB(NOW(), INTERVAL 50 DAY) AND
hs.ending_hour <= NOW()
HAVING tot >= '14:00:00'
ORDER BY hs.starting_hour ASC;

Calculate Daily Outage in Postgresql

I have for example outage Start DateTime: '2017-09-09 06:56:22' and End DateTime: '2017-09-13 14:22:45'.
Now I want to get the outage duration Daily, if the whole there was outage then give me '24:00:00' but if there was outage for just part of the day then we do subtraction ex: startTime - DayEndTime(StartTime) that is 00:00:00 the end of that start day.
So each day will be calculated for if there is outage the whole day then its 24:00:00.
How do i solve this in postgresql? Please help
see table below
StartTime EndTime OutageTime
2017-09-09 6:56:32 2017-09-10 0:00:00 17:03:28
2017-09-10 0:00:00 2017-09-11 0:00:00 24:00:00
2017-09-11 0:00:00 2017-09-12 0:00:00 24:00:00
2017-09-12 0:00:00 2017-09-13 0:00:00 24:00:00
2017-09-13 0:00:00 2017-09-13 14:22:45 14:22:45
You can just calculate the difference and replace it with 24:00:00 whenever it is a whole day:
SELECT starttime,
endtime,
CASE endtime - starttime
WHEN INTERVAL '1 day'
THEN '24:00:00'
WHEN INTERVAL '1 day 1 hour'
THEN '25:00:00'
ELSE CAST(endtime - starttime AS text)
END AS outagetime
FROM outages;
with t (startTime, endTime) as (values
('2017-09-09 6:56:32'::timestamptz,'2017-09-10 0:00:00'::timestamptz),
('2017-09-10 0:00:00','2017-09-11 0:00:00'),
('2017-09-11 0:00:00','2017-09-12 0:00:00'),
('2017-09-12 0:00:00','2017-09-13 0:00:00'),
('2017-09-13 0:00:00','2017-09-13 14:22:45'))
select startTime, endTime, upper(i) - lower(i) as a
from
(
select tstzrange(startTime, endTime) as tstzr, startTime, endTime
from t
) t
inner join (
select tstzrange(d, d + interval '1 day') as d
from
generate_series (
(select min(startTime)::date from t),
(select max(endTime) from t),
'1 day'
) gs (d)
) gs on d && tstzr
cross join lateral (
select tstzr * d as i
) cjl
;
starttime | endtime | a
------------------------+------------------------+----------
2017-09-09 06:56:32-03 | 2017-09-10 00:00:00-03 | 17:03:28
2017-09-10 00:00:00-03 | 2017-09-11 00:00:00-03 | 1 day
2017-09-11 00:00:00-03 | 2017-09-12 00:00:00-03 | 1 day
2017-09-12 00:00:00-03 | 2017-09-13 00:00:00-03 | 1 day
2017-09-13 00:00:00-03 | 2017-09-13 14:22:45-03 | 14:22:45
You can do this using generate_series(). The following is the logic for getting the results each day:
select (case when date_trunc('day', g.dte) = date_trunc('day', o.start_time)
then g.dte
else date_trunc('day', g.dte)
end),
(case when date_trunc('day', g.dte) < date_trunc('day', o.end_time)
then date_trunc('day', g.dte) + interval '1 day'
else o.end_time
end)
from outages o, lateral
generate_series(start_time, end_time, interval '1 day') g(dte)
where g.dte < o.end_time;
You can use a subquery to get the span on each day.

SQL: Create a table with 24 hourly rows

In PostreSQL, it is fairly easy to create a timestamp with date and the current hour:
vioozer=> SELECT to_char(NOW(), 'YYYY-MM-DD HH24:00');
to_char
------------------
2014-08-12 12:00
(1 row)
The previous hour can be shown using NOW()-interval '1 hour':
vioozer=> SELECT to_char(NOW()-interval '1 hour', 'YYYY-MM-DD HH24:00');
to_char
------------------
2014-08-12 11:00
(1 row)
Is there a way to easily generate a table with 24 columns for the last 24 hours, a-la:
vioozer=> SELECT MAGIC_FROM_STACK_OVERFLOW();
to_char
------------------
2014-08-12 12:00
2014-08-12 11:00
2014-08-12 10:00
...
2014-08-11 13:00
(24 row)
Use generate_series()
select i
from generate_series(current_timestamp - interval '24' hour, current_timestamp, interval '1' hour) i