Making a subquery with a set of dates (sundays) between two points - sql

I need to form several subqueries with sets of dates including only sundays between two set points, for example between 01.04.2018 and 30.06.2018.
The first thing which comes into mind is something like this:
SELECT '01.04.2018' STARTDATE FROM DUAL
UNION ALL
SELECT '08.04.2018' STARTDATE FROM DUAL
...
But it doesn't look very elegant. Is there an easier way to achieve what I need?

You can use:
SQL Fiddle
Query 1:
SELECT NEXT_DAY( DATE '2018-04-01' - 1, 'SUNDAY' ) + ( LEVEL - 1 ) * 7
AS startdate
FROM DUAL
CONNECT BY
NEXT_DAY( DATE '2018-04-01' - 1, 'SUNDAY' ) + ( LEVEL - 1 ) * 7
<= DATE '2018-06-30'
Results:
| STARTDATE |
|----------------------|
| 2018-04-01T00:00:00Z |
| 2018-04-08T00:00:00Z |
| 2018-04-15T00:00:00Z |
| 2018-04-22T00:00:00Z |
| 2018-04-29T00:00:00Z |
| 2018-05-06T00:00:00Z |
| 2018-05-13T00:00:00Z |
| 2018-05-20T00:00:00Z |
| 2018-05-27T00:00:00Z |
| 2018-06-03T00:00:00Z |
| 2018-06-10T00:00:00Z |
| 2018-06-17T00:00:00Z |
| 2018-06-24T00:00:00Z |
Does this construction always return at least one row? If I add an additional condition that NEXT_DAY( DATE '2018-07-01' - 1, 'SUNDAY' ) + ( LEVEL - 1 ) * 7 should be less than SYSDATE it still returns the first row which is larger than SYSDATE.
Yes, a hierarchical query will always return one row if the filtering is just performed in the CONNECT BY clause (since it will only check it when it tries to connect one row to its parent and needs to have generated at least one parent first to do this):
SELECT NEXT_DAY( DATE '2018-07-01' - 1, 'SUNDAY' ) + ( LEVEL - 1 ) * 7
AS startdate
FROM DUAL
CONNECT BY
NEXT_DAY( DATE '2018-07-01' - 1, 'SUNDAY' ) + ( LEVEL - 1 ) * 7
<= LEAST(
SYSDATE, -- DATE '2018-06-29'
DATE '2018-07-30'
)
Results:
| STARTDATE |
|----------------------|
| 2018-07-01T00:00:00Z | -- Greater than SYSDATE
But if you add a WHERE clause (rather than filtering in the CONNECT BY clause) then it can return zero rows:
SELECT NEXT_DAY( DATE '2018-07-01' - 1, 'SUNDAY' ) + ( LEVEL - 1 ) * 7
AS startdate
FROM DUAL
WHERE NEXT_DAY( DATE '2018-07-01' - 1, 'SUNDAY' ) + ( LEVEL - 1 ) * 7
<= SYSDATE
CONNECT BY
NEXT_DAY( DATE '2018-07-01' - 1, 'SUNDAY' ) + ( LEVEL - 1 ) * 7
<= DATE '2018-07-30'
Results:
No data found.

The brute force approach would be to just generate the entire date range and then filter it to leave only Sundays:
WITH cte AS (
SELECT TRUNC (date '2018-06-30' - ROWNUM) dt
FROM DUAL CONNECT BY ROWNUM < 100
)
SELECT * FROM cte WHERE TO_CHAR(dt, 'DAY') = 'SUN'

Related

Oracle How to list last days of months between 2 dates

