Split records based on date in sql - sql

ID EFF_DT END_DT
FLA1 2018-01-01 00:00:00 2019-12-31 00:00:00
FLA1 2020-01-01 00:00:00 9999-12-31 00:00:00
The above structure needs to be splited. And the split should be based on the date.
the output should have additional column as year
ID EFF_DT END_DT YEAR
FLA1 2018-01-01 00:00:00 2019-12-31 00:00:00 2019
FLA1 2020-01-01 00:00:00 2020-12-31 00:00:00 2020
FLA1 2021-01-01 00:00:00 9999-12-31 00:00:00 2021
I am using union for this purpose and it is generating duplicates. Any other approach / refine solution will work. Thanks in advance.

You can use a recursive sub-query factoring clause:
WITH split ( ID, EFF_DT, END_DT, MAX_DT ) AS (
SELECT id,
eff_dt,
LEAST(
ADD_MONTHS( TRUNC( SYSDATE, 'YY' ), 12 ) - INTERVAL '1' DAY,
end_dt
),
end_dt
FROM table_name
UNION ALL
SELECT id,
end_dt + INTERVAL '1' DAY,
max_dt,
max_dt
FROM split
WHERE end_dt < max_dt
)
SELECT id,
eff_dt,
end_dt
FROM split;
Which, for your sample data:
CREATE TABLE table_name ( ID, EFF_DT, END_DT ) AS
SELECT 'FLA1', DATE '2018-01-01', DATE '2019-12-31' FROM DUAL UNION ALL
SELECT 'FLA1', DATE '2020-01-01', DATE '9999-12-31' FROM DUAL;
Outputs:
ID | EFF_DT | END_DT
:--- | :------------------ | :------------------
FLA1 | 2018-01-01 00:00:00 | 2019-12-31 00:00:00
FLA1 | 2020-01-01 00:00:00 | 2020-12-31 00:00:00
FLA1 | 2021-01-01 00:00:00 | 9999-12-31 00:00:00
db<>fiddle here

If you want to generate all years of data, then:
with cte (id, eff_dt, end_dt, orig_end_dt)
select id, eff_dt, end_dt, end_dt
from t
union all
select cte.id, end_dt + interval '1' day,
least(orig_end_dte, trunc(end_dt, 'YYYY') + interval '1' year
from cte
where trunc(eff_dt, 'YYYY') < trunc(end_dt, 'YYYY')
)
select id, eff_dt, end_dt, to_char(end_dt, 'YYYY') as year
from cte;
Note: This produces a separate row for every year in the period.
If you want a limit on the year, then it would be something like this:
with cte (id, eff_dt, end_dt, orig_end_dt)
select id, eff_dt, end_dt, end_dt
from t
union all
select cte.id, end_dt + interval '1' day,
least(orig_end_dte, trunc(end_dt, 'YYYY') + interval '1' year
from cte
where trunc(eff_dt, 'YYYY') < least(trunc(end_dt, 'YYYY'), date '2021-01-01')
)
select id, eff_dt,
(case when end_dt = date '2021-12-31' then orig_end_dt else end_dt end),
to_char(end_dt, 'YYYY') as year
from cte;

Related

How to fill date range gaps Oracle SQL

