Check whether two dates contain a given month - sql

My problem is simple... or may be not. I've got a table that contains two dates:
StartDate
EndDate
And I have a constant which is a month. For example:
DECLARE #MonthCode AS INT
SELECT #MonthCode = 11 /* NOVEMBER */
I need a SINGLE QUERY to find all records whose StartDate and EndDate includes the given month. For example:
/* Case 1 */ Aug/10/2009 - Jan/01/2010
/* Case 2 */ Aug/10/2009 - Nov/15/2009
/* Case 3 */ Nov/15/2009 - Jan/01/2010
/* Case 4 */ Nov/15/2009 - Nov/15/2009
/* Case 5 */ Oct/01/2010 - Dec/31/2010
The first and last case need special attention: Both dates are outside November but the cross over it.
The following query does not take care of case 1 and 5:
WHERE MONTH( StartDate ) = #MonthCode OR MONTH( EndDate ) = #MonthCode
The following query also failed because Aug < Nov AND Nov < Jan = false:
WHERE MONTH( StartDate ) = #MonthCode OR MONTH( EndDate ) = #MonthCode OR (
MONTH( StartDate ) < #MonthCode AND #MonthCode < MONTH( EndDate )
)

I understand that you are looking for a way to select all the ranges that intersect November, in any year.
Here is the logic:
if the range falls on a single year (e.g. 2009), the start month must be before or equal to November AND the end month after or equal to November
if the range falls on two subsequent years (e.g. 2009-2010), the start month must be before or equal to November OR the end month after or equal to November
if the range falls on two years with more than 1 year in difference (e.g. 2008-2010), November is always included in the range (here November 2009)
Translated in pseudo-code, the condition is:
// first case
(
(YEAR(StartDate)=YEAR(EndDate)) AND
(MONTH(StartDate)<=MonthCode AND MONTH(EndDate)>=MonthCode)
)
OR
// second case
(
(YEAR(EndDate)-YEAR(StartDate)=1) AND
(MONTH(StartDate)<=MonthCode OR MONTH(EndDate)>=MonthCode)
)
OR
// third case
(
YEAR(EndDate)-YEAR(StartDate)>1
)

DECLARE #MonthCode AS INT
SELECT #MonthCode = 11 /* NOVEMBER */
declare #yourtable table(
startdate datetime
, enddate datetime
)
insert into #yourtable(
startdate
, enddate
)
(
select '8/10/2009', '01/01/2010'
union all
select '8/10/2009' , '11/15/2009'
union all
select '11/15/2009' , '01/01/2010'
union all
select '11/15/2009' , '11/15/2009'
union all
select '10/01/2010' , '12/31/2010'
union all
select '05/01/2009', '10/30/2009'
)
select *
from #yourtable
where DateDiff(mm, startdate, enddate) > #MonthCode -- can't go over 11 months without crossing date
OR (Month(startdate) <= #MonthCode -- before Month selected
AND (month(enddate) >=#MonthCode -- after month selected
OR year(enddate) > year(startdate) -- or crosses into next year
)
)
OR (Month(startdate) >= #MonthCode -- starts after in same year after month
and month(enddate) >= #MonthCode -- must end on/after same month assume next year
and year(enddate) > year(startdate)
)