I manage to get all the days between 2 dates.
But I would like to get all the lasts day of months between 2 dates (using one request).
All days between 2 dates:
select to_date('01/01/2000','dd/mm/yyyy') + (LEVEL-1) as jour
from dual
connect by level <= to_date('31/12/2050','dd/mm/yyyy')-to_date('01/01/2000','dd/mm/yyyy')
Last day of current month:
select LAST_DAY(sysdate) FROM dual
I don't know how to mix both and get the expected result:
20000131
20000228
20000331
etc...
That would be DISTINCT + LAST_DAY, I presume.
Setting date format (so that it matches yours; alternatively, apply TO_CHAR to the jour value with appropriate format mask):
SQL> alter session set nls_Date_format = 'yyyymmdd';
Session altered.
I shortened time span to 2 years (to save space :)).
SQL> select distinct last_day(to_date('01/01/2000','dd/mm/yyyy') + (LEVEL-1)) as jour
2 from dual
3 connect by level <= to_date('31/12/2002','dd/mm/yyyy')-to_date('01/01/2000','dd/mm/yyyy')
4 order by 1;
JOUR
--------
20000131
20000229
20000331
20000430
20000531
20000630
20000731
20000831
<snip>
20020630
20020731
20020831
20020930
20021031
20021130
20021231
36 rows selected.
SQL>
I like to use standard recursive queries rather than Oracle's specific CONNECT BY syntax. Here, you could enumerate the start of months, then offset to the end of months:
with cte (dt) as (
select date '2020-01-01' dt from dual
union all
select dt + interval '1' month from cte where dt + interval '1' month < date '2051-01-01'
)
select last_day(dt) dt from cte order by dt
Note that this uses standard date literals (date 'YYYY-MM-DD') rather than to_date() - this makes the query shorter, and, again, more standard.
Demo on DB Fiddle:
| DT |
| :-------- |
| 31-JAN-20 |
| 29-FEB-20 |
| 31-MAR-20 |
| 30-APR-20 |
| 31-MAY-20 |
...
| 31-OCT-50 |
| 30-NOV-50 |
| 31-DEC-50 |
You can do this with a CONNECT BY query. (You can also do it with a recursive query, like GMB has proposed, but it would have to be adapted to solve the problem you posed - it should allow for input start and end date, and it should return zero rows if there are no ends of month between the two dates.)
In the query below I use a WITH clause to give the start and end date. More likely, in your problem they are bind variables. (Or are they read from a table?)
Pay attention to the START WITH clause. The CONNECT BY condition is applied only to levels 2 and above; you need the START WITH condition for level=1, for the case when there are NO ends of month between the give dates (such as, between 10 January and 23 January of the same year).
with
input_dates(start_dt, end_dt) as (
select date '2020-01-22', date '2020-04-03' from dual
)
select add_months(last_day(start_dt), level - 1) as eom
from input_dates
start with last_day(start_dt) <= end_dt
connect by add_months(last_day(start_dt), level - 1) <= end_dt
;
EOM
----------
2020-01-31
2020-02-29
2020-03-31
Your initial query just needs some adjustment. Instead of just level-1 (which winds up do daily) convert it to a monthly increment "level-1) * interval '1' month. Then for the connect by just get month_between the desired date. Note: I have converted to ISO standard format for dates instead of to_date function. Makes query shorter and easier to read.
select last_day(date '2000-01-01' + (level-1)*interval '1' month) as jour
from dual
connect by level <= 1+months_between(date '2050-12-31',date '2000-01-01');
You can use a recursive sub-query.
This will work for:
Multiple input ranges;
Inputs when the start date is at the end of the month;
When the range does not contain the end of any month.
WITH input_ranges ( start_date, end_date ) AS (
-- Should return a single row.
SELECT DATE '2020-01-31', DATE '2020-02-01' FROM DUAL UNION ALL
-- Should return multiple rows.
SELECT DATE '2021-02-01', DATE '2021-06-01' FROM DUAL UNION ALL
-- Should not return any rows as there is no end of the month in the range.
SELECT DATE '2021-10-06', DATE '2021-10-20' FROM DUAL UNION ALL
-- Should work even though February does not have 30 days.
SELECT DATE '2022-01-30', DATE '2022-03-02' FROM DUAL
),
month_ends ( month_end, end_date ) AS (
SELECT LAST_DAY( start_date ),
end_date
FROM input_ranges
WHERE LAST_DAY( start_date ) <= end_date
UNION ALL
SELECT ADD_MONTHS( month_end, 1 ),
end_date
FROM month_ends
WHERE ADD_MONTHS( month_end, 1 ) <= end_date
)
SELECT month_end
FROM month_ends
ORDER BY month_end;
Which outputs:
| MONTH_END |
| :------------------ |
| 2020-01-31 00:00:00 |
| 2021-02-28 00:00:00 |
| 2021-03-31 00:00:00 |
| 2021-04-30 00:00:00 |
| 2021-05-31 00:00:00 |
| 2022-01-31 00:00:00 |
| 2022-02-28 00:00:00 |
db<>fiddle here

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 date as fraction of month

