Oracle SQL count dates that do not exist between range of dates - sql

I have range of dates:
| date |
| -------- |
| 1/1/2022 |
| 2/1/2022 |
| 3/1/2022 |
| 5/1/2022 |
| 6/1/2022 |
| 7/1/2022 |
| 8/1/2022 |
| 10/1/2022 |
I want to get the dates that are not included between these dates, in this case 4/1 and 9/1, I want the count of these dates, in this case 2, so I want the count of dates that do not exist between a specific range of dates, how can I achieve that?

select (max(date) - min(date) + 1) - count(distinct date)
from table_name
https://dbfiddle.uk/cSKZloYA
(max(date) - min(date) + 1) will give the total number of days in the range.
count(distinct date) will be the number of existing (different) days in the table.
The difference between these is the number of non-existing days.
Note: date is a reserved word, so if it's the actual column name, it has to be delimited as "date". (https://en.wikipedia.org/wiki/List_of_SQL_reserved_words)

You can use the LAG analytic function to find the previous date and then work out the number of days difference and if it is more than 1 then you have that many missing days:
SELECT SUM(missing_dates) AS num_missing
FROM (
SELECT GREATEST("DATE" - LAG("DATE") OVER (ORDER BY "DATE") - 1, 0)
AS missing_dates
FROM table_name
);
Which, for the sample data:
CREATE TABLE table_name ("DATE") AS
SELECT DATE '2020-01-01' FROM DUAL UNION ALL
SELECT DATE '2020-01-02' FROM DUAL UNION ALL
SELECT DATE '2020-01-03' FROM DUAL UNION ALL
SELECT DATE '2020-01-05' FROM DUAL UNION ALL
SELECT DATE '2020-01-06' FROM DUAL UNION ALL
SELECT DATE '2020-01-07' FROM DUAL UNION ALL
SELECT DATE '2020-01-08' FROM DUAL UNION ALL
SELECT DATE '2020-01-10' FROM DUAL;
Outputs:
NUM_MISSING
2
fiddle

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

Finding overlapping and calculation of min and max dates with in overlap in oracle [duplicate]

This question already has answers here:
print start and end date in one row for continous or overlaping date ranges in oracle SQL
(2 answers)
Closed 2 years ago.
How can we find the between the overlap lap b/w the dates .
overlap means when start date and end date are within same range
for below example row 1 has no over lap.
Row 2to 5 can be considered as one set of over lap as there start date and end are over lap with themselves
Row 6 & 7 can be considered as one set of over lap
for eg. row 6 & 7 --> start date of row 7 is in same range with respect to end date of row 6
so it becomes an overlap
Once overlap is found then and need to find out min(start date) and max(end date) and
want to assign a unique id to each overlap and in the S.NO column should show which rows are overlapped .Below is the I/p and O/p
I/p
You can do it simply and efficiently using MATCH_RECOGNIZE to perform a row-by-row comparison and aggregation:
SELECT id, start_date, end_date
FROM table_name
MATCH_RECOGNIZE (
PARTITION BY id
ORDER BY start_date
MEASURES FIRST(start_date) AS start_date,
MAX(end_date) AS end_date
PATTERN ( overlapping_dates* last_date )
DEFINE overlapping_dates as MAX(end_date) >= NEXT(start_date)
);
Which, for the sample data:
CREATE TABLE table_name ( sno, id, start_date, end_date ) AS
SELECT 1, 1, DATE '2019-10-11', DATE '2019-10-11' FROM DUAL UNION ALL
SELECT 2, 1, DATE '2019-11-04', DATE '2019-12-11' FROM DUAL UNION ALL
SELECT 3, 1, DATE '2019-11-05', DATE '2019-11-10' FROM DUAL UNION ALL
SELECT 4, 1, DATE '2019-11-06', DATE '2019-11-10' FROM DUAL UNION ALL
SELECT 5, 1, DATE '2019-11-20', DATE '2019-12-20' FROM DUAL UNION ALL
SELECT 6, 1, DATE '2020-01-01', DATE '2020-01-20' FROM DUAL UNION ALL
SELECT 7, 1, DATE '2020-01-15', DATE '2020-03-25' FROM DUAL;
Outputs:
ID | START_DATE | END_DATE
-: | :------------------ | :------------------
1 | 2019-10-11 00:00:00 | 2019-10-11 00:00:00
1 | 2019-11-04 00:00:00 | 2019-12-20 00:00:00
1 | 2020-01-01 00:00:00 | 2020-03-25 00:00:00
db<>fiddle here

