SQL computing and reusing fiscal year calculation in sql query - sql

I have a condition in my SQL query, using Oracle 11g database, that depends on a plan starting or ending with in a fiscal year:
(BUSPLAN.START_DATE BETWEEN (:YEAR || '-04-01') AND (:YEAR+1 || '-03-31')) OR
(BUSPLAN.END_DATE BETWEEN (:YEAR || '-04-01') AND (:YEAR+1 || '-03-31'))
For now, I am passing in YEAR as a parameter. It can be computed as (pseudocode):
IF CURRENT MONTH IN (JAN, FEB, MAR):
USE CURRENT YEAR // e.g. 2015
ELSE:
USE CURRENT YEAR + 1 // e.g. 2016
Is there a way I could computer the :YEAR parameter within in an SQL query and reuse it for the :YEAR parameter?

CTEs are easy, you can make little tables on the fly. With a 1 row table you just cross join it and then you have that value available every row:
WITH getyear as
(
SELECT
CASE WHEN to_char(sysdate,'mm') in ('01','02','03') THEN
EXTRACT(YEAR FROM sysdate)
ELSE
EXTRACT(YEAR FROM sysdate) + 1
END as ynum from dual
), mydates as
(
SELECT getyear.ynum || '-04-01' as startdate,
getyear.ynum+1 || '-03-31' as enddate
from getyear
)
select
-- your code here
from BUSPLAN, mydates -- this is a cross join
where
(BUSPLAN.START_DATE BETWEEN mydates.startdate AND mydates.enddate) OR
(BUSPLAN.END_DATE BETWEEN mydates.startdate AND mydates.enddate)
note, values statement is probably better if Oracle has values then the first CTE would look like this:
VALUES(CASE WHEN to_char(sysdate,'mm') in ('01','02','03') THEN
EXTRACT(YEAR FROM sysdate)
ELSE
EXTRACT(YEAR FROM sysdate) + 1)
I don't have access to Oracle so I might have bugs typos etc since I didn't test.

