Oracle Sort of gaps and island query - sql

Instead of writing long sentences and paragraphs let me show the data and what I want to achieve :
create table ssb_price (itm_no varchar2(10), price number, price_code varchar2(10), valid_from_dt date, valid_to_dt date);
insert into ssb_price values ('A001', 83, 'AB', '01-JAN-21', '05-JAN-21');
insert into ssb_price values ('A001', 83, 'AB', '06-JAN-21', '12-JAN-21');
insert into ssb_price values ('A001', 98, 'SPQ', '13-JAN-21', '17-JAN-21');
insert into ssb_price values ('A001', 83, 'AB', '19-JAN-21', '24-JAN-21');
insert into ssb_price values ('A001', 83, 'DE', '25-JAN-21', '30-JAN-21');
insert into ssb_price values ('A001', 83, 'DE', '31-JAN-21', '04-FEB-21');
insert into ssb_price values ('A001', 77, 'XY', '07-FEB-21', '12-FEB-21');
insert into ssb_price values ('A001', 77, 'XY', '15-FEB-21', '20-FEB-21');
insert into ssb_price values ('A001', 62, 'SD', '23-FEB-21', '26-FEB-21');
insert into ssb_price values ('A001', 59, 'SD', '26-FEB-21', '03-MAR-21');
For particular itm_no and price if the from and to dates are continuous then I should get that value. For price 77 there is a gap of 2 days (13th and 14th) between to date and the next from date so its not continuous. Lemme paste what the desired output should look like :(taken the snip from excel)
I have asked this question clubbed with another post. But that post was old and haven't got any feedback so creating this. Please let me know if I should merge this post with the previous one.

This is basically a gaps-and-islands problem. But instead of aggregating to reduce the number of rows, you want to use window functions at the last step.
In your data, the time frames neatly tile. That suggests using lag() and a cumulative sum to define the groups:
select p.*,
min(valid_from_dt) over (partition by itm_no, price, price_code, grp) as new_valid_from_dt,
max(valid_to_dt) over (partition by itm_no, price, price_code, grp) as new_valid_to_dt
from (select p.*,
sum(case when valid_from_dt = prev_valid_to_dt + interval '1' day then 0 else 1 end) over
(partition by itm_no, price, price_code order by valid_from_dt) as grp
from (select p.*,
lag(valid_to_dt) over (partition by itm_no, price, price_code order by valid_from_dt) as prev_valid_to_dt
from ssb_price p
) p
) p
order by itm_no, valid_from_dt;
Here is a db<>fiddle.

