Count each days of week between two dates without loop - sql

I can do it with loop, but if many day is slow. So I need do without loop.
Here is my code:
DECLARE
#FRDT date = '01-SEP-2019'
,#TODT date = '30-SEP-2019'
,#N int
,#SUN int = 0
,#MON int = 0
,#TUE int = 0
,#WED int = 0
,#THU int = 0
,#FRI int = 0
,#SAT int = 0
WHILE #FRDT <= #TODT
BEGIN
SET #N = DATEPART(WEEKDAY, #FRDT)
IF #N = 1
SET #SUN = #SUN + 1
ELSE IF #N = 2
SET #MON = #MON + 1
ELSE IF #N = 3
SET #TUE = #TUE + 1
ELSE IF #N = 4
SET #WED = #WED + 1
ELSE IF #N = 5
SET #THU = #THU + 1
ELSE IF #N = 6
SET #FRI = #FRI + 1
ELSE IF #N = 7
SET #SAT = #SAT + 1
SET #FRDT = DATEADD(DAY, 1, #FRDT)
END
SELECT 1 AS [NO], 'Sunday' AS [DAYNAME], #SUN AS [NUMBEROFDAY]
UNION SELECT 2, 'Monday', #MON
UNION SELECT 3, 'Tuesday', #TUE
UNION SELECT 4, 'Wednesday', #WED
UNION SELECT 5, 'Thursday', #THU
UNION SELECT 6, 'Friday', #FRI
I want to result like code above, but not use loop for better performance.