I would like to get a table of months between two dates with a fraction of each month that the two dates cover.
For example with a start date of 15/01/2017 and end date of 01/03/2017 it would output:
01/2017 : 0.5483..
02/2017 : 1
03/2017: 0.0322..
where for January and March the calculations are 17/31 and 1/31 respectively. I currently have the query:
WITH dates_between as (SELECT ADD_MONTHS(TRUNC(TO_DATE(:givenStartDate,'dd/mm/yyyy'), 'MON'), ROWNUM - 1) date_out
FROM DUAL
CONNECT BY ADD_MONTHS(TRUNC(TO_DATE(:givenStartDate,'dd/mm/yyyy'), 'MON'), ROWNUM - 1)
<= TRUNC(TO_DATE(:givenEndDate,'dd/mm/yyyy'), 'MON')
)
select * from dates_between
This outputs each month between two dates and formats it to the start of the month. I just need another column to give me the fraction the start and end dates cover. I'm not sure of a way to do this without it getting messy.
The months_between() function "calculates the fractional portion of the result based on a 31-day month". That means that if your range starts or ends in a month that doesn't have 31 days, the fraction you get might not be quite what you expect:
select months_between(date '2017-04-02', date '2017-04-01') as calc from dual
CALC
----------
.0322580645
... which is 1/31, not 1/30. To get 0.0333... instead you'd need to calculate the number of days in each month, at least for the first and last month. This uses a recursive CTE (11gR2+) to get the months, using a couple of date ranges provided by another CTE as a demo to show the difference (you can use a hierarchical query too of course):
with ranges (id, start_date, end_date) as (
select 1, date '2017-01-15', date '2017-03-01' from dual
union all select 2, date '2017-01-31', date '2017-03-01' from dual
union all select 3, date '2017-02-28', date '2017-04-01' from dual
),
months (id, month_start, month_days, range_start, range_end) as (
select id,
trunc(start_date, 'MM'),
extract(day from last_day(start_date)),
start_date,
end_date
from ranges
union all
select id,
month_start + interval '1' month,
extract(day from last_day(month_start + interval '1' month)),
range_start,
range_end
from months
where month_start < range_end
)
select id,
to_char(month_start, 'YYYY-MM-DD') as month_start,
month_days,
case when month_start = trunc(range_start, 'MM')
then month_days - extract(day from range_start) + 1
when month_start = trunc(range_end, 'MM')
then extract(day from range_end)
else month_days end as range_days,
(case when month_start = trunc(range_start, 'MM')
then month_days - extract(day from range_start) + 1
when month_start = trunc(range_end, 'MM')
then extract(day from range_end)
else month_days end) / month_days as fraction
from months
order by id, month_start;
which gets:
ID MONTH_STAR MONTH_DAYS RANGE_DAYS FRACTION
------ ---------- ---------- ---------- --------
1 2017-01-01 31 17 0.5483
1 2017-02-01 28 28 1
1 2017-03-01 31 1 0.0322
2 2017-01-01 31 1 0.0322
2 2017-02-01 28 28 1
2 2017-03-01 31 1 0.0322
3 2017-02-01 28 1 0.0357
3 2017-03-01 31 31 1
3 2017-04-01 30 1 0.0333
The first CTE ranges is just the demo data. The second, recursive, CTE months generates the start and number of days in each month, while keeping track of the original range dates too. The final query just calculates the fractions based on the number of days in the month in the range against the number of days in that month overall.
The month_days and range_days are only shown in the output so you can see what the calculation is based on, you can obviously omit those from your actual result, and format the month start date however you want.
With your original single pair of bind variables the equivalent would be:
with months (month_start, month_days, range_start, range_end) as (
select trunc(to_date(:givenstartdate, 'DD/MM/YYYY'), 'MM'),
extract(day from last_day(to_date(:givenstartdate, 'DD/MM/YYYY'))),
to_date(:givenstartdate, 'DD/MM/YYYY'),
to_date(:givenenddate, 'DD/MM/YYYY')
from dual
union all
select month_start + interval '1' month,
extract(day from last_day(month_start + interval '1' month)),
range_start,
range_end
from months
where month_start < range_end
)
select to_char(month_start, 'MM/YYYY') as month,
(case when month_start = trunc(range_start, 'MM')
then month_days - extract(day from range_start) + 1
when month_start = trunc(range_end, 'MM')
then extract(day from range_end)
else month_days end) / month_days as fraction
from months
order by month_start;
MONTH FRACTION
------- --------
01/2017 0.5483
02/2017 1
03/2017 0.0322
Here's how I would do it (n.b. I have expanded your dates_between to work against multiple rows, purely for demonstration purposes. If you're only working with a single set of parameters, you wouldn't need to do that):
WITH params AS (SELECT 1 ID, '15/01/2017' givenstartdate, '01/03/2017' givenenddate FROM dual UNION ALL
SELECT 2 ID, '15/01/2017' givenstartdate, '23/01/2017' givenenddate FROM dual UNION ALL
SELECT 3 ID, '01/01/2017' givenstartdate, '07/04/2017' givenenddate FROM dual),
dates_between AS (SELECT ID,
to_date(givenstartdate, 'dd/mm/yyyy') givenstartdate,
to_date(givenenddate, 'dd/mm/yyyy') givenenddate,
add_months(trunc(to_date(givenstartdate, 'dd/mm/yyyy'), 'MON'), LEVEL - 1) start_of_month,
last_day(add_months(trunc(to_date(givenstartdate, 'dd/mm/yyyy'), 'MON'), LEVEL - 1)) end_of_month
FROM params
CONNECT BY add_months(trunc(to_date(givenstartdate, 'dd/mm/yyyy'), 'MON'), LEVEL - 1) <=
trunc(to_date(givenenddate, 'dd/mm/yyyy'), 'MON')
AND PRIOR ID = ID
AND PRIOR sys_guid() IS NOT NULL)
SELECT ID,
givenstartdate,
givenenddate,
start_of_month date_out,
end_of_month,
months_between(LEAST(givenenddate, end_of_month) + 1, GREATEST(start_of_month, givenstartdate))
FROM dates_between;
ID GIVENSTARTDATE GIVENENDDATE DATE_OUT END_OF_MONTH DIFF
1 15/01/2017 01/03/2017 01/01/2017 31/01/2017 0.54838709
1 15/01/2017 01/03/2017 01/02/2017 28/02/2017 1
1 15/01/2017 01/03/2017 01/03/2017 31/03/2017 0.03225806
2 15/01/2017 23/01/2017 01/01/2017 31/01/2017 0.29032258
3 01/01/2017 07/04/2017 01/01/2017 31/01/2017 1
3 01/01/2017 07/04/2017 01/02/2017 28/02/2017 1
3 01/01/2017 07/04/2017 01/03/2017 31/03/2017 1
3 01/01/2017 07/04/2017 01/04/2017 30/04/2017 0.22580645
N.B. You may need to add a case statement to decide whether you want to add 1 or not to the diff calculation, based on your requirements.
Try this
For first month, I have calculated remaining days / total days and for last month, I subtracted it by 1 to get days passed / total days.
DBFiddle Demo
WITH tbl AS
(SELECT date '2017-01-15' AS givenStartDate
,date '2017-03-01' AS givenEndDate
FROM dual
)
SELECT ADD_MONTHS(TRUNC(givenStartDate, 'MON'), ROWNUM - 1) AS date_out ,
CASE
WHEN
rownum - 1 = 0
THEN months_between(last_day(givenStartDate), givenStartDate)
WHEN ADD_MONTHS(TRUNC(givenStartDate, 'MON'), ROWNUM - 1) = TRUNC(givenEndDate, 'MON')
THEN 1 - (months_between(last_day(givenEndDate), givenEndDate))
ELSE 1
END AS perc
FROM tbl
CONNECT BY ADD_MONTHS(TRUNC(givenStartDate, 'MON'), ROWNUM - 1)
<= TRUNC(givenEndDate, 'MON');
Output
+-----------+-------------------------------------------+
| DATE_OUT | PERC |
+-----------+-------------------------------------------+
| 01-JAN-17 | .5161290322580645161290322580645161290323 |
| 01-FEB-17 | 1 |
| 01-MAR-17 | .0322580645161290322580645161290322580645 |
+-----------+-------------------------------------------+