In the code you shared there is a problem and a potential problem.
Problem, implicit conversion to date without format string.
In (BUSPLAN.START_DATE BETWEEN (:YEAR || '-04-01') AND (:YEAR+1 || '-03-31')) two strings are being formed and then converted to dates. The conversion to date is going to change depending on the value of NLS_DATE_FORMAT. To insure that the string is converted correctly to_date(:YEAR || '-04-01', 'YYYY-MM-DD').
Potential problem, boundary at the end of the year when time <> midnight.
Oracle's date type holds both date and time. A test like someDate between startDate and endDate will miss all records that happened after midnight on endDate. One simple fix that precludes use of indexes on someDate is trunc(someDate) between startDate and endDate.
A more general approach is to define date ranges and closed open intervals. lowerBound <= aDate < upperBound where lowerBound is the same asstartDateabove andupperBoundisendDate` plus one day.
Note: Some applications used Oracle date columns as dates and always store midnight, if your application is of that sort, then this is not a problem. And check constraints like check (trunc(dateColumn) = dateColumn) would make sure it stays that way.
And now, to answer the question actually asked.
Using subquery factoring (Oracle's terminology) / common table expression (SQL Server's terminology) one can avoid repetition within a query.
Instead of figuring out the proper year, and then using strings to put together dates, the code below starts by getting January 1 at Midnight of the current calendar year, trunc(sysdate, 'YEAR')). Then it adds an offset in months. When the months are Jan, Feb, Mar, the current fiscal year started last year on 4/1, or nine months before the start of this year. The offset is -9. Else the current fiscal year started 4/1 of this calendar year, start of this year plus three months.
Instead of end date, an upper bound is calculated, similar to lower bound, but with the offsets being 12 greater than lower bound to get 4/1 the following year.
with current_fiscal_year as (select add_months(trunc(sysdate, 'YEAR')
, case when extract(month from sysdate) <= 3 then -9 else 3 end) as LowerBound
, add_months(trunc(sysdate, 'YEAR')
, case when extract(month from sysdate) <= 3 then 3 else 15 end) as UpperBound
from dual)
select *
from busplan
cross join current_fiscal_year CFY
where (CFY.LowerBound <= busplan.start_date and busplan.start_date < CFY.UpperBound)
or (CFY.LowerBound <= busplan.end_date and busplan.end_date < CFY.UpperBound)
And yet more unsolicited advise.
The times I've had to deal with fiscal year stuff, avoiding repetition within a query was low hanging fruit. Having the fiscal year calculations consistent and correct among many queries, that was the essence of the work. So I'd recommend a developing PL/SQL package that centralizes fiscal calculations. It might include a function like:
create or replace function GetFiscalYearStart(v_Date in date default sysdate)
return date
as begin
return add_months(trunc(v_Date, 'YEAR')
, case when extract(month from v_Date) <= 3 then -9 else 3 end);
end GetFiscalYearStart;
Then the query above becomes:
select *
from busplan
where (GetFiscalYearStart() <= busplan.start_date
and busplan.start_date < add_months(GetFiscalYearStart(), 12))
or (GetFiscalYearStart() <= busplan.end_date
and busplan.end_date < add_months(GetFiscalYearStart(), 12))

Related

Convert integer years or months into days in SQL impala

I have two columns; both have integer values. One Representing years, and the other representing months.
My goal is to perform calculations in days (integer), so I have to convert both to calendar days, to achieve that, taking in consideration that we have years with both 365 and 366 days.
Example in pseudo code:
Select Convert(years_int) to days, Convert(months int) to days
from table.
Real Example:
if --> Years = 1 and Months = 12
1) Convert both to days to compare them: Years = 365 days; Months = 365 days
After conversion : (Years = Months) Returns TRUE.
The problem is when we have years = 10 (for example), we must take in account the fact that at least two of them have 366 days. The same with Months - we have 30 and 31 days. So I need to compensate that fact to get the most accurate possible value in days.
Thanks in advance
From integers to timestamp can be done in PostgreSQL. I do not have impala, but hopefully below script will help you getting this done using impala:
with
year as (select 2022 as y union select 2023),
month as (select generate_series(1,12) as m),
day as(select generate_series(1,31) as d )
select y,m,d,dt from (
select
y,m,d,
to_date(ds,'YYYYMMDD')+(((d-1)::char(2))||' day')::interval dt
from ( select
*,
y::char(4)|| right('0'||m::char(2),2) || right('0'||0::char(2),2) as ds
from year,month,day
) x
) y
where extract(year from dt)=y and extract(month from dt)=m
order by dt
;
see: DBFIDDLE
Used functions in this query and, a way, to convert them to imapala (remember I do not use that tool/language/dialect)
function
impala alternative
to_date(a,b)
This will convert the string a to a date using the format b. Using impala you can use CAST(expression AS type FORMAT pattern)
y::char(4)
Cast y to a char(4), Using imala you can use: CAST(expression AS type)
right(a,b)
Use: right()
\\
Use: concat()
generate_series(a,b)
This generates a serie of numbers from a to (an inclusing) b. A SQL altervative is to write SELECT 1 as x union SELECT 2 union SELECT 3, which generates the same series as generate_series(1,3) in PostgreSQL
extract(year from a)
Get the year from the datetime field a, see YEAR()
One special case is this one to_date(ds,'YYYYMMDD')+(((d-1)::char(2))||' day')::interval
This will convert ds (with datatype CHAR(8)) to a date, and then add (using +) a number of days (like: '4 day')
Because I included all days until 31, this will fail in Februari, April, June, September, November because those months do not have 31 days. This is corrected by the WHERE clause in the end (where extract(year from dt)=y and extract(month from dt)=m)

in rails i need to get all records based on a date range, ignoring the year

i am trying to get all records within a date range in rails that fall after a given start date and before a given end date, ignoring the year. the start date will just be a month. the end of the date range is a month and date.
the following example is to get users with a hire date month equal to january or greater, but less than 2(february) / 28(day).
i was trying this but it doesnt work:
users.where('extract(month from hire_date) >= ?', 1).where('extract(month from hire_date) <= ? AND extract(day from hire_date) <= ?', 2, 28)
the reason this doesnt work is it will exclude users, for example who fall on 1/29, because their day in january is not less than 28.
is there a good way to make this work, ignoring the year on the date field?
I would use to_char to transform the dates into strings including just the month and the day. For example 2021-12-31 could be translated into "12-31". That string can then be compared to the range of date strings you are interested in.
users.where("to_char(hire_date, 'MM-DD') BETWEEN '01-01' AND '02-29'")
Note that this, in theory, this would also match invalid date strings like '01-40'. But I guess it is safe to assume that to_char with a valid date will never return such invalid date strings.
Let me start by saying that I'm not familiar with ruby-on-rails, sql, or activerecord but this question really piqued my interest because it contains a number of complexities that I've never had to deal with. Most notably:
How to accommodate leap years when the test range and target date may
have differing leap year statuses.
How to deal with a hire date of February 29th. This may seem to be
the same as the first item on my list, but it has its own unique
nuances.
How to deal with ranges that started in one year and then overlap into the next.
I did some searching myself and there seems to be very little information out there on the subject, particularly any that seems to properly deal with the various complexities listed above, so I decided to see if I could come up with a logical approach of my own. It seems like it may be a novel approach, but ultimately I decided that converting month and day to float values would allow an easy way to address the leap year issue as well as the issue with February 29th, and that testing the date ranges using an if/else statement along with opposing boolean operations would solve the ranges that overlap years.
You may have to extrapolate and reconfigure for your specific needs, but here's the general idea written out in pure ruby. It's rather verbose, but I did so just to try and make it more clear what I'm doing. I could be made much more compact:
hire_date = Time.new(2004, 1, 22)
provided_start_date = Time.new(2008, 12, 22)
day_value = (60*60*24) #60 seconds * 60 minutes * 24 hours = 1 day
num_days = 30
calculated_end_date = provided_start_date + num_days*day_value
start_range = provided_start_date.month + provided_start_date.day/100.0
end_range = calculated_end_date.month + calculated_end_date.day/100.0
day_float = hire_date.month + hire_date.day/100.0
if start_range < end_range
result = day_float >= start_range and day_float <= end_range
else
result = day_float >= start_range or day_float <= end_range
end
result
So i figured out something that worked for this.
given the following:
start_date = 01 Jan 2021
end_date = 02 Feb 2021
Users.where("(extract(month from hire_date) * 100 + extract(day from hire_date)) BETWEEN ? AND ?", ((start_date.month * 100) + start_date.day), ((end_date.month * 100) + end_date.day))
start date and end date can be any dates and this should work to get all users between, since SQL between is inclusive, it will also get users whos end and start dates fall on the start / end date, ignoring year.

Timestamps and Intervals: NUMTOYMINTERVAL SYSTDATE CALCULATION SQL QUERY

I am working on a homework problem, I'm close but need some help with a data conversion I think. Or sysdate - start_date calculation
The question is:
Using the EX schema, write a SELECT statement that retrieves the date_id and start_date from the Date_Sample table (format below), followed by a column named Years_and_Months_Since_Start that uses an interval function to retrieve the number of years and months that have elapsed between the start_date and the sysdate. (Your values will vary based on the date you do this lab.) Display only the records with start dates having the month and day equal to Feb 28 (of any year).
DATE_ID START_DATE YEARS_AND_MONTHS_SINCE_START
2 Sunday , February 28, 1999 13-8
4 Monday , February 28, 2005 7-8
5 Tuesday , February 28, 2006 6-8
Our EX schema that refers to this question is simply a Date_Sample Table with two columns:
DATE_ID NUMBER NOT Null
START_DATE DATE
I Have written this code:
SELECT date_id, TO_CHAR(start_date, 'Day, MONTH DD, YYYY') AS start_date ,
NUMTOYMINTERVAL((SYSDATE - start_date), 'YEAR') AS years_and_months_since_start
FROM date_sample
WHERE TO_CHAR(start_date, 'MM/DD') = '02/28';
But my Years and months since start column is not working properly. It's getting very high numbers for years and months when the date calculated is from 1999-ish. ie, it should be 13-8 and I'm getting 5027-2 so I know it's not correct. I used NUMTOYMINTERVAL, which should be correct, but don't think the sysdate-start_date is working. Data Type for start_date is simply date. I tried ROUND but maybe need some help to get it right.
Something is wrong with my calculation and trying to figure out how to get the correct interval there. Not sure if I have provided enough information to everyone but I will let you know if I figure it out before you do.
It's a question from Murach's Oracle and SQL/PL book, chapter 17 if anyone else is trying to learn that chapter. Page 559.
you'll want MONTHS_BETWEEN in that numtoyminterval as the product of subtracting two date variables gives the answer in days which isn't usable to you and the reason its so high is you've told Oracle the answer was in years! Also use the fm modifier on the to_char to prevent excess whitespace.
select date_id,
to_char(start_date, 'fmDay, Month DD, YYYY') as start_date,
extract(year from numtoyminterval(months_between(trunc(sysdate), start_date), 'month') )
|| '-' ||
extract(month from numtoyminterval(months_between(trunc(sysdate), start_date), 'month') )
as years_and_months_since_start
from your_table
where to_char(start_date, 'MM/DD') = '02/28';
You can simplify the answer like this
SELECT date_id, start_date, numtoyminterval(months_between(sysdate, start_date), 'month') as "Years and Months Since Start"
FROM date_sample
WHERE EXTRACT (MONTH FROM start_date) = 2 AND EXTRACT (DAY FROM start_date) = 28;

AS400 SQL query to determine records for who is 70.5 years old for the current year

I am trying to find individuals that will turn 70.5 years old in the current year.
dob7 = DECIMAL(7) YYYYDDD
select acctno, name, address, status, year(curdate()) - year(date(digits(dob7))) as Age
from mydata.cdmast cdmast
left join mydata.cfmast cfmast
on cdmast.cifno = cfmast.cifno
where status <> 'R' and year(curdate()) - year(date(digits(dob7))) >= 70
The code above returns the following error:
[Error Code: -181, SQL State: 22008] [IBM][System i Access ODBC Driver][DB2 for i5/OS]SQL0181 - Value in date, time, or timestamp string not valid.
After seeing the other answers, I'm submitting my own. This should have the benefit of using any indicies on dob7, and should work without too many 'tricks'.
I've modified the WHERE clause in your original query. I'm assuming '.5 years' means '6 months', although this is adjustable. I deliberately wrapped the calculations in CTEs to 'encapsulate' the logic; the operations should be nearly no-cost.
WITH Youngest (dateOfBirth) as (
SELECT CURRENT_DATE - 70 YEARS - 6 MONTHS
FROM sysibm/sysdummy1),
Converted (dateOfBirth, formatted) as (
SELECT dateOfBirth, YEAR(dateOfBirth) * 1000 + DAYOFYEAR(dateOfBirth)
FROM Youngest)
SELECT acctno, name, address, status,
YEAR(CURRENT_DATE) - INT(dob7 / 1000)
- CASE WHEN DAYOFYEAR(CURRENT_DATE) < MOD(dbo7, 1000)
THEN 1
ELSE 0 END as Age
FROM myData.cdMast cdMast
JOIN Converted
ON Converted.formatted >= dob7
LEFT JOIN myData.cfMast cfMast
ON cdMast.cifno = cfMast.cifno
WHERE status <> 'R'
Please note that it will consider people born on a leap day to have had their birthday on March 1st (due to DAYOFYEAR()).
From the DATE scalar function documentation:
A string with an actual length of 7 that represents a valid date in the form yyyynnn, where yyyy are digits denoting a year, and nnn are digits between 001 and 366 denoting a day of that year.
Reformat the date with:
DATE(SUBSTR(DIGITS(DOB7),4,4) || SUBSTR(DIGITS(DOB7),1,3))
To select 70.5 or older by the end of the current year:
YEAR(CURRENT_DATE) - YEAR(DATE(SUBSTR(DIGITS(DOB7),4,4) || SUBSTR(DIGITS(DOB7),1,3))) = 70
AND MONTH(DATE(SUBSTR(DIGITS(DOB7),4,4) || SUBSTR(DIGITS(DOB7),1,3))) >= 6
OR YEAR(CURRENT_DATE) - YEAR(DATE(SUBSTR(DIGITS(DOB7),4,4) || SUBSTR(DIGITS(DOB7),1,3))) > 70
The error message is saying that the contents of DOB7 cannot be converted to a date. Does the value of DOB7 match one of the valid formats? Note that many require quotation marks. http://publib.boulder.ibm.com/infocenter/iseries/v6r1m0/index.jsp?topic=/db2/rbafzscadate.htm
Try this instead:
(year(curdate()) - mod(dob7, 10000)) >= 70
This is using modular arithmetic to extract the year, rather than trying to convert it to a date.
By the way, storing the date this way seems very awkward. Databases have built-in support for dates and times, so it is usually better to store them in the native format.
If you date of birth is really yyyymmm, then the following should work for years:
(year(curdate()) - cast(dob7/1000 as int)) >= 70
For the half year:
(year(curdate()) - cast(dob7/1000 as int))+(1-mod(dob7,1000)/365.0) >= 70.5

Selecting employees with birthdays in given range using Oracle SQL

For selecting birthdays between two months where FROMDATE and TODATE are some parameters in a prepared statement I figured something like this:
select
p.id as person_id,
...
...
where e.active = 1
and extract(month from TODATE) >= extract(month from e.dateOfBirth)
and extract(month from e.dateOfBirth) >= extract(month from FROMDATE)
order by extract(month from e.dateOfBirth) DESC,
extract(day from e.dateOfBirth) DESC
How can this be improved to work with days as well?
There's more than one way to search date ranges in Oracle. For your scenario I suggest you turn the month and day elements of all the dates involved into numbers.
select
p.id as person_id,
...
...
where e.active = 1
and to_number (to_char( e.dateOfBirth, 'MMDD'))
between to_number (to_char( FROMDATE, 'MMDD'))
and to_number (to_char( TODATE, 'MMDD'))
order by extract(month from e.dateOfBirth) DESC,
extract(day from e.dateOfBirth) DESC
This won't use any index on the e.dateOfBirth column. Whether that matters depends on how often you want to run the query.
#AdeelAnsari comments:
" I don't like to to_char the predicate, in order to make use of
index"
What index? A normal index on dateOfBirth isn't going to be of any use, because the index entries will lead with the year element. So it won't help you find all the records of people born on any 23rd December.
A function-based index - or in 11g, a virtual column with an index (basically the same thing) - is the only way of indexing parts of a date column.
Do you need maximum performance, and so are willing to make a change to the schema? Or are the number of records so small, and performance relatively un-important, that you want a query that will work with the data as-is?
The simplest and fastest way to do this is to store a second data-of-birth field, but 'without' the year. I put quotes around 'without' because a date can't actually not have a year. So, instead, you just base it on another year.
Re-dating every DoB to the year 2000 is a good choice in my experience. Because it includes a leap-year and is a nice round number. Every DoB and FromDate and ToDate will work in the year 2000...
WHERE
DoB2000 >= FromDate
AND DoB2000 <= ToDate
(This assumes you also index the new field to make the search quick, otherwise you still get a scan, though it MAY be faster than the following alternative anyway.)
Alternatively, you can keep using the EXTRACT pattern. But that will have an unfortunate consequence; it's extremely messy and you will never get an Index Seek, you will always get an Index Scan. This is because of the fact that the searched field is wrapped in a function call.
WHERE
( EXTRACT(month FROM e.DateOfBirth) > EXTRACT(month FROM FromDate)
OR ( EXTRACT(month FROM e.DateOfBirth) = EXTRACT(month FROM FromDate)
AND EXTRACT(day FROM e.DateOfBirth) >= EXTRACT(day FROM FromDate)
)
)
AND
( EXTRACT(month FROM e.DateOfBirth) < EXTRACT(month FROM ToDate)
OR ( EXTRACT(month FROM e.DateOfBirth) = EXTRACT(month FROM ToDate)
AND EXTRACT(day FROM e.DateOfBirth) <= EXTRACT(day FROM ToDate)
)
)
you should be able to use
SELECT * FROM mytbale
where dateofbirth between start_dt and end_dt
alternate:
you can convert dates to the day of the year using:
to_char( thedate, 'ddd' )
then check the range (note this has the same issue as #Dems answer where you should not span the end of the year as in Dec 25th through Jan 10th.)
This is the query that I am using for birtdates in the next 20 days:
SELECT A.BIRTHDATE,
CEIL(ABS (MONTHS_BETWEEN(A.BIRTHDATE, SYSDATE) / 12)) AGE_NOW,
CEIL(ABS (MONTHS_BETWEEN(A.BIRTHDATE, SYSDATE + 20) / 12)) AGE_IN_20_DAYS
FROM USERS A
WHERE
CEIL(ABS (MONTHS_BETWEEN(A.BIRTHDATE, SYSDATE) / 12)) <> CEIL(ABS (MONTHS_BETWEEN(A.BIRTHDATE, SYSDATE + 20) / 12));
ABS (MONTHS_BETWEEN(A.BIRTHDATE, SYSDATE) / 12)) return the age in format 38.9, 27.2, etc
Applying ceil() will give us the difference in years that we need to determinate if this person is near to have a birthdate. Eg.
Age: 29.9
29.9 + (20 days) = 30.2
ceil(30.1) - ceil(29.9) = 1
This is the result of querying at december 16:
BIRTHDATE AGE_NOW AGE_IN_20_DAYS
12/29/1981 35 36
12/29/1967 49 50
1/3/1973 44 45
1/4/1968 49 50
I don't know how this behaves in terms of performance, but I'd try using regular date subtraction. Say, for example, you want birth days between January 1st and July 1st. Anyone who is between 25 and 25.5 would qualify. That is, anyone whose partial age is less than 1/2 year would qualify. The math is easy at 1/2, but it is the same regardless of the window. In other words, "within one month" means +/- 1/12 of a year, within 1 day means +/- 1/365 of a year, and so on.
The year does not matter in this method, so I'll use current year when creating the variable.
Also, I would think of the center of the range, and then do absolute values (either that, or always use a future date).
For example
select personid
from mytable
where abs(mod(dob - target_birth_date)) < 1/52
would give you everyone with a birthday within a week.
I realize this code is barely pseudocode, but is seems like it might allow you to do it, and you might still use indices if you tweaked it a little. Just trying to think outside the box.
At the end we picked litter different solution where we add fist create the anniversary date :
where
...
and (to_char(sysdate,'yyyy') - to_char(e.dateofbirth,'yyyy')) > 0
and add_months(e.dateofbirth,
(to_char(sysdate,'yyyy') - to_char(e.dateofbirth,'yyyy')) * 12)
>:fromDate:
and :toDate: > add_months(e.dateofbirth,
(to_char(sysdate,'yyyy') - to_char(e.dateofbirth,'yyyy')) * 12)
order by extract(month from e.dateofbirth) DESC,
extract(day from e.dateofbirth) DESC)
That's the solution!!!
SELECT
*
FROM
PROFILE prof
where
to_date(to_char(prof.BIRTHDATE, 'DDMM'), 'DDMM') BETWEEN sysdate AND sysdate+5
or
add_months(to_date(to_char(prof.BIRTHDATE, 'DDMM'), 'DDMM'),12) BETWEEN sysdate AND sysdate+5
Guys I have a simpler solution to this problem
step 1. convert the month into number,
step 2. concatenate the day(in two digits) to the month
step 3. then convert the result to number by doing to_number(result)
step 4. Once you have this for the start date and end date, then do a between function on it and u are done.
sample code:
SELECT date_of_birth
FROM xxxtable
where to_number(to_number(to_char(to_date(substr(date_of_birth,4,3),'mon'),'mm'))||substr(date_of_birth,1,2)) between
to_number(to_number(to_char(to_date(substr(:start_date_variable,4,3),'mon'),'mm'))||(substr(:start_date_variable,1,2))) and
to_number(to_number(to_char(to_date(substr(:end_date_variable,4,3),'mon'),'mm'))||(substr(:end_date_variable,1,2)));