SQL: Getting all dates between a set of date pairs

I have a table with some data and a time period i.e. start date and end date
------------------------------
| id | start_date | end_date |
|------------------------------|
| 0 | 1-1-2019 | 3-1-2019 |
|------------------------------|
| 1 | 6-1-2019 | 8-1-2019 |
|------------------------------|
I want to run a query that will return the id and all the dates that are within those time periods. for instance, the result of the query for the above table will be:
------------------
| id | date |
|------------------|
| 0 | 1-1-2019 |
|------------------|
| 0 | 2-1-2019 |
|------------------|
| 0 | 3-1-2019 |
|------------------|
| 1 | 6-1-2019 |
|------------------|
| 1 | 7-1-2019 |
|------------------|
| 1 | 8-1-2019 |
------------------
I am using Redshift therefor I need it supported in Postgres and take this into consideration
Your help will be greatly appriciated
The common way this is done is to create a calendar table with a list of dates. In fact, a calendar table can be extended to include columns like:
Day number (in year)
Week number
First day of month
Last day of month
Weekday / Weekend
Public holiday
Simply create the table in Excel, save as CSV and then COPY it into Redshift.
You could then just JOIN to the table, like:
SELECT
table.id,
calendar.date
FROM table
JOIN calendar
WHERE
calendar.date BETWEEN table.start_date AND table.end_date
This question was originally tagged Postgres.
Use generate_series():
select t.id, gs.dte
from t cross join lateral
generate_series(t.start_date, t.end_date, interval '1 day') as gs(dte);
ok, It took me a while to get there but this is what I did (though not really proud of it):
I created a query that generates a calendar for the last 6 years, cross joined it with my table and then selected the relevant dates from my calendar table.
WITH
days AS (select 0 as num UNION select 1 as num UNION select 2 UNION select 3 UNION select 4 UNION select 5 UNION select 6 UNION select 7 UNION select 8 UNION select 9 UNION select 10 UNION select 11 UNION select 12 UNION select 13 UNION select 14 UNION select 15 UNION select 16 UNION select 17 UNION select 18 UNION select 19 UNION select 20 UNION select 21 UNION select 22 UNION select 23 UNION select 24 UNION select 25 UNION select 26 UNION select 27 UNION select 28 UNION select 29 UNION select 30 UNION select 31),
month AS (select num from days where num <= 12),
years AS (select num from days where num <= 6),
rightnow AS (select CAST( TO_CHAR(GETDATE(), 'yyyy-mm-dd hh24') || ':' || trim(TO_CHAR((ROUND((DATEPART (MINUTE, GETDATE()) / 5), 1) * 5 ),'09')) AS TIMESTAMP) as start),
calendar as
(
select
DATEADD(years, -y.num, DATEADD( month, -m.num, DATEADD( days, -d.num, n.start ) ) ) AS period_date
from days d, month m, years y, rightnow n
)
select u.id, calendar.period_date
from periods u
cross join calendar
where date_part(DAY, u.finishedat) >= date_part(DAY, u.startedat) + 1 and date_part(DAY, calendar.period_date) < date_part(DAY, u.finishedat) and date_part(DAY, calendar.period_date) > date_part(DAY, u.startedat) and calendar.period_date < u.finishedat and calendar.period_date > u.startedat
This was based on the answer here: Using sql function generate_series() in redshift

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 running total in Oracle