Count if date in date column is between start and end date [ Oracle SQL ]

This is my first post, so I hope I've posted this one correctly.
My problem:
I want to count the number of active customers per day, the last 30 days.
What I have so far:
In the first column I want to print today, and the last 29 days. This I have done with
select distinct trunc(sysdate-dayincrement, 'DD') AS DATES
from (
select level as dayincrement
from dual
connect by level <= 30
)
I've picked it up here at stackoverflow, and it works perfectly. I can even extend the number of days returned to ex. 365 days. Perfect!
I also have a table that looks like this
|Cust# | Start date | End date |
| 1000 | 01.01.2015 | 31.12.2015|
| 1001 | 02.01.2015 | 31.12.2016|
| 1002 | 02.01.2015 | 31.03.2015|
| 1003 | 03.01.2015 | 31.08.2015|
This is where I feel the problem starts
I would like to get this result:
| Dates | # of cust |
|04.01.2015| 4 |
|03.01.2015| 4 |
|02.01.2015| 3 |
|01.01.2015| 1 |
Here the query would count 1 if:
Start date <= DATES
End date >= DATES
Else count 0.
I just don't know how to structure the query.
I tried this, but it didn't work.
count(
IF ENDDATE <= DATES THEN
IF STARTDATE >= DATES THEN 1 ELSE 0 END IF
ELSE
0
END IF
) AS CUST
Any ideas?
The following produces the results you're looking for. I had change the date generator to start on 04-JAN-2015 instead of SYSDATE (which is, of course, in the year 2016), and to use LEVEL-1 to include 'current' day:
WITH CUSTS AS (SELECT 1000 AS CUST_NO, TO_DATE('01-JAN-2015', 'DD-MON-YYYY') AS START_DATE, TO_DATE('31-DEC-2015', 'DD-MON-YYYY') AS END_DATE FROM DUAL UNION ALL
SELECT 1001 AS CUST_NO, TO_DATE('02-JAN-2015', 'DD-MON-YYYY') AS START_DATE, TO_DATE('31-DEC-2016', 'DD-MON-YYYY') AS END_DATE FROM DUAL UNION ALL
SELECT 1002 AS CUST_NO, TO_DATE('02-JAN-2015', 'DD-MON-YYYY') AS START_DATE, TO_DATE('31-MAR-2015', 'DD-MON-YYYY') AS END_DATE FROM DUAL UNION ALL
SELECT 1003 AS CUST_NO, TO_DATE('03-JAN-2015', 'DD-MON-YYYY') AS START_DATE, TO_DATE('31-AUG-2015', 'DD-MON-YYYY') AS END_DATE FROM DUAL ),
DATES AS (SELECT DISTINCT TRUNC(TO_DATE('04-JAN-2015', 'DD-MON-YYYY') - DAYINCREMENT, 'DD') AS DT
FROM (SELECT LEVEL-1 AS DAYINCREMENT
FROM DUAL
CONNECT BY LEVEL <= 30))
SELECT d.DT, COUNT(*)
FROM CUSTS c
CROSS JOIN DATES d
WHERE d.DT BETWEEN c.START_DATE AND c.END_DATE
GROUP BY d.DT
ORDER BY DT DESC
Best of luck.
You could write a CASE expression equivalent to your IF-ELSE construct.
For example,
SQL> SELECT COUNT(
2 CASE
3 WHEN hiredate <= sysdate
4 THEN 1
5 ELSE 0
6 END ) AS CUST
7 FROM emp;
CUST
----------
14
SQL>
However, looking at your desired output, it seems, you just need to use COUNT and GROUP BY. The date conditions should be in the filter predicate.
For example,
SELECT dates, COUNT(*)
FROM table_name
WHERE dates BETWEEN start_date AND end_date
GROUP BY dates;