With a given dataset:
WITH ranges AS (
select to_date('01.01.2021 00:00:00','DD.MM.YYYY hh24:mi:ss') date_from,
to_date('31.03.2021 00:00:00','DD.MM.YYYY hh24:mi:ss') date_to
from dual
union
select to_date('27.03.2021 00:00:00','DD.MM.YYYY hh24:mi:ss') date_from,
to_date('27.04.2021 00:00:00','DD.MM.YYYY hh24:mi:ss') date_to
from dual
union
select to_date('01.05.2021 00:00:00','DD.MM.YYYY hh24:mi:ss') date_from,
to_date('31.12.2021 00:00:00','DD.MM.YYYY hh24:mi:ss') date_to
from dual
)
SELECT * FROM ranges;
How to find the gap 28.04.2021-30.04.2021.? Also consider that there can be multiple gaps in between and ranges can overlap.
Any suggestion?
Try this query, tune to your needs:
WITH steps AS (
SELECT date_from as dt, 1 as step FROM ranges
UNION ALL
SELECT date_to as dt, -1 as step FROM ranges
)
SELECT dt as dt_from,
lead(dt) over (order by dt) as dt_to,
sum(step) over (order by dt) as cnt_ranges
FROM steps;
dt_from | dt_to | cnt_ranges
------------------------+-------------------------+-----------
2021-01-01 00:00:00.000 | 2021-03-27 00:00:00.000 | 1
2021-03-27 00:00:00.000 | 2021-03-31 00:00:00.000 | 2
2021-03-31 00:00:00.000 | 2021-04-27 00:00:00.000 | 1
2021-04-27 00:00:00.000 | 2021-05-01 00:00:00.000 | 0
2021-05-01 00:00:00.000 | 2021-12-31 00:00:00.000 | 1
2021-12-31 00:00:00.000 | | 0
You are modeling date ranges incorrectly; an interval ending at midnight on 02-14-2021, for example, should not include 02-14-2021. In your model it does.
This leads to unnecessary complications in all the queries you write against your model. In the solution below I need to add 1 to end dates first, do all the processing, and then subtract 1 at the end.
with
ranges (date_from, date_to) as (
select to_date('01.01.2021 00:00:00','DD.MM.YYYY hh24:mi:ss'),
to_date('31.03.2021 00:00:00','DD.MM.YYYY hh24:mi:ss')
from dual
union all
select to_date('27.03.2021 00:00:00','DD.MM.YYYY hh24:mi:ss'),
to_date('27.04.2021 00:00:00','DD.MM.YYYY hh24:mi:ss')
from dual
union all
select to_date('01.05.2021 00:00:00','DD.MM.YYYY hh24:mi:ss'),
to_date('31.12.2021 00:00:00','DD.MM.YYYY hh24:mi:ss')
from dual
)
select first_missing, last_missing - 1 as last_missing
from (
select dt as first_missing,
lead(df) over (order by dt) as last_missing
from (select date_from, date_to + 1 as date_to from ranges)
match_recognize(
order by date_from
measures first(date_from) as df, max(date_to) as dt
pattern (a* b)
define a as max(date_to) >= next (date_from)
)
)
where last_missing is not null
;
FIRST_MISSING LAST_MISSING
------------------- -------------------
28.04.2021 00:00:00 30.04.2021 00:00:00

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

Row for each date from start date to end date

What I'm trying to do is take a record that looks like this:
Start_DT End_DT ID
4/5/2013 4/9/2013 1
and change it to look like this:
DT ID
4/5/2013 1
4/6/2013 1
4/7/2013 1
4/8/2013 1
4/9/2013 1
it can be done in Python but I am not sure if it is possible with SQL Oracle? I am having difficult time making this work. Any help would be appreciated.
Thanks
Use a recursive subquery-factoring clause:
WITH ranges ( start_dt, end_dt, id ) AS (
SELECT start_dt, end_dt, id
FROM table_name
UNION ALL
SELECT start_dt + INTERVAL '1' DAY, end_dt, id
FROM ranges
WHERE start_dt + INTERVAL '1' DAY <= end_dt
)
SELECT start_dt,
id
FROM ranges;
Which for your sample data:
CREATE TABLE table_name ( start_dt, end_dt, id ) AS
SELECT DATE '2013-04-05', DATE '2013-04-09', 1 FROM DUAL
Outputs:
START_DT | ID
:------------------ | -:
2013-04-05 00:00:00 | 1
2013-04-06 00:00:00 | 1
2013-04-07 00:00:00 | 1
2013-04-08 00:00:00 | 1
2013-04-09 00:00:00 | 1
db<>fiddle here
connect by level is useful for these problems. suppose the first CTE named "table_DT" is your table name so you can use the select statement after that.
with table_DT as (
select
to_date('4/5/2013','mm/dd/yyyy') as Start_DT,
to_date('4/9/2013', 'mm/dd/yyyy') as End_DT,
1 as ID
from dual
)
select
Start_DT + (level-1) as DT,
ID
from table_DT
connect by level <= End_DT - Start_DT +1
;

Oracle - Find monthly, quarterly and yearly dates

