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;
Related
I need to get max date for each row over other ids. Of course I can do this with CROSS JOIN and JOIN .
Like this
WITH t AS (
SELECT 1 AS id, rep_date FROM UNNEST(GENERATE_DATE_ARRAY('2021-09-01','2021-09-09', INTERVAL 1 DAY)) rep_date
UNION ALL
SELECT 2 AS id, rep_date FROM UNNEST(GENERATE_DATE_ARRAY('2021-08-20','2021-09-03', INTERVAL 1 DAY)) rep_date
UNION ALL
SELECT 3 AS id, rep_date FROM UNNEST(GENERATE_DATE_ARRAY('2021-08-25','2021-09-05', INTERVAL 1 DAY)) rep_date
)
SELECT id, rep_date, MAX(rep_date) OVER (PARTITION BY id) max_date, max_date_over_others FROM t
JOIN (
SELECT t.id, MAX(max_date) max_date_over_others FROM t
CROSS JOIN (
SELECT id, MAX(rep_date) max_date FROM t
GROUP BY 1
) t1
WHERE t1.id <> t.id
GROUP BY 1
) USING (id)
But it's too wired for huge tables. So I'm looking for the some simpler way to do this. Any ideas?
Your version is good enough I think. But if you want to try other options - consider below approach. It might looks more verbose from first look - but should be more optimal and cheaper to compare with your version with cross join
temp as (
select id,
greatest(
ifnull(max(max_date_for_id) over preceding_ids, '1970-01-01'),
ifnull(max(max_date_for_id) over following_ids, '1970-01-01')
) as max_date_for_rest_ids
from (
select id, max(rep_date) max_date_for_id
from t
group by id
)
window
preceding_ids as (order by id rows between unbounded preceding and 1 preceding),
following_ids as (order by id rows between 1 following and unbounded following)
)
select *
from t
join temp
using (id)
Assuming your original table data just has columns id and dt - wouldn't this solve it? I'm using the fact that if an id has the max dt of everything, then it gets the second-highest over the other id values.
WITH max_dates AS
(
SELECT
id,
MAX(dt) AS max_dt
FROM
data
GROUP BY
id
),
with_top1_value AS
(
SELECT
*,
MAX(dt) OVER () AS max_overall_dt_1,
MIN(dt) OVER () AS min_overall_dt
FROM
max_dates
),
with_top2_values AS
(
SELECT
*,
MAX(CASE WHEN dt = max_overall_dt_1 THEN min_overall_dt ELSE dt END) AS max_overall_dt2
FROM
with_top1_value
),
SELECT
*,
CASE WHEN dt = max_overall_dt1 THEN max_overall_dt2 ELSE max_overall_dt1 END AS max_dt_of_others
FROM
with_top2_values
I want to fetch customers balances at the maximum date of every month, in every year in database. The Balance table has balances at the end of everyday when customer does transaction.
I just want to pick the balance at the maximum date of every month.Any help??
Below is a snip of My dataset.
You can try using window function - row_number()
select * from
(
SELECT *,row_number() over(partition by extract(YEAR FROM Date), extract(MONTH FROM Date) order by date desc) as rn
FROM t
)rn=1
You can do it also without a sub-query:
WITH b(ID, "date",bal) AS
(
SELECT 'CUST_I',DATE '2013-07-27', 14777.44 FROM dual UNION ALL
SELECT 'CUST_H',DATE '2013-07-26', 71085.13 FROM dual UNION ALL
SELECT 'CUST_I',DATE '2013-08-27', 66431.35656 FROM dual UNION ALL
SELECT 'CUST_H',DATE '2013-08-26', 63102.68622 FROM dual UNION ALL
SELECT 'CUST_H',DATE '2013-08-20', 6310.68622 FROM dual UNION ALL
SELECT 'CUST_H',DATE '2013-08-10', 630.68622 FROM dual UNION ALL
SELECT 'CUST_G',DATE '2013-09-25', 89732.04889 FROM dual UNION ALL
SELECT 'CUST_E',DATE '2013-09-23', 83074.70822 FROM dual
)
SELECT ID,
MAX("date") KEEP (DENSE_RANK FIRST ORDER BY "date" desc) AS MAX_DATE,
MAX(bal) KEEP (DENSE_RANK FIRST ORDER BY "date" desc) AS MAX_BAL
FROM b
GROUP BY ID, TRUNC("date", 'MM');
+-----------------------------+
|ID |MAX_DATE |MAX_BAL |
+-----------------------------+
|CUST_E|23.09.2013|83074.70822|
|CUST_G|25.09.2013|89732.04889|
|CUST_H|26.07.2013|71085.13 |
|CUST_H|26.08.2013|63102.68622|
|CUST_I|27.07.2013|14777.44 |
|CUST_I|27.08.2013|66431.35656|
+-----------------------------+
You may use a self join for your table call cust_balances :
select c1.*
from cust_balances c1
join
(
select max("date") max_date
from cust_balances
group by to_char("date",'yyyymm')
) c2 on ( c1."date" = c2.max_date );
SQL Fiddle Demo
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;
I have a table containing Dates and Statuses. I wish to get the date that the status changed to the most recent status. Sample data:
DATE STATUS
01/01/2000 P
02/01/2000 A
03/01/2000 C
04/01/2000 A
05/01/2000 A
06/01/2000 A
So in this instance the most recent status is A and it changed to this on 04/01/2000. (The 02/01/2000 row should be ignored in this situation)
Any suggestions for how to go about selecting this row?
At first, I misunderstood the question. You need to get the earliest date of the last status.
You can group sequences of like statuses using a trick -- a difference of row numbers. The difference (in the query below) is constant for sequences that are the same. Then you can use aggregation to get the minimum date and select the latest one:
select mindate
from (select min(date) as mindate
from (select t.*,
row_number() over (order by date) as seqnum1,
row_number() over (partition by status order by date) as seqnum2
from table t
) t
group by status, (seqnum1 - seqnum2)
order by mindate desc
) t
where rownum = 1
EDIT:
In any case, the right way to do this is using lag():
select max(date)
from (select t.*, lag(status) over (order by date) as prev_status
from table t
)
where prev_status <> status or prev_status is null;
Here is the SQL Fiddle.
You can do this using lag or lead. Here I'm using lead, ordering by date descending to find the previous status date (if it's null I'm just supplying the date, which is needed in case there's only one record).
select max(date)
from (
select status, date, nvl(lead(status) over (order by date desc),date) as previous_status
from t
order by date desc
)
where status <> previous_status;
Something like this ought to do the trick:
with sample_data as (select to_date('01/01/2000', 'dd/mm/yyyy') dt, 'P' status from dual union all
select to_date('02/01/2000', 'dd/mm/yyyy') dt, 'A' status from dual union all
select to_date('03/01/2000', 'dd/mm/yyyy') dt, 'C' status from dual union all
select to_date('04/01/2000', 'dd/mm/yyyy') dt, 'A' status from dual union all
select to_date('05/01/2000', 'dd/mm/yyyy') dt, 'A' status from dual union all
select to_date('06/01/2000', 'dd/mm/yyyy') dt, 'A' status from dual),
results1 as (select dt,
status,
row_number() over (order by dt) - row_number() over (partition by status order by dt) grp
from sample_data),
results2 as (select status, min(dt) min_dt, grp, max(min(dt)) over () max_min_dt
from results1
group by status, grp)
select status, min_dt
from results2
where min_dt = max_min_dt;
STATUS MIN_DT
------ ----------
A 04/01/2000
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;