Generating dates between two dates - sql

I need to generate all dates between two given dates. This works fine as long as there is just one date range. However, if I have multiple date ranges, this solution doesn't work. I have searched here as well as on asktom, but couldn't locate relevant pointers/solution.
I tried both the solutions using all_objects and CONNECT BY ROWNUM, but no luck. Here is the problem statement: sqlfiddle
Input
ID START_DATE END_DATE
101 April, 01 2013 April, 10 2013
102 May, 10 2013 May, 12 2013
Output
ID Dates
101 April, 01 2013
101 April, 02 2013
101 April, 03 2013
101 April, 04 2013
101 April, 05 2013
101 April, 06 2013
101 April, 07 2013
101 April, 08 2013
101 April, 09 2013
101 April, 10 2013
102 May, 10 2013
102 May, 11 2013
102 May, 12 2013

select
A.ID,
A.START_DATE+delta dt
from
t_dates A,
(
select level-1 as delta
from dual
connect by level-1 <= (
select max(end_date - start_date) from t_dates
)
)
where A.START_DATE+delta <= A.end_date
order by 1, 2

Please try:
select
distinct ID,
START_DATE+level-1 DATES
from dual a, TABLE_DATES b
connect by level <= (END_DATE-START_DATE)+1
order by ID;

select g.cycle_dt
from
(select to_date(d,'DD-MM-YYYY') cycle_dt
from dual
model
dimension by (trunc(to_date('30092015', 'DDMMYYYY')) d)
measures (0 y)
rules (
y[for d from trunc(to_date('30092015', 'DDMMYYYY')) to to_date('30102015', 'DDMMYYYY') increment 1]=0
)) g
order by g.cycle_dt;

WITH NUMS AS (
SELECT LEVEL-1 DaysToAdd
FROM DUAL
CONNECT BY LEVEL <= (
SELECT MAX(END_DATE - START_DATE + 1)
FROM HOLIDAY)
)
SELECT START_DATE + DaysToAdd DATE_TIME, DESCRIPTION, IS_SCHOOL_HOLIDAY, UPDATE_BY, CREATE_DATE
FROM HOLIDAY
CROSS JOIN NUMS
WHERE HOLIDAY.END_DATE - HOLIDAY.START_DATE > DaysToAdd
ORDER BY 1, 2;

SELECT TO_DATE('1-JAN-21', 'dd-mm-yy') + ROWNUM - 1
FROM DUAL
CONNECT BY LEVEL <= (TO_DATE('26-APR-22', 'dd-mm-yy') -
TO_DATE('1-JAN-21', 'dd-mm-yy')) + 1;

Related

Group By Month between two dates

I have a table that lists all employees and their respective start and end dates
I want to be able to count the number of active employees in each month. Is there a way to do this via a single query (eg groupBy) rather than generating multiple queries for each month?
=================================
Employee ID | StartDt | EndDt
123 | 01 Feb 2022 |
234 | 01 jan 2022 | 28 Feb 2022
456 | 01 dec 2021 | 28 Feb 2022
As an example, the table above should return:
Dec 2021: 1
Jan 2022: 2
Feb 2022: 3
Mar 2022: 1
Apr 2022: 1
You can generate a calendar and join to that:
WITH calendar (month) AS (
SELECT ADD_MONTHS(min_startdt, LEVEL - 1)
FROM (
SELECT MIN(startdt) AS min_startdt,
MAX(COALESCE(enddt, SYSDATE)) AS max_enddt
FROM employees
)
CONNECT BY LEVEL - 1 <= MONTHS_BETWEEN(max_enddt, min_startdt)
)
SELECT c.month,
COUNT(e.employee_id)
FROM calendar c
LEFT OUTER JOIN employees e
ON (e.startdt <= c.month AND (c.month <= e.enddt OR e.enddt IS NULL))
GROUP BY
c.month
Which, for the sample data:
CREATE TABLE employees (Employee_ID, StartDt, EndDt ) AS
SELECT 123, DATE '2022-02-01', NULL FROM DUAL UNION ALL
SELECT 234, DATE '2022-01-01', DATE '2022-02-28' FROM DUAL UNION ALL
SELECT 456, DATE '2021-12-01', DATE '2022-02-28' FROM DUAL;
Outputs:
MONTH
COUNT(E.EMPLOYEE_ID)
2021-12-01 00:00:00
1
2022-01-01 00:00:00
2
2022-02-01 00:00:00
3
2022-03-01 00:00:00
1
2022-04-01 00:00:00
1
db<>fiddle here