From Oracle 12, you can use MATCH_RECOGNIZE:
SELECT itm_no,
price,
price_code,
valid_from_dt,
valid_to_dt,
MIN( valid_from_dt ) OVER ( PARTITION BY itm_no, mnum ) AS new_valid_from_dt,
MAX( valid_to_dt ) OVER ( PARTITION BY itm_no, mnum ) AS new_valid_to_dt
FROM ssb_price
MATCH_RECOGNIZE(
PARTITION BY itm_no
ORDER BY valid_from_dt, valid_to_dt
MEASURES
MATCH_NUMBER() AS mnum
ALL ROWS PER MATCH
PATTERN ( start_range continued_range* )
DEFINE
continued_range AS (
valid_from_dt = PREV( valid_to_dt ) + 1
AND price = PREV( price )
)
)
and, from Oracle 10g, you can use the MODEL clause:
SELECT itm_no,
price,
price_code,
valid_from_dt,
valid_to_dt,
mn,
MIN( valid_from_dt ) OVER ( PARTITION BY itm_no, mn ) AS new_valid_from_dt,
MAX( valid_to_dt ) OVER ( PARTITION BY itm_no, mn ) AS new_valid_to_dt
FROM (
SELECT *
FROM (
SELECT s.*,
ROW_NUMBER() OVER ( PARTITION BY itm_no ORDER BY valid_from_dt ) AS rn
FROM ssb_price s
)
MODEL
PARTITION BY ( itm_no )
DIMENSION BY ( rn )
MEASURES ( price, price_code, valid_from_dt, valid_to_dt, 1 AS mn )
RULES (
mn[rn>1] = mn[cv(rn)-1]
+
CASE
WHEN valid_from_dt[cv(rn)] = valid_to_dt[cv(rn)-1] + 1
AND price[cv(rn)] = price[cv(rn) - 1]
THEN 0
ELSE 1
END
)
)
Which, for the sample data:
create table ssb_price (itm_no, price, price_code, valid_from_dt, valid_to_dt) AS
SELECT 'A001', 83, 'AB', DATE '2021-01-01', DATE '2021-01-05' FROM DUAL UNION ALL
SELECT 'A001', 83, 'AB', DATE '2021-01-06', DATE '2021-01-12' FROM DUAL UNION ALL
SELECT 'A001', 98, 'SPQ', DATE '2021-01-13', DATE '2021-01-17' FROM DUAL UNION ALL
SELECT 'A001', 83, 'AB', DATE '2021-01-19', DATE '2021-01-24' FROM DUAL UNION ALL
SELECT 'A001', 83, 'DE', DATE '2021-01-25', DATE '2021-01-30' FROM DUAL UNION ALL
SELECT 'A001', 83, 'DE', DATE '2021-01-31', DATE '2021-02-04' FROM DUAL UNION ALL
SELECT 'A001', 77, 'XY', DATE '2021-02-07', DATE '2021-02-12' FROM DUAL UNION ALL
SELECT 'A001', 77, 'XY', DATE '2021-02-15', DATE '2021-02-20' FROM DUAL UNION ALL
SELECT 'A001', 62, 'SD', DATE '2021-02-23', DATE '2021-02-26' FROM DUAL UNION ALL
SELECT 'A001', 59, 'SD', DATE '2021-02-26', DATE '2021-03-03' FROM DUAL;
Outputs:
ITM_NO
PRICE
PRICE_CODE
VALID_FROM_DT
VALID_TO_DT
NEW_VALID_FROM_DT
NEW_VALID_TO_DT
A001
83
AB
2021-01-01 00:00:00
2021-01-05 00:00:00
2021-01-01 00:00:00
2021-01-12 00:00:00
A001
83
AB
2021-01-06 00:00:00
2021-01-12 00:00:00
2021-01-01 00:00:00
2021-01-12 00:00:00
A001
98
SPQ
2021-01-13 00:00:00
2021-01-17 00:00:00
2021-01-13 00:00:00
2021-01-17 00:00:00
A001
83
AB
2021-01-19 00:00:00
2021-01-24 00:00:00
2021-01-19 00:00:00
2021-02-04 00:00:00
A001
83
DE
2021-01-25 00:00:00
2021-01-30 00:00:00
2021-01-19 00:00:00
2021-02-04 00:00:00
A001
83
DE
2021-01-31 00:00:00
2021-02-04 00:00:00
2021-01-19 00:00:00
2021-02-04 00:00:00
A001
77
XY
2021-02-07 00:00:00
2021-02-12 00:00:00
2021-02-07 00:00:00
2021-02-12 00:00:00
A001
77
XY
2021-02-15 00:00:00
2021-02-20 00:00:00
2021-02-15 00:00:00
2021-02-20 00:00:00
A001
62
SD
2021-02-23 00:00:00
2021-02-26 00:00:00
2021-02-23 00:00:00
2021-02-26 00:00:00
A001
59
SD
2021-02-26 00:00:00
2021-03-03 00:00:00
2021-02-26 00:00:00
2021-03-03 00:00:00
db<>fiddle here

Related

Analytic function/logic to get min and max record date in Oracle

