oracle sql query months - sql

I'm fairly new to SQL and I have a question regarding a query.
I would like to use a "with-clause" to create a temporary table that displays all months between two dates.
I will then use this temporary table to apply a filter to my actual table.
My query:
WITH monlist(mon, next_mon) as (
select add_months(trunc(to_date(:P6_DATUM_VON,'DD-MM-YYYY'), 'MM'), level - 1), add_months(trunc(to_date(:P6_DATUM_VON,'DD-MM-YYYY'), 'MM'), level)
from dual
connect by level <= (months_between(to_date(:P6_DATUM_BIS,'DD-MM-YYYY'), to_date(:P6_DATUM_VON,'DD-MM-YYYY')))
The problem is that I want to narrow it down to days, i.e. if I want to start from 05-01-2022 (DD-MM-YYY), this is not possible.
The temporary table starts with the beginning of the month (01-01-2022).
It is similar with the end - it is rounded up to the full month.
Is there a way to change the query so that the initial and final values are accepted correctly?
I would like to get the following output:
mon next_mon
05-01.2022 - 01-02-2022
01-02-2022 - 28-02-2022
...
01-10-2022 - 15-10-2022 (If end-date is 15-10-2022)

If it were PL/SQL, you'd use IF-THEN-ELSE; in SQL, use CASE expression. Something like this (example ran in SQL*Plus, hence substitution variables):
SQL> alter session set nls_date_format = 'dd-mm-yyyy';
Session altered.
SQL> set ver off
SQL>
SQL> WITH monlist as
2 (select add_months(trunc(to_date('&&P6_DATUM_VON','DD-MM-YYYY'), 'MM'), level - 1) mon,
3 add_months(trunc(to_date('&&P6_DATUM_VON','DD-MM-YYYY'), 'MM'), level) next_mon,
4 level lvl
5 from dual
6 connect by level <= months_between(to_date('&&P6_DATUM_BIS','DD-MM-YYYY'),
7 to_date('&&P6_DATUM_VON','DD-MM-YYYY')
8 ) + 1
9 )
10 select case when lvl = 1
11 then to_date('&&P6_DATUM_VON','DD-MM-YYYY') else mon end mon,
12 case when lvl = (select max(lvl) from monlist)
13 then to_date('&&P6_DATUM_BIS','DD-MM-YYYY') else next_mon end next_mon
14 from monlist
15 order by lvl;
Enter value for p6_datum_von: 05-01-2022
Enter value for p6_datum_bis: 15-10-2022
MON NEXT_MON
---------- ----------
05-01-2022 01-02-2022
01-02-2022 01-03-2022
01-03-2022 01-04-2022
01-04-2022 01-05-2022
01-05-2022 01-06-2022
01-06-2022 01-07-2022
01-07-2022 01-08-2022
01-08-2022 01-09-2022
01-09-2022 01-10-2022
01-10-2022 15-10-2022
10 rows selected.
SQL>
In Apex (you do use it, right? Regarding :P6_... item names?), that would be just
WITH monlist as
(select add_months(trunc(to_date(:P6_DATUM_VON,'DD-MM-YYYY'), 'MM'), level - 1) mon,
add_months(trunc(to_date(:P6_DATUM_VON,'DD-MM-YYYY'), 'MM'), level) next_mon,
level lvl
from dual
connect by level <= months_between(to_date(:P6_DATUM_BIS,'DD-MM-YYYY'),
to_date(:P6_DATUM_VON,'DD-MM-YYYY')
) + 1
)
select case when lvl = 1
then to_date(:P6_DATUM_VON,'DD-MM-YYYY') else mon end mon,
case when lvl = (select max(lvl) from monlist)
then to_date(:P6_DATUM_BIS,'DD-MM-YYYY') else next_mon end next_mon
from monlist
order by lvl;

Related

Generate a range of records depending on from-to dates

I have a table of records like this:
Item
From
To
A
2018-01-03
2018-03-16
B
2021-05-25
2021-11-10
The output of select should look like:
Item
Month
Year
A
01
2018
A
02
2018
A
03
2018
B
05
2021
B
06
2021
B
07
2021
B
08
2021
Also the range should not exceed the current month. In example above we are asuming current day is 2021-08-01.
I am trying to do something similar to THIS with CONNECT BY LEVEL but as soon as I also select my table next to dual and try to order the records the selection never completes. I also have to join few other tables to the selection but I don't think that would make a difference.
I would very much appreciate your help.
Row generator it is, but not as you did it; most probably you're missing lines #11 - 16 in my query (or their alternative).
SQL> with test (item, date_from, date_to) as
2 -- sample data
3 (select 'A', date '2018-01-03', date '2018-03-16' from dual union all
4 select 'B', date '2021-05-25', date '2021-11-10' from dual
5 )
6 -- query that returns desired result
7 select item,
8 extract(month from (add_months(date_from, column_value - 1))) month,
9 extract(year from (add_months(date_from, column_value - 1))) year
10 from test cross join
11 table(cast(multiset
12 (select level
13 from dual
14 connect by level <=
15 months_between(trunc(least(sysdate, date_to), 'mm'), trunc(date_from, 'mm')) + 1
16 ) as sys.odcinumberlist))
17 order by item, year, month;
ITEM MONTH YEAR
----- ---------- ----------
A 1 2018
A 2 2018
A 3 2018
B 5 2021
B 6 2021
B 7 2021
B 8 2021
7 rows selected.
SQL>
Recursive CTEs are the standard SQL approach to this type of problem. In Oracle, this looks like:
with cte(item, fromd, tod) as (
select item, fromd, tod
from t
union all
select item, add_months(fromd, 1), tod
from cte
where add_months(fromd, 1) < last_day(tod)
)
select item, extract(year from fromd) as year, extract(month from fromd) as month
from cte
order by item, fromd;
Here is a db<>fiddle.

Expand date range to get all dates between series of date ranges from a table with excluded dates

I need to get all dates between DATE_FROM and DATE_TO of every ID of table LEAVE excluding weekends, work suspensions and holidays. Considering this record (ID, DATE_FROM, DATE_TO):
001 04-OCT-2018 09-OCT-2018
002 05-OCT-2018 05-OCT-2018
...
n 01-OCT-2018 05-OCT-2018
I need to get all the dates between those ranges in this format (ID, DAY_TOKEN):
001 04-OCT-2018
001 05-OCT-2018
001 08-OCT-2018
001 09-OCT-2018
002 05-OCT-2018
...
n 01-OCT-2018
n 02-OCT-2018
n 03-OCT-2018
n 04-OCT-2018
n 05-OCT-2018
I am using this query modified from the queries I found:
SELECT ID, a.date_from + rnum - 1 AS day_token
FROM (SELECT a.ID, a.date_from, a.date_to, ROWNUM AS rnum
FROM all_objects, leave a
-- Aside from ALL_OBJECT, I cross join it with my LEAVE table
WHERE ROWNUM <= a.date_to - a.date_from + 1) a
WHERE TO_CHAR (a.date_from + rnum - 1, 'DY') NOT IN ('SAT', 'SUN');
AND NOT EXISTS (SELECT 1
FROM holiday b
WHERE b.schedule = d.date_from + rnum - 1)
AND NOT EXISTS (SELECT 1
FROM suspension c
WHERE c.schedule = d.date_from + rnum - 1)
The problem is that only the first record will expand properly and the other records will not be included in the record set unless the DATE_FROM and DATE_TO is of the same date.
I want to avoid using a PL-SQL function as much as possible, but if it's impossible to achieve the resultset I needed without using a function, please tell me at least the reason why.
Here's how to create as many rows for each ID as there are days between FROM and TO dates, without weekends (Saturdays and Sundays):
SQL> with leave (id, date_from, date_to) as
2 (select '001', date '2018-10-04', date '2018-10-09' from dual union all
3 select '002', date '2018-10-05', date '2018-10-05' from dual union all
4 select '003', date '2018-10-02', date '2018-10-08' from dual
5 ),
6 inter as
7 (select l.id,
8 l.date_from + column_value datum,
9 to_char(l.date_from + column_value, 'day') day
10 from leave l,
11 table(cast(multiset(select level from dual
12 connect by level <= l.date_to - l.date_from + 1
13 ) as sys.odcinumberlist))
14 )
15 select id, datum
16 from inter
17 where to_char(datum, 'dy') not in ('sat', 'sun');
ID DATUM
--- -----------
001 05-oct-2018
001 08-oct-2018
001 09-oct-2018
001 10-oct-2018
003 03-oct-2018
003 04-oct-2018
003 05-oct-2018
003 08-oct-2018
003 09-oct-2018
9 rows selected.
SQL>
As line 18 (and so forth), add additional conditions (remove holidays, suspensions, whatever).
(BTW, I wonder who & why downvoted your question; it is well-formed, shows what you have, your attempt to solve it ... really, a mystery to me).

Counting the number of days excluding sunday between two dates

I am trying to calculate number of days betwen two dates excluding sundays. This is my query,
SELECT F_PLANHM_END_DT
- F_PLANHM_ST_DT
- 2
* (TO_CHAR (F_PLANHM_END_DT, 'WW') - TO_CHAR (F_PLANHM_ST_DT, 'WW'))
FROM VW_S_CURV_PROC
WHERE HEAD_MARK = 'IGG-BLH-BM 221';
SELECT COUNT (*)
FROM (SELECT SYSDATE + l trans_date
FROM ( SELECT LEVEL - 1 l
FROM VW_S_CURV_PROC
CONNECT BY LEVEL <= ( (SYSDATE + 7) - SYSDATE)))
WHERE TO_CHAR (trans_date, 'dy') NOT IN ('sun');
I am retrieving date from a view called VW_S_CURV_PROC with start date : F_PLANHM_ST_DT and end date F_PLANHM_END_DT. Somehow i cant make this to work. Please help me...
You could use the ROW GENERATOR technique to first generate the dates for a given range, and then exclude the SUNDAYs.
For example, this query will give me the total count of days between 1st Jan 2014 and 31st Dec 2014, excluding the Sundays -
SQL> WITH DATA AS
2 (SELECT to_date('01/01/2014', 'DD/MM/YYYY') date1,
3 to_date('31/12/2014', 'DD/MM/YYYY') date2
4 FROM dual
5 )
6 SELECT SUM(holiday) holiday_count
7 FROM
8 (SELECT
9 CASE
10 WHEN TO_CHAR(date1+LEVEL-1, 'DY','NLS_DATE_LANGUAGE=AMERICAN') <> 'SUN'
11 THEN 1
12 ELSE 0
13 END holiday
14 FROM data
15 CONNECT BY LEVEL <= date2-date1+1
16 )
17 /
HOLIDAY_COUNT
-------------
313
SQL>

How to get all days in month? [duplicate]

This question already has answers here:
How to populate calendar table in Oracle?
(3 answers)
Closed 8 years ago.
Possible to get all days in month in Oracle SQL/PLSQL as table ?
select * from bla-bla-bla DUAL
results:
1 rec: 01.01.2012
2 rec: 02.01.2012
3 rec: 03.01.2012
...
4 rec: 31.01.2012
?
this is what you are looking for;
select to_date('01.01.2012','dd.mm.yyyy')+level-1
from dual
connect by level <= TO_CHAR(LAST_DAY(to_date('01.01.2012','dd.mm.yyyy')),'DD')
ADD_MONTHS (dt, 1) - dt is also an alternative.
For a particular month of an year, you need to hardcode the date :
WITH d AS
(SELECT TRUNC ( to_date('01.01.2012','dd.mm.yyyy'), 'MM' ) - 1 AS dt
FROM dual
)
SELECT dt + LEVEL
FROM d
CONNECT BY LEVEL <= ADD_MONTHS (dt, 1) - dt
/
Else, to get the list for current month, simply use SYSDATE instead of hardcoding the date :
SQL> WITH d AS
2 (SELECT TRUNC(sysdate,'MM') -1 AS dt FROM dual
3 )
4 SELECT dt + LEVEL
5 FROM d
6 CONNECT BY LEVEL <= ADD_MONTHS (dt, 1) - dt
7 /
DT+LEVEL
---------
01-OCT-14
02-OCT-14
03-OCT-14
04-OCT-14
05-OCT-14
06-OCT-14
07-OCT-14
08-OCT-14
09-OCT-14
10-OCT-14
11-OCT-14
12-OCT-14
13-OCT-14
14-OCT-14
15-OCT-14
16-OCT-14
17-OCT-14
18-OCT-14
19-OCT-14
20-OCT-14
21-OCT-14
22-OCT-14
23-OCT-14
24-OCT-14
25-OCT-14
26-OCT-14
27-OCT-14
28-OCT-14
29-OCT-14
30-OCT-14
31-OCT-14
31 rows selected.
SQL>

Oracle : break down multiple entries monthwise

i have data in table as below
start_date end_date amount
15-01-2012 25-01-2012 100
26-01-2012 10-02-2012 100
11-02-2012 29-02-2012 100
i want these entries to be break down monthwise as below
15-01 to 25-01 = 100
26-01 to 31-01 = 100/((10-02-2012 - 26-01-2012) +1 ) * ((31-01-2012 - 26-01-2012) + 1) = 37.5
so for period of 15-01-2012 to 31-01-2012 = 100 + 37.5 = 137.5
for month of february amount should be
01-02 to 10-02 = 100/((10-02-2012 - 26-01-2012) +1 ) * ((01-02-2012 - 10-02-2012) + 1) = 62.5
11-02 to 29-02 = 100
so for the february 01-02-2012 to 29-02-2012 = 62.5 + 100 = 162.5
so that final output should be
start_date end_date amount
15-01-2012 31-01-2012 137.5
01-02-2012 29-02-2012 162.5
is there any method to achieve this without using PLSQL
you can use a LAG function to determine the daily average between one row and a previous (sorted) row.
once you have a daily average, you can multiply by the number of days in the period.
all in the same sql statement.
I'm not sure how you want to calculate the values, but as a start, try to break the records monthly:
with dts as (select last_day(add_months(
to_date('20111201','yyyymmdd'),level)) ld,
add_months(
to_date('20111201','yyyymmdd'),level) sd
from dual connect by level < 12)
select case when start_date >= sd then start_date else sd end st_dt,
case when end_date <= ld then end_date else ld end en_dt, amount,
ld-sd days_in_month,
case when end_date <= ld then end_date else ld end-
case when start_date >= sd then start_date else sd end part
from t, dts
where (start_date >= sd and to_char(start_date, 'yyyymm') =
to_char(sd, 'yyyymm'))
or (end_date <= ld and to_char(end_date, 'yyyymm') =
to_char(ld, 'yyyymm'))
SQL> select * from q10315606;
START_DATE END_DATE AMOUNT
---------- ---------- ----------
2012-01-15 2012-01-25 100
2012-01-26 2012-02-10 100
2012-02-11 2012-02-29 100
SQL> with day_generator as
2 -- this block expands out every date in the entire date range
3 (
4 select min_start_date + level - 1 as date_value
5 from (select min(start_date) as min_start_date,
6 max(end_date) - min(start_date) + 1 as total_days
7 from q10315606
8 )
9 connect by level <= total_days
10 ),
11 range_averages as
12 -- this block generates a daily average for each date range
13 (
14 select start_date, end_date,
15 end_date - start_date + 1 as days_inclusive,
16 amount / (end_date - start_date + 1) as daily_amount
17 from q10315606
18 )
19 select start_date, end_date, sum(amount) as amount
20 from (
21 -- here we cast all the dates to the minimum and maximum value found in the month
22 select min(dg.date_value)
23 over (partition by last_day(dg.date_value) ) as start_date,
24 max(dg.date_value)
25 over (partition by last_day(dg.date_value) ) as end_date,
26 ra.daily_amount as amount
27 from range_averages ra,
28 day_generator dg
29 where dg.date_value between ra.start_date and ra.end_date
30 )
31 group by start_date, end_date
32* order by start_date, end_date
amusch#AGDEV:SQL> /
START_DATE END_DATE AMOUNT
---------- ---------- ----------
2012-01-15 2012-01-31 137.5
2012-02-01 2012-02-29 162.5
This is a try:
I had a join with months. If a row is on two months will be "duplicated", more precisely will by distributed on both months.
For every row*month I apply the formula amount/(days in current row) * (days in current month)
with months as
(select add_months('1-Jan-2012', level -1) as mon
from dual
connect by level < 12)
select mon, sum(amount)
from(
select
months.mon,
amount/(end_date - start_date +1)*
(case when trunc(start_date,'MM') = months.mon
then least(last_day(start_date), end_date) - start_date +1
else end_date - greatest(months.mon, start_date) +1
end) as amount
from your_table y
join months on
(months.mon between trunc(start_date,'MM') and trunc(end_date, 'MM'))
)
group by mon;
This code was tested and gives exactly your desired results.
I see A.B. Cade beat me to it, but here's my solution:
SQL> ALTER SESSION SET NLS_DATE_FORMAT = 'DD-MM-YYYY'
2 /
Session altered.
SQL> CREATE TABLE t (start_date DATE, end_date DATE, amount NUMBER);
Table created.
SQL> INSERT INTO t VALUES (TO_DATE('20120115','YYYYMMDD'),TO_DATE('20120125','YYYYMMDD'),100);
1 row created.
SQL> INSERT INTO t VALUES (TO_DATE('20120126','YYYYMMDD'),TO_DATE('20120210','YYYYMMDD'),100);
1 row created.
SQL> INSERT INTO t VALUES (TO_DATE('20120211','YYYYMMDD'),TO_DATE('20120229','YYYYMMDD'),100);
1 row created.
SQL>
Having set up some test data...
SQL> COL for_month FOR A9
SQL> COL pro_rated_start FOR A15
SQL> COL pro_rated_end FOR A13
SQL>
... and formatted some columns ...
SQL> WITH months AS (
2 SELECT TRUNC(MIN(start_date),'MM') min_start_month
3 , MONTHS_BETWEEN(TRUNC(MAX(end_date),'MM'),TRUNC(MIN(start_date),'MM')) + 1 mos
4 FROM t
5 )
6 , offset AS (
7 SELECT ROWNUM - 1 r
8 FROM (SELECT NULL
9 FROM DUAL
10 CONNECT BY LEVEL <= (SELECT mos FROM months))
11 )
12 , ranges AS (
13 SELECT ADD_MONTHS(months.min_start_month, offset.r) mo_start
14 , LAST_DAY(ADD_MONTHS(months.min_start_month, offset.r)) mo_end
15 FROM offset
16 , months
17 )
18 SELECT TO_CHAR(GREATEST(t.start_date,ranges.mo_start),'Mon YYYY') for_month
19 , MIN(GREATEST(t.start_date,ranges.mo_start)) pro_rated_start
20 , MAX(LEAST(t.end_date,ranges.mo_end)) pro_rated_end
21 , SUM(t.amount
22 * CASE
23 WHEN t.end_date < ranges.mo_end
24 AND t.start_date > ranges.mo_start
25 THEN 1
26 ELSE ((LEAST(t.end_date,ranges.mo_end)
27 - GREATEST(t.start_date,ranges.mo_start) + 1)
28 / (t.end_date - t.start_date + 1))
29 END) pro_rated_amount
30 FROM t
31 , ranges
32 WHERE t.start_date <= ranges.mo_end
33 AND t.end_date >= ranges.mo_start
34 GROUP BY TO_CHAR(GREATEST(t.start_date,ranges.mo_start),'Mon YYYY');
FOR_MONTH PRO_RATED_START PRO_RATED_END PRO_RATED_AMOUNT
--------- --------------- ------------- ----------------
Jan 2012 15-01-2012 31-01-2012 137.5
Feb 2012 01-02-2012 29-02-2012 162.5
SQL>