Query Optimization for my code in Oracle SQL

Here is my code:
select
(case when c.yr = 2019 and c.mon = 10 then 'October 2019'
when c.yr = 2019 and c.mon = 11 then 'November 2019'
when c.yr =2019 and c.mon = 12 then 'December 2019' end) as dae
from (
select substr(d,-4,4) as yr, substr(d,1,2) as mon
from
(select '10/11/2019' as d from dual) )c;
`
So I don't want to hard code the dates for the next 5 years, Is there a function that makes this easier.
Here is the Sample Input I want to try
10/11/2019
11/11/2019
12/11/2019
01/11/2020
Expected Output
October 2019
November 2019
December 2019
January 2020
You could use to_date() to turn your string to a date, and then convert it back to a string in the desired format with to_char():
to_char(to_date(d, 'mm/dd/yyyy'), 'Month yyyy')
Demo on DB Fiddle:
with t as (
select '10/11/2019' d from dual
union all select '11/11/2019' from dual
union all select '12/11/2019' from dual
union all select '01/11/2020' from dual
)
select to_char(to_date(d, 'mm/dd/yyyy'), 'Month yyyy') new_dt from t
| NEW_DT |
| :------------- |
| October 2019 |
| November 2019 |
| December 2019 |
| January 2020 |
Use connect by to generate as many dates as you want. Here the gen_dates CTE starts with your start_date and returns a total of 4 months per your example. To increase the number of months to generate, increase the number 4 to a higher number.
with gen_dates(date_in) as (
select add_months('11-OCT-2019', level -1) date_in
from dual
connect by level <= 4
)
select date_in, to_char(date_in, 'Month yyyy') date_out
from gen_dates;
DATE_IN DATE_OUT
--------- --------------
11-OCT-19 October 2019
11-NOV-19 November 2019
11-DEC-19 December 2019
11-JAN-20 January 2020
4 rows selected.

How can compute monthly average price of an invoice in ORACLE (Varray&loop)

I have a table for invoices and there are millions of data. In my table, there are invoices and their first and last dates for customers. My target is to compute monthly average price of the invoices. For instance i have:
CUSTOMER_ID INVOICE_ID FIRST_DATE LAST_DATE AMOUNT_OF_INVOICE
9876543 1a 1 Jan 2017 17 Jan 2017 32$
9876543 1b 17 Jan 2017 10 Feb 2017 72$
9876543 1c 10 Feb 2017 7 March 2017 100$
9876543 1d 7 March 2017 1 April 2017 25$
9870011 2a 1 Jan 2017 10 Jan 2017 18$
9870011 2b 10 Jan 2017 10 Feb 2017 62$
9870011 2c 10 Feb 2017 1 April 2017 50$
my target is:
CUSTOMER_ID MONTH MONTHLY_AVERAGE_PRICE
9876543 January 2017 77$ (=16x2+15x3)
9876543 February 2017 103$ (=9x3+19x4)
9876543 March 2017 49$ (=6x4+25x1)
9870011 January 2017 62$ (=9x2+22x2)
9870011 February 2017 37$ (=9x2+19x1)
9870011 March 2017 31$ (=31x1)
For instance I compute 77$ (=16x2+15x3) by:
First invoice which INVOICE_ID is 1a there are 16 days from 1 Jan 2017 to 17 Jan 2017 (not incuding 17 Jan). And the price of invoice is 32$. Therefore average price for one day is 32/16 = 2$. The second invoice is 1b and there are 24 days from 17 Jan 2017 to 10 Feb 2017. Therefore average consumption per day is 3$. And the part of this invoice for January is 15 days (From 17 January to 31 January including 31 January). All in all, for January average consumption: 16x2$+15x3$=77$.
Here I think, I have to use varray for storage the data on months and I have to use a loop to find the days between FIRST_DATE and LAST_DATE. However I couldn't do it. Or are there in other ways?
Oracle Query:
WITH month_invoices ( c_id, i_id, first_date, last_date, month_start, month_end, amount )
AS (
SELECT customer_id,
invoice_id,
first_date,
last_date,
first_date,
LEAST( ADD_MONTHS( TRUNC( first_date, 'MM' ), 1 ), last_date ),
amount_of_invoice
FROM your_table
UNION ALL
SELECT c_id,
i_id,
first_date,
last_date,
month_end,
LEAST( ADD_MONTHS( month_end, 1 ), last_date ),
amount
FROM month_invoices
WHERE month_end < last_date
)
SELECT c_id AS customer_id,
TRUNC( month_start, 'MM' ) AS month,
SUM( amount * ( month_end - month_start ) / ( last_date - first_date ) )
AS average_monthly_price
FROM month_invoices
GROUP BY c_id, TRUNC( month_start, 'MM' )
ORDER BY customer_id, month;
Output:
CUSTOMER_ID MONTH AVERAGE_MONTHLY_PRICE
----------- ---------- ---------------------
9876543 2017-01-01 77
9876543 2017-02-01 103
9876543 2017-03-01 49
9870011 2017-01-01 62
9870011 2017-02-01 37
9870011 2017-03-01 31
This is what I came up with.
The inner query creates a row for each day of each invoice.
The outer query sums them up.
It assumes that invoices will only be a maximum of 999 days long.
select customer_id, month, sum(average_cost_per_day) average_cost_per_day
from (
select max(customer_id) customer_id,
invoice_id,
to_char(first_date + n-1, 'MONTH YYYY') month,
count(1)*max(amount_of_invoice)/max(last_date-first_date) average_cost_per_day
from your_table
inner join (
select level n
from dual
connect by level <= 1000
)
on (first_date + n-1 < last_date)
group by invoice_id, to_char(first_date + n-1, 'MONTH YYYY')
)
group by customer_id, month
order by customer_id desc, to_date(month,'MONTH YYYY');

Oracle - Find nearest values and date difference

I have two table:
table1
user start end parameter
1 1 jan 2010 31 mar 2010 abc
1 1 apr 2010 30 jun 2010 abc
1 1 jan 2010 31 mar 2010 xyz
1 1 apr 2010 30 jun 2010 xyz
1 1 jan 2010 31 mar 2010 qqq
1 1 apr 2010 30 jun 2010 qqq
table2
start end parameter value
1 jan 2009 31 mar 2009 abc 100
1 apr 2009 30 jun 2009 abc 200
1 jan 2009 31 mar 2009 xyz 300
1 apr 2009 30 jun 2009 xyz 400
1 jan 2009 31 mar 2009 qqq 500
1 apr 2009 30 jun 2009 qqq 600
I have to associate the 2 tables based on parameter, start and end. I need to find the nearest values. So, for ex. 1 jan 2010 - 31 mar 2010 for parameter abc, we don't have any values in table2, so get the nearest value, i.e. 1 apr 2009 - 30 jun 2009 with parameter abc and associate value 200. Also, find the difference of days from start of both tables.
The resulting table should look like:
table3:
user start end parameter value diff
1 1 jan 2010 31 mar 2010 abc 200 270 days
1 1 apr 2010 30 jun 2010 abc 200 365 days
1 1 jan 2010 31 mar 2010 xyz 400 270 days
1 1 apr 2010 30 jun 2010 xyz 400 365 days
1 1 jan 2010 31 mar 2010 qqq 600 270 days
1 1 apr 2010 30 jun 2010 qqq 600 365 days
Here's one way:
with table1 as (select 1 usr, to_date('01/01/2010', 'dd/mm/yyyy') start_date, to_date('31/03/2010', 'dd/mm/yyyy') end_date, 'abc' parameter from dual union all
select 1 usr, to_date('01/04/2010', 'dd/mm/yyyy') start_date, to_date('30/06/2010', 'dd/mm/yyyy') end_date, 'abc' parameter from dual union all
select 1 usr, to_date('01/01/2010', 'dd/mm/yyyy') start_date, to_date('31/03/2010', 'dd/mm/yyyy') end_date, 'xyz' parameter from dual union all
select 1 usr, to_date('01/04/2010', 'dd/mm/yyyy') start_date, to_date('30/06/2010', 'dd/mm/yyyy') end_date, 'xyz' parameter from dual union all
select 1 usr, to_date('01/01/2010', 'dd/mm/yyyy') start_date, to_date('31/03/2010', 'dd/mm/yyyy') end_date, 'qqq' parameter from dual union all
select 1 usr, to_date('01/04/2010', 'dd/mm/yyyy') start_date, to_date('30/06/2010', 'dd/mm/yyyy') end_date, 'qqq' parameter from dual),
table2 as (select 1 usr, to_date('01/01/2009', 'dd/mm/yyyy') start_date, to_date('31/03/2009', 'dd/mm/yyyy') end_date, 'abc' parameter, 100 value from dual union all
select 1 usr, to_date('01/04/2009', 'dd/mm/yyyy') start_date, to_date('30/06/2009', 'dd/mm/yyyy') end_date, 'abc' parameter, 200 value from dual union all
select 1 usr, to_date('01/01/2009', 'dd/mm/yyyy') start_date, to_date('31/03/2009', 'dd/mm/yyyy') end_date, 'xyz' parameter, 300 value from dual union all
select 1 usr, to_date('01/04/2009', 'dd/mm/yyyy') start_date, to_date('30/06/2009', 'dd/mm/yyyy') end_date, 'xyz' parameter, 400 value from dual union all
select 1 usr, to_date('01/01/2009', 'dd/mm/yyyy') start_date, to_date('31/03/2009', 'dd/mm/yyyy') end_date, 'qqq' parameter, 500 value from dual union all
select 1 usr, to_date('01/04/2009', 'dd/mm/yyyy') start_date, to_date('30/06/2009', 'dd/mm/yyyy') end_date, 'qqq' parameter, 600 value from dual)
-- end of mimicking your tables; see SQL below:
select usr,
start_date,
end_date,
parameter,
latest_value,
diff_days
from (select usr,
start_date,
end_date,
parameter,
last_value(value ignore nulls) over (partition by usr, parameter order by start_date) latest_value,
start_date - last_value(case when value is not null then start_date end ignore nulls) over (partition by usr, parameter order by start_date) diff_days
from (select usr,
start_date,
end_date,
parameter,
cast(null as number) value
from table1
union all
select usr,
start_date,
end_date,
parameter,
value
from table2))
where diff_days > 0;
USR START_DATE END_DATE PARAMETER LATEST_VALUE DIFF_DAYS
---------- ----------- ----------- --------- ------------ ----------
1 01 jan 2010 31 mar 2010 abc 200 275
1 01 apr 2010 30 jun 2010 abc 200 365
1 01 jan 2010 31 mar 2010 qqq 600 275
1 01 apr 2010 30 jun 2010 qqq 600 365
1 01 jan 2010 31 mar 2010 xyz 400 275
1 01 apr 2010 30 jun 2010 xyz 400 365
This uses the analytic function last_value() to find the latest non-null value for the value and its corresponding end_date, and then does the necessary subtraction to get the difference between the later periods' and the latest available period's start dates.
N.B. This assumes that there aren't overlapping periods across both tables.

Oracle count days per month

I wrote this SQL statement to calculate the days for each month
(select count(*) DAYs FROM
(
select trunc(ADD_MONTHS(sysdate,-1),'MM') + level -1 Dates from dual connect by
level <= ADD_MONTHS(trunc(sysdate,'MM'),1)-1 - trunc(sysdate,'MM')+1
) Where To_char(dates,'DY') NOT IN ('SA','SO'))
At the moment this statement ignores Saturdays and Sundays and it calculates the days from the month before the sysdate (June).
June has 22 days without weekends but sadly my statement says it has 23. I found out it includes the 1st July, which is wrong.
Do you know how I can tell my little statement it only calculates the days from the month I want to get not including days from another month?
Doing this sort of thing is always going to look not pretty... here's one way, which does it for the entire current year. You can restrict to a single month by adding an additional statement to the where clause:
select to_char(trunc(sysdate, 'y') + level - 1, 'fmMON') as month, count(*)
from dual
where to_char(trunc(sysdate, 'y') + level - 1, 'fmDY', 'nls_date_language=english') not in ('SAT','SUN')
connect by level <= trunc(add_months(sysdate, 12), 'y') - trunc(sysdate, 'y')
group by to_char(trunc(sysdate, 'y') + level - 1, 'fmMON')
As I said, not pretty.
Note a couple of things:
Use of the fm format model modifier to remove leading spaces
Explicit use of nls_date_language to ensure it'll work in all environments
I've added 12 months to the current date and then truncated it to the first of January to get the first day of the new year for simplicity
If you want to do this by month it might be worth looking at the LAST_DAY() function
The same statement (using LAST_DAY()) for the previous month only would be:
select count(*)
from dual
where to_char(trunc(sysdate, 'y') + level - 1, 'fmDY', 'nls_date_language=english') not in ('SAT','SUN')
connect by level <= last_day(add_months(trunc(sysdate, 'mm'), -1)) - add_months(trunc(sysdate, 'mm'), -1) + 1
Firstly, your inner query (select trunc(ADD_MONTHS(sysdate,-1),'MM') + level -1 Dates from dual connect by level <= ADD_MONTHS(trunc(sysdate,'MM'),1)-1 - trunc(sysdate,'MM')+1) returns the days of the month plus one extra day from the next month.
Secondly, a simpler query could use the LAST_DAY function which gets the last day of the month.
Finally, use the 'D' date format to get the day of the week as a number.
SELECT COUNT(*) FROM (
SELECT TO_CHAR(TRUNC(SYSDATE,'MM') + ROWNUM - 1, 'D') d
FROM dual CONNECT BY LEVEL <= TO_NUMBER(TO_CHAR(LAST_DAY(SYSDATE),'DD'))
) WHERE d BETWEEN 1 AND 5;
Without having to generate all days of the month and then count them:
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE FUNCTION WORK_DAYS_IN_MONTH(
dt DATE
) RETURN NUMBER DETERMINISTIC
AS
first_day DATE := TRUNC( dt, 'MM' );
remainder NUMBER := LAST_DAY( dt ) - ( first_day + INTERVAL '27' DAY );
BEGIN
RETURN 20 + CASE first_day - TRUNC( first_day, 'IW' )
WHEN 0 THEN remainder -- Monday
WHEN 1 THEN remainder -- Tuesday
WHEN 2 THEN remainder -- Wednesday
WHEN 3 THEN LEAST( remainder, 2 ) -- Thursday
WHEN 4 THEN LEAST( remainder, 1 ) -- Friday
WHEN 5 THEN GREATEST( remainder-2, 0 ) -- Saturday
ELSE GREATEST( remainder-1, 0 ) -- Sunday
END;
END;
//
Query 1:
SELECT ADD_MONTHS( DATE '2014-12-01', LEVEL ) AS "Month",
WORK_DAYS_IN_MONTH( ADD_MONTHS( DATE '2014-12-01', LEVEL ) ) AS "# Work Days"
FROM DUAL
CONNECT BY LEVEL <= 12
Results:
| Month | # Work Days |
|-----------------------------|-------------|
| January, 01 2015 00:00:00 | 22 |
| February, 01 2015 00:00:00 | 20 |
| March, 01 2015 00:00:00 | 22 |
| April, 01 2015 00:00:00 | 22 |
| May, 01 2015 00:00:00 | 21 |
| June, 01 2015 00:00:00 | 22 |
| July, 01 2015 00:00:00 | 23 |
| August, 01 2015 00:00:00 | 21 |
| September, 01 2015 00:00:00 | 22 |
| October, 01 2015 00:00:00 | 22 |
| November, 01 2015 00:00:00 | 21 |
| December, 01 2015 00:00:00 | 23 |