Home > Back-end >  Calculate Average Daily Uptime based on Status
Calculate Average Daily Uptime based on Status

Time:07-10

I am using PostgreSQL and I have a table that tracks when a system is up or down. There are three columns: interval_date with datatype DATE, interval_time_utc with datatype VARCHAR, and status with datatype INTEGER. A status of 0 is up and 1 is down. I am trying to calculate the average uptime on a daily basis. Below is a table example.

interval_date interval_time_utc status
2022-07-08 04:00 0
2022-07-07 20:00 1
2022-07-07 04:00 0
2022-07-06 17:45 1
2022-07-06 01:15 0
2022-07-05 18:40 1

So based on the data below:
System was up from 2022-07-05 00:00 to 2022-07-05 18:39 then down from 2022-07-05 18:40 to 2022-07-05 23:59.
System was down from 2022-07-06 00:00 to 2022-07-06 01:14 then the system was up from 2022-07-06 01:15 to 2022-07-06 17:44 then the system was down from 2022-07-06 17:45 to 2022-07-06 23:59.
System was down from 2022-07-07 00:00 to 2022-07-07 03:59 then the system was up from 2022-07-07 04:00 to 2022-07-07 19:59 then the system was down from 2022-07-07 20:00 to 2022-07-07 23:59.
System was down from 2022-07-08 00:00 to 2022-07-08 03:59 then the system was up from 2022-07-08 04:00 to the current UTC time.

Link to fiddle containing schema: https://www.db-fiddle.com/f/v7RCBBNowhv7DmfBoor64X/0

I would expect the following results (assuming my calculations are correct):

interval_date average_uptime
2022-07-05 0.78
2022-07-06 0.69
2022-07-07 0.69
2022-07-08 0.83

CodePudding user response:

Assuming:

  • A current version of Postgres.

  • A UNIQUE constraint on the timestamp.

  • The leading fraction of the first day that is not covered by actual entry is the opposite status of the first entry.

  • The trailing fraction of the last day that is not covered by actual entry is the opposite status of the last entry.

  • Other than that, every row is allowed to switch on or off (redundantly).

  • The result shall contain all days between the first and last entry - including days without any change (all up or all down).

And I converted the table to a sane schema before working with it:

CREATE TABLE fixed AS
SELECT interval_date   interval_time_utc::time AS interval_ts_utc, NOT status::bool AS up
FROM   system_power_history;

Then this does the job:

SELECT the_day, round(extract('epoch' FROM sum(uptime)) / 86400, 2)
FROM  (
   -- add start interval for 1st day (if up)
   SELECT interval_ts_utc::date AS the_day, interval_ts_utc::time::interval AS uptime
   FROM  (SELECT * FROM fixed ORDER BY interval_ts_utc LIMIT 1) sub
   WHERE  NOT up

   UNION ALL
   SELECT the_day, upper(range) - lower(range) AS uptime
   FROM  (
      SELECT the_day::date
           , tsrange(the_day, the_day   interval '1 day') * tsrange(ts, next_ts) AS range  -- intersect with day
      FROM  (
         SELECT interval_ts_utc AS ts
              , lead(interval_ts_utc, 1, date_trunc('day', interval_ts_utc)   interval '1 day')  -- default to start of next day
                     OVER (ORDER BY interval_ts_utc) AS next_ts
              , up
         FROM   fixed
         ) sub
      CROSS  JOIN LATERAL generate_series(date_trunc('day', ts)
                                        , next_ts - interval '1 us'
                                        , interval '1 day') the_day  -- get 1 row per intersecting day
      WHERE  up
      ) sub
   ) sub2
GROUP  BY 1
ORDER  BY 1;

db<>fiddle here

Using the range intersection operator * for convenience.

CodePudding user response:

Here is my take on it - get daily down-time and subtract from 24 hours, except for today, where the subtraction is from current utc time.

select interval_date, 
case when interval_date = current_date then 
round(cast((case when sum(int_time) < 0 then trunc(EXTRACT(EPOCH FROM current_time at time zone 'utc')/60 ) - sum(int_time)
else sum(int_time) end)/trunc(EXTRACT(EPOCH FROM current_time at time zone 'utc')/60 ) as numeric), 2) 
else
round(cast((case when sum(int_time) < 0 then 1440 - sum(int_time)
else sum(int_time) end)/1440 as numeric), 2) 
end up_time
from 
(
select interval_date, interval_time_utc,
case when status = 0 then (-1) * EXTRACT(EPOCH FROM interval_time_utc::time)/60 
else EXTRACT(EPOCH FROM interval_time_utc::time)/60 - 1 end int_time,
status
from system_power_history
  ) t
  group by interval_date
  order by interval_date;

Fiddle here.

  • Related