I have a requirement to fetch value based on eff_dt and end date. given below sample data.
Database : Oracle 11g
Example data:
id
val
eff_date
end_date
10
100
01-Jan-21
04-Jan-21
10
105
05-Jan-21
07-Jan-21
10
100
08-Jan-21
10-Jan-21
10
100
11-Jan-21
17-Jan-21
10
100
18-Jan-21
21-Jan-21
10
110
22-Jan-21
null
output:
id
val
eff_date
end_date
10
100
01-Jan-21
04-Jan-21
10
105
05-Jan-21
07-Jan-21
10
100
08-Jan-21
21-Jan-21
10
110
22-Jan-21
null
You can use the ROW_NUMBER analytic function and then aggregate:
SELECT id,
val,
MIN(eff_date) AS eff_date,
MAX(end_date) AS end_date
FROM (
SELECT t.*,
ROW_NUMBER() OVER (PARTITION BY id ORDER BY eff_date)
- ROW_NUMBER() OVER (PARTITION BY id, val ORDER BY eff_date) AS grp
FROM table_name t
)
GROUP BY id, val, grp
ORDER BY id, eff_date;
Which, for the sample data:
CREATE TABLE table_name (id, val, eff_date, end_date) AS
SELECT 10, 100, DATE '2021-01-01', DATE '2021-01-04' FROM DUAL UNION ALL
SELECT 10, 105, DATE '2021-01-05', DATE '2021-01-07' FROM DUAL UNION ALL
SELECT 10, 100, DATE '2021-01-08', DATE '2021-01-10' FROM DUAL UNION ALL
SELECT 10, 100, DATE '2021-01-11', DATE '2021-01-17' FROM DUAL UNION ALL
SELECT 10, 100, DATE '2021-01-18', DATE '2021-01-21' FROM DUAL UNION ALL
SELECT 10, 110, DATE '2021-01-22', null FROM DUAL;
Outputs:
ID
VAL
EFF_DATE
END_DATE
10
100
2021-01-01 00:00:00
2021-01-04 00:00:00
10
105
2021-01-05 00:00:00
2021-01-07 00:00:00
10
100
2021-01-08 00:00:00
2021-01-21 00:00:00
10
110
2021-01-22 00:00:00
null
From Oracle 12, you can use MATCH_RECOGNIZE to perform row-by-row processing:
SELECT *
FROM table_name t
MATCH_RECOGNIZE(
PARTITION BY id
ORDER BY eff_date
MEASURES
FIRST(val) AS val,
FIRST(eff_date) AS eff_date,
LAST(end_date) AS end_date
PATTERN (same_val+)
DEFINE same_val AS FIRST(val) = val
)
Which has the same output and is likely to be more efficient.
fiddle

Is there a way to group values getting only the ones that verifies certain condition?

I'm trying to write a query in Oracle SQL that aggregates values by some ids, where I have the following table as an input:
ID
SOME_DATE
RANK_POSITION
301
20211201
1
301
20211202
2
301
20211203
3
649
20211201
1
649
20211202
2
649
20211206
3
649
20211208
4
649
20211211
5
758
20211212
1
758
20211222
2
And y want to obtain something like this:
ID
FIRST_IN_RANK_DATE
SECOND_IN_RANK_DATE
301
01/12/2021
02/12/2021
649
01/12/2021
02/12/2021
758
12/12/2021
22/12/2021
Where FIRST_IN_RANK_DATE, is the date that is the first in the RANK_POSITION for the ID, and SECOND_IN_RANK_DATE is the date that is second in RANK_POSITION for the specific ID.
You can use conditional aggregation:
SELECT id,
MAX(CASE rank_position WHEN 1 THEN some_date END) AS first_in_rank_date,
MAX(CASE rank_position WHEN 2 THEN some_date END) AS second_in_rank_date
FROM table_name
GROUP BY id
Or PIVOT:
SELECT *
FROM table_name
PIVOT (
MAX(some_date)
FOR rank_position IN (
1 AS first_in_rank_date,
2 AS second_in_rank_date
)
)
Or, from Oracle 12, MATCH_RECOGNIZE:
SELECT *
FROM table_name
MATCH_RECOGNIZE (
PARTITION BY id
ORDER BY rank_position
MEASURES
rank1.some_date AS first_in_rank_date,
rank2.some_date AS second_in_rank_date
PATTERN ( ^ rank1 rank2 )
DEFINE
rank1 AS rank_position = 1,
rank2 AS rank_position = 2
)
Which, for the sample data:
CREATE TABLE table_name (ID, SOME_DATE, RANK_POSITION) AS
SELECT 301, DATE '2021-12-01', 1 FROM DUAL UNION ALL
SELECT 301, DATE '2021-12-02', 2 FROM DUAL UNION ALL
SELECT 301, DATE '2021-12-03', 3 FROM DUAL UNION ALL
SELECT 649, DATE '2021-12-01', 1 FROM DUAL UNION ALL
SELECT 649, DATE '2021-12-02', 2 FROM DUAL UNION ALL
SELECT 649, DATE '2021-12-06', 3 FROM DUAL UNION ALL
SELECT 649, DATE '2021-12-08', 4 FROM DUAL UNION ALL
SELECT 649, DATE '2021-12-11', 5 FROM DUAL UNION ALL
SELECT 758, DATE '2021-12-12', 1 FROM DUAL UNION ALL
SELECT 758, DATE '2021-12-22', 2 FROM DUAL;
All output:
ID
FIRST_IN_RANK_DATE
SECOND_IN_RANK_DATE
301
2021-12-01 00:00:00
2021-12-02 00:00:00
649
2021-12-01 00:00:00
2021-12-02 00:00:00
758
2021-12-12 00:00:00
2021-12-22 00:00:00
db<>fiddle here