The date range is 30 days, dividing by 7 gives quotient 4 and remainder 2.
So every day of the week gets 4 and two days need an additional one. These are the ones corresponding to #start_date and the following day in this case.
SQL to implement this approach is below (demo)
SELECT DATENAME(WEEKDAY,base_date),
quotient + IIF(Nums.N < remainder, 1, 0)
FROM (VALUES
(0),
(1),
(2),
(3),
(4),
(5),
(6)) Nums(N)
CROSS APPLY(SELECT 1 + DATEDIFF(DAY,#start_date,#end_date)) DC(day_count)
CROSS APPLY(SELECT DATEADD(DAY, Nums.N, #start_date), day_count/7, day_count% 7) D(base_date, quotient, remainder)
ORDER BY DATEPART(DW,base_date)

You can do it with using recursive CTE as below-
DECLARE #start_date DATE= '01-SEP-2019', #end_date DATE= '30-SEP-2019';
WITH cte
AS (
SELECT #start_date AS date_
UNION ALL
SELECT CAST(DATEADD(day, 1, date_) AS DATE)
FROM cte
WHERE date_ < #end_date
)
SELECT DATEPART(DW,date_) No,
DATENAME(DW,date_) Day_Name,
COUNT(*) Num_Day
FROM cte
GROUP BY DATEPART(DW,date_),DATENAME(DW,date_)
ORDER BY DATEPART(DW,date_)
OPTION(MAXRECURSION 0);
Output-
No Day_Name Num_Day
1 Sunday 5
2 Monday 5
3 Tuesday 4
4 Wednesday 4
5 Thursday 4
6 Friday 4
7 Saturday 4

For such situation you need to have a Number table or Date Table.
In my example I am using a Number table. You can create number table anyway you want and it will help in many situations.
Create Table tblNumber(Number int primary key)
insert into tblNumber (Number) values(1),(2)...... till thousands or millions
Edit: You could generate the numbers for this number table using:
INSERT INTO tblNumber
SELECT TOP 100000 ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS [N]
FROM dbo.syscolumns tb1,dbo.syscolumns tb2
Keep this table permanently as it is useful.
DECLARE #FromDT DATETIME= '2019-09-01';
DECLARE #ToDT DATETIME= '2019-09-30';
SELECT COUNT(*), wkday
FROM
(
SELECT DATEname(weekday, DATEADD(day, number, #FromDT)) wkday
FROM tblNumber
WHERE number BETWEEN DATEPART(day, #FromDT) AND DATEPART(day, #ToDT)
) tbl
GROUP BY wkday;
If you have a Date table then it is more efficient in this situation.

Related

Want a SQL to calculate number of work days between 2 dates

I want SQL to calculate number of work days between 2 dates. for example, the start date is 3/1/2017 and end date is 3/10/2017, so the result should be 8 days not 10 days. how to achieve that in SQL server. thanks
If you want to show the days which are not in saturday and sunday within a date range. Then,
Query
declare #start as date = '2017-03-01';
declare #end as date = '2017-03-10';
declare #i as int = 0;
declare #j as int = datediff(day, #start, #end)
declare #t as table([date] date, [dayname] varchar(50));
while(#i <= #j)
begin
insert into #t([date], [dayname]) values
(dateadd(day, #i, #start), Datename(weekday, dateadd(day, #i, #start)));
set #i += 1;
end
select * from #t
where [dayname] not in('Saturday', 'Sunday');
**Demo Here**
You can try this Query:
;with work_days as (
-- CTE of work days
select 2 [day_of_week] union -- Monday
select 3 union -- Tuesday
select 4 union -- Wednesday
select 5 union -- Thursday
select 6 -- Friday
)
,dates_between as (
-- recursive CTE, for dates in range
select cast('20170301' as date) [day]
union all
select dateadd(day,1,[day]) from dates_between
where [day]=[day] and [day]<'20170310'
)
select *
from dates_between join work_days
on work_days.day_of_week = DATEPART(dw, dates_between.[day])
order by dates_between.[day]
OPTION (MAXRECURSION 0) -- if dates range more than 100 days

How To Get Elapsed Time Between Two Dates, Ignoring Some Time Range?

I have a table with a DATETIME column 'Start' and a DATETIME column 'End'. I want to return the number of minutes between the start and the end (End is always after than Start). Usually I'd just use 'DateDiff()' but this time I need to exclude another date range. For example - From Tuesday at 9am until Wednesday at 6pm, of each week, should be ignored.
If a row has a Start of Tuesday at 8am and an End of Wednesday at 7pm - the elapsed time should be two hours (120 minutes) - because of the ignored date range.
I'm having trouble coming up with a decent way of doing this and my searching online hasn't found quite what I'm looking for. Can someone help me along?
Try This:
--total time span to calculate difference
DECLARE #StartDate DATETIME = '2015-11-10 8:00:00AM',
#EndDate DATETIME = '2015-11-11 7:00:00PM'
--get the day of week (-1 because sunday is counted as first weekday)
DECLARE #StartDayOfWeek INT = (SELECT DATEPART(WEEKDAY, #StartDate)) -1
DECLARE #EndDayOfWeek INT = (SELECT DATEPART(WEEKDAY, #EndDate)) -1
--set the time span to exclude
DECLARE #InitialDOWToExclude TINYINT = 2
DECLARE #InitialTODToExclude VARCHAR(100) = '9:00:00 AM'
DECLARE #EndDOWToExclude TINYINT = 3
DECLARE #EndTODToExclude VARCHAR(100) = '6:00:00 PM'
--this will be the final output in hours
DECLARE #ElapsedHours INT = (SELECT DATEDIFF(HOUR, #StartDate, #EndDate))
DECLARE #WeeksBetween INT = (SELECT DATEDIFF(WEEK, #StartDate, #EndDate))
DECLARE #Iterator INT = 0
WHILE (#Iterator <= #WeeksBetween)
BEGIN
DECLARE #InitialDaysBetween INT = #StartDayOfWeek - #InitialDOWToExclude
DECLARE #StartDateToExclude DATETIME = (SELECT DATEADD(DAY, #InitialDaysBetween, DATEADD(WEEK, #Iterator, #StartDate)))
SET #StartDateToExclude =CAST(DATEPART(YEAR, #StartDateToExclude) AS VARCHAR(100))
+ CAST(DATEPART(MONTH, #StartDateToExclude) AS VARCHAR(100))
+ CAST(DATEPART(DAY, #StartDateToExclude) AS VARCHAR(100))
+ ' '
+ CAST(#InitialTODToExclude AS VARCHAR(100))
DECLARE #EndDaysBetween INT = #EndDayOfWeek - #EndDOWToExclude
DECLARE #EndDateToExclude DATETIME = (SELECT DATEADD(DAY, #EndDaysBetween, DATEADD(WEEK, #Iterator, #EndDate)))
SET #EndDateToExclude =CAST(DATEPART(YEAR, #EndDateToExclude) AS VARCHAR(100))
+ CAST(DATEPART(MONTH, #EndDateToExclude) AS VARCHAR(100))
+ CAST(DATEPART(DAY, #EndDateToExclude) AS VARCHAR(100))
+ ' '
+ CAST(#EndTODToExclude AS VARCHAR(100))
SET #ElapsedHours = #ElapsedHours - DATEDIFF(HOUR, #StartDateToExclude, #EndDateToExclude)
SET #Iterator = #Iterator + 1
END
SELECT #ElapsedHours
This might get you pretty close..
DECLARE #Table1 TABLE ([Id] INT, [Start] DATETIME, [End] DATETIME)
INSERT INTO #Table1 VALUES
(1, '2015-11-08 00:00:00', '2015-11-10 21:45:38'),
(2, '2015-11-09 00:00:00', '2015-11-11 21:45:38')
;
-- hours to exclude
WITH excludeCTE AS
(
SELECT *
FROM (VALUES('Tuesday', 9, 0), ('Wednesday', 0, 0)) AS T([Day], [Hour], [Amount])
UNION ALL
SELECT [Day], [Hour] + 1, [Amount]
FROM excludeCTE
WHERE ([Day] = 'Tuesday' AND [Hour] < 23) OR ([Day] = 'Wednesday' AND [Hour] < 18)
),
-- all hours between start and end
dateCTE AS
(
SELECT [Id],
[Start],
[End],
DATENAME(weekday, [Start])[Day],
DATENAME(hour, [Start])[Hour]
FROM #Table1 t
UNION ALL
SELECT cte.[Id],
DATEADD(HOUR, 1, cte.[Start]),
cte.[End],
DATENAME(weekday, DATEADD(HOUR, 1, cte.[Start]))[Day],
DATENAME(hour, DATEADD(HOUR, 1, cte.[Start]))[Hour]
FROM #Table1 t
JOIN dateCTE cte ON t.Id = cte.Id
WHERE DATEADD(HOUR, 1, cte.[Start]) <= t.[End]
)
SELECT t.[Id],
t.[Start],
t.[End],
SUM(COALESCE(e.[Amount], 1)) [Hours]
FROM #Table1 t
INNER JOIN dateCTE d ON t.[Id] = d.[Id]
LEFT JOIN excludeCTE e ON d.[Day] = e.[Day] AND d.[Hour] = e.[Hour]
GROUP BY t.[Id],
t.[Start],
t.[End]
OPTION (MAXRECURSION 0) -- allow more than 100 hours
Putting the additional constraint that there can only be one excluded range between any two date
CREATE TABLE worktable (
_Id INT
, _Start DATETIME
, _End DATETIME
);
INSERT INTO worktable VALUES
(1, '2015-11-09 00:00:00', '2015-11-09 00:45:00') -- Start and End before excluded range
, (2, '2015-11-09 00:00:00', '2015-11-11 21:45:00') -- Start before, End after
, (3, '2015-11-09 00:00:00', '2015-11-10 21:00:00') -- Start before, End between
, (4, '2015-11-10 10:00:00', '2015-11-11 10:00:00') -- Start between, End between
, (5, '2015-11-10 10:00:00', '2015-11-11 21:45:00') -- Start between, End after
With getDates As (
SELECT _Id
, a = _Start
, b = _End
, c = DATEADD(hh, 9
, DATEADD(DAY,DATEDIFF(DAY, 0, _Start) / 7 * 7
+ 7 * Cast(Sign(1 - DatePart(dw, _Start)) + 1 as bit), 1))
, d = DATEADD(hh, 18
, DATEADD(DAY,DATEDIFF(DAY, 0, _Start) / 7 * 7
+ 7 * Cast(Sign(1 - DatePart(dw, _Start)) + 1 as bit), 2))
FROM worktable
), getDiff As (
SELECT c_a = DATEDIFF(mi, a, c)
, c_b = DATEDIFF(mi, b, c)
, b_d = DATEDIFF(mi, d, b)
, a, b, c, d, _id
FROM getDates
)
Select _id
, (c_a + ABS(c_a)) / 2
- (c_b + ABS(c_b)) / 2
+ (b_d + ABS(b_d)) / 2
FROM getDiff;
c is the date of the first Tuesday after the start date (Find the next occurance of a day of the week in SQL) you may need to adjust the last value depending from DATEFIRST
d is the date of the first Wednesday after the start date in the same week of c
Cast(Sign(a - b) + 1 as bit) is 1 if a is more than or equal b, 0 otherwise
(x + ABS(x)) / 2 is x if not negative, otherwise 0
Given that the formula to get the elapsed time with the excluded range is:
+ (Exclusion Start - Start) If (Start < Exclusion Start)
- (Exclusion Start - End) If (End < Exclusion Start)
+ (End - Exclusion End) If (Exclusion End < End)
-- excluded range (weekday numbers run from 1 to 7)
declare #x datetime = /*ignore*/ '1900012' + /*start day # and time*/ '3 09:00am';
declare #y datetime = /*ignore*/ '1900012' + /* end day # and time*/ '4 06:00pm';
-- normalize date to 1900-01-21, which was a Sunday
declare #s datetime =
dateadd(day, 19 + datepart(weekday, #start), cast(cast(#start as time) as datetime));
declare #e datetime =
dateadd(day, 19 + datepart(weekday, #end), cast(cast(#end as time) as datetime));
-- split range into two parts, one before #x and the other after #y
-- each part collapses to zero when #s and #e respectively fall between #x and #y
select (
datediff(second, -- diff in minutes would truncate so count seconds
case when #s < #x then #s else #x end, -- minimum of #s, #x
case when #e < #x then #e else #x end -- minimum of #e, #x
) +
datediff(second,
case when #s > #y then #s else #y end, -- maximum of #s, #y
case when #e > #y then #e else #y end -- maximum of #e, #y
)
) / 60; -- convert seconds to minutes, truncating with integer division
I glanced at the earlier answers and I thought that surely there was something more straightforward and elegant. Perhaps this is easier to understand and one clear advantage over some solutions is that it's trivial to change the excluded range and that range doesn't have to be limited to a single day.
I'm assuming that your dates never span more than one regular calendar week. It wouldn't be too difficult to extend it to handle more though. One approach would be to handle starting and ending partial weeks plus the full weeks in the middle.
Imagine that your start time is 8:59:30am and your end time is 6:00:30pm. In such a case I'm figuring that you'd want to accumulate the half minutes on each side to get a full minute in total after subtracting the 9-6 block. If you use datediff(minute, ...) you would be truncating the partial minutes and never get the chance to add them together: so that's why I count seconds and then divide by sixty at the end. Of course, if you're only dealing in whole minutes then you won't need to do it that way.
I've chosen my reference date somewhat arbitrarily. At first I thought it might possibly be handy to look at a real and convenient date on the calendar but ultimately it only really matters that it falls on a Sunday. So I settled on the first Sunday falling on a date ending in the digit 1.
Note that the solution also relies on datefirst being set to Sunday. That could be tweaked or made more portable if necessary.

Get all dates between two dates in SQL Server

How to get all the dates between two dates?
I have a variable #MAXDATE which is storing the maximum date from the table. Now I want to get the all dates between #Maxdate and GETDATE() and want to store these dates in a cursor.
So far I have done as follows:
;with GetDates As
(
select DATEADD(day,1,#maxDate) as TheDate
UNION ALL
select DATEADD(day,1, TheDate) from GetDates
where TheDate < GETDATE()
)
This is working perfectly but when I am trying to store these values in a cursor
SET #DateCurSor = CURSOR FOR
SELECT TheDate
FROM GetDates
Compilation Error
Incorrect syntax near the keyword 'SET'.
How to solve this?
My first suggestion would be use your calendar table, if you don't have one, then create one. They are very useful. Your query is then as simple as:
DECLARE #MinDate DATE = '20140101',
#MaxDate DATE = '20140106';
SELECT Date
FROM dbo.Calendar
WHERE Date >= #MinDate
AND Date < #MaxDate;
If you don't want to, or can't create a calendar table you can still do this on the fly without a recursive CTE:
DECLARE #MinDate DATE = '20140101',
#MaxDate DATE = '20140106';
SELECT TOP (DATEDIFF(DAY, #MinDate, #MaxDate) + 1)
Date = DATEADD(DAY, ROW_NUMBER() OVER(ORDER BY a.object_id) - 1, #MinDate)
FROM sys.all_objects a
CROSS JOIN sys.all_objects b;
For further reading on this see:
Generate a set or sequence without loops – part 1
Generate a set or sequence without loops – part 2
Generate a set or sequence without loops – part 3
With regard to then using this sequence of dates in a cursor, I would really recommend you find another way. There is usually a set based alternative that will perform much better.
So with your data:
date | it_cd | qty
24-04-14 | i-1 | 10
26-04-14 | i-1 | 20
To get the quantity on 28-04-2014 (which I gather is your requirement), you don't actually need any of the above, you can simply use:
SELECT TOP 1 date, it_cd, qty
FROM T
WHERE it_cd = 'i-1'
AND Date <= '20140428'
ORDER BY Date DESC;
If you don't want it for a particular item:
SELECT date, it_cd, qty
FROM ( SELECT date,
it_cd,
qty,
RowNumber = ROW_NUMBER() OVER(PARTITION BY ic_id
ORDER BY date DESC)
FROM T
WHERE Date <= '20140428'
) T
WHERE RowNumber = 1;
You can use this script to find dates between two dates. Reference taken from this Article:
DECLARE #StartDateTime DATETIME
DECLARE #EndDateTime DATETIME
SET #StartDateTime = '2015-01-01'
SET #EndDateTime = '2015-01-12';
WITH DateRange(DateData) AS
(
SELECT #StartDateTime as Date
UNION ALL
SELECT DATEADD(d,1,DateData)
FROM DateRange
WHERE DateData < #EndDateTime
)
SELECT DateData
FROM DateRange
OPTION (MAXRECURSION 0)
GO
Just saying...here is a more simple approach to this:
declare #sdate date = '2017-06-25'
, #edate date = '2017-07-24';
with dates_CTE (date) as (
select #sdate
Union ALL
select DATEADD(day, 1, date)
from dates_CTE
where date < #edate
)
select *
from dates_CTE;
Easily create a Table Value Function that will return a table with all dates.
Input dates as string
You can customize the date in the the format you like '01/01/2017' or '01-01-2017' in string formats (103,126 ...)
Try this
CREATE FUNCTION [dbo].[DateRange_To_Table] ( #minDate_Str NVARCHAR(30), #maxDate_Str NVARCHAR(30))
RETURNS #Result TABLE(DateString NVARCHAR(30) NOT NULL, DateNameString NVARCHAR(30) NOT NULL)
AS
begin
DECLARE #minDate DATETIME, #maxDate DATETIME
SET #minDate = CONVERT(Datetime, #minDate_Str,103)
SET #maxDate = CONVERT(Datetime, #maxDate_Str,103)
INSERT INTO #Result(DateString, DateNameString )
SELECT CONVERT(NVARCHAR(10),#minDate,103), CONVERT(NVARCHAR(30),DATENAME(dw,#minDate))
WHILE #maxDate > #minDate
BEGIN
SET #minDate = (SELECT DATEADD(dd,1,#minDate))
INSERT INTO #Result(DateString, DateNameString )
SELECT CONVERT(NVARCHAR(10),#minDate,103), CONVERT(NVARCHAR(30),DATENAME(dw,#minDate))
END
return
end
To execute the function do this:
SELECT * FROM dbo.DateRange_To_Table ('01/01/2017','31/01/2017')
The output will be
01/01/2017 Sunday
02/01/2017 Monday
03/01/2017 Tuesday
04/01/2017 Wednesday
05/01/2017 Thursday
06/01/2017 Friday
07/01/2017 Saturday
08/01/2017 Sunday
09/01/2017 Monday
10/01/2017 Tuesday
11/01/2017 Wednesday
12/01/2017 Thursday
13/01/2017 Friday
14/01/2017 Saturday
15/01/2017 Sunday
16/01/2017 Monday
17/01/2017 Tuesday
18/01/2017 Wednesday
19/01/2017 Thursday
20/01/2017 Friday
21/01/2017 Saturday
22/01/2017 Sunday
23/01/2017 Monday
24/01/2017 Tuesday
25/01/2017 Wednesday
26/01/2017 Thursday
27/01/2017 Friday
28/01/2017 Saturday
29/01/2017 Sunday
30/01/2017 Monday
31/01/2017 Tuesday
This can be considered as bit tricky way as in my situation, I can't use a CTE table, so decided to join with sys.all_objects and then created row numbers and added that to start date till it reached the end date.
See the code below where I generated all dates in Jul 2018. Replace hard coded dates with your own variables (tested in SQL Server 2016):
select top (datediff(dd, '2018-06-30', '2018-07-31')) ROW_NUMBER()
over(order by a.name) as SiNo,
Dateadd(dd, ROW_NUMBER() over(order by a.name) , '2018-06-30') as Dt from sys.all_objects a
You can try this:
SET LANGUAGE SPANISH
DECLARE #startDate DATE = GETDATE() -- Your start date
DECLARE #endDate DATE = DATEADD(MONTH, 16, GETDATE()) -- Your end date
DECLARE #years INT = YEAR(#endDate) - YEAR(#startDate)
CREATE TABLE #TMP_YEARS (
[year] INT
)
-- Get all posible years between the start and end date
WHILE #years >= 0
BEGIN
INSERT INTO #TMP_YEARS
([year])
SELECT YEAR(#startDate) + #years
SET #years = #years - 1
END
;WITH [days]([day]) AS -- Posible days at a month
(
SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 UNION ALL -- days lower than 10
SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12 UNION ALL SELECT 13 UNION ALL SELECT 14 UNION ALL SELECT 15 UNION ALL SELECT 16 UNION ALL SELECT 17 UNION ALL SELECT 18 UNION ALL SELECT 19 UNION ALL -- days lower than 20
SELECT 20 UNION ALL SELECT 21 UNION ALL SELECT 22 UNION ALL SELECT 23 UNION ALL SELECT 24 UNION ALL SELECT 25 UNION ALL SELECT 26 UNION ALL SELECT 27 UNION ALL SELECT 28 UNION ALL SELECT 29 UNION ALL -- days lower than 30
SELECT 30 UNION ALL SELECT 31 -- days higher 30
),
[months]([month]) AS -- All months at a year
(
SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 UNION ALL SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12
)
SELECT CONVERT(VARCHAR, a.[year]) + '-' + REPLICATE('0', 2 - LEN(CONVERT(VARCHAR, n.[month]))) + CONVERT(VARCHAR, n.[month]) + '-' + REPLICATE('0', 2 - LEN(CONVERT(VARCHAR, d.[day]))) + CONVERT(VARCHAR, d.[day]) as [date]
FROM #TMP_YEARS a
CROSS JOIN [months] n -- Join all years with all months
INNER JOIN [days] d on DAY(EOMONTH(CONVERT(VARCHAR, a.[year]) + '-' + REPLICATE('0', 2 - LEN(CONVERT(VARCHAR, n.[month]))) + CONVERT(VARCHAR, n.[month]) + '-' + CONVERT(VARCHAR, DAY(EOMONTH(CAST(CONVERT(VARCHAR, a.[year]) + '-' + CONVERT(varchar, n.[month]) + '-15' AS DATE)))))) >= d.[day] AND -- The number of the day can't be higher than the last day of the current month and the current year
CONVERT(VARCHAR, a.[year]) + '-' + REPLICATE('0', 2 - LEN(CONVERT(VARCHAR, n.[month]))) + CONVERT(VARCHAR, n.[month]) + '-' + REPLICATE('0', 2 - LEN(CONVERT(VARCHAR, d.[day]))) + CONVERT(VARCHAR, d.[day]) <= ISNULL(#endDate, GETDATE()) AND -- The current date can't be higher than the end date
CONVERT(VARCHAR, a.[year]) + '-' + REPLICATE('0', 2 - LEN(CONVERT(VARCHAR, n.[month]))) + CONVERT(VARCHAR, n.[month]) + '-' + REPLICATE('0', 2 - LEN(CONVERT(VARCHAR, d.[day]))) + CONVERT(VARCHAR, d.[day]) >= ISNULL(#startDate, GETDATE()) -- The current date should be higher than the start date
ORDER BY a.[year] ASC, n.[month] ASC, d.[day] ASC
The output will be something like this, you can format the date as you like:
2019-01-24
2019-01-25
2019-01-26
2019-01-27
2019-01-28
2019-01-29
2019-01-30
2019-01-31
2019-02-01
2019-02-02
2019-02-03
2019-02-04
2019-02-05
2019-02-06
2019-02-07
2019-02-08
2019-02-09
...
create procedure [dbo].[p_display_dates](#startdate datetime,#enddate datetime)
as
begin
declare #mxdate datetime
declare #indate datetime
create table #daterange (dater datetime)
insert into #daterange values (#startdate)
set #mxdate = (select MAX(dater) from #daterange)
while #mxdate < #enddate
begin
set #indate = dateadd(day,1,#mxdate)
insert into #daterange values (#indate)
set #mxdate = (select MAX(dater) from #daterange)
end
select * from #daterange
end
I listed dates of 2 Weeks later. You can use variable #period OR function datediff(dd, #date_start, #date_end)
declare #period INT, #date_start datetime, #date_end datetime, #i int;
set #period = 14
set #date_start = convert(date,DATEADD(D, -#period, curent_timestamp))
set #date_end = convert(date,current_timestamp)
set #i = 1
create table #datesList(dts datetime)
insert into #datesList values (#date_start)
while #i <= #period
Begin
insert into #datesList values (dateadd(d,#i,#date_start))
set #i = #i + 1
end
select cast(dts as DATE) from #datesList
Drop Table #datesList
This is the method that I would use.
DECLARE
#DateFrom DATETIME = GETDATE(),
#DateTo DATETIME = DATEADD(HOUR, -1, GETDATE() + 2); -- Add 2 days and minus one hour
-- Dates spaced a day apart
WITH MyDates (MyDate)
AS (
SELECT #DateFrom
UNION ALL
SELECT DATEADD(DAY, 1, MyDate)
FROM MyDates
WHERE MyDate < #DateTo
)
SELECT
MyDates.MyDate
, CONVERT(DATE, MyDates.MyDate) AS [MyDate in DATE format]
FROM
MyDates;
Here is a similar example, but this time the dates are spaced one hour apart to further aid understanding of how the query works:
-- Alternative example with dates spaced an hour apart
WITH MyDates (MyDate)
AS (SELECT #DateFrom
UNION ALL
SELECT DATEADD(HOUR, 1, MyDate)
FROM MyDates
WHERE MyDate < #DateTo
)
SELECT
MyDates.MyDate
FROM
MyDates;
As you can see, the query is fast, accurate and versatile.
You can use SQL Server recursive CTE
DECLARE
#MinDate DATE = '2020-01-01',
#MaxDate DATE = '2020-02-01';
WITH Dates(day) AS
(
SELECT CAST(#MinDate as Date) as day
UNION ALL
SELECT CAST(DATEADD(day, 1, day) as Date) as day
FROM Dates
WHERE CAST(DATEADD(day, 1, day) as Date) < #MaxDate
)
SELECT* FROM dates;
declare #start_dt as date = '1/1/2021'; -- Date from which the calendar table will be created.
declare #end_dt as date = '1/1/2022'; -- Calendar table will be created up to this date (not including).
declare #dates as table (
date_id date primary key,
date_year smallint,
date_month tinyint,
date_day tinyint,
weekday_id tinyint,
weekday_nm varchar(10),
month_nm varchar(10),
day_of_year smallint,
quarter_id tinyint,
first_day_of_month date,
last_day_of_month date,
start_dts datetime,
end_dts datetime
)
while #start_dt < #end_dt
begin
insert into #dates(
date_id, date_year, date_month, date_day,
weekday_id, weekday_nm, month_nm, day_of_year, quarter_id,
first_day_of_month, last_day_of_month,
start_dts, end_dts
)
values(
#start_dt, year(#start_dt), month(#start_dt), day(#start_dt),
datepart(weekday, #start_dt), datename(weekday, #start_dt), datename(month, #start_dt), datepart(dayofyear, #start_dt), datepart(quarter, #start_dt),
dateadd(day,-(day(#start_dt)-1),#start_dt), dateadd(day,-(day(dateadd(month,1,#start_dt))),dateadd(month,1,#start_dt)),
cast(#start_dt as datetime), dateadd(second,-1,cast(dateadd(day, 1, #start_dt) as datetime))
)
set #start_dt = dateadd(day, 1, #start_dt)
end
-- sample of the data
select
top 50 *
--into master.dbo.DimDate
from #dates d
order by date_id
DECLARE #FirstDate DATE = '2018-01-01'
DECLARE #LastDate Date = '2018-12-31'
DECLARE #tbl TABLE(ID INT IDENTITY(1,1) PRIMARY KEY,CurrDate date)
INSERT #tbl VALUES( #FirstDate)
WHILE #FirstDate < #LastDate
BEGIN
SET #FirstDate = DATEADD( day,1, #FirstDate)
INSERT #tbl VALUES( #FirstDate)
END
INSERT #tbl VALUES( #LastDate)
SELECT * FROM #tbl

T-SQL get number of working days between 2 dates

I want to calculate the number of working days between 2 given dates. For example if I want to calculate the working days between 2013-01-10 and 2013-01-15, the result must be 3 working days (I don't take into consideration the last day in that interval and I subtract the Saturdays and Sundays). I have the following code that works for most of the cases, except the one in my example.
SELECT (DATEDIFF(day, '2013-01-10', '2013-01-15'))
- (CASE WHEN DATENAME(weekday, '2013-01-10') = 'Sunday' THEN 1 ELSE 0 END)
- (CASE WHEN DATENAME(weekday, DATEADD(day, -1, '2013-01-15')) = 'Saturday' THEN 1 ELSE 0 END)
How can I accomplish this? Do I have to go through all the days and check them? Or is there an easy way to do this.
Please, please, please use a calendar table. SQL Server doesn't know anything about national holidays, company events, natural disasters, etc. A calendar table is fairly easy to build, takes an extremely small amount of space, and will be in memory if it is referenced enough.
Here is an example that creates a calendar table with 30 years of dates (2000 -> 2029) but requires only 200 KB on disk (136 KB if you use page compression). That is almost guaranteed to be less than the memory grant required to process some CTE or other set at runtime.
CREATE TABLE dbo.Calendar
(
dt DATE PRIMARY KEY, -- use SMALLDATETIME if < SQL Server 2008
IsWorkDay BIT
);
DECLARE #s DATE, #e DATE;
SELECT #s = '2000-01-01' , #e = '2029-12-31';
INSERT dbo.Calendar(dt, IsWorkDay)
SELECT DATEADD(DAY, n-1, '2000-01-01'), 1
FROM
(
SELECT TOP (DATEDIFF(DAY, #s, #e)+1) ROW_NUMBER()
OVER (ORDER BY s1.[object_id])
FROM sys.all_objects AS s1
CROSS JOIN sys.all_objects AS s2
) AS x(n);
SET DATEFIRST 1;
-- weekends
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE DATEPART(WEEKDAY, dt) IN (6,7);
-- Christmas
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE MONTH(dt) = 12
AND DAY(dt) = 25
AND IsWorkDay = 1;
-- continue with other holidays, known company events, etc.
Now the query you're after is quite simple to write:
SELECT COUNT(*) FROM dbo.Calendar
WHERE dt >= '20130110'
AND dt < '20130115'
AND IsWorkDay = 1;
More info on calendar tables:
http://web.archive.org/web/20070611150639/http://sqlserver2000.databases.aspfaq.com/why-should-i-consider-using-an-auxiliary-calendar-table.html
More info on generating sets without loops:
http://www.sqlperformance.com/tag/date-ranges
Also beware of little things like relying on the English output of DATENAME. I've seen several applications break because some users had a different language setting, and if you're relying on WEEKDAY be sure you set your DATEFIRST setting appropriately...
For stuff like this i tend to maintain a calendar table that also includes bank holidays etc.
The script i use for this is as follows (Note that i didnt write it # i forget where i found it)
SET DATEFIRST 1
SET NOCOUNT ON
GO
--Create ISO week Function (thanks BOL)
CREATE FUNCTION ISOweek ( #DATE DATETIME )
RETURNS INT
AS
BEGIN
DECLARE #ISOweek INT
SET #ISOweek = DATEPART(wk, #DATE) + 1 - DATEPART(wk, CAST(DATEPART(yy, #DATE) AS CHAR(4)) + '0104')
--Special cases: Jan 1-3 may belong to the previous year
IF ( #ISOweek = 0 )
SET #ISOweek = dbo.ISOweek(CAST(DATEPART(yy, #DATE) - 1 AS CHAR(4)) + '12' + CAST(24 + DATEPART(DAY, #DATE) AS CHAR(2))) + 1
--Special case: Dec 29-31 may belong to the next year
IF ( ( DATEPART(mm, #DATE) = 12 )
AND ( ( DATEPART(dd, #DATE) - DATEPART(dw, #DATE) ) >= 28 )
)
SET #ISOweek = 1
RETURN(#ISOweek)
END
GO
--END ISOweek
--CREATE Easter algorithm function
--Thanks to Rockmoose (http://www.sqlteam.com/forums/topic.asp?TOPIC_ID=45689)
CREATE FUNCTION fnDLA_GetEasterdate ( #year INT )
RETURNS CHAR(8)
AS
BEGIN
-- Easter date algorithm of Delambre
DECLARE #A INT ,
#B INT ,
#C INT ,
#D INT ,
#E INT ,
#F INT ,
#G INT ,
#H INT ,
#I INT ,
#K INT ,
#L INT ,
#M INT ,
#O INT ,
#R INT
SET #A = #YEAR % 19
SET #B = #YEAR / 100
SET #C = #YEAR % 100
SET #D = #B / 4
SET #E = #B % 4
SET #F = ( #B + 8 ) / 25
SET #G = ( #B - #F + 1 ) / 3
SET #H = ( 19 * #A + #B - #D - #G + 15 ) % 30
SET #I = #C / 4
SET #K = #C % 4
SET #L = ( 32 + 2 * #E + 2 * #I - #H - #K ) % 7
SET #M = ( #A + 11 * #H + 22 * #L ) / 451
SET #O = 22 + #H + #L - 7 * #M
IF #O > 31
BEGIN
SET #R = #O - 31 + 400 + #YEAR * 10000
END
ELSE
BEGIN
SET #R = #O + 300 + #YEAR * 10000
END
RETURN #R
END
GO
--END fnDLA_GetEasterdate
--Create the table
CREATE TABLE MyDateTable
(
FullDate DATETIME NOT NULL
CONSTRAINT PK_FullDate PRIMARY KEY CLUSTERED ,
Period INT ,
ISOWeek INT ,
WorkingDay VARCHAR(1) CONSTRAINT DF_MyDateTable_WorkDay DEFAULT 'Y'
)
GO
--End table create
--Populate table with required dates
DECLARE #DateFrom DATETIME ,
#DateTo DATETIME ,
#Period INT
SET #DateFrom = CONVERT(DATETIME, '20000101')
--yyyymmdd (1st Jan 2000) amend as required
SET #DateTo = CONVERT(DATETIME, '20991231')
--yyyymmdd (31st Dec 2099) amend as required
WHILE #DateFrom <= #DateTo
BEGIN
SET #Period = CONVERT(INT, LEFT(CONVERT(VARCHAR(10), #DateFrom, 112), 6))
INSERT MyDateTable
( FullDate ,
Period ,
ISOWeek
)
SELECT #DateFrom ,
#Period ,
dbo.ISOweek(#DateFrom)
SET #DateFrom = DATEADD(dd, +1, #DateFrom)
END
GO
--End population
/* Start of WorkingDays UPDATE */
UPDATE MyDateTable
SET WorkingDay = 'B' --B = Bank Holiday
--------------------------------EASTER---------------------------------------------
WHERE FullDate = DATEADD(dd, -2, CONVERT(DATETIME, dbo.fnDLA_GetEasterdate(DATEPART(yy, FullDate)))) --Good Friday
OR FullDate = DATEADD(dd, +1, CONVERT(DATETIME, dbo.fnDLA_GetEasterdate(DATEPART(yy, FullDate))))
--Easter Monday
GO
UPDATE MyDateTable
SET WorkingDay = 'B'
--------------------------------NEW YEAR-------------------------------------------
WHERE FullDate IN ( SELECT MIN(FullDate)
FROM MyDateTable
WHERE DATEPART(mm, FullDate) = 1
AND DATEPART(dw, FullDate) NOT IN ( 6, 7 )
GROUP BY DATEPART(yy, FullDate) )
---------------------MAY BANK HOLIDAYS(Always Monday)------------------------------
OR FullDate IN ( SELECT MIN(FullDate)
FROM MyDateTable
WHERE DATEPART(mm, FullDate) = 5
AND DATEPART(dw, FullDate) = 1
GROUP BY DATEPART(yy, FullDate) )
OR FullDate IN ( SELECT MAX(FullDate)
FROM MyDateTable
WHERE DATEPART(mm, FullDate) = 5
AND DATEPART(dw, FullDate) = 1
GROUP BY DATEPART(yy, FullDate) )
--------------------AUGUST BANK HOLIDAY(Always Monday)------------------------------
OR FullDate IN ( SELECT MAX(FullDate)
FROM MyDateTable
WHERE DATEPART(mm, FullDate) = 8
AND DATEPART(dw, FullDate) = 1
GROUP BY DATEPART(yy, FullDate) )
--------------------XMAS(Move to next working day if on Sat/Sun)--------------------
OR FullDate IN ( SELECT CASE WHEN DATEPART(dw, FullDate) IN ( 6, 7 ) THEN DATEADD(dd, +2, FullDate)
ELSE FullDate
END
FROM MyDateTable
WHERE DATEPART(mm, FullDate) = 12
AND DATEPART(dd, FullDate) IN ( 25, 26 ) )
GO
---------------------------------------WEEKENDS--------------------------------------
UPDATE MyDateTable
SET WorkingDay = 'N'
WHERE DATEPART(dw, FullDate) IN ( 6, 7 )
GO
/* End of WorkingDays UPDATE */
--SELECT * FROM MyDateTable ORDER BY 1
DROP FUNCTION fnDLA_GetEasterdate
DROP FUNCTION ISOweek
--DROP TABLE MyDateTable
SET NOCOUNT OFF
Once you have created the table, finding the number of working days is easy peasy:
SELECT COUNT(FullDate) AS WorkingDays
FROM dbo.tbl_WorkingDays
WHERE WorkingDay = 'Y'
AND FullDate >= CONVERT(DATETIME, '10/01/2013', 103)
AND FullDate < CONVERT(DATETIME, '15/01/2013', 103)
Note that this script includes UK bank holidays, i'm not sure what region you're in.
Here's a simple function that counts working days not including Saturday and Sunday (when counting holidays isn't necessary):
CREATE FUNCTION dbo.udf_GetBusinessDays (
#START_DATE DATE,
#END_DATE DATE
)
RETURNS INT
WITH EXECUTE AS CALLER
AS
BEGIN
DECLARE #NUMBER_OF_DAYS INT = 0;
DECLARE #DAY_COUNTER INT = 0;
DECLARE #BUSINESS_DAYS INT = 0;
DECLARE #CURRENT_DATE DATE;
DECLARE #DAYNAME NVARCHAR(9)
SET #NUMBER_OF_DAYS = DATEDIFF(DAY, #START_DATE, #END_DATE);
WHILE #DAY_COUNTER <= #NUMBER_OF_DAYS
BEGIN
SET #CURRENT_DATE = DATEADD(DAY, #DAY_COUNTER, #START_DATE)
SET #DAYNAME = DATENAME(WEEKDAY, #CURRENT_DATE)
SET #DAY_COUNTER += 1
IF #DAYNAME = N'Saturday' OR #DAYNAME = N'Sunday'
BEGIN
CONTINUE
END
ELSE
BEGIN
SET #BUSINESS_DAYS += 1
END
END
RETURN #BUSINESS_DAYS
END
GO
This is the method I normally use (When not using a calendar table):
DECLARE #T TABLE (Date1 DATE, Date2 DATE);
INSERT #T VALUES ('20130110', '20130115'), ('20120101', '20130101'), ('20120611', '20120701');
SELECT Date1, Date2, WorkingDays
FROM #T t
CROSS APPLY
( SELECT [WorkingDays] = COUNT(*)
FROM Master..spt_values s
WHERE s.Number BETWEEN 1 AND DATEDIFF(DAY, t.date1, t.Date2)
AND s.[Type] = 'P'
AND DATENAME(WEEKDAY, DATEADD(DAY, s.number, t.Date1)) NOT IN ('Saturday', 'Sunday')
) wd
If like I do you have a table with holidays in you can add this in too:
SELECT Date1, Date2, WorkingDays
FROM #T t
CROSS APPLY
( SELECT [WorkingDays] = COUNT(*)
FROM Master..spt_values s
WHERE s.Number BETWEEN 1 AND DATEDIFF(DAY, t.date1, t.Date2)
AND s.[Type] = 'P'
AND DATENAME(WEEKDAY, DATEADD(DAY, s.number, t.Date1)) NOT IN ('Saturday', 'Sunday')
AND NOT EXISTS
( SELECT 1
FROM HolidayTable ht
WHERE ht.Date = DATEADD(DAY, s.number, t.Date1)
)
) wd
The above will only work if your dates are within 2047 days of each other, if you are likely to be calculating larger date ranges you can use this:
SELECT Date1, Date2, WorkingDays
FROM #T t
CROSS APPLY
( SELECT [WorkingDays] = COUNT(*)
FROM ( SELECT [Number] = ROW_NUMBER() OVER(ORDER BY s.number)
FROM Master..spt_values s
CROSS JOIN Master..spt_values s2
) s
WHERE s.Number BETWEEN 1 AND DATEDIFF(DAY, t.date1, t.Date2)
AND DATENAME(WEEKDAY, DATEADD(DAY, s.number, t.Date1)) NOT IN ('Saturday', 'Sunday')
) wd
I did my code in SQL SERVER 2008 (MS SQL) . It works fine for me. I hope it will help you.
DECLARE #COUNTS int,
#STARTDATE date,
#ENDDATE date
SET #STARTDATE ='01/21/2013' /*Start date in mm/dd/yyy */
SET #ENDDATE ='01/26/2013' /*End date in mm/dd/yyy */
SET #COUNTS=0
WHILE (#STARTDATE<=#ENDDATE)
BEGIN
/*Check for holidays*/
IF ( DATENAME(weekday,#STARTDATE)<>'Saturday' and DATENAME(weekday,#STARTDATE)<>'Sunday')
BEGIN
SET #COUNTS=#COUNTS+1
END
SET #STARTDATE=DATEADD(day,1,#STARTDATE)
END
/* Display the no of working days */
SELECT #COUNTS
By Combining #Aaron Bertrand's answer and the Easter Calculation from #HeavenCore's and adding some code of my own, this code creates a calendar from 2000 to 2049 that includes UK (England) Bank Holidays. Usage and notes as per Aaron's answer:
DECLARE #s DATE, #e DATE;
SELECT #s = '2000-01-01' , #e = '2049-12-31';
-- Insert statements for procedure here
CREATE TABLE dbo.Calendar
(
dt DATE PRIMARY KEY, -- use SMALLDATETIME if < SQL Server 2008
IsWorkDay BIT
);
INSERT dbo.Calendar(dt, IsWorkDay)
SELECT DATEADD(DAY, n-1, '2000-01-01'), 1
FROM
(
SELECT TOP (DATEDIFF(DAY, #s, #e)+1) ROW_NUMBER()
OVER (ORDER BY s1.[object_id])
FROM sys.all_objects AS s1
CROSS JOIN sys.all_objects AS s2
) AS x(n);
SET DATEFIRST 1;
-- weekends
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE DATEPART(WEEKDAY, dt) IN (6,7);
-- Christmas
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE IsWorkDay = 1 and MONTH(dt) = 12 and
(DAY(dt) in (25,26) or
(DAY(dt) in (27, 28) and DATEPART(WEEKDAY, dt) IN (1,2)) );
-- New Year
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE IsWorkDay = 1 and MONTH(dt) = 1 AND
( DAY(dt) = 1 or (DAY(dt) IN (2,3) AND DATEPART(WEEKDAY, dt)=1 ));
-- Easter
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE dt = DATEADD(dd, -2, CONVERT(DATETIME, dbo.fnDLA_GetEasterdate(DATEPART(yy, dt)))) --Good Friday
OR dt = DATEADD(dd, +1, CONVERT(DATETIME, dbo.fnDLA_GetEasterdate(DATEPART(yy, dt)))) --Easter Monday
-- May Day (first Monday in May)
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE MONTH(dt) = 5 AND DATEPART(WEEKDAY, dt)=1 and DAY(DT)<8;
-- Spring Bank Holiday (last Monday in May apart from 2022 when moved to include Platinum Jubilee bank holiday)
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE
(YEAR(dt)=2022 and MONTH(dt) = 6 AND DAY(dt) IN (2,3)) OR
(YEAR(dt)<>2022 and MONTH(dt) = 5 AND DATEPART(WEEKDAY, dt)=1 and DAY(DT)>24);
-- Summer Bank Holiday (last Monday in August)
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE MONTH(dt) = 8 AND DATEPART(WEEKDAY, dt)=1 and DAY(DT)>24;

Summing and grouping the number of records in a month

I have the following table which has employees' absence:
RecordId EmpID ActivityCode DateFrom DateTo
---------------------------------------------------------------
666542 1511 AB 29/01/2011 02/02/2011
666986 1511 AB 11/11/2011 11/11/2011
666996 1511 EL 13/11/2011 17/11/2011
755485 1787 SL 01/11/2011 14/11/2011
758545 1787 SL 15/11/2011 03/12/2011
796956 1954 AB 09/11/2011 09/11/2011
799656 1367 AB 09/11/2011 09/11/2011
808845 1527 EL 16/11/2011 16/11/2011
823323 1527 EL 17/11/2011 17/11/2011
823669 1527 EL 18/11/2011 18/11/2011
899555 1123 AB 09/11/2011 09/12/2011
990990 1511 AB 12/11/2011 12/11/2011
Now I want a report generated by a stored proc to sum all the absence days for a specific absence code for each month in a given year, for example If i want to know the totals of absence in 2011 from the previous table I will get something similar to:
Month TotalDays
---------------------------------
JAN 2011 201
FEB 2011 36
MAR 2011 67
APR 2011 91
....
The stored proc will have two params (#Year INT, #AbsCode NVARCHAR(3)).
Please Note, Sometimes a record overlaps another month (like the first row in the example table) and that should be counted separately for each month. I have tried using loops but with no luck. I am so weak in TSQL.
UPDATE
Right now I am using a scalar value user function and a stored procedure to do the job, Its ugly and hard to trace. Here it is any way:
The User function:
ALTER FUNCTION [dbo].[GetActivityTotalDaysInMonth]
(
#ActivityCode CHAR(3)
,#Year INT
,#Month INT
)
RETURNS INT
AS
BEGIN
DECLARE #FirstDayOfMonth DATETIME
DECLARE #LastDayOfMonth DATETIME
SET #FirstDayOfMonth = CAST(CAST(#Year AS varchar) + '-' + CAST(#Month AS varchar) + '-' + CAST(1 AS varchar) AS DATETIME)
SET #LastDayOfMonth = DATEADD(s, -1, DATEADD(M, 1, #FirstDayOfMonth))
DECLARE #TotalDays INT
SELECT #TotalDays =
SUM(DATEDIFF(DAY,
(CASE WHEN ActivityDateFrom < #FirstDayOfMonth THEN #FirstDayOfMonth ELSE ActivityDateFrom END)
, (CASE WHEN ActivityDateTo > #LastDayOfMonth THEN #LastDayOfMonth ELSE ActivityDateTo END))+1)
FROM Activities
WHERE
ActivityCode=#ActivityCode
AND ((ActivityDateFrom < #FirstDayOfMonth AND ActivityDateTo >= #FirstDayOfMonth)
OR (ActivityDateFrom >= #FirstDayOfMonth AND ActivityDateTo <= #LastDayOfMonth)
OR (ActivityDateFrom <= #LastDayOfMonth AND ActivityDateTo > #LastDayOfMonth))
RETURN #TotalDays
END
Now, I call this function inside a loop in a stored procedure:
ALTER PROCEDURE GetAnnualActivityTotalDays
(
#ActivityCode CHAR(3)
,#Year INT
)
AS
BEGIN
SET NOCOUNT ON;
DECLARE #Stats TABLE
([Month] NVARCHAR(50), TotalDays INT)
DECLARE #MonthNo INT
DECLARE #Month DATETIME
SET #MonthNo = 1
WHILE #MonthNo <= 12
BEGIN
SET #Month = CAST(CAST(#Year AS varchar) + '-' + CAST(#MonthNo AS varchar) + '-' + CAST(1 AS varchar) AS DATETIME)
INSERT INTO #Stats ([Month], TotalDays)
SELECT UPPER(SUBSTRING(DATENAME(mm, #Month), 1, 3)) + ', ' + CAST(#Year AS NVARCHAR),
dbo.GetActivityTotalDaysInMonth(#ActivityCode
,#Year
,#MonthNo
,#Base)
SET #MonthNo = #MonthNo + 1
END
SELECT * FROM #Stats
END
As you can see, this is ugly code which I believe it can be done in an easier way.. Any suggestions?
You'd need to create a calendar table which will allow you to easily count the days for each month that the start and end dates encompass. For example recordid = 666542 has 3 days in January and 2 days in February. You'd be able to get that number by a query like
select calyear, calmonth, caldate
from calendar
join activities on calendar.caldate between activities.activitydatefrom and activities.activitydateto
where activitycode = 'AB'
If you wrap that in a common table expression you can perform aggregation queries afterwards on the CTE.
with mycte as (
select calyear, calmonth, caldate
from calendar
join activities on calendar.caldate between activities.activitydatefrom and activities.activitydateto
where activitycode = 'AB'
)
select calyear, calmonth, count(caldate)
from mycte
group by calyear, calmonth
order by calyear, calmonth
To generate the calendar table you can use code similar to
create table calendar (calyear, calmonth, caldate)
declare #numdays int --number of days to generate in the calendar
declare #datestart datetime --the date to begin from in the calendar
set #numdays = 365
set #datestart = 'jan 1 2011';
with num as (
select 0 number
union
select 1 number
union
select 2 number
union
select 3 number
union
select 4 number
union
select 5 number
union
select 6 number
union
select 7 number
union
select 8 number
union
select 9 number
),
numberlist as (
select ((hundreds.number * 100) + (tens.number * 10) + ones.number) n
from num hundreds
cross join num tens
cross join num ones
where ((hundreds.number * 100) + (tens.number * 10) + ones.number) < #numdays
)
insert into calendar (calyear, calmonth, caldate)
select
datepart(yy,dateadd(dd,n,#datestart)) calyear,
datepart(mm,dateadd(dd,n,#datestart)) calmonth,
dateadd(dd,n,#datestart)caldate
from numberlist