I have a table:
table1
start end
1/jan/2012 15/jan/2012
1/feb/2013 5/april/2013
I need to find all the possible monthly, quarterly and yearly timeframes. For ex.
1)
1/jan/2012 15/jan/2012
will fall between:
1/jan/2012 31/jan/2012
1/jan/2012 31/march/2012
1/jan/2012 31/dec/2012
2)
1/feb/2013 5/april/2013
will fall between:
1/feb/2013 28/feb/2013
1/march/2013 31/march/2013
1/april/2013 30/april/2013
1/jan/2013 31/march/2013
1/april/2013 30/june/2013
1/jan/2013 31/dec/2013
Is it possible to do it through SQL query to get all the possible date combinations?
Hope it helps:
-- test data
with table1 as
(select 1 as id,
to_date('20120101', 'YYYYMMDD') as start_dt,
to_date('20120115', 'YYYYMMDD') as end_dt
from dual
union all
select 2 as id,
to_date('20130201', 'YYYYMMDD') as start_dt,
to_date('20130405', 'YYYYMMDD') as end_dt
from dual),
-- get sequences in range [0..max date interval-1]
idx_tab as
(select level - 1 as idx
from dual
connect by level < (select max(end_dt - start_dt) from table1)),
-- expand interval [start_dt; end_dt] by day
dt_tb as
(select t.id, t.start_dt, t.end_dt, t.start_dt + i.idx as dt
from table1 t, idx_tab i
where t.start_dt + idx <= t.end_dt)
select 'Month-' || to_char(dt, 'YYYY-MM'), id, start_dt, end_dt
from dt_tb
union
select 'Quarter-' || to_char(dt, 'YYYY-Q'), id, start_dt, end_dt
from dt_tb
union
select 'Year-' || to_char(dt, 'YYYY'), id, start_dt, end_dt
from dt_tb
order by 1, 2;
WITH date_range AS (
SELECT TO_DATE('2012-01-01', 'YYYY-MM-DD') AS start_date, TO_DATE('2012-01-15', 'YYYY-MM-DD') AS end_date FROM DUAL
UNION
SELECT TO_DATE('2012-02-01', 'YYYY-MM-DD') AS start_date, TO_DATE('2012-04-05', 'YYYY-MM-DD') AS end_date FROM DUAL
), monthly_range AS (
SELECT dr.start_date
, dr.end_date
, 'Monthly' AS range_type
, ADD_MONTHS(TRUNC(dr.start_date, 'MM'), LEVEL - 1) AS month_start
, ADD_MONTHS(LAST_DAY(dr.start_date), LEVEL - 1) AS month_end
FROM date_range dr
CONNECT BY LEVEL <= CEIL(MONTHS_BETWEEN(dr.end_date, dr.start_date))
), quarterly_range AS (
SELECT
dr.start_date
, dr.end_date
, 'Quarterly' AS range_type
, ADD_MONTHS(TRUNC(dr.start_date, 'MM'), (LEVEL - 1) * 3) AS range_start
, ADD_MONTHS(TRUNC(dr.start_date, 'MM'), LEVEL * 3) - 1 AS range_end
FROM date_range dr
CONNECT BY LEVEL <= CEIL(MONTHS_BETWEEN(dr.end_date, dr.start_date)/3)
), yearly_range AS (
SELECT
dr.start_date
, dr.end_date
, 'Yearly' AS range_type
, ADD_MONTHS(TRUNC(dr.start_date, 'MM'), (LEVEL - 1) * 12) AS range_start
, ADD_MONTHS(TRUNC(dr.start_date, 'MM'), LEVEL * 12) - 1 AS range_end
FROM date_range dr
CONNECT BY LEVEL <= CEIL(MONTHS_BETWEEN(dr.end_date, dr.start_date)/12)
)
SELECT mr.* FROM monthly_range mr
UNION
SELECT qr.* FROM quarterly_range qr
UNION
SELECT yr.* FROM yearly_range yr
ORDER BY 1,2,3,4;
SQL Fiddle
Query 1:
WITH dates ( date_start, date_end ) AS (
SELECT DATE '2013-02-01', DATE '2013-04-05' FROM DUAL
)
SELECT 'M' AS period,
ADD_MONTHS( TRUNC( date_start, 'MM' ), LEVEL - 1 ) AS range_start,
ADD_MONTHS( TRUNC( date_start, 'MM' ), LEVEL ) - INTERVAL '1' DAY AS range_end
FROM dates
CONNECT BY
ADD_MONTHS( TRUNC( date_start, 'MM' ), LEVEL - 1 ) <= TRUNC( date_end, 'MM' )
UNION ALL
SELECT 'Q' AS period,
ADD_MONTHS( TRUNC( date_start, 'Q' ), 3 * ( LEVEL - 1) ) AS range_start,
ADD_MONTHS( TRUNC( date_start, 'Q' ), 3 * LEVEL ) - INTERVAL '1' DAY AS range_end
FROM dates
CONNECT BY
ADD_MONTHS( TRUNC( date_start, 'Q' ), 3 * (LEVEL - 1) ) <= TRUNC( date_end, 'Q' )
UNION ALL
SELECT 'Y' AS period,
ADD_MONTHS( TRUNC( date_start, 'Y' ), 12 * ( LEVEL - 1) ) AS range_start,
ADD_MONTHS( TRUNC( date_start, 'Y' ), 12 * LEVEL ) - INTERVAL '1' DAY AS range_end
FROM dates
CONNECT BY
ADD_MONTHS( TRUNC( date_start, 'Y' ), 12 * (LEVEL - 1) ) <= TRUNC( date_end, 'Y' )
Results:
| PERIOD | RANGE_START | RANGE_END |
|--------|----------------------------|----------------------------|
| M | February, 01 2013 00:00:00 | February, 28 2013 00:00:00 |
| M | March, 01 2013 00:00:00 | March, 31 2013 00:00:00 |
| M | April, 01 2013 00:00:00 | April, 30 2013 00:00:00 |
| Q | January, 01 2013 00:00:00 | March, 31 2013 00:00:00 |
| Q | April, 01 2013 00:00:00 | June, 30 2013 00:00:00 |
| Y | January, 01 2013 00:00:00 | December, 31 2013 00:00:00 |

