Looping with Dates in SQL - sql

I'm working in SQL Developer and calculating the average amount for all active cases at the end of a given month. The way I’ve written it, if I want results for every month in the past year, I have to re-run the code 12 times:
-- DEFINE month_end = '28/02/21';
-- DEFINE month_end = '31/03/21';
DEFINE month_end = '30/04/21';
with active_at_month_end as (
SELECT amount
FROM table
WHERE start_date <= '&month_end'
AND end_date > '&month_end'
)
SELECT extract(year from to_date('&month_end','DD/MM/YY')) as year,
extract(month from to_date('&month_end','DD/MM/YY')) as month,
avg(amount) as avg_amount
FROM active_at_month_end
Is there a way I could rewrite it (maybe using a for loop?) so I only have to run it once and get results like this?
Year
Month
avg_amt
2021
2
###
2021
3
###
2021
4
###

If you're using Oracle, you may use something like below -
DECLARE
month_end DATE := '31-DEC-2020'; -- assuming you want to display output from Jan-21
no_Of_Months NUMBER := 12; -- if you want to display 12 months
BEGIN
FOR i IN 1 .. no_Of_Months
LOOP
month_end := ADD_MONTHS(month_end, 1); -- output will start from Jan-2021
Select year, month, avg(amount) as avg_amount
from
(SELECT extract(year from month_end) as year,
extract(month from month_end) as month,
amount
FROM table
WHERE start_date <= month_end
AND end_date > month_end)
) temp
group by year, month;
END LOOP;
END;

Related

Calculating monthly churn

I am trying to calculate a monthly churn rate (for a given month: number_of_people_who_unsubscribed / number_of_subscribers_at_beginning_of_month).
I have a subscribers table that looks like this:
id
start_date
end_date
1
2020-03-17
null
2
2020-06-21
2020-09-03
I can calculate churn for a single month with a query like this:
select
(
/* Subscriptions that ended during January */
select count(*)::decimal from subscriptions
where end_date is not null
and end_date >= '2021-01-01'
and end_date <= '2021-01-31'
) /
(
/* Subscriptions that were active at the beginning of January */
select count(*)::decimal from subscriptions
where end_date is null
or end_date >= '2021-01-01'
) as churn
That gives me a single percentage of users who unsubscribed during January. However, I'd like to output this percentage for every month, so I can display it as a line chart. I'm not sure where to start - it feels like I need to loop and run the query for each month but that feels wrong. How could I make the same calculation, but without specifying the month manually? We can assume that there is at least one start_date and one end_date per month, so some kind of group by might work.
Ultimately I'm looking for an output that looks something like:
month
churn
2020-03
0.076
2020-04
0.081
2020-05
0.062
Using your data logic and a sequence of months:
select to_char(running_month, 'yyyy-mm') as "month",
(
/* Subscriptions that ended during January */
select count(*)::numeric from subscriptions
where end_date is not null
and end_date >= running_month
and end_date <= running_month + interval '1 month - 1 day'
) /
(
/* Subscriptions that were active at the beginning of January */
select count(*)::numeric from listings
where end_date is null
or end_date >= running_month
) as churn
from generate_series ('2020-01-01'::date, '2020-12-01'::date, interval '1 month') as running_month;

Oracle SQL - Define the year element of a date dependent on the current month

I am trying to create a view in SQL Developer based on this statement:
SELECT * FROM ORDERS WHERE START_DATE > '01-JUL-2020'
The year element of the date needs to set to the year of the current date if the current month is between July and December otherwise it needs to be the previous year.
The statement below returns the required year but I don't know how to incorporate it (or a better alternative) into the statement above:
select
case
when month(sysdate) > 6 then
year(sysdate)
else
year(sysdate)-1
end year
from dual
Thanks
Oracle doesn't have a built-in month function so I'm assuming that is a user-defined function that you've created. Assuming that's the case, it sounds like you want
where start_date > (case when month(sysdate) > 6
then trunc(sysdate,'yyyy') + interval '6' month
else trunc(sysdate,'yyyy') - interval '6' month
end)
Just subtract six months and compare the dates:
SELECT *
FROM ORDERS
WHERE trunc(add_months(sysdate, -6), 'YYYY') = trunc(start_date, 'YYYY')
This compares the year of the date six months ago to the year on the record -- which seems to be the logic you want.

