sqlpostgresqlamazon-redshiftgaps-and-islandsapache-superset

Insert missing rows in time series consumption history


I'm looking for assistance with writing a PostgreSQL query in Apache Superset.

I have metered consumption data, where the meters have unique IDs. Normally, a meter is read at the start of every hour, and the difference is calculated in the consumption column. There are times, however, when an error occurs and the meter is not read, leading to missing hours in the data. I would like to add new rows and create a new column that would say "Missing Data" in those cases.

Here is an example of what the data looks like. The read_time_locals are in a timestamp without timezone format

meter_id start_read_time_local end_read_time_local start_read end_read consumption
1111 9/10/2024 0:00 9/10/2024 1:00 79118.91 79118.93 0.02
1111 9/10/2024 3:00 9/10/2024 4:00 79122.47 79123.99 1.52
2222 9/10/2024 0:00 9/10/2024 1:00 1937135 1937174 39
2222 9/10/2024 1:00 9/10/2024 2:00 1937174 1937191 17
2222 9/10/2024 2:00 9/10/2024 3:00 1937191 1937197 6
2222 9/10/2024 3:00 9/10/2024 4:00 1937197 1937202 5

What I would like is to create a query that would do this:

meter_id start_read_time_local end_read_time_local start_read end_read consumption Notes
1111 9/10/2024 0:00 9/10/2024 1:00 79118.91 79118.93 0.02
1111 9/10/2024 1:00 9/10/2024 2:00 null Missing Data
1111 9/10/2024 2:00 9/10/2024 3:00 null Missing Data
1111 9/10/2024 3:00 9/10/2024 4:00 79122.47 79123.99 1.52
2222 9/10/2024 0:00 9/10/2024 1:00 1937135 1937174 39
2222 9/10/2024 1:00 9/10/2024 2:00 1937174 1937191 17
2222 9/10/2024 2:00 9/10/2024 3:00 1937191 1937197 6
2222 9/10/2024 3:00 9/10/2024 4:00 1937197 1937202 5

I have looked and tried various solutions, but have not been able to successfully do this. My initial attempts were to use a combination of min/max of the end_read_time_local and generate_series() to create a sequence of timestamps that I would then right join to the water_consumption table, the result being the missing hours would have no consumption/reading values.

I was using this command to create the series and just make a list of 10 hours:

select (date_trunc('hour',current_timestamp) + ((a-1)||' hour')::interval)::timestamptz
from generate_series(1, 10, 1) as a

I'm struggling to join it, and I have yet to figure out how I will join these for each meter ID. In other words, I need the join to check to see if meter 1111 has a complete record as well as meter 2222.

I have a table account with distinct devices, where device_id matches water_consumption.meter_id.

Following d r's advice, I tried the following:

WITH 
   grid AS
    ( Select     a.device_id as meter_id, 
                 DATEADD('hrs', gs, DATE_TRUNC('day', wc.start_read_time_local)) as read_start, 
                 DATEADD('hrs', gs, DATE_TRUNC('day', wc.end_read_time_local)) as read_end,
                 Min(wc.start_read_time_local) as min_start_time, 
                 Max(wc.start_read_time_local) as max_end_time
      From       account a
      Inner Join water_consumption wc ON(wc.meter_id = a.device_id)
      Cross Join generate_series(0, 23) gs
      Group By   a.device_id, gs, DATE_TRUNC( 'day', wc.start_read_time_local)
    ) 
Select     g.meter_id, 
           Coalesce(wc.start_read_time_local, g.read_start) as start_read_time_local, 
           Coalesce(wc.end_read_time_local, g.read_end) as end_read_time_local, 
           wc.start_read, wc.end_read, wc.consumption, 
           Case When wc.start_read_time_local Is Null Then 'Missing Data' End as notes
From        grid g
Left Join   water_consumption wc ON(wc.meter_id = g.meter_id And 
                                    wc.start_read_time_local = g.read_start )
Where      read_start >= min_start_time And read_start <= max_end_time
Order By    g.meter_id, g.read_start

But I got this Redshift error: column "wc.end_read_time_local" must appear in the GROUP BY clause or be used in an aggregate function

I tired to add it to the group by function, but didn't work.


