Not another SQL time between dates minus weekends question - sql

I have two dates: CREATION_DATE and START_DATE. START_DATE will always be later than CREATION_DATE. I need to calculate the number of minutes between them, except for minutes which happen on a weekend.
Every solution I can find assumes one of those dates occurs on a weekend, but alas, if CREATION_DATE is on a Friday, and START_DATE is a Monday, all of Saturday and Sunday is counted.
I've even tried calculating minutes from CREATION_DATE to the next 12am occurs plus minutes from first 12am Monday to START_DATE, but that doesn't work either.
I have found a solution if I only wanted to count days. I need to know down to minutes.
Our DB is hosted an I am not able to create VB functions so my solution must be all SQL.

The basic idea is to generate a record for all minutes between the start and finish, including those on weekends. Then use the WHERE clause to filter out those you don't want. In many cases, this is done by joining to a Calendar table, so you can also look at holidays or other special events, but for this purpose we can just use the DATEPART() function.
One this is done, we use a GROUP BY to roll things back up to the original date values and the COUNT() function to know how much work we did.
This basic concept works whether you're counting days, minutes, months, whatever.
It's not clear in the question, but I'm gonna assume your start and end values are columns in a table, rather than variable names (no #).
WITH Numbers(Number) AS
(
SELECT ROW_NUMBER() OVER (ORDER BY s1.[object_id]) - 1
FROM sys.all_columns AS s1
CROSS JOIN sys.all_columns AS s2
)
SELECT t.CREATION_DATE, t.START_DATE, COUNT(*) AS Num_Minutes
FROM [MyTable] t
INNER JOIN Numbers n on n.Number <= DATEDIFF(minute, t.CREATION_DATE, t.START_DATE)
WHERE DATEPART(dw, DATEADD(minute, n.Number, t.CREATION_DATE)) NOT IN (7,1)
GROUP BY t.CREATION_DATE, t.START_DATE
But this has the potential to be very slow, depending on how far apart the dates are. You can improve this by using various other ways to generate the Numbers table to get a starting point that better approximates the needs of your actual data.

If you aren't worried about accounting for holidays, you can do this as a simple math problem without having to monkey around with a tally table or doing any counting.
The following works by dropping the time portion off the begin and end date parameters and calculates the number of working days from that and multiples that figure by 3660. From there, if the begin date is a week day the begin date mins are subtracted... if end date is a weekday, those mins are added.
DECLARE
#BegDate DATETIME = '2018-09-13 03:30:30',
#EndDate DATETIME = '2018-09-18 03:35:27';
SELECT
working_mins = bm.base_mins
- ((1 - (x.is_beg_sat + x.is_beg_sun)) * x.beg_mins) -- if the begin date is a week day, subtract the mins from midnight.
+ ((1 - (x.is_end_sat + x.is_end_sun)) * x.end_mins) -- if the end date is a week day add the mins from midnight.
--,*
FROM
( VALUES (
DATEADD(DAY, DATEDIFF(DAY, 0, #BegDate), 0),
DATEADD(DAY, DATEDIFF(DAY, 0, #EndDate), 0)
) ) d (beg_date, end_date)
CROSS APPLY ( VALUES (
DATEDIFF(MINUTE, d.beg_date, #BegDate),
DATEDIFF(MINUTE, d.end_date, #EndDate),
DATEDIFF(DAY, d.beg_date, d.end_date),
DATEDIFF(WEEK, d.beg_date, d.end_date) * 2,
1 - SIGN(DATEPART(WEEKDAY, d.beg_date) % 7),
1 - SIGN(DATEPART(WEEKDAY, d.end_date) % 7),
1 - SIGN((DATEPART(WEEKDAY, d.beg_date) + 7) % 8),
1 - SIGN((DATEPART(WEEKDAY, d.end_date) + 7) % 8)
) ) x (beg_mins, end_mins, total_days, weekend_days, is_beg_sat, is_end_sat, is_beg_sun, is_end_sun)
CROSS APPLY ( VALUES (1440 * (x.total_days - x.weekend_days + x.is_beg_sat - x.is_end_sat)) ) bm (base_mins);

You can take a look at this solution, and see if meets your needs. Basically, I did the following:
Take the number of whole days betwen StartDate and EndDate that aren't weekend days, and multiply by 2:
SELECT COUNT(*) * 24 * 60 FROM WholeDaysBetween WHERE wkday <= 5
Take the minutes from StartDate (hours*60 + minutes)
(24 * 60) - (DATEPART(HOUR, #StartDate) * 60) - (DATEPART(MINUTE, #StartDate))
Take the minutes from EndDate (hours*60 + minutes)
(DATEPART(HOUR, #EndDate) * 60) + (DATEPART(MINUTE, #EndDate))
To get the number of whole days between, I used a recursive CTE:
WITH
WholeDaysBetween(dt, wkday) AS
(
SELECT DATEADD(DAY, 1, #StartDate), DATEPART(WEEKDAY, DATEADD(DAY, 1, #StartDate))
UNION ALL
SELECT DATEADD(DAY, 1, dt), DATEPART(WEEKDAY, DATEADD(DAY, 1, dt))
FROM WholeDaysBetween
WHERE dt < DATEADD(DAY, -1, #EndDate)
)
Of course, for this to work, you have to adjust your datefirst settings.
The final query is as follows (I used the same sample data as in your comment):
set datefirst 1; -- day starts on Monday
declare #StartDate datetime = '2018-09-21 23:59:00';
declare #EndDate datetime = '2018-09-24 00:01:00';
WITH
WholeDaysBetween(dt, wkday) AS
(
SELECT DATEADD(DAY, 1, #StartDate), DATEPART(WEEKDAY, DATEADD(DAY, 1, #StartDate))
UNION ALL
SELECT DATEADD(DAY, 1, dt), DATEPART(WEEKDAY, DATEADD(DAY, 1, dt))
FROM WholeDaysBetween
WHERE dt < DATEADD(DAY, -1, #EndDate)
)
SELECT
-- whole weekdays between #StartDate and #EndDate,
-- multiplied by minutes per day
(
SELECT COUNT(*) * 24 * 60
FROM WholeDaysBetween
WHERE wkday <= 5
)
+
-- minutes from #StartDate date to end of #StartDate
-- as long as #StartDate isn't on weekend
(
SELECT
CASE
WHEN DATEPART(WEEKDAY, #StartDate) <= 5
THEN
(24 * 60) -
(DATEPART(HOUR, #StartDate) * 60) -
(DATEPART(MINUTE, #StartDate))
ELSE 0
END
)
+
-- minutes from start of #EndDate's date to #EndDate
-- as long as #EndDate isn't on weekend
(
SELECT
CASE
WHEN DATEPART(WEEKDAY, #EndDate) <= 5
THEN
(DATEPART(HOUR, #EndDate) * 60) +
(DATEPART(MINUTE, #EndDate))
ELSE 0
END
)

Related

SQL Function using WHILE takes a long time to run

I've created a SQL function that takes two dates, then finds the number of days between them, excluding weekends and holidays. It works, but the issue is that it takes 1.5 seconds per row to run. This is a bummer, because we're running this for reports that are potentially thousands of rows long.
For a little more background, the cteTally table is just a generic tally table. The HLD1 table is a table that looks like the following:
Calendar StrDate EndDate
2016Holidays 1/1/16 1/1/16
2016Holidays 5/30/16 5/30/16
2016Holidays 7/4/16 7/4/16
2016Holidays 9/5/16 9/5/16
2016Holidays 11/24/16 11/25/16
2016Holidays 12/26/16 12/26/16
Essentially what it's intent is is that when a customer calls in on a repair request for a washing machine, we want to track the response time, but only counting business days in our count. So, for example, if a customer called in on June 30 at 4:30 PM, and a tech went out to repair on July 5 at 8:30 AM, we would count Friday 7/1 (a non-holiday, non-weekend date), not count Saturday 7/2 or Sunday 7/3 (weekend days), and not count Monday 7/4 (a holiday), and count Tuesday 7/5 to get an accurate count of the overnights.
The answer that it would then return is 2.
In order to do this, it looks at the days in the function and says "when these days meet these criteria, add or subtract a day so the start or end date is not a weekend or holiday."
I'm about 98.36% sure that there has to be a better/easier way to do this, and I'm way overthinking what's going on, but I can't for the life of me determine what I could do differently, and I've been working on it on and off for about 3 days now.
EDIT (before I even posted): I've now gotten it down to 19 seconds for the 14560 rows returning that I'm using the function in. However, before I modified the function to take holidays into account, it was able to return all of the rows in less than a second. The change that I made was to limit the "holidays" that were getting returned to holidays in between the start and end date. However, I think the "WHILE" loop is still causing me some issues.
I've also tried creating a temp table or a cte table to store the holidays in so it only has to calculate them once per line instead of 4 times per line, but it doesn't appear that that is possible within a function.
I'm going to keep working on this for now, but would love any help that y'all can provide.
EDIT 2: I put all of the "SELECT #variables" that were in a row in the same select statement instead of having separate ones, and that cut the time down by a further 9 seconds for 14000 rows. However, I'd still like to get it under the 2 second barrier, if possible, or at least the 5 second barrier.
Here is the text of the function:
CREATE FUNCTION [dbo].[dateDiffHolidays] (
#startdaytime DATETIME,
#enddaytime DATETIME
)
RETURNS INT
AS
BEGIN
DECLARE #answer INT;
DECLARE #START Date;
DECLARE #END Date;
DECLARE #AddDays int;
SET #answer = 0
-- Strip Times
SELECT #START = dateadd(dd,0, datediff(dd,0,#StartDayTime))
SELECT #END = dateadd(dd,0, datediff(dd,0,#EndDayTime))
SELECT #AddDays = count(*) from (SELECT
dateadd(dd,ctetally.n-1,#START) date1
from
cteTally
where dateadd(dd,ctetally.n-1,#START) <= #END) s1 where s1.date1 in (select cast( DATEADD(day, t.N - 1, StrDate) as date) as ResultDate
from HLD1 s join cteTally t on t.N <= DATEDIFF(day, StrDate, EndDate) + 1)
or datepart(dw,s1.date1) in (1,7)
-- handle end conditions
DECLARE #firstWeekDayInRange datetime, #lastWeekDayInRange datetime;
SET #firstWeekDayInRange = #START
set #lastWeekDayInRange = #END
WHILE #firstWeekDayInRange in (select cast( DATEADD(day, t.N - 1, StrDate) as date) as ResultDate
from HLD1 s join cteTally t on t.N <= DATEDIFF(day, StrDate, EndDate) + 1)
or datepart(dw,#firstWeekDayInRange) in (1,7)
BEGIN
SELECT #firstWeekDayInRange =
CASE
WHEN #firstWeekDayInRange in (select cast( DATEADD(day, t.N - 1, StrDate) as date) from HLD1 s join cteTally t on t.N <= DATEDIFF(day, StrDate, EndDate) + 1)
or datepart(dw,#firstWeekDayInRange) in (1,7)
THEN dateadd(DAY,1,#firstWeekDayInRange)
ELSE #firstWeekDayInRange
END
END
WHILE #lastWeekDayInRange in (select cast( DATEADD(day, t.N - 1, StrDate) as date) as ResultDate
from HLD1 s join cteTally t on t.N <= DATEDIFF(day, StrDate, EndDate) + 1)
or datepart(dw,#lastWeekDayInRange) in (1,7)
BEGIN
SELECT #lastWeekDayInRange =
CASE
WHEN #lastWeekDayInRange in (select cast( DATEADD(day, t.N - 1, StrDate) as date) from HLD1 s join cteTally t on t.N <= DATEDIFF(day, StrDate, EndDate) + 1)
or datepart(dw,#lastWeekDayInRange) in (1,7)
THEN dateadd(DAY,-1,#lastWeekDayInRange)
ELSE #lastWeekDayInRange
END
END
-- add one day to answer (to count Friday) if enddate was on a weekend
SELECT #answer = #answer +
CASE
-- triggered if start and end date are on same weekend
WHEN dateDiff(DAY,#firstWeekDayInRange,#lastWeekDayInRange) < 0 THEN (#answer * -1)
-- otherwise count the days and substract 2 days per weekend in between dates
ELSE (DateDiff(DAY, #firstWeekDayInRange, #lastWeekDayInRange) - #AddDays)
END
RETURN #answer
END
GO
Perhaps this can help.
Declare #HLD1 table (Calendar varchar(50),StrDate Date,EndDate Date)
Insert Into #HLD1 values
('2016Holidays','1/1/16','1/1/16'),
('2016Holidays','5/30/16','5/30/16'),
('2016Holidays','7/4/16','7/4/16'),
('2016Holidays','9/5/16','9/5/16'),
('2016Holidays','11/24/16','11/25/16'),
('2016Holidays','12/26/16','12/26/16')
Declare #Date1 Date = '2016-06-30 16:30:00'
Declare #Date2 Date = '2016-07-05 08:30:00'
Select DateDiff(DD,#Date1,#Date2)-sum(Excl)
From (
Select RetVal,Excl=max(Excl)
From (
Select *,Excl=IIF(DatePart(WEEKDAY,RetVal) in (7,1),1,0) From [dbo].[udf-Create-Range-Date](#Date1,#Date2,'DD',1)
Union All
Select RetVal=StrDate,Excl=1 From #HLD1 Where StrDate Between #Date1 and #Date2
) A
Group By RetVal
) A
Returns
2
You can still use your tally table, but I prefer my UDF to create
Dynamic Date Ranges
CREATE FUNCTION [dbo].[udf-Create-Range-Date] (#DateFrom datetime,#DateTo datetime,#DatePart varchar(10),#Incr int)
Returns
#ReturnVal Table (RetVal datetime)
As
Begin
With DateTable As (
Select DateFrom = #DateFrom
Union All
Select Case #DatePart
When 'YY' then DateAdd(YY, #Incr, df.dateFrom)
When 'QQ' then DateAdd(QQ, #Incr, df.dateFrom)
When 'MM' then DateAdd(MM, #Incr, df.dateFrom)
When 'WK' then DateAdd(WK, #Incr, df.dateFrom)
When 'DD' then DateAdd(DD, #Incr, df.dateFrom)
When 'HH' then DateAdd(HH, #Incr, df.dateFrom)
When 'MI' then DateAdd(MI, #Incr, df.dateFrom)
When 'SS' then DateAdd(SS, #Incr, df.dateFrom)
End
From DateTable DF
Where DF.DateFrom < #DateTo
)
Insert into #ReturnVal(RetVal) Select DateFrom From DateTable option (maxrecursion 32767)
Return
End
-- Syntax Select * from [dbo].[udf-Create-Range-Date]('2016-10-01','2020-10-01','YY',1)
-- Syntax Select * from [dbo].[udf-Create-Range-Date]('2016-10-01','2020-10-01','DD',1)
-- Syntax Select * from [dbo].[udf-Create-Range-Date]('2016-10-01','2016-10-31','MI',15)
-- Syntax Select * from [dbo].[udf-Create-Range-Date]('2016-10-01','2016-10-02','SS',1)
Scalar UDFs can be slow: http://dataeducation.com/scalar-functions-inlining-and-performance-an-entertaining-title-for-a-boring-post/
If you can turn it into an inline table valued UDF, your query will probably be much faster. Although having looked at your UDF code, it might not be possible/easy to do.

Convert iso_week to calendar date in SQL

I've been searching though the archives without finding what I am looking for- I'd be happy for some guidance.
I have a data set where I want to report aggregated number of appointments by provider (STAFFID) and work week, the latter defined by the week's Monday date. I've played with datepart(iso_week, appointment_date) as week_of_yr which gets me part of the way there- I can group by week to get the right numbers. However, I can't figure out if there's a simple way to display the date of the week's Monday given the iso_week integer (and year).
I found ISO8601 Convert Week Date to Calendar Date helpful, though I do not know whether (or how) I can automate that process for many values at once.
Here's the tidbit of code I have. Ideally I could add another expression to the select statement which would display the desired date.
select STAFFID
, count(*) as appointment_ct
, datepart(iso_week, appointment_date) as iso_wk --this returns the week # of the year as an int
from [dbo].[view_APPT_DATA]
where program_code in ('99999')
and appointment_date >= '1/1/2016' and appointment_date <='3/31/2016'
group by iso_wk, STAFFID
I would find the first Monday of that year and then use DATEADD to add the number of weeks to that day
select STAFFID
, count(*) as appointment_ct
, datepart(iso_week, appointment_date) as iso_wk --this returns the week # of the year as an int
, dateadd(week, datepart(week, DATEADD(DAY, (##DATEFIRST - DATEPART(WEEKDAY, dateadd(year, datepart(year, appointment_date) - 1900, 0)) + (8 - ##DATEFIRST) * 2) % 7, dateadd(year, datepart(year, appointment_date) - 1900, 0))) as monday_wk
from [dbo].[view_APPT_DATA]
where program_code in ('99999')
and appointment_date >= '1/1/2016' and appointment_date <='3/31/2016'
group by iso_wk, STAFFID, monday_wk
I didn't quite get the cha's query to return correct results for "special" years that have overlapping weeks.
This is the (inline) function i ended up with to calculate iso week to first day of that week:
CREATE OR ALTER FUNCTION dbo.FN_ISOWEEK_TO_DAY (
-- ISOWeek in format YYYYWW
#pISOWeek INT
)
RETURNS TABLE AS RETURN
SELECT DATEADD(week, CAST(RIGHT(#pISOWeek, 2) AS INT) - 1
- CASE WHEN (##DATEFIRST - DATEPART(WEEKDAY, DATEADD(YEAR, cast(LEFT(#pISOWeek, 4) AS INT) - 1900, 0)) + (8 - ##DATEFIRST) * 2) % 7 >= 4 -- means first monday is one week ahead
THEN 1 ELSE 0 END
, DATEADD(DAY, (##DATEFIRST - DATEPART(WEEKDAY, DATEADD(YEAR, cast(LEFT(#pISOWeek, 4) AS INT) - 1900, 0)) + (8 - ##DATEFIRST) * 2) % 7, DATEADD(YEAR, cast(LEFT(#pISOWeek, 4) AS INT) - 1900, 0)))
AS firstDay
Some test code:
SELECT *
FROM (
SELECT 202001, 20191230
UNION ALL
SELECT 202053, 20201228
) x (dt, expectedValue)
CROSS APPLY dbo.FN_ISOWEEK_TO_DAY(x.dt) y

Group days by week

Is there is a way to group dates by week of month in SQL Server?
For example
Week 2: 05/07/2012 - 05/13/2012
Week 3: 05/14/2012 - 05/20/2012
but with Sql server statement
I tried
SELECT SOMETHING,
datediff(wk, convert(varchar(6), getdate(), 112) + '01', getdate()) + 1 AS TIME_
FROM STATISTICS_
GROUP BY something, TIME_
ORDER BY TIME_
but it returns the week number of month. (means 3)
How to get the pair of days for current week ?
For example, now we are in third (3rd) week and I want to show 05/14/2012 - 05/20/2012
I solved somehow:
SELECT DATEADD(ww, DATEDIFF(ww,0,<my_column_name>), 0)
select DATEADD(ww, DATEDIFF(ww,0,<my_column_name>), 0)+6
Then I will get two days and I will concatenate them later.
All right, bear with me here. We're going to build a temporary calendar table that represents this month, including the days from before and after the month that fall into your definition of a week (Monday - Sunday). I do this in a lot of steps to try to make the process clear, but I probably haven't excelled at that in this case.
We can then generate the ranges for the different weeks, and you can join against your other tables using that.
SET DATEFIRST 7;
SET NOCOUNT ON;
DECLARE #today SMALLDATETIME, #fd SMALLDATETIME, #rc INT;
SELECT #today = DATEADD(DAY, DATEDIFF(DAY, 0, GETDATE()), 0), -- today
#fd = DATEADD(DAY, 1-DAY(#today), #today), -- first day of this month
#rc = DATEPART(DAY, DATEADD(DAY, -1, DATEADD(MONTH, 1, #fd)));-- days in month
DECLARE #thismonth TABLE (
[date] SMALLDATETIME,
[weekday] TINYINT,
[weeknumber] TINYINT
);
;WITH n(d) AS (
SELECT TOP (#rc+12) DATEADD(DAY, ROW_NUMBER() OVER
(ORDER BY [object_id]) - 7, #fd) FROM sys.all_objects
)
INSERT #thismonth([date], [weekday]) SELECT d, DATEPART(WEEKDAY, d) FROM n;
DELETE #thismonth WHERE [date] < (SELECT MIN([date]) FROM #thismonth WHERE [weekday] = 2)
OR [date] > (SELECT MAX([date]) FROM #thismonth WHERE [weekday] = 1);
;WITH x AS ( SELECT [date], weeknumber, rn = ((ROW_NUMBER() OVER
(ORDER BY [date])-1) / 7) + 1 FROM #thismonth ) UPDATE x SET weeknumber = rn;
-- now, the final query given all that (I've only broken this up to get rid of the vertical scrollbars):
;WITH ranges(w,s,e) AS (
SELECT weeknumber, MIN([date]), MAX([date]) FROM #thismonth GROUP BY weeknumber
)
SELECT [week] = CONVERT(CHAR(10), r.s, 120) + ' - ' + CONVERT(CHAR(10), r.e, 120)
--, SOMETHING , other columns from STATISTICS_?
FROM ranges AS r
-- LEFT OUTER JOIN dbo.STATISTICS_ AS s
-- ON s.TIME_ >= r.s AND s.TIME_ < DATEADD(DAY, 1, r.e)
-- comment this out if you want all the weeks from this month:
WHERE w = (SELECT weeknumber FROM #thismonth WHERE [date] = #today)
GROUP BY r.s, r.e --, SOMETHING
ORDER BY [week];
Results with WHERE clause:
week
-----------------------
2012-05-14 - 2012-05-20
Results without WHERE clause:
week
-----------------------
2012-04-30 - 2012-05-06
2012-05-07 - 2012-05-13
2012-05-14 - 2012-05-20
2012-05-21 - 2012-05-27
2012-05-28 - 2012-06-03
Note that I chose YYYY-MM-DD on purpose. You should avoid regional formatting like M/D/Y especially for input but also for display. No matter how targeted you think your audience is, you're always going to have someone who thinks 05/07/2012 is July 5th, not May 7th. With YYYY-MM-DD there is no ambiguity whatsoever.
Create a calendar table, then you can query week numbers, first/last days of specific weeks and months etc. You can also join on it queries to get a date range etc.
How about a case statement?
case when datepart(day, mydatetime) between 1 and 7 then 1
when datepart(day, mydatetime) between 8 and 14 then 2
...
You'll also have to include the year & month unless you want all the week 1s in the same group.
It's not clear of you want to "group dates by week of month", or alternately "select data from a given week"
If you mean "group" this little snippet should get you 'week of month':
SELECT <stuff>
FROM CP_STATISTICS
WHERE Month(<YOUR DATE COL>) = 5 --april
GROUP BY Year(<YOUR DATE COL>),
Month(<YOUR DATE COL>),
DATEDIFF(week, DATEADD(MONTH, DATEDIFF(MONTH, 0, <YOUR DATE COL>), 0)
, <YOUR DATE COL>) +1
Alternately, if you want "sales for week 1 of April, ordered by date" You could do something like..
DECLARE #targetDate datetime2 = '5/3/2012'
DECLARE #targetWeek int = DATEDIFF(week, DATEADD(MONTH,
DATEDIFF(MONTH, 0, #targetDate), 0), #targetDate) +1
SELECT <stuff>
FROM CP_STATISTICS
WHERE MONTH(#targetDate) = Month(myDateCol) AND
YEAR(#targetDate) = Year (myDateCol) AND
#targetWeek = DATEDIFF(week, DATEADD(MONTH,
DATEDIFF(MONTH, 0, myDateCol), 0), myDateCol) +1
ORDER BY myDateCol
Note, things would get more complicated if you use non-standard weeks, or want to reach a few days into an earlier month for weeks that straddle a month boundary.
EDIT 2
From looking at your 'solved now' section. I think your question is "how do I get data out of a table for a given week?"
Your solution appears to be:
DECLARE #targetDate datetime2 = '5/1/2012'
DECLARE #startDate datetime2 = DATEADD(ww, DATEDIFF(ww,0,targetDate), 0)
DECLARE #endDate datetime2 = DATEADD(ww, DATEDIFF(ww,0,#now), 0)+6
SELECT <stuff>
FROM STATISTICS_
WHERE dateStamp >= #startDate AND dateStamp <= #endDate
Notice how if the date is 5/1 this solution results in a start date of '4/30/2012'. I point this out because your solution crosses month boundaries. This may or may not be desirable.

Getting Number of weeks in a Month from a Datetime Column

I have a table called FcData and the data looks like:
Op_Date
2011-02-14 11:53:40.000
2011-02-17 16:02:19.000
2010-02-14 12:53:40.000
2010-02-17 14:02:19.000
I am looking to get the Number of weeks in That Month from Op_Date. So I am looking for output like:
Op_Date Number of Weeks
2011-02-14 11:53:40.000 5
2011-02-17 16:02:19.000 5
2010-02-14 12:53:40.000 5
2010-02-17 14:02:19.000 5
This page has some good functions to figure out the last day of any given month: http://www.sql-server-helper.com/functions/get-last-day-of-month.aspx
Just wrap the output of that function with a DATEPART(wk, last_day_of_month) call. Combining it with an equivalent call for the 1st-day-of-week will let you get the number of weeks in that month.
Use this to get the number of week for ONE specific date. Replace GetDate() by your date:
declare #dt date = cast(GetDate() as date);
declare #dtstart date = DATEADD(day, -DATEPART(day, #dt) + 1, #dt);
declare #dtend date = dateadd(DAY, -1, DATEADD(MONTH, 1, #dtstart));
WITH dates AS (
SELECT #dtstart ADate
UNION ALL
SELECT DATEADD(day, 1, t.ADate)
FROM dates t
WHERE DATEADD(day, 1, t.ADate) <= #dtend
)
SELECT top 1 DatePart(WEEKDAY, ADate) weekday, COUNT(*) weeks
FROM dates d
group by DatePart(WEEKDAY, ADate)
order by 2 desc
Explained: the CTE creates a result set with all dates for the month of the given date. Then we query the result set, grouping by week day and count the number of occurrences. The max number will give us how many weeks the month overlaps (premise: if the month has 5 Mondays, it will cover five weeks of the year).
Update
Now, if you have multiple dates, you should tweak accordingly, joining your query with the dates CTE.
Here is my take on it, might have missed something.
In Linq:
from u in TblUsers
let date = u.CreateDate.Value
let firstDay = new DateTime(date.Year, date.Month, 1)
let lastDay = firstDay.AddMonths(1)
where u.CreateDate.HasValue
select Math.Ceiling((lastDay - firstDay).TotalDays / 7)
And generated SQL:
-- Region Parameters
DECLARE #p0 Int = 1
DECLARE #p1 Int = 1
DECLARE #p2 Float = 7
-- EndRegion
SELECT CEILING(((CONVERT(Float,CONVERT(BigInt,(((CONVERT(BigInt,DATEDIFF(DAY, [t3].[value], [t3].[value2]))) * 86400000) + DATEDIFF(MILLISECOND, DATEADD(DAY, DATEDIFF(DAY, [t3].[value], [t3].[value2]), [t3].[value]), [t3].[value2])) * 10000))) / 864000000000) / #p2) AS [value]
FROM (
SELECT [t2].[createDate], [t2].[value], DATEADD(MONTH, #p1, [t2].[value]) AS [value2]
FROM (
SELECT [t1].[createDate], CONVERT(DATETIME, CONVERT(NCHAR(2), DATEPART(Month, [t1].[value])) + ('/' + (CONVERT(NCHAR(2), #p0) + ('/' + CONVERT(NCHAR(4), DATEPART(Year, [t1].[value]))))), 101) AS [value]
FROM (
SELECT [t0].[createDate], [t0].[createDate] AS [value]
FROM [tblUser] AS [t0]
) AS [t1]
) AS [t2]
) AS [t3]
WHERE [t3].[createDate] IS NOT NULL
According to this MSDN article: http://msdn.microsoft.com/en-us/library/ms174420.aspx you can only get the current week in the year, not what that month returns.
There may be various approaches to implementing the idea suggested by #Marc B. Here's one, where no UDFs are used but the first and the last days of month are calculated directly:
WITH SampleData AS (
SELECT CAST('20110214' AS datetime) AS Op_Date
UNION ALL SELECT '20110217'
UNION ALL SELECT '20100214'
UNION ALL SELECT '20100217'
UNION ALL SELECT '20090214'
UNION ALL SELECT '20090217'
),
MonthStarts AS (
SELECT
Op_Date,
MonthStart = DATEADD(DAY, 1 - DAY(Op_Date), Op_Date)
/* alternatively: DATEADD(MONTH, DATEDIFF(MONTH, 0, Op_Date), 0) */
FROM FcData
),
Months AS (
SELECT
Op_Date,
MonthStart,
MonthEnd = DATEADD(DAY, -1, DATEADD(MONTH, 1, MonthStart))
FROM FcData
)
Weeks AS (
SELECT
Op_Date,
StartWeek = DATEPART(WEEK, MonthStart),
EndWeek = DATEPART(WEEK, MonthEnd)
FROM MonthStarts
)
SELECT
Op_Date,
NumberOfWeeks = EndWeek - StartWeek + 1
FROM Weeks
All calculations could be done in one SELECT, but I chose to split them into steps and place every step in a separate CTE so it could be seen better how the end result was obtained.
You can get number of weeks per month using the following method.
Datepart(WEEK,
DATEADD(DAY,
-1,
DATEADD(MONTH,
1,
DATEADD(DAY,
1 - DAY(GETDATE()),
GETDATE())))
-
DATEADD(DAY,
1 - DAY(GETDATE()),
GETDATE())
+1
)
Here how you can get accurate amount of weeks:
DECLARE #date DATETIME
SET #date = GETDATE()
SELECT ROUND(cast(datediff(day, dateadd(day, 1-day(#date), #date), dateadd(month, 1, dateadd(day, 1-day(#date), #date))) AS FLOAT) / 7, 2)
With this code for Sep 2014 you'll get 4.29 which is actually true since there're 4 full weeks and 2 more days.

SQL Select Upcoming Birthdays

I'm trying to write a stored procedure to select employees who have birthdays that are upcoming.
SELECT * FROM Employees WHERE Birthday > #Today AND Birthday < #Today + #NumDays
This will not work because the birth year is part of Birthday, so if my birthday was '09-18-1983' that will not fall between '09-18-2008' and '09-25-2008'.
Is there a way to ignore the year portion of date fields and just compare month/days?
This will be run every monday morning to alert managers of birthdays upcoming, so it possibly will span new years.
Here is the working solution that I ended up creating, thanks Kogus.
SELECT * FROM Employees
WHERE Cast(DATEDIFF(dd, birthdt, getDate()) / 365.25 as int)
- Cast(DATEDIFF(dd, birthdt, futureDate) / 365.25 as int)
<> 0
Note: I've edited this to fix what I believe was a significant bug. The currently posted version works for me.
This should work after you modify the field and table names to correspond to your database.
SELECT
BRTHDATE AS BIRTHDAY
,FLOOR(DATEDIFF(dd,EMP.BRTHDATE,GETDATE()) / 365.25) AS AGE_NOW
,FLOOR(DATEDIFF(dd,EMP.BRTHDATE,GETDATE()+7) / 365.25) AS AGE_ONE_WEEK_FROM_NOW
FROM
"Database name".dbo.EMPLOYEES EMP
WHERE 1 = (FLOOR(DATEDIFF(dd,EMP.BRTHDATE,GETDATE()+7) / 365.25))
-
(FLOOR(DATEDIFF(dd,EMP.BRTHDATE,GETDATE()) / 365.25))
Basically, it gets the # of days from their birthday to now, and divides that by 365 (to avoid rounding issues that come up when you convert directly to years).
Then it gets the # of days from their birthday to a week from now, and divides that by 365 to get their age a week from now.
If their birthday is within a week, then the difference between those two values will be 1. So it returns all of those records.
In case someone is still looking for a solution in MySQL (slightly different commands), here's the query:
SELECT
name,birthday,
FLOOR(DATEDIFF(DATE(NOW()),birthday) / 365.25) AS age_now,
FLOOR(DATEDIFF(DATE_ADD(DATE(NOW()),INTERVAL 30 DAY),birthday) / 365.25) AS age_future
FROM user
WHERE 1 = (FLOOR(DATEDIFF(DATE_ADD(DATE(NOW()),INTERVAL 30 DAY),birthday) / 365.25)) - (FLOOR(DATEDIFF(DATE(NOW()),birthday) / 365.25))
ORDER BY MONTH(birthday),DAY(birthday)
Best use of datediff and dateadd. No rounding, no approximates, no 29th of february bug, nothing but date functions
ageOfThePerson = DATEDIFF(yyyy,dateOfBirth, GETDATE())
dateOfNextBirthday = DATEADD(yyyy,ageOfThePerson + 1, dateOfBirth)
daysBeforeBirthday = DATEDIFF(d,GETDATE(), dateofNextBirthday)
Thanks to #Gustavo Cardoso, new definition for the age of the person
ageOfThePerson = FLOOR(DATEDIFF(d,dateOfBirth, GETDATE())/365.25)
Liked the approach of #strelc, but his sql was a bit off. Here's an updated version that works well and is simple to use:
SELECT * FROM User
WHERE (DATEDIFF(dd, getdate(), DATEADD(yyyy,
DATEDIFF(yyyy, birthdate, getdate()) + 1, birthdate)) + 1) % 366 <= <number of days>
edit 10/2017: add single day to end
You could use the DAYOFYEAR function but be careful when you want to look for January birthdays in December. I think you'll be fine as long as the date range you're looking for doesn't span the New Year.
Sorry didn't see the requirement to neutralize the year.
select * from Employees
where DATEADD (year, DatePart(year, getdate()) - DatePart(year, Birthday), Birthday)
between convert(datetime, getdate(), 101)
and convert(datetime, DateAdd(day, 5, getdate()), 101)
This should work.
My guess is using "365.25" soon or later would be fail.
So I test the working solution using "365.25"
And It don't return the same numbers of rows for every case.
Here an example:
http://sqlfiddle.com/#!3/94c3ce/7
test with year 2016 and 2116 and you will see the difference. I only can post one link but change de /7 by /8 to see both queries. (/10 and /11 for the first answer)
So, I suggest this another query, where the point is determinate next birthday from a starting date and then compare if it is in my range of interest.
SELECT * FROM Employees
WHERE
CASE WHEN (DATEADD(yyyy,DATEDIFF(yyyy, birthdt, #fromDate),birthdt) < #fromDate )
THEN DATEADD(yyyy,DATEDIFF(yyyy, birthdt, #fromDate)+1,birthdt)
ELSE DATEADD(yyyy,DATEDIFF(yyyy, birthdt, #fromDate),birthdt) END
BETWEEN #fromDate AND #toDate
This is solution for MS SQL Server:
It returns employees with birthdays in 30 days.
SELECT * FROM rojstni_dnevi
WHERE (DATEDIFF (dd,
getdate(),
DATEADD ( yyyy,
DATEDIFF(yyyy, rDan, getdate()),
rDan)
nex )
+365) % 365 < 30
I found the solution for this. This may save someone's precious time.
select EmployeeID,DOB,dates.date from emp_tb_eob_employeepersonal
cross join dbo.GetDays(Getdate(),Getdate()+7) as dates where weekofmonthnumber>0
and month(dates.date)=month(DOB) and day(dates.date)=day(DOB)
GO
/****** Object: UserDefinedFunction [dbo].[GetDays] Script Date: 11/30/2011 13:19:17 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
--SELECT [dbo].[GetDays] ('02/01/2011','02/28/2011')
ALTER FUNCTION [dbo].[GetDays](#startDate datetime, #endDate datetime)
RETURNS #retValue TABLE
(Days int ,Date datetime, WeekOfMonthNumber int, WeekOfMonthDescription varchar(10), DayName varchar(10))
AS
BEGIN
DECLARE #nextDay int
DECLARE #nextDate datetime
DECLARE #WeekOfMonthNum int
DECLARE #WeekOfMonthDes varchar(10)
DECLARE #DayName varchar(10)
SELECT #nextDate = #startDate, #WeekOfMonthNum = DATEDIFF(week, DATEADD(MONTH, DATEDIFF(MONTH,0,#startDate),0),#startDate) + 1,
#WeekOfMonthDes = CASE #WeekOfMonthNum
WHEN '1' THEN 'First'
WHEN '2' THEN 'Second'
WHEN '3' THEN 'Third'
WHEN '4' THEN 'Fourth'
WHEN '5' THEN 'Fifth'
WHEN '6' THEN 'Sixth'
END,
#DayName
= DATENAME(weekday, #startDate)
SET #nextDay=1
WHILE #nextDate <= #endDate
BEGIN
INSERT INTO #retValue values (#nextDay,#nextDate, #WeekOfMonthNum, #WeekOfMonthDes, #DayName)
SELECT #nextDay=#nextDay + 1
SELECT #nextDate = DATEADD(day,1,#nextDate),
#WeekOfMonthNum
= DATEDIFF(week, DATEADD(MONTH, DATEDIFF(MONTH,0, #nextDate),0), #nextDate) + 1,
#WeekOfMonthDes
= CASE #WeekOfMonthNum
WHEN '1' THEN 'First'
WHEN '2' THEN 'Second'
WHEN '3' THEN 'Third'
WHEN '4' THEN 'Fourth'
WHEN '5' THEN 'Fifth'
WHEN '6' THEN 'Sixth'
END,
#DayName
= DATENAME(weekday, #nextDate)
CONTINUE
END
WHILE(#nextDay <=31)
BEGIN
INSERT INTO #retValue values (#nextDay,#nextDate, 0, '', '')
SELECT #nextDay=#nextDay + 1
END
RETURN
END
Make a cross join with the dates and check for the comparison of month and dates.
In less than a month:
SELECT * FROM people WHERE MOD( DATEDIFF( CURDATE( ) , `date_birth`) /30, 12 ) <1 and (((month(`date_birth`)) = (month(curdate())) and (day(`date_birth`)) > (day (curdate() ))) or ((month(`date_birth`)) > (month(curdate())) and (day(`date_birth`)) < (day (curdate() ))))
You could use DATE_FORMAT to extract the day and month parts of the birthday dates.
EDIT: sorry i didn't see that he wasn't using MySQL.
Assuming this is T-SQL, use DATEPART to compare the month and date separately.
http://msdn.microsoft.com/en-us/library/ms174420.aspx
Alternatively, subtract January 1st of the current year from everyone's birthday, and then compare using the year 1900 (or whatever your epoch year is).
Most of these solutions are close, but you have to remember a few extra scenarios. When working with birthdays and a sliding scale, you must be able to handle the transition into the next month.
For example Stephens example works great for birthdays up until the last 4 days of the month. Then you have a logic fault as the valid dates if today was the 29th would be :29, 30, AND then 1, 2, 3 of the NEXT month, so you have to condition for that as well.
An alternative would be to parse the date from the birthday field, and sub in the current year, then do a standard range comparison.
Another thought: Add their age in whole years to their birthday (or one more if their Birthday hasn't happened yet and then compare as you do above. Use DATEPART and DATEADD to do this.
http://msdn.microsoft.com/en-us/library/ms186819.aspx
The edge case of a range spanning the year would have to have special code.
Bonus tip: consider using BETWEEN...AND instead of repeating the Birthday operand.
This should work...
DECLARE #endDate DATETIME
DECLARE #today DATETIME
SELECT #endDate = getDate()+6, #today = getDate()
SELECT * FROM Employees
WHERE
(DATEPART (month, birthday) >= DATEPART (month, #today)
AND DATEPART (day, birthday) >= DATEPART (day, #today))
AND
(DATEPART (month, birthday) < DATEPART (month, #endDate)
AND DATEPART (day, birthday) < DATEPART (day, #endDate))
I faced the same problem with my college project a few years ago. I responded (in a rather weasel way) by splitting the year and the date(MM:DD) in two separate columns. And before that, my project mate was simply getting all the dates and programatically going through them. We changed that because it was too inefficient - not that my solution was any more elegant either. Also, its probably not possible to do in a database that has been in use for a while by multiple apps.
Give this a try:
SELECT * FROM Employees
WHERE DATEADD(yyyy, DATEPART(yyyy, #Today)-DATEPART(yyyy, Birthday), Birthday) > #Today
AND DATEADD(yyyy, DATEPART(yyyy, #Today)-DATEPART(yyyy, Birthday), Birthday) < DATEADD(dd, #NumDays, #Today)
Nuts! A good solution between when I started thinking about this and when I came back to answer. :)
I came up with:
select (365 + datediff(d,getdate(),cast(cast(datepart(yy,getdate()) as varchar(4)) + '-' + cast(datepart(m,birthdt) as varchar(2)) + '-' + cast(datepart(d,birthdt) as varchar(2)) as datetime))) % 365
from employees
where (365 + datediff(d,getdate(),cast(cast(datepart(yy,getdate()) as varchar(4)) + '-' + cast(datepart(m,birthdt) as varchar(2)) + '-' + cast(datepart(d,birthdt) as varchar(2)) as datetime))) % 365 < #NumDays
You don't need to cast getdate() as a datetime, right?
This is a combination of a couple of the answers that was tested. This will find the next brithday after a certain date and the age they will be. Also the numdays will limit the range you are looking 7 days = week etc.
SELECT DISTINCT FLOOR(DATEDIFF(dd,Birthday, #BeginDate) / 365.25) + 1 age,
DATEADD(yyyy, FLOOR(DATEDIFF(dd,Birthday, #BeginDate) / 365.25) + 1, Birthday) nextbirthday, birthday
FROM table
WHERE DATEADD(yyyy, FLOOR(DATEDIFF(dd,Birthday, #BeginDate) / 365.25) + 1, Birthday) > #BeginDate
AND DATEADD(yyyy, FLOOR(DATEDIFF(dd,Birthday, #BeginDate) / 365.25) + 1, Birthday) < DATEADD(dd, #NumDays, #BeginDate)
order by nextbirthday
The best way to achieve the same is
DECLARE #StartDate DATETIME
DECLARE #EndDate DATETIME
SELECT Member.* from vwMember AS Member
WHERE (DATEADD(YEAR, (DATEPART(YEAR, #StartDate) -
DATEPART(YEAR, Member.dBirthDay)), Member.dBirthDay)
BETWEEN #StartDate AND #EndDate)
Upcoming Birthday for the Employee - Sqlserver
DECLARE #sam TABLE
(
EmployeeIDs int,
dob datetime
)
INSERT INTO #sam (dob, EmployeeIDs)
SELECT DOBirth, EmployeeID FROM Employee
SELECT *
FROM
(
SELECT *, bd_this_year = DATEADD(YEAR, DATEPART(YEAR, GETDATE()) - DATEPART(YEAR, dob), dob)
FROM #sam s
) d
WHERE d.bd_this_year > DATEADD(DAY, DATEDIFF(DAY, 0, GETDATE()), 0)
AND d.bd_this_year <= DATEADD(DAY, DATEDIFF(DAY, 0, GETDATE()), 3)
I used this for MySQL, probably not the most efficient way to query but simple enough to implement.
select * from `schema`.`table` where date_format(birthday,'%m%d') >= date_format(now(),'%m%d') and date_format(birthday,'%m%d') < date_format(DATE_ADD(NOW(), INTERVAL 5 DAY),'%m%d');
i believe this ticket has been closed ages ago but for the benefit of getting the correct sql query please have a look.
SELECT Employee_Name, DATE_OF_BIRTH
FROM Hr_table
WHERE
/**
fetching the original birth_date and replacing the birth year to the current but have to deduct 7 days to adjust jan 1-7 birthdate.
**/
datediff(d,getdate(),DATEADD(year,datediff(year,DATEADD(d,-7,hr.DATE_OF_BIRTH),getdate()),hr.date_of_birth)) between 0 and 7
-- current date looks ahead to 7 days for upcoming modified year birth date.
order by
-- sort by no of days before the birthday
datediff(d,getdate(),DATEADD(year,datediff(year,DATEADD(d,-7,hr.DATE_OF_BIRTH),getdate()),hr.date_of_birth))
Better and easy solution:
select * from users with(nolock)
where date_of_birth is not null
and
(
DATEDIFF(dd,
DATEADD(yy, -(YEAR(GETDATE())-1900),GETDATE()), --Today
DATEADD(yy, -(YEAR(date_of_birth)-1901),date_of_birth)
) % 365
) = 30
I hope this helps u in some way....
select Employeename,DOB
from Employeemaster
where day(Dob)>day(getdate()) and month(DOB)>=month(getDate())
This solution also takes care for birthdays in the next year and the ordering:
(dob = day of birth; bty = birthday this year; nbd = next birthday)
with rs (bty) as (
SELECT DATEADD(Year, DATEPART(Year, GETDATE()) - DATEPART(Year, dob), dob) as bty FROM Employees
),
rs2 (nbd) as (
select case when bty < getdate() then DATEADD(yyyy, 1, bty) else bty end as nbd from rs
)
select nbd, DATEDIFF(d, getdate(), nbd) as diff from rs2 where DATEDIFF(d, getdate(), nbd) < 14 order by diff
This version, which avoids comparison of the dates, could be faster:
with rs (dob, bty) as (
SELECT dob, DATEADD(Year, DATEPART(Year, GETDATE()) - DATEPART(Year, DOB), DOB) as bty FROM employee
),
rs2 (dob, nbd) as (
select dob, DATEADD(yyyy, FLOOR(ABS((-1*(SIGN(DATEDIFF(d, getdate(), bty))))+0.1)), bty) as nbd from rs
),
rs3 (dob, diff) as (
select dob, datediff(d, getdate(), nbd) as diff from rs2
)
select dob, diff from rs3 where diff < 14 order by diff
If the range covers the 29 of February in the next year, then use:
with rs (dob, ydiff) as (
select dob, DATEPART(Year, GETDATE()) - DATEPART(Year, DOB) as ydiff from Employee
),
rs2 (dob, bty, ydiff) as (
select dob, DATEADD(Year, ydiff, dob) as bty, ydiff from rs
),
rs3 (dob, nbd) as (
select dob, DATEADD(yyyy, FLOOR(ABS((-1*(SIGN(DATEDIFF(d, getdate(), bty))))+0.1)) + ydiff, dob) as nbd from rs2
),
rs4 (dob, ddiff, nbd) as (
select dob, datediff(d, getdate(), nbd) as diff, nbd from rs3
)
select dob, nbd, ddiff from rs4 where ddiff < 68 order by ddiff
You can also use DATEPART:
-- To find out Today's Birthday
DECLARE #today DATETIME
SELECT #today = getdate()
SELECT *
FROM SMIS_Registration
WHERE (DATEPART (month, DOB) >= DATEPART (month, #today)
AND DATEPART (day, DOB) = DATEPART (day, #today))
Below query will return all next birthday of employee, it is shortest query.
SELECT
Employee.DOB,
DATEADD(
mm,
(
(
(
(
DATEPART(yyyy, getdate())-DATEPART(yyyy, Employee.DOB )
)
+
(
1-
(
((DATEPART(mm, Employee.DOB)*100)+DATEPART(dd, Employee.DOB))
/
((DATEPART(mm, getdate())*100) + DATEPART(dd, getdate()))
)
)
)
*12
)
),
Employee.DOB
) NextDOB
FROM
Employee
ORDER BY
NextDOB ;
Above query will cover all next month excluding current date.
Solution for SQLite3:
SELECT
*,
strftime('%j', birthday) - strftime('%j', 'now') AS days_remaining
FROM
person
WHERE :n_days >= CASE
WHEN days_remaining >= 0 THEN days_remaining
ELSE days_remaining + strftime('%j', strftime('%Y-12-31', 'now'))
END
;
The solutions dividing by 325.25 to get the age, or bringing the birthdate to the current year etc. didn't work for me.
What this does is computes the delta of the two daysOfTheYear (1-366). If the birthday didn't happen yet this year, you automatically get the correct number of remaining days, which you can compare to.
If the birthday already happened, remaining_days will be negative, and you can get the correct number of remaining days by still adding the total amount of days in the current year. This also correctly handles leap years, since in that case the extra day will be added as well (By using dayOfYear(Dec 31.))
select BirthDate,Name from Employees
order by Case
WHEN convert(nvarchar(5),BirthDate,101) > convert(nvarchar(5),GETDATE(),101) then 2
WHEN convert(nvarchar(5),BirthDate,101) < convert(nvarchar(5),GETDATE(),101) then 3
WHEN convert(nvarchar(5),BirthDate,101) = convert(nvarchar(5),GETDATE(),101) then 1 else 4 end ,convert(nvarchar(2),BirthDate,101),convert(nvarchar(2),BirthDate,105)