running total for each employee - sql

I have the following query which works great to return a running total of carpool reimbursements for an individual staff member. (employee earns $30 for each 20 trips, trips roll over until the end of the year).
-- carpool quarter stats
use TRPTracking
declare #employeeID NVarChar(100), #year Char(4)
Set #employeeID = 'PSmith'
Set #year = '2014'
------
declare #startDate DateTime, #endDate DateTime
Set #startDate = '1/1/' + #year
Set #endDate = DateAdd(d,-1,DateAdd(yyyy,1,#startDate))
DECLARE #calendar TABLE (Date datetime)
WHILE (#startDate <= #endDate) BEGIN
INSERT INTO #Calendar VALUES (#startDate)
SET #startDate = DATEADD(quarter, 1, #startDate)
END
DECLARE #CarpoolTbl TABLE (quarter varchar(250), value decimal(18,1), runningTotal decimal(18,1), earned money)
DECLARE #runningTotal decimal(18,1), #earned money
SET #runningTotal = 0
SET #earned = 0
INSERT INTO #CarpoolTbl
SELECT CASE DatePart(q, c.date)
WHEN 1 THEN 'Jan-Mar'
WHEN 2 THEN 'Apr-Jun'
WHEN 3 THEN 'Jul-Sep'
WHEN 4 THEN 'Oct-Dec' END AS quarter,
IsNULL(Sum(t.value),0) AS value,
null,
0
FROM #calendar c
LEFT OUTER JOIN events e ON (DatePart(q, c.date) = DatePart(q, e.eventDate) AND e.employeeID = #employeeID AND e.eventType = 'CP' AND Year(eventDate) = #year)
LEFT JOIN types t ON t.typeID = e.eventType
GROUP BY DatePart(q, c.date)
UPDATE #CarpoolTbl
SET #earned = earned = Floor((#runningTotal + value)/20) - Floor(#runningTotal/20),
#runningTotal = runningTotal = #runningTotal + value
FROM #CarpoolTbl
SELECT quarter, value, runningTotal, earned * 30 AS earned
FROM #CarpoolTbl
Now, I want a query that returns this information for all employees. I remove the portion that relates to employeeID and I get what looks to be good. But... what is happening is my running total is running for everyone. I need it to restart for each employee. I can't quite figure out where to add the employeeID grouping in the running total.
-- carpool quarter stats
use TRPTracking
declare #year Char(4)
Set #year = '2014'
------
declare #startDate DateTime, #endDate DateTime
Set #startDate = '1/1/' + #year
Set #endDate = DateAdd(d,-1,DateAdd(yyyy,1,#startDate))
DECLARE #calendar TABLE (Date datetime)
WHILE (#startDate <= #endDate) BEGIN
INSERT INTO #Calendar VALUES (#startDate)
SET #startDate = DATEADD(quarter, 1, #startDate)
END
DECLARE #CarpoolTbl TABLE (dateQ int, quarter varchar(250), employeeID varchar(255), value decimal(18,1), runningTotal decimal(18,1), earned money)
DECLARE #runningTotal decimal(18,1), #earned money
SET #runningTotal = 0
SET #earned = 0
INSERT INTO #CarpoolTbl
SELECT
DatePart(q, c.date),
CASE DatePart(q, c.date)
WHEN 1 THEN 'Jan-Mar'
WHEN 2 THEN 'Apr-Jun'
WHEN 3 THEN 'Jul-Sep'
WHEN 4 THEN 'Oct-Dec' END AS quarter,
e.employeeID,
IsNULL(Sum(t.value),0) AS value,
null,
0
FROM #calendar c
LEFT OUTER JOIN events e ON (DatePart(q, c.date) = DatePart(q, e.eventDate)
AND e.eventType = 'CP' AND Year(eventDate) = #year)
LEFT JOIN types t ON t.typeID = e.eventType
GROUP BY e.employeeID, DatePart(q, c.date)
UPDATE #CarpoolTbl
SET #earned = earned = Floor((#runningTotal + value)/20) - Floor(#runningTotal/20),
#runningTotal = runningTotal = #runningTotal + value
FROM #CarpoolTbl
SELECT c.quarter, c.employeeID, a.DisplayName AS employee, c.value AS trips, earned * 30 AS earned
FROM #CarpoolTbl c
LEFT JOIN SBAIntranet.dbo.NTAuth a ON 'SBA\' + c.employeeID = a.AccountName
ORDER BY dateQ, employeeID
Any thoughts?
EDIT:
Unexpected result with running total for all employees, not by employee:
Qtr Employee trips tripsRunningTotal
Jan-Mar Cathy 5.0 5.0
Apr-Jun Cathy 3.0 375.5
Jul-Sep Cathy 4.0 757.0
Jan-Mar Carol 3.5 8.5
Apr-Jun Carol 16.0 391.5
Jul-Sep Carol 44.5 801.5
EDIT 2:
Ok, here is my revised, cleaner, accurate version. Now, to figure out the earned dollars portion.
-- carpool quarter stats
use TRPTracking
declare #employeeID NVarChar(100), #year Char(4)
Set #year = '2014'
------
declare #startDate DateTime, #endDate DateTime
Set #startDate = '1/1/' + #year
Set #endDate = DateAdd(d,-1,DateAdd(yyyy,1,#startDate))
;WITH cte
AS (SELECT Datepart(qq, e.eventDate) AS quarterNum,
CASE DatePart(qq, e.eventDate)
WHEN 1 THEN 'Jan-Mar'
WHEN 2 THEN 'Apr-Jun'
WHEN 3 THEN 'Jul-Sep'
WHEN 4 THEN 'Oct-Dec' END AS quarter,
e.employeeID,
Sum(t.value) AS trips
FROM events e
LEFT JOIN types t ON t.typeID = e.eventType
WHERE e.eventType = 'CP' AND Year(eventDate) = #year
GROUP BY Datepart(quarter, eventDate), e.employeeID)
SELECT a.quarter, a.employeeID, nta.DisplayName AS employee, trips,
(SELECT Sum(trips)
FROM cte b
WHERE a.employeeID = b.employeeID
AND a.quarter >= b.quarter) as runningTotal
FROM cte a
LEFT JOIN SBAIntranet.dbo.NTAuth nta ON 'SBA\' + a.employeeID = nta.AccountName
ORDER BY a.employeeID, a.quarterNum

Use Correlated Subquery to find the running total after grouping by quarter. Inner Join is another option to do this
;WITH cte
AS (SELECT Datepart(qq, dates) qq,
employee,
Sum(trips) trips
FROM Yourtable
GROUP BY Datepart(qq, dates),
employee)
SELECT Employee,
CASE qq
WHEN 1 THEN 'Jan-Mar'
WHEN 2 THEN 'Apr-Jun'
WHEN 3 THEN 'Jul-Sep'
WHEN 4 THEN 'Oct-Dec'
END AS quarter,
Trips,
(SELECT Sum(trips)
FROM cte b
WHERE a.Employee = b.Employee
AND a.qq >= b.qq) Running_Total
FROM cte a
SQLFIDDLE DEMO

Related

how to get the list from calendar table and merge to attendance table and leave table

I am using SQL Server 2008 R2. I have a problem in merging tables. We have 500+ employees
I have the following tables:
Calendar table - holds the dates from 1/1/2005 to 12/31/2016
Attendance table - for the attendance of employees
LeaveHistory table - for the leave history
LeaveBreakDown table -for leave break down
Holiday table - for holidays
Our goal, with date range from calendar (11/1/2015 - 11/30/2015)
we want to show the complete days even if the attendance is not equal to the total numbers of days.
Here's my first solution but too slow and without calendar table
FETCH NEXT FROM Employees INTO #EmployeeID,#BranchCode,#IsOfficer, #FirstName, #MiddleName, #LastName, #RankCode;
WHILE ##FETCH_STATUS = 0
BEGIN
WHILE #StartDate <= #EndDate
BEGIN
INSERT INTO #tblData(ActualDate,EmployeeID,BranchCode,IsOfficer, FirstName, MiddleName, LastName, RankCode, LeaveCode, Gender, ShiftCode,ShiftIn,ShiftOut,IsRestDay)
SELECT
#StartDate
, #EmployeeID
, #BranchCode
, #IsOfficer
, #FirstName
, #MiddleName
, #LastName
, #RankCode
, LB.LeaveBreakDownCode
, E.Gender
,ShiftCode = SS.ShiftCode
,ShiftIn = CONVERT(DATETIME, CONVERT(VARCHAR(20), DATEADD(day,0,#StartDate), 101) + ' ' + CONVERT(VARCHAR(20), SS.ShiftIn,108))
,ShiftOut = CASE
WHEN SS.ShiftOut < SS.ShiftIn THEN CONVERT(DATETIME, CONVERT(VARCHAR(20), DATEADD(day,1,#StartDate), 101) + ' ' + CONVERT(VARCHAR(20), SS.ShiftOut ,108))
ELSE CONVERT(DATETIME, CONVERT(VARCHAR(20), DATEADD(day,0,#StartDate), 101) + ' ' + CONVERT(VARCHAR(20), SS.ShiftOut ,108))
END
,IsRestDay = CASE
WHEN SS.Sunday = 1 AND DATEPART(weekday, #StartDate) = 1 THEN 1
WHEN SS.Monday = 1 AND DATEPART(weekday, #StartDate) = 2 THEN 1
WHEN SS.Tuesday = 1 AND DATEPART(weekday, #StartDate) = 3 THEN 1
WHEN SS.Wednesday = 1 AND DATEPART(weekday, #StartDate) = 4 THEN 1
WHEN SS.Thursday = 1 AND DATEPART(weekday, #StartDate) = 5 THEN 1
WHEN SS.Friday = 1 AND DATEPART(weekday, #StartDate) = 6 THEN 1
WHEN SS.Saturday = 1 AND DATEPART(weekday, #StartDate) = 7 THEN 1
ELSE 0
END
FROM Employees E
LEFT JOIN (
SELECT
LB.LeaveBreakDownCode
, LB.EmployeeID
FROM LeaveBreakDown LB
INNER JOIN LeaveHistory LH ON LH.LeaveHistoryCode = LB.LeaveHistoryCode AND LB.DateLeave = #StartDate AND LH.Status IN ('0','1')
WHERE LB.EmployeeID = #EmployeeID
) LB ON LB.EmployeeID = E.EmployeeID
LEFT JOIN ShiftSchedule SS ON SS.EmployeeID = E.EmployeeID AND #StartDate BETWEEN SS.EffectivityDate AND SS.EndDate
WHERE E.Status='1' AND E.ResignedDate IS NULL AND E.EmployeeID = #EmployeeID
SET #StartDate = DATEADD(day,1,#StartDate)
END
SET #StartDate = #InitialDate -- Reinitialize Start Date
FETCH NEXT FROM Employees INTO #EmployeeID, #BranchCode,#IsOfficer,#FirstName, #MiddleName, #LastName, #RankCode;
END;
CLOSE Employees;
DEALLOCATE Employees;
With this solution, if we are going to run the script. it took 3 minutes and sometimes 6minutes.
Might be the structure
Dates ('11/1/2015' - '11/30/2015')
-> Attendance
-> LeaveHistory
All dates from Dates table in date range will be filled with values from different table.
Don't use cursor.In fact it can be done without using RBAR/cursor.
You need to explain little more your table structure.Or start slowly with fewer column and table and ask where you are struck.
Calender Table is not require.What is the purpose of "ShiftSchedule" ?
You can do so by using recursive CTE.
Tell us more then we help you accordingly.
Recursive CTE sample.you can implemented your example here with fewer table and column then gradually increase.
DECLARE #StartDate DATETIME = '11/1/2015'
DECLARE #EndDate DATETIME = '11/30/2015';
WITH CTE
AS (
SELECT #StartDate DT
UNION ALL
SELECT DATEADD(day, 1, DT)
FROM CTE
WHERE DT < #EndDate
)
SELECT *
FROM CTE

Get total working hours from SQL table

I have an attendance SQL table that stores the start and end day's punch of employee. Each punch (punch in and punch out) is in a separate record.
I want to calculate the total working hour of each employee for a requested month.
I tried to make a scalar function that takes two dates and employee ID and return the calculation of the above task, but it calculate only the difference of one date between all dates.
The data is like this:
000781 2015-08-14 08:37:00 AM EMPIN 539309898
000781 2015-08-14 08:09:48 PM EMPOUT 539309886
My code is:
#FromDate NVARCHAR(10)
,#ToDate NVARCHAR(10)
,#EmpID NVARCHAR(6)
CONVERT(NVARCHAR,DATEDIFF(HOUR
,(SELECT Time from PERS_Attendance att where attt.date between convert(date,#fromDate) AND CONVERT(Date,#toDate)
AND (EmpID= #EmpID OR ISNULL(#EmpID, '') = '') AND Funckey = 'EMPIN')
,(SELECT Time from PERS_Attendance att where attt.date between convert(date,#fromDate) AND CONVERT(Date,#toDate)
AND (EmpID= #EmpID OR ISNULL(#EmpID, '') = '') AND Funckey = 'EMPOUT') ))
FROM PERS_Attendance attt
One more approach that I think is simple and efficient.
It doesn't require modern functions like LEAD
it works correctly if the same person goes in and out several times during the same day
it works correctly if the person stays in over the midnight or even for several days in a row
it works correctly if the period when person is "in" overlaps the start OR end date-time.
it does assume that data is correct, i.e. each "in" is matched by "out", except possibly the last one.
Here is an illustration of a time-line. Note that start time happens when a person was "in" and end time also happens when a person was still "in":
All we need to do it calculate a plain sum of time differences between each event (both in and out) and start time, then do the same for end time. If event is in, the added duration should have a positive sign, if event is out, the added duration should have a negative sign. The final result is a difference between sum for end time and sum for start time.
summing for start:
|---| +
|----------| -
|-----------------| +
|--------------------------| -
|-------------------------------| +
--|====|--------|======|------|===|=====|---|==|---|===|====|----|=====|--- time
in out in out in start out in out in end out in out
summing for end:
|---| +
|-------| -
|----------| +
|--------------| -
|------------------------| +
|-------------------------------| -
|--------------------------------------| +
|-----------------------------------------------| -
|----------------------------------------------------| +
I would recommend to calculate durations in minutes and then divide result by 60 to get hours, but it really depends on your requirements. By the way, it is a bad idea to store dates as NVARCHAR.
DECLARE #StartDate datetime = '2015-08-01 00:00:00';
DECLARE #EndDate datetime = '2015-09-01 00:00:00';
DECLARE #EmpID nvarchar(6) = NULL;
WITH
CTE_Start
AS
(
SELECT
EmpID
,SUM(DATEDIFF(minute, (CAST(att.[date] AS datetime) + att.[Time]), #StartDate)
* CASE WHEN Funckey = 'EMPIN' THEN +1 ELSE -1 END) AS SumStart
FROM
PERS_Attendance AS att
WHERE
(EmpID = #EmpID OR #EmpID IS NULL)
AND att.[date] < #StartDate
GROUP BY EmpID
)
,CTE_End
AS
(
SELECT
EmpID
,SUM(DATEDIFF(minute, (CAST(att.[date] AS datetime) + att.[Time]), #StartDate)
* CASE WHEN Funckey = 'EMPIN' THEN +1 ELSE -1 END) AS SumEnd
FROM
PERS_Attendance AS att
WHERE
(EmpID = #EmpID OR #EmpID IS NULL)
AND att.[date] < #EndDate
GROUP BY EmpID
)
SELECT
CTE_End.EmpID
,(SumEnd - ISNULL(SumStart, 0)) / 60.0 AS SumHours
FROM
CTE_End
LEFT JOIN CTE_Start ON CTE_Start.EmpID = CTE_End.EmpID
OPTION(RECOMPILE);
There is LEFT JOIN between sums for end and start times, because there can be EmpID that has no records before the start time.
OPTION(RECOMPILE) is useful when you use Dynamic Search Conditions in T‑SQL. If #EmpID is NULL, you'll get results for all people, if it is not NULL, you'll get result just for one person.
If you need just one number (a grand total) for all people, then wrap the calculation in the last SELECT into SUM(). If you always want a grand total for all people, then remove #EmpID parameter altogether.
It would be a good idea to have an index on (EmpID,date).
My approach would be as follows:
CREATE FUNCTION [dbo].[MonthlyHoursByEmpID]
(
#StartDate Date,
#EndDate Date,
#Employee NVARCHAR(6)
)
RETURNS FLOAT
AS
BEGIN
DECLARE #TotalHours FLOAT
DECLARE #In TABLE ([Date] Date, [Time] Time)
DECLARE #Out TABLE ([Date] Date, [Time] Time)
INSERT INTO #In([Date], [Time])
SELECT [Date], [Time]
FROM PERS_Attendance
WHERE [EmpID] = #Employee AND [Funckey] = 'EMPIN' AND ([Date] > #StartDate AND [Date] < #EndDate)
INSERT INTO #Out([Date], [Time])
SELECT [Date], [Time]
FROM PERS_Attendance
WHERE [EmpID] = #Employee AND [Funckey] = 'EMPOUT' AND ([Date] > #StartDate AND [Date] < #EndDate)
SET #TotalHours = (SELECT SUM(CONVERT([float],datediff(minute,I.[Time], O.[Time]))/(60))
FROM #in I
INNER JOIN #Out O
ON I.[Date] = O.[Date])
RETURN #TotalHours
END
Assuming the entries are properly paired (in -> out -> in -> out -> in etc).
SQL Server 2012 and later:
DECLARE #Year int = 2015
DECLARE #Month int = 8
;WITH
cte AS (
SELECT EmpID,
InDate = LAG([Date], 1) OVER (PARTITION BY EmpID ORDER BY [Date]),
OutDate = [Date],
HoursWorked = DATEDIFF(hour, LAG([Date], 1) OVER (PARTITION BY EmpID ORDER BY [Date]), [Date]),
Funckey
FROM PERS_Attendance
)
SELECT EmpID,
TotalHours = SUM(HoursWorked)
FROM cte
WHERE Funckey = 'EMPOUT'
AND YEAR(InDate) = #Year
AND MONTH(InDate) = #Month
GROUP BY EmpID
SQL Server 2005 and later:
;WITH
cte1 AS (
SELECT *,
rn = ROW_NUMBER() OVER (PARTITION BY EmpID ORDER BY [Date])
FROM PERS_Attendance
),
cte2 AS (
SELECT a.EmpID, b.[Date] As InDate, a.[Date] AS OutDate,
HoursWorked = DATEDIFF(hour, b.[Date], a.[Date])
FROM cte1 a
LEFT JOIN cte1 b ON a.EmpID = b.EmpID and a.rn = b.rn + 1
WHERE a.Funckey = 'EMPOUT'
)
SELECT EmpID,
TotalHours = SUM(HoursWorked)
FROM cte2
WHERE YEAR(InDate) = #Year
AND MONTH(InDate) = #Month
GROUP BY EmpID

get best sales rep weekly SQL

I need a bit of help with a SQL Server issue.
I have 2 tables:
complete_sales_raw
(
Id int Identity(1,1) PK,
RepId int FK in sale_reps,
Revenue decimal(15,2),
Sale_date datetime2(7)
)
and
sale_reps
(
Id int Identity(1,1) PK,
RepName nvarchar(50)
)
What I need to do is get best sales rep based on the total revenue for each week, starting with 2014-06-01 and ending at current date.
Each week has 7 days and the first day is 2014-06-01.
So far I got to here:
SELECT TOP(1)
sr.RepName as RepName,
SUM(csr.Revenue) as Revenue
INTO #tmp1
FROM complete_sales_raw csr
JOIN sale_reps sr on csr.RepId = sr.Id
WHERE DATEDIFF( d,'2014-06-01', Sale_date ) BETWEEN 0 and 6
GROUP BY sr.RepName
ORDER BY 2 desc
But this only returns the best sale rep for the first week and I need it for each week.
All help is appreciated.
ok so, I created a week table like so
IF ( OBJECT_ID('dbo.tmp4') IS NOT NULL )
DROP TABLE dbo.tmp4
GO
Create Table tmp4(
StartDate datetime,Enddate datetime,WeekNo varchar(20)
)
DECLARE
#start_date DATETIME,
#end_date DATETIME,
#start_date1 DATETIME,
#end_date1 DATETIME
DECLARE #Table table(StartDate datetime,Enddate datetime,WeekNo varchar(20))
Declare #WeekDt as varchar(10)
SET #start_date = '2014-06-01'
SET #end_date = '2015-01-03'
Set #WeekDt = DATEPART(WEEK,#start_date)
SET #start_date1 = #start_date
While #start_date<=#end_date
Begin
--Select #start_date,#start_date+1
IF #WeekDt<>DATEPART(WEEK,#start_date)
BEGIN
Set #WeekDt = DATEPART(WEEK,#start_date)
SET #end_date1=#start_date-1
INSERT INTO tmp4 Values(#start_date1,#end_date1,DATEPART(WEEK,#start_date1))
SET #start_date1 = #start_date
END
set #start_date = #start_date+1
END
GO
and then I used Gordon's answer and made this:
SELECT t.StartDate as StartDate, sr.RepName as RepName, SUM(csr.Revenue) as Revenue,
RANK() OVER (PARTITION BY (t.StartDate) ORDER BY SUM(csr.Revenue) desc) as seqnum into tmp1
FROM tmp4 t,
complete_sales_raw csr
JOIN sale_reps sr on csr.RepId = sr.Id
WHERE DATEDIFF( d,t.StartDate, MAS_PostDate ) BETWEEN 0 and 6
GROUP BY sr.RepName, t.StartDate
SELECT * FROM tmp1
WHERE seqnum = 1
ORDER BY StartDate
which returns the best sales_rep for each week
You can do an aggregation to get the total sales by week. This requires some manipulation of the dates to calculate the number of weeks -- basically dividing the days by 7.
Then, use rank() (or row_number() if you only want one when there are ties) to get the top value:
SELECT s.*
FROM (SELECT tsr.RepName as RepName,
(DATEDIFF(day, '2014-06-01', MAS_PostDate ) - 1) / 7 as weeknum,
SUM(csr.Revenue) as Revenue,
RANK() OVER (PARTITION BY (DATEDIFF(day, '2014-06-01', MAS_PostDate ) - 1) / 7 ORDER BY SUM(csr.Revenue)) as seqnum
FROM complete_sales_raw csr JOIN
sale_reps sr
on csr.RepId = sr.Id
WHERE DATEDIFF(day, '2014-06-01', MAS_PostDate ) BETWEEN 0 and 6
GROUP BY sr.RepName, (DATEDIFF(day, '2014-06-01', MAS_PostDate ) - 1) / 7
) s
WHERE seqnum = 1;

sql query get all data from Jan to current month even if there are no records

I'm not good with sql so any help world be great
I have a SQL query that gets records who has register from Jan to current month
My code example
SELECT DatePart(YEAR, p.createStamp) as TheYear, DatePart(MONTH, p.createStamp) as TheMonth , COUNT(p.pId) AS TOTALCOUNT
FROM profile p with(nolock)
where DatePart(YEAR, p.createStamp) = DATEPART(YEAR, GETDATE())
GROUP BY YEAR(p.createStamp), MONTH(p.createStamp)
ORDER BY YEAR(p.createStamp), MONTH(p.createStamp)
how ever the query will bring back
February = 2, March = 3, April = 4 and May = 5
i would like to have it bring back Jan = 1 with a total count of 0 and June = 6 with the total count of 0 as well any ideas how to do this?
thank you.
Here is a loop to create the month/year combinations and use this as the base of your query:
declare #startDate as datetime
set #startDate = '1/1/13'
declare #currentDate as datetime
set #currentDate = '6/6/13'
select
month(#currentDate) as monthOfDate
,year(#currentDate) as yearOfDate
into #allDates
where 1=0
while (#startDate <= #currentDate)
begin
insert into #allDates values (month(#startDate),year(#startDate))
set #startDate = dateadd(m,1,#startDate)
end
select
_monthYear.yearofDate
,_monthYear.monthOfDate
, COUNT(p.pId) as total
from #allDates _monthYear
left join profile p with(nolock)
on month(p.createStamp) = _monthYear.monthOfDate
and year(p.createStamp) = _monthYear.yearOfDate
group by
_monthYear.yearofDate
,_monthYear.montOfDate
drop table #allDates
You can't select what's not there, so I suggest making a lookup table:
CREATE TABLE #Months (Year_ INT, Month_ INT)
GO
SET NOCOUNT ON
DECLARE #intFlag INT
SET #intFlag = 1
WHILE (#intFlag <=20)
BEGIN
--Do Stuff
INSERT INTO #Months
SELECT YEAR(DATEADD(MONTH,#intflag,'20121201')),MONTH(DATEADD(MONTH,#intflag,'20121201'))
SET #intFlag = #intFlag + 1
END
GO
You can change the '20' to be any number of months you want, and change '20121201' in both spots to the month before the month you want to start your lookup.
Then Joining to that table, I believe the following will work:
SELECT m.Year_ as TheYear, m.Month_ as TheMonth , ISNULL(COUNT(p.pId),0) AS TOTALCOUNT
FROM profile p
RIGHT JOIN #Months m
ON DatePart(YEAR, p.createStamp) = m.Year_
AND DatePart(MONTH, p.createStamp) = m.Month_
where DatePart(YEAR, p.createStamp) = DATEPART(YEAR, GETDATE())
GROUP BY m.Year_, m.Month_
ORDER BY m.Year_, m.Month_

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;