ORACLE SQL: Create new row on the basis of date range

For example, I am having a table name test_cross_months and the data is as below :
id
start_date
end_date
44
2020-01-04
2020-01-04
44
2020-01-30
2020-02-10
44
2020-02-27
2020-03-03
Expected result:
id
start_date
end_date
44
2020-01-04
2020-01-04
44
2020-01-30
2020-01-31
44
2020-02-01
2020-02-10
44
2020-02-27
2020-02-29
44
2020-03-01
2020-03-03
So for
|44|2020-01-30 |2020-02-10|
there should be two rows that are from 30-Jan-2020 to 31-Jan-2020 and 1-Feb-2020 to 10-Feb-2020
I tried by comparing the end date with the last day for the start_date but facing issues as a new row is not getting created for the end_date range.
Could any please suggest a solution?
You can use a recursive query (which will work regardless of how many months your ranges span):
WITH months ( id, start_date, end_date, final_date ) AS (
SELECT id,
start_date,
LEAST( LAST_DAY( start_date ), end_date ),
end_date
FROM table_name
UNION ALL
SELECT id,
end_date + INTERVAL '1' DAY,
LEAST( ADD_MONTHS( end_date, 1 ), final_date ),
final_date
FROM months
WHERE end_date < final_date
)
SEARCH DEPTH FIRST BY final_date SET dt_order
SELECT id,
start_date,
end_date
FROM months;
Which, for the sample data:
CREATE TABLE table_name (id, start_date, end_date) AS
SELECT 44, DATE '2020-01-04', DATE '2020-01-04' FROM DUAL UNION ALL
SELECT 44, DATE '2020-01-30', DATE '2020-02-10' FROM DUAL UNION ALL
SELECT 44, DATE '2020-02-27', DATE '2020-03-03' FROM DUAL;
Outputs:
ID
START_DATE
END_DATE
44
2020-01-04 00:00:00
2020-01-04 00:00:00
44
2020-01-30 00:00:00
2020-01-31 00:00:00
44
2020-02-01 00:00:00
2020-02-10 00:00:00
44
2020-02-27 00:00:00
2020-02-29 00:00:00
44
2020-03-01 00:00:00
2020-03-03 00:00:00
db<>fiddle here
Using a table of numbers and date arithmetic
-- example of table of numbers
with nmbrs(n) as(
select 0 from dual union all
select 1 from dual union all
select 2 from dual
)
select t.id,
case when n=0 then t.start_date else trunc(t.start_date, 'MM') + NUMTOYMINTERVAL(n, 'MONTH') end s,
case when n=MONTHS_BETWEEN(last_day(t.end_date), last_day(t.start_date)) then t.end_date
else last_day(t.start_date + NUMTOYMINTERVAL(n, 'MONTH')) end e
from test_cross_months t
join nmbrs on nmbrs.n <= MONTHS_BETWEEN(last_day(t.end_date), last_day(t.start_date))
order by t.id, s
db<>fiddle

How to do a query on Oracle SQL to get time intervals, grouping by specific fields