Solution

  • To fill in the gaps in hours of a dates create a grid of all hours per date and meter_id - left join your readings data and use Coalesce() function to show every hour then define notes column as 'Missing Data' for hours without readings.

    --    S a m p l e    D a t a : 
    CREATE TABLE account AS
      Select 1111 as device_id Union All
      Select 2222;
    
    CREATE TABLE water_consumption AS
      Select 1111 as meter_id, To_Timestamp('09/10/2024 00:00', 'mm/dd/yyyy hh24:mi') as start_read_time_local,  To_Timestamp('09/10/2024 01:00', 'mm/dd/yyyy hh24:mi') as end_read_time_local, 79118.91 as start_read  , 79118.93 as end_read, 0.02 as consumption Union All
      Select 1111,  To_Timestamp('09/10/2024 03:00', 'mm/dd/yyyy hh24:mi'),     To_Timestamp('09/10/2024 04:00', 'mm/dd/yyyy hh24:mi'), 79122.47,     79123.99, 1.52 Union All
      Select 2222,  To_Timestamp('09/10/2024 00:00', 'mm/dd/yyyy hh24:mi'),     To_Timestamp('09/10/2024 01:00', 'mm/dd/yyyy hh24:mi'),  1937135,      1937174, 39 Union All
      Select 2222,  To_Timestamp('09/10/2024 01:00', 'mm/dd/yyyy hh24:mi'),     To_Timestamp('09/10/2024 02:00', 'mm/dd/yyyy hh24:mi'),  1937174,      1937191, 17 Union All
      Select 2222,  To_Timestamp('09/10/2024 02:00', 'mm/dd/yyyy hh24:mi'),     To_Timestamp('09/10/2024 03:00', 'mm/dd/yyyy hh24:mi'),  1937191,      1937197, 6 Union All
      Select 2222,  To_Timestamp('09/10/2024 03:00', 'mm/dd/yyyy hh24:mi'),     To_Timestamp('09/10/2024 04:00', 'mm/dd/yyyy hh24:mi'),  1937197,      1937202, 5;
    

    ... create a cte grid that will have a row for every hour for every date of reading and for every meter_id ...

    WITH 
       grid AS
        ( Select     a.device_id as meter_id, 
                     DATE_TRUNC('day', wc.start_read_time_local) + interval '1 minute' * (gs * 60) as read_start,
                     DATE_TRUNC('day', wc.start_read_time_local) + interval '1 minute' * ((gs + 1) * 60) as read_end,
                     Min(wc.start_read_time_local) as min_start_time, 
                     Max(wc.start_read_time_local) as max_end_time
          From       account a
          Inner Join water_consumption wc ON(wc.meter_id = a.device_id)
          Cross Join generate_series(0, 23) gs
          Group By   a.device_id, gs, DATE_TRUNC( 'day', wc.start_read_time_local)
        ) 
    

    ... if you want the result as presented in your question - filter the data with WHERE clause as below. Without WHERE clause you will get all hours of a date per meter_id (see it in the fiddle - link is below)

    --      M a i n    S Q L : 
    Select     g.meter_id, 
               Coalesce(wc.start_read_time_local, g.read_start) as start_read_time_local, 
               Coalesce(wc.end_read_time_local, g.read_end) as end_read_time_local, 
               wc.start_read, wc.end_read, wc.consumption, 
               Case When wc.start_read_time_local Is Null Then 'Missing Data' End as notes
    From        grid g
    Left Join   water_consumption wc ON(wc.meter_id = g.meter_id And 
                                        wc.start_read_time_local = g.read_start )
    Where      read_start >= min_start_time And read_start <= max_end_time
    Order By    g.meter_id, g.read_start
    
    /*            R e s u l t : 
      meter_id  start_read_time_local   end_read_time_local     start_read     end_read consumption  notes
    ----------  ----------------------  ----------------------  ----------  ----------- -----------  ---------------
          1111  2024-09-10 00:00:00+00  2024-09-10 01:00:00+00    79118.91     79118.93        0.02  null
          1111  2024-09-10 01:00:00+00  2024-09-10 02:00:00+00        null         null        null  Missing Data
          1111  2024-09-10 02:00:00+00  2024-09-10 03:00:00+00        null         null        null  Missing Data
          1111  2024-09-10 03:00:00+00  2024-09-10 04:00:00+00    79122.47     79123.99        1.52  null
          2222  2024-09-10 00:00:00+00  2024-09-10 01:00:00+00     1937135      1937174          39  null
          2222  2024-09-10 01:00:00+00  2024-09-10 02:00:00+00     1937174      1937191          17  null
          2222  2024-09-10 02:00:00+00  2024-09-10 03:00:00+00     1937191      1937197           6  null
          2222  2024-09-10 03:00:00+00  2024-09-10 04:00:00+00     1937197      1937202           5  null          */
    

    See the fiddle here.

    NOTE
    This is tested with postgres but it should work on redshift with one adjustment in grid cte - adding hours to a date uses different syntax.

    -- read the redshift docs to deal with it - but it should be like this
    DATEADD('hrs', gs, DATE_TRUNC('day', wc.start_read_time_local)) as read_start, 
    DATEADD('hrs', gs, DATE_TRUNC('day', wc.end_read_time_local)) as read_end
    --  instead of Postgres syntax used in cte grid definition code for columns read_start and read_end