Date between with only the days and month

I have this date :
Date1 = 14/10/2015
Date2 = 01/10/2011
Date3 = 01/11/2011
I'm trying to make this req :
Date1 between date2 and date3
How can i make this without paying attention to the years (only sql (oracle)).
The req should be true.
Thanks
In Oracle you can get first day of month for date1 and date2 and last day of month for date3 then use between, something like:
WHERE TRUNC(date1, 'MONTH') BETWEEN TRUNC(date2, 'MONTH') AND LAST_DAY(TO_DATE(date3,'MM/DD/YYYY'))
As an alternative you could just cast the numeric representation of MMDD to a number, and check if this number is in a specific range.
select *
from (select to_date('14/10/2015', 'DD/MM/YYYY') as datee from dual union
select to_date('01/10/2011', 'DD/MM/YYYY') as datee from dual union
select to_date('01/11/2011', 'DD/MM/YYYY') as datee from dual)
where to_number(to_char(datee,'MMDD')) between 1014 and 1131
In Oracle you can use the EXTRACT function to get individual parts of the date and use that to compare:
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE TEST( Date1 ) AS
SELECT DATE '2015-01-01' FROM DUAL
UNION ALL SELECT DATE '2015-01-15' FROM DUAL
UNION ALL SELECT DATE '2015-02-01' FROM DUAL
UNION ALL SELECT DATE '2015-09-01' FROM DUAL
UNION ALL SELECT DATE '2014-01-10' FROM DUAL
UNION ALL SELECT DATE '2014-01-20' FROM DUAL
UNION ALL SELECT DATE '2014-02-02' FROM DUAL
UNION ALL SELECT DATE '2013-01-14' FROM DUAL
Query 1:
WITH Dates ( Date2, Date3 ) AS (
SELECT DATE '2015-01-10', DATE '2015-02-01' FROM DUAL
)
SELECT t.*
FROM TEST t
CROSS JOIN Dates d
WHERE ( EXTRACT( MONTH FROM Date1 ) > EXTRACT( MONTH FROM Date2 )
OR ( EXTRACT( MONTH FROM Date1 ) = EXTRACT( MONTH FROM Date2 )
AND EXTRACT( DAY FROM Date1 ) >= EXTRACT( DAY FROM Date2 )
)
)
AND ( EXTRACT( MONTH FROM Date1 ) < EXTRACT( MONTH FROM Date3 )
OR ( EXTRACT( MONTH FROM Date1 ) = EXTRACT( MONTH FROM Date3 )
AND EXTRACT( DAY FROM Date1 ) <= EXTRACT( DAY FROM Date3 )
)
)
Results:
| DATE1 |
|----------------------------|
| January, 15 2015 00:00:00 |
| February, 01 2015 00:00:00 |
| January, 10 2014 00:00:00 |
| January, 20 2014 00:00:00 |
| January, 14 2013 00:00:00 |
Use the DDD format string to get the number of the day of the year, i.e.:
select to_char(to_date('14/10/2015','DD/MM/YYYY'),'DDD') d1
,to_char(to_date('01/10/2011','DD/MM/YYYY'),'DDD') d2
,to_char(to_date('01/11/2011','DD/MM/YYYY'),'DDD') d3
from dual;
D1 D2 D3
=== === ===
287 274 305
with params as (
select to_date('14/10/2015','DD/MM/YYYY') d1
,to_date('01/10/2011','DD/MM/YYYY') d2
,to_date('01/11/2011','DD/MM/YYYY') d3
from dual)
select 'Yes' a from params
where to_char(d1,'DDD') between to_char(d2,'DDD') and to_char(d3,'DDD');
A
===
Yes