How to calculate number of leap years between two dates in Oracle?

I want to calculate the number of leap years between the hire date of the employee and the current date (on hr.employees table in Oracle SQL Developer). How to do this?
A leap year consists of 366 days. I assume a "leap year" between two dates consists of all the days from Jan 1 to Dec 31 of a year with Feb 29th.
Based on this understanding, there is a pretty simple solution.
Count the number of days between the Jan 1 of the year following the hire date and Jan 1 of the year of the end date.
Count the number of years between those two dates.
Subtract the difference between the days and the number of years * 365
Happily, built-in functions do most of the work.
This results in:
(trunc(sysdate, 'YYYY') -
trunc(hiredate + interval '1' year, 'YYYY') -
365 * (extract(year from sysdate) - extract(year from hiredate) - 1)
) as num_years
It is a little trickier to count leap days but that is not what the question is asking.
You can reuse the code Oracle has already written: just check if creating a leap day raises an exception:
SELECT TO_DATE('2016-02-29','YYYY-MM-DD') FROM DUAL;
29.02.2016
but
SELECT TO_DATE('2018-02-29','YYYY-MM-DD') FROM DUAL;
ORA-01839: date not valid for month specified
So you just have to count the exceptions:
CREATE OR REPLACE FUNCTION count_leap_years (p_from DATE, p_to DATE) RETURN NUMBER
IS
number_of_leap_days NUMBER := 0;
date_not_valid EXCEPTION;
PRAGMA EXCEPTION_INIT(date_not_valid, -1839);
BEGIN
FOR y IN EXTRACT(YEAR FROM p_from) .. EXTRACT(YEAR FROM p_to) LOOP
DECLARE
d DATE;
BEGIN
d := TO_DATE(to_char(y,'fm0000')||'-02-29', 'YYYY-MM-DD');
IF p_from < d AND d < p_to THEN
number_of_leap_days := number_of_leap_days + 1;
END IF;
EXCEPTION WHEN date_not_valid THEN
NULL;
END;
END LOOP;
RETURN number_of_leap_days;
END count_leap_years;
/
Something like this will allow you to select numbers, in this case, from 1 to 2999.
Select Rownum year
From dual
Connect By Rownum <= 2999
Something like this will allow you to check if a specific year is a leap year or not
CASE WHEN ((MOD(YEAR, 4) = 0 AND (MOD(YEAR, 100) <> 0)) OR MOD(year, 400) = 0) THEN 1 ELSE 0 END as isLeapYear
Now you just need to add the filtering and the sum of the leap years.
Select sum(isLeapYear)
from (
Select year, CASE WHEN ((MOD(YEAR, 4) = 0 AND (MOD(YEAR, 100) <> 0)) OR MOD(year, 400) = 0) THEN 1 ELSE 0 END as isLeapYear
FROM (
Select Rownum year
From dual
Connect By Rownum <= 2999
) a
Where a.year >= EXTRACT(YEAR FROM DATE %DateStart%) and a.year <= EXTRACT(YEAR FROM DATE %DateEnd%)
) b
This can be really improved in terms of performance, but like this I think its easier do understand what each step is doing and then you can decompose it into what you really want and expect.
try this:
CREATE OR REPLACE FUNCTION LEAP_YEARS(EMP_ID IN NUMBER)
RETURN NUMBER
IS
HIRE_YEAR NUMBER:=0;
SYS_YEAR NUMBER:=0;
NUMBER_LEAP_YEARS NUMBER:=0;
BEGIN
SELECT EXTRACT(YEAR FROM HIRE_DATE) INTO HIRE_YEAR
FROM EMPLOYEE
WHERE EMPLOYEE_ID = EMP_ID;
SELECT EXTRACT(YEAR FROM SYSDATE()) INTO SYS_YEAR
FROM DUAL;
FOR IDX IN HIRE_YEAR..SYS_YEAR LOOP
IF MOD(IDX,4)=0 THEN
NUMBER_LEAP_YEARS := NUMBER_LEAP_YEARS + 1;
END IF;
END LOOP;
RETURN NUMBER_LEAP_YEARS;
END;
I tested it on a toy example and it works. Maybe you need to improve it (hint: maybe it needs some exceptions and it assume that hire_date < sysdate).
Then you can use it as:
SELECT LEAP_YEARS(E.EMPLOYEE_ID) FROM EMPLOYEE E;

