Ievgen
Ievgen

Reputation: 45

SQL query to interpolate timestamp basing on adjacent records

I use Oracle and have the following table:

create table test as
select to_date('02.05.2017 00:00', 'DD.MM.YYYY HH24:MI') as DT, 203.4 as VAL from dual union all
select to_date('02.05.2017 01:00', 'DD.MM.YYYY HH24:MI') as DT, 206.7 as VAL from dual union all
select to_date('02.05.2017 02:00', 'DD.MM.YYYY HH24:MI') as DT, 208.9 as VAL from dual union all
select to_date('02.05.2017 03:00', 'DD.MM.YYYY HH24:MI') as DT, 211.8 as VAL from dual union all
select to_date('02.05.2017 04:45', 'DD.MM.YYYY HH24:MI') as DT, 212.3 as VAL from dual union all
select to_date('02.05.2017 06:15', 'DD.MM.YYYY HH24:MI') as DT, 214.5 as VAL from dual union all
select to_date('02.05.2017 08:12', 'DD.MM.YYYY HH24:MI') as DT, 215 as VAL from dual
;


DT                   VAL
----------------------------
02.05.2017 00:00     203.4
02.05.2017 01:00     206.7
02.05.2017 02:00     208.9
02.05.2017 03:00     211.8
02.05.2017 04:45     212.3
02.05.2017 06:15     214.5
02.05.2017 08:12     215 

I need to write SQL query (or PL/SQL procedure) so as to interpolate the value (VAL) for any timestamp (DT), assuming that value is increasing constantly between two neighbour records in table (ie. linear interpolation).

Example:

What is the simplest way to solve such task?

Upvotes: 2

Views: 648

Answers (4)

Wernfried Domscheit
Wernfried Domscheit

Reputation: 59523

I think this one is even more compact, it avoids the self-join:

WITH t AS 
    (SELECT DT, VAL, 
        LEAD(DT, 1, DT) OVER (ORDER BY DT) AS FOLLOWING_DT,
        LEAD(VAL, 1, VAL) OVER (ORDER BY VAL) AS FOLLOWING_VAL
    FROM TEST)
SELECT VAL + (FOLLOWING_VAL - VAL) * ( (:timestamp - DT) / (FOLLOWING_DT - DT) )
FROM t
WHERE :timestamp BETWEEN DT AND FOLLOWING_DT;

Upvotes: 3

Avrajit Roy
Avrajit Roy

Reputation: 3303

You can easily achieve this by using Analytical functions. Hope this belo snippet helps.

SELECT *
FROM
  (SELECT c.*,
    ((lead(c.dt) over(order by 1) -c.dt)*24*60) lead_val,
    (lead(c.val) over(order by 1 )-c.val) lead_val_diff,
    lead(c.val) over(order by 1 ) - (((lead(c.dt) over(order by 1)- to_date('&enter_date','DD.MM.YYYY HH24:MI'))*24*60)/((lead(c.dt) over(order by 1) -c.dt)*24*60))*(lead(c.val) over(order by 1 )-c.val)polated_val
  FROM
    (SELECT so_test.*,
      row_number() over(order by dt) rn1
    FROM SO_TEST
    )c
  WHERE ((c.rn1) IN
    (SELECT MAX(RN)
    FROM
      (SELECT ROW_NUMBER() OVER(ORDER BY A.DT ) RN,
        A.*
      FROM SO_TEST A
      WHERE A.DT <= '&enter_date'
      )B
    ))
  OR (c.rn1       IN
    (SELECT MAX(RN)+1
    FROM
      (SELECT ROW_NUMBER() OVER(ORDER BY A.DT ) RN,
        A.*
      FROM SO_TEST A
      WHERE A.DT <= '&enter_date'
      )B
    ))
  )d
WHERE d.polated_val IS NOT NULL;

Upvotes: 0

Gordon Linoff
Gordon Linoff

Reputation: 1270513

You don't need cursors for this. You need to find the two closest records, one above and one below and then take their average. Something like this:

select :timestamp,
       (case when lower.val = upper.val then val
             else lower.val + (upper.val - lower.val) * ( (:timestamp - lower.dt) / (upper.dt - lower.dt) )
        end) as imputed_val
from (select *
      from (select dt, val
            from t
            where dt <= :timestamp
            order by dt desc
           )
      where rownum = 1
     ) lower cross join
     (select *
      from (select dt, val
            from t
            where dt >= :timestamp
            order by dt asc
           )
      where rownum = 1
     ) upper;

Upvotes: 0

Kirby
Kirby

Reputation: 724

I think the easiest way to accomplish this would be with a PL/SQL function similar to the following:

create or replace function get_val(dt in date) return number
is
  cursor exact_cursor(dt in date) is
    select t.val from t where t.dt = exact_cursor.dt;
  cursor earlier_cursor(dt in date) is
    select t.dt, t.val from t where t.dt < earlier_cursor.dt
    order by t.dt desc;
  cursor later_cursor(dt in date) is
    select t.dt, t.val from t where t.dt > later_cursor.dt
    order by t.dt asc;

  result      number;
  factor      number;
  earlier_rec earlier_cursor%rowtype;
  later_rec   later_cursor%rowtype;
begin
  open exact_cursor(dt);
  fetch exact_cursor into result;
  close exact_cursor;
  if result is not null then
    return result;
  end if;

  -- No exact match. Perform linear interpolation between values
  -- from earlier and later records.
  open earlier_cursor(dt);
  fetch earlier_cursor into earlier_rec;
  close earlier_cursor;

  open later_cursor(dt);
  fetch later_cursor into later_rec;
  close later_cursor;

  -- Return NULL unless earlier and later records found
  if earlier_rec.dt is null or later_rec.dt is null then
    return null;
  end if;

  factor := (dt - earlier_rec.dt) / (later_rec.dt - earlier_rec.dt);
  result := earlier_rec.val + factor * (later_rec.val - earlier_rec.val);
  return result;
end;
/

Upvotes: 0

Related Questions