Count of days in a period

I have list of items that have start and end date. Items belong to user. For one item the period can range from 1-5 years. I want to find the count of days that are between the given date range which I would pass from query. Period start is always sysdate and end sysdate - 5 years
The count of days returned must also be in the period range.
Example:
I initiate a query as of 15.05.2015) as me being user, so I need to find all days between 15.05.2010 and 15.05.2015
During that period 2 items have belong to me:
Item 1) 01.01.2010 - 31.12.2010. Valid range: 15.05.2010 - 31.12.2010 = ~195 days
Item 2) 01.01.2015 - 31.12.2015. Valid range: 01.01.2015 - 15.05.2015 = ~170 days
I need a sum of these days that are exactly in that period.
For query right now I just have the count which takes the full range of an item (making it simple):
SELECT SUM(i.end_date - i.start_date) AS total_days
FROM items i
WHERE i.start_date >= to_date('2010-15-05', 'yyyy-mm-dd')
AND i.end_date <= to_date('2015-15-05', 'yyyy-mm-dd')
AND i.user = 'me'
So right now this would give me about count of 2 year period dates which is wrong, how should I update my select sum to include the dates that are in the period? Correct result would be 195 + 170. Currently I would get like 365 + 365 or something.
Period start is always sysdate and end sysdate - 5 years
You can get this using: SYSDATE and SYSDATE - INTERVAL '5' YEAR
Item 1) 01.01.2010 - 31.12.2010. Valid range: 15.05.2010 - 31.12.2010
= ~195 days
Item 2) 01.01.2015 - 31.12.2015. Valid range: 01.01.2015 - 15.05.2015
= ~170 days
Assuming these examples show start_date - end_date and the valid range is your expected answer for that particular SYSDATE then you can use:
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE items ( "user", start_date, end_date ) AS
SELECT 'me', DATE '2010-01-01', DATE '2010-12-31' FROM DUAL
UNION ALL SELECT 'me', DATE '2015-01-01', DATE '2015-12-31' FROM DUAL
UNION ALL SELECT 'me', DATE '2009-01-01', DATE '2009-12-31' FROM DUAL
UNION ALL SELECT 'me', DATE '2009-01-01', DATE '2016-12-31' FROM DUAL
UNION ALL SELECT 'me', DATE '2012-01-01', DATE '2012-12-31' FROM DUAL
UNION ALL SELECT 'me', DATE '2013-01-01', DATE '2013-01-01' FROM DUAL;
Query 1:
SELECT "user",
TO_CHAR( start_date, 'YYYY-MM-DD' ) AS start_date,
TO_CHAR( end_date, 'YYYY-MM-DD' ) AS end_date,
TO_CHAR( GREATEST(TRUNC(i.start_date), TRUNC(SYSDATE)-INTERVAL '5' YEAR), 'YYYY-MM-DD' ) AS valid_start,
TO_CHAR( LEAST(TRUNC(i.end_date),TRUNC(SYSDATE)), 'YYYY-MM-DD' ) AS valid_end,
LEAST(TRUNC(i.end_date),TRUNC(SYSDATE))
- GREATEST(TRUNC(i.start_date), TRUNC(SYSDATE)-INTERVAL '5' YEAR)
+ 1
AS total_days
FROM items i
WHERE i."user" = 'me'
AND TRUNC(i.start_date) <= TRUNC(SYSDATE)
AND TRUNC(i.end_date) >= TRUNC(SYSDATE) - INTERVAL '5' YEAR
Results:
| user | START_DATE | END_DATE | VALID_START | VALID_END | TOTAL_DAYS |
|------|------------|------------|-------------|------------|------------|
| me | 2010-01-01 | 2010-12-31 | 2010-05-21 | 2010-12-31 | 225 |
| me | 2015-01-01 | 2015-12-31 | 2015-01-01 | 2015-05-21 | 141 |
| me | 2009-01-01 | 2016-12-31 | 2010-05-21 | 2015-05-21 | 1827 |
| me | 2012-01-01 | 2012-12-31 | 2012-01-01 | 2012-12-31 | 366 |
| me | 2013-01-01 | 2013-01-01 | 2013-01-01 | 2013-01-01 | 1 |
This assumes that the start date is at the beginning of the day (00:00) and the end date is at the end of the day (24:00) - so, if the start and end dates are the same then you are expecting the result to be 1 total day (i.e. the period 00:00 - 24:00). If you are, instead, expecting the result to be 0 then remove the +1 from the calculation of the total days value.
Query 2:
If you want the sum of all these valid ranges and are happy to count dates in overlapping ranges multiple times then just wrap it in the SUM aggregate function:
SELECT SUM( LEAST(TRUNC(i.end_date),TRUNC(SYSDATE))
- GREATEST(TRUNC(i.start_date), TRUNC(SYSDATE)-INTERVAL '5' YEAR)
+ 1 )
AS total_days
FROM items i
WHERE i."user" = 'me'
AND TRUNC(i.start_date) <= TRUNC(SYSDATE)
AND TRUNC(i.end_date) >= TRUNC(SYSDATE) - INTERVAL '5' YEAR
Results:
| TOTAL_DAYS |
|------------|
| 2560 |
Query 3:
Now if you want to get a count of all the valid days in the range and not count overlap in ranges multiple times then you can do:
WITH ALL_DATES_IN_RANGE AS (
SELECT TRUNC(SYSDATE) - LEVEL + 1 AS valid_date
FROM DUAL
CONNECT BY LEVEL <= SYSDATE - (SYSDATE - INTERVAL '5' YEAR) + 1
)
SELECT COUNT(1) AS TOTAL_DAYS
FROM ALL_DATES_IN_RANGE a
WHERE EXISTS ( SELECT 'X'
FROM items i
WHERE a.valid_date BETWEEN i.start_date AND i.end_date
AND i."user" = 'me' )
Results:
| TOTAL_DAYS |
|------------|
| 1827 |
Assuming the time periods have no overlaps:
SELECT SUM(LEAST(i.end_date, DATE '2015-05-15') -
GREATEST(i.start_date, DATE '2010-05-15')
) AS total_days
FROM items i
WHERE i.start_date >= DATE '2010-05-15' AND
i.end_date <= DATE '2015-05-15' AND
i.user = 'me';
Use a case statement to evaluate the dates set start and end dates based on the case.
Select SUM(
(case when i.end_date > to_date('2015-15-05','yyyy-mm-dd') then
to_date('2015-15-05','yyyy-mm-dd') else
i.end_date end) -
(case when i.start_date< to_date('2010-15-05','yyyy-mm-dd') then
to_date('2010-15-05','yyyy-mm-dd') else
i.end_date end)) as total_days
FROM items i
WHERE i.start_date >= to_date('2010-15-05', 'yyyy-mm-dd')
AND i.end_date <= to_date('2015-15-05', 'yyyy-mm-dd')
AND i.user = 'me'