Returning a Date Range based on Day of the Month

Select *
From Orders
WHERE (
extract(day from sysdate)<=21
and
to_date(SCHEDULEDATE , 'yyyy/mm/dd') between
to_date((to_char(sysdate, 'YYYY')||'/'||cast((extract(month from sysdate)-1)as char)||'/22'),'yyyy/mm/dd') and to_date((to_char(sysdate,'YYYY')||'/'||cast((extract(month from sysdate))as char)||'/21'),'yyyy/mm/dd')
)
or
(
extract(day from sysdate)>21
and
to_date(SCHEDULEDATE , 'yyyy/mm/dd') between
to_date((to_char(sysdate, 'YYYY')||'/'||cast((extract(month from sysdate))as char)||'/22'),'yyyy/mm/dd') and to_date((to_char(sysdate, 'YYYY')||'/'||cast((extract(month from sysdate)+1)as char)||'/21'),'yyyy/mm/dd')
)
I'm trying to figure out a simple way of returning a set of date ranges based on the day of the Month. If the Day of the month of less than or Equal to I want it to return all orders the have a schedule date between the 22 of the Month before and the 21st of the Current month. If the Day of the month is greater than 21 I would like it return all orders that have a schedule date of the current month up to the end of the month. I've tried to use a case in the where with no luck. What I have now doesn't seem to work either. Any help would be appreciated.
I think this does what you want:
WHERE (extract(day from sysdate) <= 21 and
scheduledate >= add_months(trunc(sysdate, 'MON'), -1) + 21 and
scheduledate < trunc(sysdate, 'MON') + 21
) or
(extract(day from sysdate) > 21 and
trunc(scheduledate, 'MON') = trunc(sysdate, 'MON')
)

sql database function (cut-off time on 4.30pm)

I'm try to write SQL Database function. It should count number of working day with a cut-off point at 4.30pm. So any orders before 4.30pm appear in the total for the day before, and after 4.30pm in the total for that day. I found the code that count number of working day but I know how to add the cut-off point into the code?
create or replace
function "number_of_worked_day"
(p_start_dt date,
p_end_dt date
)
return number as
L_Number_Of_Days Number;
L_Start_Dt Date;
L_end_dt DATE;
Begin
L_Start_Dt :=Trunc(P_Start_Dt);
L_end_dt := trunc(p_end_dt);
SELECT COUNT(*)
into l_number_of_days
FROM (
WITH date_tab AS (SELECT TO_DATE (L_Start_Dt) + LEVEL - 1 business_date
FROM DUAL
CONNECT BY LEVEL <=
TO_DATE (L_end_dt)
- TO_DATE (L_Start_Dt)
+ 1)
SELECT business_date
FROM date_tab
WHERE TO_CHAR (business_date, 'DY') NOT IN ('SAT', 'SUN')
AND NOT EXISTS
( SELECT 1
FROM PUBLIC_HOLIDAY
WHERE business_date between START_DT and END_DT));
return l_number_of_days;
end;
I find your code a bit hard to follow. But the logic for handling a day starting at 4:30 p.m. is to subtract 16.5 hours from the time. For instance:
SELECT trunc(business_date - 16.5/24), count(*)
FROM date_tab
GROUP BY trunc(business_date - 16.5/24)
ORDER BY trunc(business_date - 16.5/24);