I love a good challenge, but this one has been breaking my head for too long. :)
I'm trying to build a query to get dates intervals, grouping the information by one field.
Let me try to explain it in a simple way.
We have this table:
I need to get the intervals a soldier spent on each ranking, so the end result I need to get should be something like this:
As you can see the soldier can be promoted/demoted along the time.
Any suggestion on how to build a query to do this?
THANK YOU!
From Oracle 12, you can use MATCH_RECOGNIZE:
SELECT *
FROM table_name
MATCH_RECOGNIZE (
PARTITION BY id
ORDER BY start_date, end_date
MEASURES
FIRST( name ) AS name,
FIRST( ranking ) AS ranking,
FIRST( start_date ) AS start_date,
LAST( end_Date ) AS end_Date
PATTERN ( same_rank+ )
DEFINE same_rank AS FIRST( ranking ) = ranking
)
Which, for the sample data:
CREATE TABLE table_name ( id, name, ranking, start_date, end_date ) AS
SELECT 1001, 'Jones', 'Lieutenant', DATE '2000-03-20', DATE '2002-08-15' FROM DUAL UNION ALL
SELECT 1001, 'Jones', 'Lieutenant', DATE '2002-08-16', DATE '2003-03-18' FROM DUAL UNION ALL
SELECT 1001, 'Jones', 'Lieutenant', DATE '2003-03-19', DATE '2004-06-01' FROM DUAL UNION ALL
SELECT 1001, 'Jones', 'Lieutenant', DATE '2004-06-02', DATE '2004-10-01' FROM DUAL UNION ALL
SELECT 1001, 'Jones', 'Captain', DATE '2004-10-02', DATE '2005-04-20' FROM DUAL UNION ALL
SELECT 1001, 'Jones', 'Captain', DATE '2005-04-21', DATE '2007-02-20' FROM DUAL UNION ALL
SELECT 1001, 'Jones', 'Major', DATE '2007-02-21', DATE '2008-10-22' FROM DUAL UNION ALL
SELECT 1001, 'Jones', 'Major', DATE '2008-10-23', DATE '2010-01-26' FROM DUAL UNION ALL
SELECT 1001, 'Jones', 'Captain', DATE '2010-01-27', DATE '2013-11-25' FROM DUAL UNION ALL
SELECT 1001, 'Jones', 'Captain', DATE '2013-11-26', DATE '2014-05-11' FROM DUAL UNION ALL
SELECT 1001, 'Jones', 'Major', DATE '2014-05-12', DATE '2016-04-22' FROM DUAL UNION ALL
SELECT 1001, 'Jones', 'General', DATE '2016-04-23', DATE '2020-10-10' FROM DUAL UNION ALL
SELECT 1001, 'Jones', 'General', DATE '2020-10-11', DATE '2020-11-30' FROM DUAL;
Outputs:
ID | NAME | RANKING | START_DATE | END_DATE
---: | :---- | :--------- | :------------------ | :------------------
1001 | Jones | Lieutenant | 2000-03-20 00:00:00 | 2004-10-01 00:00:00
1001 | Jones | Captain | 2004-10-02 00:00:00 | 2007-02-20 00:00:00
1001 | Jones | Major | 2007-02-21 00:00:00 | 2010-01-26 00:00:00
1001 | Jones | Captain | 2010-01-27 00:00:00 | 2014-05-11 00:00:00
1001 | Jones | Major | 2014-05-12 00:00:00 | 2016-04-22 00:00:00
1001 | Jones | General | 2016-04-23 00:00:00 | 2020-11-30 00:00:00
db<>fiddle here
This is a type of gaps and islands problem. You want to find groups of rows that are the same, which you can do using lag() to compare the ranking and then a cumulative sum to keep track of the changes:
select soldier_id, soldier_name, ranking,
min(start_date), max(end_date)
from (select t.*,
sum(case when prev_end_date = start_date - interval '1' day then 0 else 1 end)
(partition by soldier_id order by start_date) as island
from (select t.*,
lag(end_date) over (partition by soldier_id, ranking order by start_date) as prev_end_date
from t
) t
) t
group by soldier_id, soldier_name, ranking, island;
Note: This assumes that the soldier_name does not change over time for a given soldier. If that is something you need to deal with, then ask a new question with appropriate sample data and desired results.

