SQL 查询基于相邻记录插入时间戳

SQL query to interpolate timestamp basing on adjacent records

我使用 Oracle 并具有以下 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 

我需要编写 SQL 查询(或 PL/SQL 过程)以便为任何时间戳 (DT) 插入值 (VAL),假设两个相邻记录之间的值不断增加在 table(即 linear interpolation)。

示例:

解决此类任务的最简单方法是什么?

我认为完成此操作的最简单方法是使用类似于以下内容的 PL/SQL 函数:

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;
/

你不需要游标。您需要找到两个最接近的记录,一个在上方,一个在下方,然后取它们的平均值。像这样:

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;

您可以使用分析函数轻松实现这一点。希望这个 belo 片段对您有所帮助。

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;

我觉得这个更紧凑,它避免了自连接:

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;