SQL query to interpolate timestamp basing on adjacent records - sql

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:
When I select value for timestamp '02.05.2017 00:00', query should give me 203.4 (record with such timestamp exists in table)
When I select value for timestamp '02.05.2017 00:30', query should give me 205.05 (record with such timestamp doesn't exist in table, so we take a 'middle' between 203.4 and 206.7, because wanted timestamp is in the middle between their timestamps)
When I select value for timestamp '02.05.2017 00:15', query should give me 204.225 (a 'fourth part' between 203.4 and 206.7)
What is the simplest way to solve such task?

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;

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

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;

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;

Related

How can I get all dates in a query if there is not data in that dates?

Currently, I have a ORACLE query that gives me the following data:
As you can see, there is not data between the 11th and the 12th, and I would need to get this result if there is not data:
I currently have this query:
select
TRUNC(FECHA_PEDIDO, 'dd') AS FECHA,
PEDIDO,
COUNT(TRUNC(FECHA_PEDIDO, 'dd')) AS CANTIDAD
FROM PEDIDOS_TIENDA
GROUP BY TRUNC(FECHA_PEDIDO, 'dd'), PEDIDO
ORDER BY TRUNC(FECHA_PEDIDO, 'dd') ASC
I'm a little lost, can you help me?
You want something like:
SELECT c.fecha,
t.pedido,
COALESCE(t.cantidad, 0) AS cantidad
FROM (
SELECT min_fecha + LEVEL - 1 AS fecha
FROM (
SELECT MIN(TRUNC(FECHA_PEDIDO, 'dd')) AS min_fecha,
MAX(TRUNC(FECHA_PEDIDO, 'dd')) AS max_fecha
FROM PEDIDOS_TIENDA
)
CONNECT BY LEVEL - 1 <= max_fecha - min_fecha
) c
LEFT OUTER JOIN
(
SELECT TRUNC(FECHA_PEDIDO, 'dd') AS FECHA,
PEDIDO,
COUNT(TRUNC(FECHA_PEDIDO, 'dd')) AS CANTIDAD
FROM PEDIDOS_TIENDA
GROUP BY TRUNC(FECHA_PEDIDO, 'dd'), PEDIDO
ORDER BY TRUNC(FECHA_PEDIDO, 'dd') ASC
) t
ON c.fecha = t.fecha
ORDER BY fecha, pedido
Which, for the sample data:
CREATE TABLE PEDIDOS_TIENDA ( fecha_pedido, pedido ) AS
SELECT DATE '2020-12-09', 'Aleron' FROM DUAL UNION ALL
SELECT DATE '2020-12-09', 'Aleron' FROM DUAL UNION ALL
SELECT DATE '2020-12-10', 'Bugia' FROM DUAL UNION ALL
SELECT DATE '2020-12-10', 'Focos' FROM DUAL UNION ALL
SELECT DATE '2020-12-10', 'Ruedas' FROM DUAL UNION ALL
SELECT DATE '2020-12-10', 'Focos' FROM DUAL UNION ALL
SELECT DATE '2020-12-11', 'Llantas' FROM DUAL UNION ALL
SELECT DATE '2020-12-13', 'Llantas' FROM DUAL;
Outputs:
FECHA
PEDIDO
CANTIDAD
09-DEC-20
Aleron
2
10-DEC-20
Bugia
1
10-DEC-20
Focos
2
10-DEC-20
Ruedas
1
11-DEC-20
Llantas
1
12-DEC-20
0
13-DEC-20
Llantas
1
db<>fiddle here
You can use following query . using table t2, calculate the dates between the smallest date and the largest date, and then right outer join it to the main table.
SELECT t2.dt AS fecha,t1.pedido,COALESCE(t1.cantidad,0)
FROM mytable t1
RIGHT OUTER JOIN
(SELECT (SELECT MIN(fecha) FROM mytable) + level - 1 dt
FROM DUAL CONNECT BY level <= ((SELECT MAX(fecha) FROM mytable) - (SELECT MIN(fecha) FROM mytable) + 1)) t2
ON t1.fecha = t2.dt
ORDER BY t2.dt
demo in db<>fiddle

need to repeat the previous transaction at place of null

My store procedure:
SELECT B.ETADATE,
a.NAME,
a.CATEGORY ,
a.TYPE,
a.STOCK
FROM
(SELECT TO_CHAR(TO_DATE(FROM_DATE,'DD-MM-YYYY HH24:MI:SS')) AS "FROM_DATE",
NAME AS "NAME",
CATEGORY AS "CATEGORY",
TYPE AS "TYPE",
BALANCE AS "BALANCE"
FROM VW_NET_STOCK_POSITION
) a,
(SELECT dt + LEVEL AS ETADate
FROM
(SELECT TRUNC (TO_DATE ('01-09-2018 00:00:00', 'DD-MM-YYYY HH24:MI:SS'), 'MM') - 1 AS dt
FROM DUAL
) D
CONNECT BY LEVEL <= sysdate - dt
) B
WHERE a.FROM_DATE(+) = B.ETADATE
ORDER BY ETADate;
and my output is:
but I want that where 'null' then there should be replaced with the previous transaction.
Like this,
want output like this:
Do not use + for outer joins! The solution to your problem is LAG(. . . IGNORE NULLS). But the query can be cleaned up.
I would suggest writing this as:
WITH dates as (
SELECT (dt + level - 1) as etadate
FROM (SELECT TRUNC(DATE '2018-09-01', 'MM') - 1 AS dt
FROM DUAL
) d
CONNECT BY LEVEL <= sysdate - dt + 1 -- to get today's date
)
SELECT d.etadate,
COALESCE(nsp.name, LAG(nsp.name IGNORE NULLS) OVER (ORDER BY d.etadate)) as name,
COALESCE(nsp.category, LAG(nsp.category IGNORE NULLS) OVER (ORDER BY d.etadate)) as category,
COALESCE(LAG(nsp.type IGNORE NULLS) OVER (ORDER BY d.etadate), type) as type,
COALESCE(LAG(nsp.stock IGNORE NULLS) OVER (ORDER BY d.etadate), stock) as stock
FROM dates d LEFT JOIN
VW_NET_STOCK_POSITION nsp
ON d.etadate = TRUNC(nsp.from_date)
ORDER BY d.ETADate;
You might use LAST_VALUE analytical function with IGNORE NULLS option to make the SQL as follows :
SELECT ETADATE,
LAST_VALUE(NAME) IGNORE NULLS OVER (ORDER BY etadate) as NAME,
LAST_VALUE(CATEGORY) IGNORE NULLS OVER (ORDER BY etadate) as CATEGORY,
LAST_VALUE(TYPE) IGNORE NULLS OVER (ORDER BY etadate ) as TYPE,
LAST_VALUE(BALANCE) IGNORE NULLS OVER (ORDER BY etadate ) as STOCK
FROM
(SELECT TO_CHAR(TO_DATE(FROM_DATE,'DD-MM-YYYY HH24:MI:SS')) AS "FROM_DATE",
NAME AS "NAME",
CATEGORY AS "CATEGORY",
TYPE AS "TYPE",
BALANCE AS "BALANCE"
FROM VW_NET_STOCK_POSITION
) a LEFT OUTER JOIN
(SELECT dt + LEVEL AS ETADate
FROM
(SELECT TRUNC (TO_DATE ('01-09-2018 00:00:00', 'DD-MM-YYYY HH24:MI:SS'), 'MM') - 2
AS dt
FROM DUAL
) D
CONNECT BY LEVEL <= trunc(sysdate) - dt
) B
ON ( a.FROM_DATE = B.ETADATE )
ORDER BY ETADate;
and prefer using ANSI JOIN standard.
SQL Fiddle Demo

Oracle SQL: Get the First Value of each complex Group/Partition

How can I in Oracle with SQL retrieve for a table each first Column A,B, in case column B changes the value ordered by A???
Assume I have a table with date and value:
DATE;VALUE
01-2015;1
02-2015;1
01-2016;2
01-2016;2
01-2017:1
So what I want now, is each first line once the value changes (based on certain orderning here DATE) so from this set I want:
DATE;VALUE
01-2015;1
01-2016;2
01-2017:1
Now I cannot use a simply GROUP BY VALUE, because the value can flip back again (in this case to 1 in 2015 and 2017) and MIN(DATECOL) GROUP BY VALUECOL will not report this 2017.
So I was looking into Analytical functions something like:
SELECT FIRST_VALUE(DATECOL),FIRST_VALUE(VALUECOL) OVER (PARTITION BY
VALUECOL ORDER BY DATECOL) FROM DATATABLE
But I cannot get this working!
Tabibtosan makes this easy:
with table1 as (select to_date('01/01/2015', 'dd/mm/yyyy') dt, 1 val from dual union all
select to_date('01/02/2015', 'dd/mm/yyyy') dt, 1 val from dual union all
select to_date('01/01/2016', 'dd/mm/yyyy') dt, 2 val from dual union all
select to_date('01/01/2016', 'dd/mm/yyyy') dt, 2 val from dual union all
select to_date('01/01/2017', 'dd/mm/yyyy') dt, 1 val from dual)
-- end of mimicking a table "table1" with data in it. See sql below:
select min(dt) dt,
val
from (select dt,
val,
dense_rank() over (order by dt)
- dense_rank() over (partition by val order by dt) grp
from table1)
group by val,
grp;
DT VAL
---------- ----------
01/01/2015 1
01/01/2016 2
01/01/2017 1
I think LAG() is the appropriate function, along with some other logic:
select t.*
from (select t.*, lag(value) over (order by date) as prev_value
from datatable t
) t
where prev_value is null or prev_value <> value;
The only issue with your data is that the rows are not unique. This can cause a problem, because sorting in databases is not stable (that is, two rows can be in either order). Hopefully, in your actual data, the dates are unique or you have another id you can add to the order by to make the sort stable.
One brute force way of doing this is:
with dt as (
select dt.*, rownum as rn
from datatable dt
)
select t.*
from (select dt.*, lag(value) over (order by date, rn) as prev_value
from datatable dt
) t
where prev_value is null or prev_value <> value;

Lowest continuous date without break

I have a table and each record has a date. We can assume that a date range is contiguous if there's not a 3 month break. How can I find the start of the most recent contiguous date range?
For example, imagine if I had this data:
1990-5-1
1990-6-4
1990-10-28
1990-11-14
1990-12-19
1991-1-20
1991-4-30
1991-5-13
I'd like for it to return 1991-4-30 because it's the start of the most recent contiguous range of dates.
I think this does what you're looking for. Using my own table and column names as test data. This is on Oracle.
select * from (
select * from sm_ss_tickets t1 where exists (
select * from sm_ss_tickets t2 where t2.created_date between t1.created_date and t1.created_date+90 and t1.rowid <> t2.rowid
) order by created_date asc
) where rownum = 1;
Maybe something like the following would work:
WITH d1 AS (
SELECT date'1990-05-01' AS dt FROM dual
UNION ALL
SELECT date'1990-06-04' AS dt FROM dual
UNION ALL
SELECT date'1990-10-28' AS dt FROM dual
UNION ALL
SELECT date'1990-11-14' AS dt FROM dual
UNION ALL
SELECT date'1990-12-19' AS dt FROM dual
UNION ALL
SELECT date'1991-01-20' AS dt FROM dual
UNION ALL
SELECT date'1991-04-30' AS dt FROM dual
UNION ALL
SELECT date'1991-05-13' AS dt FROM dual
)
SELECT MAX(dt) FROM (
SELECT dt, LAG(dt) OVER ( ORDER BY dt ) AS prev_dt, LEAD(dt) OVER ( ORDER BY dt ) AS next_dt
FROM d1
) WHERE ( dt > ADD_MONTHS(prev_dt, 3) OR prev_dt IS NULL )
AND dt > ADD_MONTHS(next_dt, -3)
In the above, a date can only be the start of a contiguous sequence if there is no prior date within 3 months (either it is more than three months ago or it doesn't exist at all) and there is also a subsequent date within 3 months.
You can use LAG and LEAD. Find the query below. I think it works fine.
tmp_year is the table I have created. tdate is the column.
The records in the table are
28-JAN-15
27-JAN-15
26-JAN-15
25-JAN-15
12-JUL-14
11-JUL-14
10-JUL-14
09-JUL-14
24-DEC-13
23-DEC-13
22-DEC-13
21-DEC-13
15-SEP-13
07-JUN-13
27-FEB-13
19-NOV-12
11-AUG-12
Please find the query which returns 25th Jan 2015.
select max(d.tdate) from (
select c.tdate,c.next_date,c.date_diff,lag(date_diff) over( order by tdate) prev_diff from (
select b.tdate ,b.next_date,(next_date-tdate) date_diff from
(select a.tdate,lead(a.tdate) over(order by a.tdate) next_date from tmp_year a ) b ) c) d where d.date_diff<90 and d.prev_diff>=90;

select records with a 6 month interval

I want to select with an oracle sql statement the records with a 6 month time interval.
Example
01/06/2011 AMOUNT
01/12/2011 AMOUNT
01/06/2012 AMOUNT
01/12/2012 AMOUNT
And so on
How can I do this with oracle sql?
select ADD_MONTHS(trunc(sysdate), (rownum - 1) * 6) some_date
from dual
connect by level <= 5;
SOME_DATE
-----------
18.04.2014
18.10.2014
18.04.2015
18.10.2015
18.04.2016
WITH got_r_num AS
(
SELECT t.* -- OR WHATEVER YOU WANT
, DENSE_RANK () OVER ( PARTITION BY TRUNC (created_date, 'MONTH')
ORDER BY TRUNC (created_date) -- DESC
) AS r_num
FROM test_table
WHERE MOD ( MONTHS_BETWEEN ( TRUNC (SYSDATE)
, TRUNC (created_date)
)
, 6
) = 0
)
SELECT * -- or list all columns except r_num
FROM got_r_num
WHERE r_num = 1
;
Have a look here please .
If you need to sum of all records of 6 months in the AMOUNT field:
You can subquery with sum function and query with CONNECT BY LEVEL
SELECT x AS l_date,
(
SELECT sum(your_data)
FROM your_table
WHERE table_date >= x
AND table_date < add_months(x,6)
)AMOUNT
FROM(
SELECT add_months(to_date('01/06/2011','dd/mm/yyyy'),(LEVEL-1)*6) x
FROM dual
CONNECT BY LEVEL <= 4
);