Dense_rank query in sql(4 different columns) in

I have a table as follows:
Sn no. t_time Value rate
ABC 17-MAY-18 08:00:00 100.00 3
ABC 17-MAY-18 22:00:00 200.00 1
ABC 16-MAY-18 08:00:00 100.00 1
XYZ 14-MAY-18 01:00:00 700.00 1
XYZ 15-MAY-18 10:00:00 500.00 2
XYZ 15-MAY-18 13:00:00 100.00 2
And I want to generate the output as follows:
Sn no. New_value
ABC 150
XYZ 450
It is grouped by the Sn no. The New_value is the latest time of each date value multiplied by rate, and then averaged together.
For example ABC new_value is
Average of:[(100*1) and (200*1)]
Its a large dataset. How do I write a query for the above in the most efficient way. Please help.
You can use analytical function(row_number()) to achieve the result
SQL> WITH cte_table(Snno, t_time, Value, rate) AS (
2 SELECT 'ABC', to_date('2018-05-17 08:00:00', 'YYYY-MM-DD HH24:MI:SS'), 100.00, 3 FROM DUAL UNION ALL
3 SELECT 'ABC', to_date('2018-05-17 22:00:00', 'YYYY-MM-DD HH24:MI:SS'), 200.00, 1 FROM DUAL UNION ALL
4 SELECT 'ABC', to_date('2018-05-16 08:00:00', 'YYYY-MM-DD HH24:MI:SS'), 100.00, 1 FROM DUAL UNION ALL
5 SELECT 'XYZ', to_date('2018-05-14 01:00:00', 'YYYY-MM-DD HH24:MI:SS'), 700.00, 1 FROM DUAL UNION ALL
6 SELECT 'XYZ', to_date('2018-05-15 10:00:00', 'YYYY-MM-DD HH24:MI:SS'), 500.00, 2 FROM DUAL UNION ALL
7 SELECT 'XYZ', to_date('2018-05-15 13:00:00', 'YYYY-MM-DD HH24:MI:SS'), 100.00, 2 FROM DUAL),
8 --------------------------------
9 -- End of data preparation
10 --------------------------------
11 rn_table AS (
12 SELECT t.*, row_number() OVER (PARTITION BY TRUNC(t_time) ORDER BY t_time DESC) AS rn
13 FROM cte_table t)
14 SELECT snno,
15 AVG(VALUE * rate) new_value
16 FROM rn_table
17 WHERE rn = 1
18 GROUP BY snno;
Output:
SNNO NEW_VALUE
---- ----------
ABC 150
XYZ 450
Use the ROW_NUMBER (or RANK/DENSE_RANK if it is more appropriate) analytic function in a sub-query and then aggregate in the outer query:
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE table_name ( Snno, t_time, Value, rate ) AS
SELECT 'ABC', TIMESTAMP '2018-05-17 08:00:00', 100.00, 3 FROM DUAL UNION ALL
SELECT 'ABC', TIMESTAMP '2018-05-17 22:00:00', 200.00, 1 FROM DUAL UNION ALL
SELECT 'ABC', TIMESTAMP '2018-05-16 08:00:00', 100.00, 1 FROM DUAL UNION ALL
SELECT 'XYZ', TIMESTAMP '2018-05-14 01:00:00', 700.00, 1 FROM DUAL UNION ALL
SELECT 'XYZ', TIMESTAMP '2018-05-15 10:00:00', 500.00, 2 FROM DUAL UNION ALL
SELECT 'XYZ', TIMESTAMP '2018-05-15 13:00:00', 100.00, 2 FROM DUAL;
Query 1:
SELECT snno,
AVG( value * rate ) As new_value
FROM (
SELECT t.*,
ROW_NUMBER() OVER (
PARTITION BY snno, value
ORDER BY t_time DESC
) AS rn
FROM table_name t
)
WHERE rn = 1
GROUP BY snno
Results:
| SNNO | NEW_VALUE |
|------|-------------------|
| ABC | 250 |
| XYZ | 633.3333333333334 |