I want to make a query, which shows the progress of the number of users on my webpage by week.
I use following query to run the users database and get the number, grouped by a week:
SELECT TRUNC(FAB.LICENSE_DATE, 'IW'),
COUNT(DISTINCT FAB.STATEMENT_NUMBER) AS "Number of account statements"
FROM USERS FAB
GROUP BY TRUNC(FAB.LAST_UPDATED_TIME, 'IW');
This gives following output:
Date | Users
----------------------
2015/09/07 | 5
2015/09/14 | 4
2015/09/21 | 6
But this is actually not what I want to achieve. I want to have the following output:
Date | Users
----------------------
2015/09/07 | 5
2015/09/14 | 9 (5 + 4)
2015/09/21 | 15 (5 + 4 + 6)
How to modify the query so I get all the results?
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE USERS (
LICENSE_DATE,
LAST_UPDATED_TIME,
STATEMENT_NUMBER
) AS
SELECT DATE '2015-09-07', DATE '2015-09-07', 1 FROM DUAL
UNION ALL SELECT DATE '2015-09-08', DATE '2015-09-08', 2 FROM DUAL
UNION ALL SELECT DATE '2015-09-08', DATE '2015-09-08', 3 FROM DUAL
UNION ALL SELECT DATE '2015-09-09', DATE '2015-09-09', 4 FROM DUAL
UNION ALL SELECT DATE '2015-09-12', DATE '2015-09-12', 5 FROM DUAL
UNION ALL SELECT DATE '2015-09-14', DATE '2015-09-15', 6 FROM DUAL
UNION ALL SELECT DATE '2015-09-15', DATE '2015-09-16', 7 FROM DUAL
UNION ALL SELECT DATE '2015-09-16', DATE '2015-09-16', 8 FROM DUAL
UNION ALL SELECT DATE '2015-09-17', DATE '2015-09-18', 9 FROM DUAL
UNION ALL SELECT DATE '2015-09-21', DATE '2015-09-21', 10 FROM DUAL
UNION ALL SELECT DATE '2015-09-21', DATE '2015-09-26', 11 FROM DUAL
UNION ALL SELECT DATE '2015-09-22', DATE '2015-09-22', 12 FROM DUAL
UNION ALL SELECT DATE '2015-09-23', DATE '2015-09-25', 13 FROM DUAL
UNION ALL SELECT DATE '2015-09-24', DATE '2015-09-24', 14 FROM DUAL
UNION ALL SELECT DATE '2015-09-27', DATE '2015-09-27', 15 FROM DUAL;
Query 1:
SELECT LAST_UPDATED_WEEK,
SUM( NUM_STATEMENTS ) OVER ( ORDER BY LAST_UPDATED_WEEK ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS "Number of account statements"
FROM (
SELECT TRUNC(LAST_UPDATED_TIME, 'IW') AS LAST_UPDATED_WEEK,
COUNT(DISTINCT STATEMENT_NUMBER) AS NUM_STATEMENTS
FROM USERS
GROUP BY
TRUNC( LAST_UPDATED_TIME, 'IW')
)
Results:
| LAST_UPDATED_WEEK | Number of account statements |
|-----------------------------|------------------------------|
| September, 07 2015 00:00:00 | 5 |
| September, 14 2015 00:00:00 | 9 |
| September, 21 2015 00:00:00 | 15 |
SELECT TRUNC(FAB.LICENSE_DATE, 'IW'),
SUM(COUNT(DISTINCT FAB.STATEMENT_NUMBER)) OVER (ORDER BY TRUNC(FAB.LAST_UPDATED_TIME, 'IW')) as "Number of account statements"
FROM USERS FAB
GROUP BY TRUNC(FAB.LAST_UPDATED_TIME, 'IW');
You can use this code block for your problem :
select u.date
,(select sum(u1.users)
from users u1
where u1.ddate <= u.date) as users
from users u;
It gives this output :
07.09.2015 5
14.09.2015 9
21.09.2015 15
Good luck
Hello you can try this code too.
WITH t1 AS
( SELECT to_date('01/01/2015','mm/dd/yyyy') rn, 5 usrs FROM dual
UNION ALL
SELECT to_date('02/01/2015','mm/dd/yyyy') rn, 4 usrs FROM dual
UNION ALL
SELECT to_date('03/01/2015','mm/dd/yyyy') rn, 8 usrs FROM dual
UNION ALL
SELECT to_date('04/01/2015','mm/dd/yyyy') rn, 2 usrs FROM dual
)
SELECT rn,
usrs,
sum(usrs) over (order by rn ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) cumm_usrs
FROM t1
GROUP BY rn,
usrs;