Try this:
select * from Mytable
where
month(StartDate) = #MonthCode or month(EndDate) = #MonthCode // Nov/15/2009 - Nov/15/2009
or
dateadd(month,#MonthCode-1,convert(datetime,convert(varchar,year(StartDate))))
between StartDate and EndDate // Oct/01/2010 - Dec/31/2010
or
dateadd(month,#MonthCode-1,convert(datetime,convert(varchar,year(EndDate))))
between StartDate and EndDate // Dec/01/2009 - Dec/31/2010 - tricky one
The main ideea is to check where are 01.November.StartYear and 01.November.EndYear dates located.
Hope it helps.

Filter for the rows that start before the end of the month, and end after the start of the month. For October 2009:
select *
from YourTable
where StartDate < '2009-11-01' and EndDate >= '2009-10-01'
Or, with just the month as input:
declare #month datetime
set #month = '2009-10-01'
select *
from YourTable
where StartDate < dateadd(month,1,#month)
and EndDate >= #month

There are various functions you can use to achieve this, like DATEPART and DATETIFF. However, the real problem is not how to express the condition of StartDate or EndDate falling on the given month, but how to do this in a fashion that makes the query efficient. In other words how to express this in a SARGable fashion.
In case you search a small change table, anything under 10k pages, then it doesn't make that much of a difference, a full scan would be probably perfectly acceptable. The real question is if the table(s) are significant in size and a full scan is unacceptable.
If you don't have an index on any of the StartDate or EndDate column it makes no difference, the criteria is not searchable and the query will scan the entire table anyway. However, if there are indexes on StartDate and EndDate the way you express the condition makes all the difference. The critical part for DATETIME indexes is that you must express the search as an exact date range. Expressing the condition as a function depending on the DATETIME field will render the condition unsearchable, resulting in a full table scan. So this knowledge render itself to the correct way searching a date range:
select ... from table
where StartDate between '20091101' and '20091201'
or EndDate between '20091101' and '20091201';
This can be also expressed as:
select ... from table
where StartDate between '20091101' and '20091201'
union all
select ... from table
where EndDate between '20091101' and '20091201'
and StartDate not between '20091101' and '20091201';
Which query works better depends on a number of factors, like your table size and statistics of the actual data in the table.
However, you want the month of November from any year, which this query does not give you. The solution to this problem is against every instinct a programmer has: hard code the relevant years. Most times the tables have a small set of years anyway, something in the range of 4-5 years of past data and plan for 3-4 years more until the system will be overhauled:
select ... from table
where StartDate between '20051101' and '20051201'
or EndDate between '20051101' and '20051201'
union all
select ... from table
where StartDate between '20061101' and '20061201'
or EndDate between '20061101' and '20061201'
union all
...
select ... from table
where StartDate between '20151101' and '20151201'
or EndDate between '20151101' and '20151201';
There are 12 months in a year, write 12 separate procedures. Does this sound crazy? It sure does, but is the optimal thing from the SQL query compiler and optimizer perspective. How can one maintain such code? 12 separate procedure, with a query that repeats itself 10 times (20 times if you use the UNION between StartDate and EndDate to remove the OR), 120 repeats of code, it must be non-sense. Actually, it isn't. Use code generation to create the procedures, like XML/XSLT, so you can easily change it and maintain it. Does the client has to know about the 12 procedures and call the appropriate one? Of course not, it calls one wrapper procedure that discriminates on the #Month argument to call the right one.
I recon that anyone who will looks at the system after the facts will likely believe this query was written by a band of drunk monkeys. Yet somewhere between parameter sniffing, index SARGability and SQL DATETIME quirks the result is that this is the state of the art today when it pertains to searching calendar intervals.
Oh, and if the query hits the Index Tipping Point it will make the whole argument mute anyway...
Update
BTW there is also a cheap way out if you're willing to sacrifice some storage space: two persisted computed columns on StartMonth AS DATEPART(month, StartDate) and EndDate AS DATEPART(month, EndDate), and index on each and query WHERE StartMonth = #Month OR EndMonth = #Month (or again UNION between two queries one for Start one for End, to remove the OR).

SQL Server 200/2005, You can also do this:
select
*
from
table
where
datepart(m,startDate) = 11
and datepart(m,EndDate) = 11
UPDATE:
Removed and datepart(yyyy,startDate) = datepart(yyyy,endDate)
Do want a given month regardless of Year or Day?

Related

how do I create a calculated field that returns days remaining till end of FISCAL_QUARTER?

Current output with no DAYS_LEFT_IN_QUARTERI am new to using Snowflake and was tasked to create a Calendar Dimension table that would aid in reporting weekly / monthly /quarterly reports. I am confused on how to return days remaining in the FISCAL_QUARTER. Q1 spans from Feb - Apr.
Attached below is the code I have been writing to generate the dates projecting 14 years in the future.
--Set the start date and number of years to produce
SET START_DATE = '2012-01-01';
SET NUMBER_DAYS = (SELECT TRUNC(14 * 365));
--Set parameters to force ISO
ALTER SESSION SET WEEK_START = 1, WEEK_OF_YEAR_POLICY = 1;
WITH CTE_MY_DATE AS (
SELECT DATEADD(DAY, SEQ4(), $START_DATE) AS MY_DATE
FROM TABLE(GENERATOR(ROWCOUNT=>$NUMBER_DAYS)) -- Number of days after reference date in previous line
)
SELECT
MY_DATE::date
,YEAR(MY_DATE) AS YEAR
,MONTH(MY_DATE) AS MONTH
,MONTHNAME(MY_DATE) AS MONTH_ABBREVIATION
,DAY(MY_DATE)
,DAYOFWEEK(MY_DATE)
,WEEKOFYEAR(MY_DATE)
,DAYOFYEAR(MY_DATE)
,YEAR(ADD_MONTHS(DATE_TRUNC('month', MY_DATE),11)) AS FISCAL_YEAR
,CONCAT('Q', QUARTER(ADD_MONTHS(DATE_TRUNC('month', MY_DATE),11))) AS FISCAL_QUARTER
,MONTH(ADD_MONTHS(DATE_TRUNC('month', MY_DATE),11)) AS FISCAL_MONTH
FROM CTE_MY_DATE
;
firstly your generator will get gaps, as SEQx() function are allowed to have gaps, so you need to use SEQx() as the OVER BY of a ROW_NUMBER like so:
WITH cte_my_date AS (
SELECT DATEADD(DAY, ROW_NUMBER() OVER(ORDER BY SEQ4()), $START_DATE) AS my_date
FROM TABLE(GENERATOR(ROWCOUNT=>$NUMBER_DAYS)) -- Number of days after reference date in previous line
)
and days left in quarter, is the day truncated to quarter, +1 quarter, date-diff in days to day:
,DATEDIFF('days', my_date, DATEADD('quarter', 1, DATE_TRUNC('quarter', my_date))) AS days_left_in_quarter
How's this? You can copy/paste the code straight into snowflake to test.
Using last_day() tends to make it look a little tidier :-)
WITH CTE_MY_DATE AS (
SELECT DATEADD(DAY, SEQ4(), current_date()) AS MY_DATE
FROM TABLE(GENERATOR(ROWCOUNT=>300)))
SELECT
MY_DATE::date
,YEAR(last_day(my_date,year)) AS FISCAL_YEAR
,concat('Q',quarter(my_date)) AS FISCAL_QUARTER
,datediff(d, my_date, last_day(my_date,quarter)) AS
DAYS_LEFT_IN_QUARTER
FROM CTE_MY_DATE

Count # of Saturdays given a date range

I have a datetime field and a net field. The Sat Count field is done by =IIf(DatePart("w",Fields!DespatchDate.Value)=7,1,0)
I want to total the count of the Saturdays given a starting date and end date (typically a month).
I tried =Sum(IIf(DatePart("w",Fields!DespatchDate.Value)=7,1,0) but the total is wrong.
I also want to count Saturdays for rest of the month, e.g there's a missing 3rd Saturday in the picture.
I also want to do a total of the Net for Saturdays.
Can you point me in the direction. I can do it in SQL or in SSRS
Considering that we do not have any Input or desired output provided, I am assuming that You just want to count Saturdays in a given range:
Select COUNT(*), SUM(Net)
FROM table
WHERE Day# = 7 AND Date BETWEEN '2021-02-16' AND '2021-02-23'
Assuming you want to count saturdays even if it is not part of your dataset, what you need to do is pad out all your dates for the given range and then join it to your base data set.
This would ensure that it accounts for ALL days of the week regardless of a dispatch event occuring on that date / day.
Below is some SQL code that might help you make a start.
declare #startdate date = '2021-02-01'
declare #enddate date = '2021-02-28'
if OBJECT_ID ('tempdb..#dates') is not null
drop table #dates
;WITH mycte AS
(
SELECT CAST(#startdate AS DATETIME) DateValue
UNION ALL
SELECT DateValue + 1
FROM mycte
WHERE DateValue + 1 < #enddate
)
SELECT DateValue into #dates
FROM mycte
OPTION (MAXRECURSION 0)
select
d.DateValue
, datepart(weekday,d.DateValue) as day_no
,case when datepart(weekday,d.DateValue) = 7 then isnull(t.net,0) else 0 end as sat_net
,case when datepart(weekday,d.DateValue) = 1 then isnull(t.net,0) else 0 end as sun_net
from #dates d
left join your_table t
on d.DateValue = t.some_date
drop table #dates
Since I don't know what your required output is, I cannot summarise this any further. But you get the idea!

Looking for a birthdate Range in SQL

I am looking for a birthdate range of march 21 to April 20, and the year doesn't matter. and it seems that when i search other months and date are coming out.
select * from AllStudentInfo Where
((MONTH(birthDate) >= 3 and day(birthDate) >= 21)
OR
(MONTH(birthDate) <= 4 and day(birthDate) <= 20))
AND
(BGRStatus = 'S' OR BGRStatus = 'SL')
Chances are that you want to discover up & coming dates. In any case, you can create a virtual date as follows:
SELECT DATEFROMPARTS (2017,month(birthDate),day(birthDate) as birthday
FROM AllStudentInfo
In this case, you can use:
SELECT *
FROM AllStudentInfo
WHERE DATEFROMPARTS (2017,month(birthDate),day(birthDate)
BETWEEN '2017-03-21' AND '2017-04-20';
The year 2017 is arbitrary. The point is that the dates in the BETWEEN clause are in the same year.
Using more modern techniques, you can combine it as follows:
WITH cte AS(
SELECT *,DATEFROMPARTS (2017,month(birthDate),day(birthDate) as birthDay
FROM AllStudentInfo
)
SELECT * FROM cte WHERE birthDay BETWEEN '2017-03-21' AND '2017-04-20';
The cte is a Common Table Expression which is an alternative to using a sub query.
Here is an alternative which is closer to the spirit of the question. You can use the format function to generate an expression which is purely month & day:
format(birthDate,'MM-dd')
The MM is MSSQL’s way of saying the 2-digit month number, and dd is the 2-digit day of the month.
This way you can use:
format(birthDate,'MM-dd') BETWEEN '03-21' AND '04-20'
Again as a CTE:
WITH cte AS(
SELECT *,format(birthDate,'MM-dd') as birthDay
FROM AllStudentInfo
)
SELECT * FROM cte WHERE birthDay BETWEEN '03-21' AND '04-20';
You should get the same results, but the year is completely ignored.
Switch your statement to AND
Like so
select * from AllStudentInfo Where
((MONTH(birthDate) >= 3 and day(birthDate) >= 21)
AND --Make the change here
(MONTH(birthDate) <= 4 and day(birthDate) <= 20))
AND
(BGRStatus = 'S' OR BGRStatus = 'SL')
Using OR you are querying anything that is in that date range OR that month range. And therefore, you would get results from other every months.

Find previous equivalent dates over the past two calender years

If today is say 15th August 2012 then the query should return the following
15/01/2011,
15/02/2011,
...
...
15/07/2012
15/08/2012
If today is 31st August 2012 then the query would return
31/01/2011,
28/02/2011, <<<<this is the nearest date
...
...
31/07/2012
31/08/2012
We have a vw_DimDate in our Warehouse which should help
edit
It contains the following fields
Currently I'm using the following but it seems rather convoluted! ...
DECLARE #Dt DATETIME = '31 JUL 2012'--GETDATE()
;WITH DateSet_cte(DayMarker)
AS
(
SELECT DayMarker
FROM WHData.dbo.vw_DimDate
WHERE
DayMarker >= CONVERT(DATETIME,CONVERT(CHAR(4),DATEADD(YEAR,-1,#Dt),112) + '0101') AND
DayMarker <=#Dt
)
, MaxDate_cte(MaxDate)
AS
(
SELECT [MaxDate] = MAX(DayMarker)
FROM DateSet_cte
)
SELECT
[Mth] = CONVERT(DATETIME,CONVERT(CHAR(6),a.DayMarker,112) + '01')
, MAX(a.DayMarker) [EquivDate]
FROM DateSet_cte a
WHERE DAY(a.DayMarker) <= (SELECT DAY([MaxDate]) FROM MaxDate_cte)
GROUP BY CONVERT(DATETIME,CONVERT(CHAR(6),a.DayMarker,112) + '01')
;with Numbers as (
select distinct number from master..spt_values where number between 0 and 23
), Today as (
select CONVERT(date,CURRENT_TIMESTAMP) as d
)
select
DATEADD(month,-number,d)
from
Numbers,Today
where DATEPART(year,DATEADD(month,-number,d)) >= DATEPART(year,d) - 1
Seems odd to want a variable number of returned values based on how far through the year we are, but that's what I've implemented.
When you use DATEADD to add months to a value, then it automatically adjusts the day number if it would have produced an out of range date (e.g. 31st February), such that it's the last day of the month. Or, as the documentation puts it:
If datepart is month and the date month has more days than the return month and the date day does not exist in the return month, the last day of the return month is returned.
Of course, if you already have a numbers table in your database, you can eliminate the first CTE. You mentioned that you "have a vw_DimDate in our Warehouse which should help", but since I have no idea on what that (presumably, a) view contains, it wasn't any help.

How to compare date (month/year) with another date (month/year)

How to compare parts of date in Informix DBMS:
I wrote the following query but I get a syntax error:
select id,name from h_history
where ( h_date::DATETIME YEAR TO MONTH >= 2/2012 )
and ( h_date::DATETIME YEAR TO MONTH <= 1/2013 )
I wanna to compare with year and month
How to do this?
If you're going to use the DATETIME route, you need to format the values correctly. As it is, you've written:
select id,name from h_history
where ( h_date::DATETIME YEAR TO MONTH >= 2/2012 )
and ( h_date::DATETIME YEAR TO MONTH <= 1/2013 )
The 2/2012 is an integer division equivalent to 0, and there's no implicit cast from integers to datetime or vice versa.
You could write:
-- Query 1
SELECT id, name
FROM h_history
WHERE (h_date::DATETIME YEAR TO MONTH >= DATETIME(2012-02) YEAR TO MONTH)
AND (h_date::DATETIME YEAR TO MONTH <= DATETIME(2013-01) YEAR TO MONTH)
That's verbose but precise. You could use a short-cut:
-- Query 2
SELECT id, name
FROM h_history
WHERE (h_date::DATETIME YEAR TO MONTH >= '2012-02')
AND (h_date::DATETIME YEAR TO MONTH <= '2013-01')
However, since h_date is a DATE rather than a DATETIME X TO Y type, there are other options available. The YEAR, MONTH, DAY functions extract the obvious parts from a DATE (and, if you pass a DATETIME to the function, then the DATETIME will be coerced into a DATE and then processed). The only fully locale-independent DATE constructor is the MDY function which takes 3 arguments, the month, day and year. All string representations are subject to interpretation by locale, and therefore won't work everywhere all the time.
You can also do:
-- Query 3
SELECT id, name
FROM h_history
WHERE (h_date >= MDY(2, 1, 2012))
AND (h_date <= MDY(1, 31, 2013))
Or:
-- Query 4
SELECT id, name
FROM h_history
WHERE ((YEAR(h_date) = 2012 AND MONTH(h_date) >= 2) OR YEAR(h_date) >= 2013)
AND ((YEAR(h_date) = 2013 AND MONTH(h_date) <= 1) OR YEAR(h_date) < 2013)
Or:
-- Query 5
SELECT id, name
FROM h_history
WHERE (YEAR(h_date) * 100 + MONTH(h_date)) >= 201202
AND (YEAR(h_date) * 100 + MONTH(h_date)) <= 201301
Given the choice, I'd probably use Query 2 as succinct but accurate, or perhaps Query 5, but all of queries 1-5 are usable.
If h_date was a column of some DATETIME type and you need to compare parts of a DATETIME, you can either use casting (as shown in Query 1) or the EXTEND function. That tends to be verbose.
Assuming that h_date is a proper date field, why wouldn't
SELECT
id
, name
FROM h_history
WHERE h_date >= '02/01/2012' and h_date <= '01/31/2013'
work for you?
Try the following
SELECT
id,name
FROM h_history
WHERE
h_date >= MDY(2, 1, 2012)
AND h_date < MDY(2, 1, 2013)
select * from table t where MONTH(t.date) = 12