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

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

Related

SQL Server - find people between date range excluding the year

I have a table EMPLOYEE containing Employee info as mentioned below:
ID NAME DOB
1 ABC 1974-01-01
2 BDS 1984-12-31
3 QWE 1959-05-27
and so on
I want to list all the employees whose DOB is in the given range.
select * from EMPLOYEE where DOB BETWEEN '1970-01-01' AND '1980-02-27'
I have a filter condition to 'include year in date comparison', which when selected 'NO', the employee DOB day and DOB month
only should be considered for comparison. and not the year.
For example: If I enter the date range as '1970-01-01' and '1980-02-27' and the filter is selected as 'NO' then it should search for only those employees
whose DOB is greater than equal to JAN-01 and less than equal to FEB-27.
When selected 'Yes', it is simply date range as mentioned in above query.
Here is what I have tried so far:
select * from EMPLOYEE where DOB BETWEEN '1970-01-01' AND '1980-02-27'
AND MONTH(DOB) >= CASE WHEN 'NO'='NO' THEN MONTH('1970-01-01')
ELSE MONTH(DOB) END
AND MONTH(DOB) <= CASE WHEN 'NO'='NO' THEN MONTH('1980-02-27')
ELSE MONTH(DOB) END
AND DAY(DOB) >= CASE WHEN 'NO'='NO' THEN DAY('1970-01-01')
ELSE DAY(DOB) END
AND DAY(DOB) <= CASE WHEN 'NO'='NO' THEN DAY('1980-02-27')
ELSE DAY(DOB) END
It works when I pass the date range where the FROM date has smaller number month than the TO date month.
For example:
It doesnt work when I pass the date range as '1970-12-01' to '1980-01-31'.
It should list the employees whose DOB is in DEC and JAN month.
Need help please.
Sample Data;
DECLARE #Date_From date; SET #Date_From = '1970-12-01'
DECLARE #Date_To date; SET #Date_To = '1974-01-31'
DECLARE #IncludeYear bit; SET #IncludeYear = 0
CREATE TABLE #Employee (ID int, Name varchar(10), DOB date)
INSERT INTO #Employee (ID, Name, DOB)
VALUES
(1,'ABC','1974-01-01')
,(2,'BDS','1984-12-31')
,(3,'QWE','1959-05-27')
This is the query I've made. Tried to cover for every eventuality.
SELECT
e.ID
,e.Name
,e.DOB
FROM #Employee e
WHERE
(
#IncludeYear = 1
AND
DOB BETWEEN #Date_From AND #Date_To
)
OR
(
#IncludeYear = 0
AND
(
(
DATEPART(DAYOFYEAR, #Date_From) = DATEPART(DAYOFYEAR, #Date_To)
AND
DATEPART(DAYOFYEAR, DOB) = DATEPART(DAYOFYEAR, #Date_To)
)
OR
(
DATEPART(DAYOFYEAR, #Date_From) < DATEPART(DAYOFYEAR, #Date_To)
AND
DATEPART(DAYOFYEAR, DOB) BETWEEN DATEPART(DAYOFYEAR, #Date_From) AND DATEPART(DAYOFYEAR, #Date_To)
)
OR
(
DATEPART(DAYOFYEAR, #Date_From) > DATEPART(DAYOFYEAR, #Date_To)
AND
(
DATEPART(DAYOFYEAR, DOB) > DATEPART(DAYOFYEAR, #Date_From)
OR
DATEPART(DAYOFYEAR, DOB) < DATEPART(DAYOFYEAR, #Date_To)
)
)
)
)
First part of the where clause checks if the #date_from and #date_to
are the same date, then only returns these.
Second part checks if the day of year for #date_from comes before
#date_to. If it does then return everything between these days of the
year.
Final part checks if the day of year for #date_to comes before
#date_from then it gets everything with the day of year after
#date_from or before #date_to
The results for this one come out as this;
ID Name DOB
1 ABC 1974-01-01
2 BDS 1984-12-31
DECLARE #includeYear bit = 0, -- if 0 - we don't include year, 1 - include
#dateFrom date ='1970-12-01',
#dateTo date ='1980-05-30'
IF #includeYear = 1
BEGIN
SELECT e.*
FROM EMPLOYEE e
INNER JOIN (SELECT #dateFrom as dF, #dateTo as dT) d
ON e.DOB BETWEEN dF AND dT
END
ELSE
BEGIN
SELECT e.*
FROM EMPLOYEE e
INNER JOIN (SELECT #dateFrom as dF, #dateTo as dT) d
ON e.DOB BETWEEN
(CASE WHEN MONTH(dF) > MONTH(dT)
THEN DATEADD(year,YEAR(e.DOB)-YEAR(d.dF)-1,dF)
ELSE DATEADD(year,YEAR(e.DOB)-YEAR(d.dF),dF) END)
AND DATEADD(year,YEAR(e.DOB)-YEAR(d.dT),dT)
OR e.DOB BETWEEN DATEADD(year,YEAR(e.DOB)-YEAR(d.dF),dF) AND
(CASE WHEN MONTH(dF) > MONTH(dT)
THEN DATEADD(year,YEAR(e.DOB)-YEAR(d.dT)+1,dT)
ELSE DATEADD(year,YEAR(e.DOB)-YEAR(d.dT),dT) END)
END
For
dateFrom dateTo
1970-12-01 1980-01-30
Output:
ID NAME DOB
1 ABC 1974-01-01
2 BDS 1984-12-31
For
dateFrom dateTo
1970-05-01 1980-06-30
Output:
ID NAME DOB
3 QWE 1959-05-27
For
dateFrom dateTo
1970-05-01 1980-05-30
Output:
ID NAME DOB
3 QWE 1959-05-27
etc
Try the function DATEPART(dayofyear, date)
In case the day-of-year of the first date is smaller than the day-of-year of the second date, then the day-of-year of the DOB should be between the specified days-of-year.
Otherwise, the day-of-year of the DOB should be either smaller than the day-of-year of the second date or greater than the day-of-year of the first date.
I hope I expressed myself well.
Rather than working case-by-case and disassembling and reassembling parts of dates, I've tried to make life easier:
declare #t table (ID int not null, Name varchar(17) not null, DOB date not null)
insert into #t(ID,NAME,DOB) values
(1,'ABC','19740101'),
(2,'BDS','19841231'),
(3,'QWE','19590527')
declare #Start date
declare #End date
declare #IncludeYear bit
select #Start='19701201',#End='19800131',#IncludeYear=0
;With Normalized as (
select
ID,
Name,
CASE WHEN #IncludeYear=1 THEN DOB
ELSE DATEADD(year,DATEDIFF(year,DOB,'20000101'),DOB)
END as DOB,
CASE WHEN #IncludeYear=1 THEN #Start
ELSE DATEADD(year,DATEDIFF(year,#Start,'20000101'),#Start)
END as StartRange,
CASE WHEN #IncludeYear=1 THEN #End
ELSE DATEADD(year,DATEDIFF(year,#End,'20000101'),#End)
END as EndRange
from
#t
)
select * from Normalized
where
DOB between StartRange and EndRange or
(
#IncludeYear=0 and StartRange>EndRange and
(
DOB < EndRange or DOB > StartRange
)
)
We create the Normalized CTE that, does nothing if #IncludeYear is 1 or, if it is zero, it resets all dates so that they occur in 2000 (arbitrarily selected).
We then do the straightforward query based on the CTE. The one circumstance where it won't correctly match is when you have your range defined over a year-end transition and we don't care about years - which we can check specifically for and cater for within the end of the WHERE clause.
Results:
ID Name DOB StartRange EndRange
----------- ----------------- ---------- ---------- ----------
1 ABC 2000-01-01 2000-12-01 2000-01-31
2 BDS 2000-12-31 2000-12-01 2000-01-31
Results with #Start='19700101',#End='19800227',#IncludeYear=1:
ID Name DOB StartRange EndRange
----------- ----------------- ---------- ---------- ----------
1 ABC 1974-01-01 1970-01-01 1980-02-27
Here is another solution
DECLARE #employee table(EmployeeID varchar(10), DOB date);
INSERT INTO #employee(EmployeeID, DOB)
VALUES('0001', '01-Dec-1990'),
('0002', '06-Jan-1993'),
('0003', '04-Mar-1987'),
('0004', '12-Feb-1996');
DECLARE #dateStart date = '01-Jan-1990';
DECLARE #dateEnd date = '27-Feb-1997';
DECLARE #includeYear bit = 0;
If #includeYear = 0
Begin
SET #dateStart = CAST(('2000-' + CAST(MONTH(#dateStart) AS varchar(10)) + '-' + CAST(DAY(#dateStart) as varchar(10))) AS date);
SET #dateEnd = CAST(('2000-' + CAST(MONTH(#dateEnd) AS varchar(10)) + '-' + CAST(DAY(#dateEnd) as varchar(10))) AS date);
End
If #includeYear = 1
Begin
SELECT *
FROM #employee
WHERE DOB BETWEEN #dateStart AND #dateEnd
End
Else
Begin
SELECT *
FROM #employee
WHERE CAST(('2000-' + CAST(MONTH(DOB) AS varchar(10)) + '-' + CAST(DAY(DOB) as varchar(10))) AS date) BETWEEN #dateStart AND #dateEnd
End
As you can see we are just making the year part of the query a constant if you don't want to include year. This query seems to be a bit slower but if you add another computed column in your table, where you save the date with a constant year then you just need to put where criteria on that particular column.
Try this.
declare #flag varchar(3) ='NO';
declare #sd date ='1980-02-27';
declare #ed date ='1970-01-01';
select tt.*
from (select sd = month(#sd)*100 + day(#sd),
ed = month(#ed)*100 + day(#ed)
) prm
cross join
-- test data, place real table here
(
values
(1,'ABC', cast('1974-01-05' as date)),
(2,'BDS','1984-12-31'),
(3,'QWE','1959-05-27')
) tt(ID,NAME, DOB)
cross apply (select md = month(DOB)*100 + day(DOB)) tx
where #flag ='YES' and DOB between #sd and #ed
or #flag ='NO' and (prm.sd<=prm.ed and tx.md between prm.sd and prm.ed
or prm.sd>prm.ed and (tx.md <= prm.ed or tx.md >= prm.sd));
Always use a SARG in your predicate. Any answer that fails to do this only results in lost performance and your DBA getting upset.
What you want is two different queries to run depending on the answer to the Procedure. Since this is a proc that likely runs a lot, store the answers into a variable in your PROC and run any adjusting code from there. Not only will this make your code more robust by flushing out errors beforehand, but SQL Server has a better chance of guessing the variables to use with your indexes.
The following PROC will work. Feel free to use part or all of it:
CREATE TABLE #table_E (ID INT, Name VARCHAR(3), DOB DATE)
INSERT INTO #table_E (ID , Name, DOB)
VALUES (1, 'ABC', '1997-01-02' )
, (2, 'BDS', '1984-12-31' )
, (3, 'QWE', '1993-03-22' )
GO
CREATE PROC USP_EmpCompare (#Date_1 DATE, #Date_2 DATE, #Compare_Year VARCHAR(3))
AS
BEGIN
DECLARE #MONTH_1 INT
, #Month_2 INT
, #Day_1 INT
, #Day_2 INT
, #Date1 DATE
, #Date2 DATE
SET #Date1 = CASE WHEN #Date_1 > #Date_2 THEN #Date_2 ELSE #Date_1 END
SET #Date2 = CASE WHEN #Date_1 > #Date_2 THEN #Date_1 ELSE #Date_2 END
SET #Month_1 = CASE WHEN DATEPART(MM, #Date2) > DATEPART(MM, #Date1) THEN DATEPART(MM, #Date1) ELSE DATEPART(MM, #Date2) END
SET #Month_2 = CASE WHEN DATEPART(MM, #Date1) > DATEPART(MM, #Date2) THEN DATEPART(MM, #Date1) ELSE DATEPART(MM, #Date2) END
SET #Day_1 = CASE WHEN DATEPART(DD, #Date2) > DATEPART(DD, #Date1) THEN DATEPART(DD, #Date1) ELSE DATEPART(DD, #Date2) END
SET #Day_2 = CASE WHEN DATEPART(DD, #Date1) > DATEPART(DD, #Date2) THEN DATEPART(DD, #Date1) ELSE DATEPART(DD, #Date2) END
-- SELECT #Date1, #Date2
IF #Compare_Year = 'no'
BEGIN
;WITH C AS (SELECT ID
, Name
, DATEPART(DD, DOB) AS Day
, DATEPART(MM, DOB) AS Month
FROM #table_E)
SELECT ID, Name, #Date1, #Date2
FROM C
WHERE C.Month >= #MONTH_1
AND C.Month <= #Month_2
AND C.Day >= #Day_1
AND C.DAy <= #Day_2
END
IF #Compare_Year = 'yes'
BEGIN
SELECT ID, Name, DOB
FROM #table_E
WHERE DOB <= #Date2
AND DOB >= #Date1
END
ELSE
PRINT WHAT! FOLLOW THE RULES YOU FOOL!!!
END
jk. that last part about fools is probably not included in your final draft. ;)
I would make it very simple not so much complex.
SELECT * FROM EMPLOYEE WHERE DOB >= '1970-01-01' AND DOB <= '1980-02-27'
This filters all dates between these two dates

Selecting certain days in a date range

I have the query below which gives me the number of days that a student was absent. DATEDIFF and DATEPART calculate the weekdays and holidays should not be counted as an absent day. The absent days stored in studentTable in two fields which are fromDate and toDate. So the absent days are in a date range. If a student is one day absent, it is recorded 11/23/2015, 11/23/2015. If a student is two days absent ,then 11/23/2015, 11/24/2015.
DECLARE #startDate DATE SET #startDate = '20151121'
DECLARE #endDate DATE SET #endDate = '20151123'
SELECT
a.studentName
,SUM(DATEDIFF(dd, fromDate, toDate)
- (DATEDIFF(wk, fromDate, toDate) * 2)
-CASE WHEN DATEPART(dw, fromDate) = 1 THEN 1 ELSE 0 END
+CASE WHEN DATEPART(dw, toDate) = 1 THEN 1 ELSE 0 END + 1 )- COUNT(h.holiday)
AS totalAbsentDay
FROM studentTable a
LEFT OUTER JOIN holiday h
ON h.holiday < a.toDate and h.holiday > a.fromDate
WHERE a.fromDate = #startDate AND a.toDate = #endDate
GROUP BY a.studentName
The problem here is that when I try to declare a start and an end date, it does not give me the correct absent days.
For example, if a student is absent between 11/23/2015 and 11/26/2015 which is 4 days absent , and I declare start date 11/22/2015 and end date 11/27/2015, the query doesn’t give me the result of 3.
This query below will work for given database scheme, may not the best solution because use three level of queries
DECLARE #startDate DATE SET #startDate = '2016-02-05'
DECLARE #endDate DATE SET #endDate = '2016-02-20'
SELECT
studentName,
SUM(AbsentDay) totalAbsentDay
FROM
(
SELECT
a.studentName
,DATEDIFF(dd, fromDate, toDate)
- (DATEDIFF(wk, fromDate, toDate) * 2)
-CASE WHEN DATEPART(dw, fromDate) = 1 THEN 1 ELSE 0 END
+CASE WHEN DATEPART(dw, toDate) = 1 THEN 1 ELSE 0 END + 1 - COUNT(h.holiday)
AS AbsentDay
FROM (
SELECT
studentName,-- Name,
CASE WHEN fromDate<#startDate THEN #startDate ELSE fromDate END fromDate,
CASE WHEN toDate>#endDate THEN #endDate ELSE toDate END toDate
FROM
StudentTable S
WHERE
S.toDate >= #startDate AND s.fromDate <= #endDate
) a
LEFT OUTER JOIN holiday h
ON h.holiday < a.toDate and h.holiday > a.fromDate
GROUP BY studentName, fromDate, toDate
) B
GROUP BY studentName
For easier query and faster execution please consider to redesign studentTable to something like idStudent, AbsentDate..just a suggestion..

SQL Count with zero values

I want to create a graph for my dataset for the last 24 hours.
I found a solution that works but this is pretty bad since the table I am outer joining cotains every single row in the DB since I am using the (now deprecated) "all" parameter in the group by.
Here is the solution that currently kind of works.
First I declare the date intervals that is 24 hours back in time from now. I declare it twice so I can use it later in the procedure aswell.
Declare #StartDate datetime = dateadd(hour, -24, getdate())
Declare #StartDateProc datetime = dateadd(hour, -24, getdate())
Declare #EndDate datetime = getdate()
I populate the dates into a temp table including a special formated datetsring.
create table #tempTable
(
Date datetime,
DateString varchar(11)
)
while #StartDate <= #EndDate
begin
insert into #tempTable (Date, DateString)
values (#StartDate, convert(varchar(8), #StartDate, 5) + '-' + convert(varchar(2), #StartDate, 108));
SET #StartDate = dateadd(hour,1, #StartDate);
end
This gives me data that looks like this:
Date DateString
---------------------------------------------
2015-12-09 13:59:01.970 09-12-15-13
2015-12-09 14:59:01.970 09-12-15-14
2015-12-09 15:59:01.970 09-12-15-15
2015-12-09 16:59:01.970 09-12-15-16
So what I want is to join my dataset on the matching date string and show the date even if the matching rows is zero.
Here is the rest of the query
select
Date = c.Date,
Amount = sum(c.Amount)
from
DbTable a
outer apply
(select
Date = b.DateString,
Amount = count(*)
from
#tempTable b
where
convert(varchar(8), a.DateColumn, 5) + '-' + convert(varchar(2), a.DateColumn, 108) = b.DateString
group by all
b.DateString) c
where
a.SomeParameter = 'test' and
a.DateColumn >= #StartDateProc and
a.DateColumn <= #EndDate
group by
c.Date
drop table #tempTable
Test to show actual data:
Declare #StartDate datetime = dateadd(hour, -24, getdate())
Declare #EndDate datetime = getdate()
select
dateString = convert(varchar(8),a.DateColumn,5) + '-' + convert(varchar(2),a.DateColumn, 108),
Amount = COUNT(*)
from
DbTable a
where
a.someParameter = 'test' and
a.DateColumn>= dateadd(hour, -24, getdate()) and
a.DateColumn<= getdate()
group by
convert(varchar(8),a.DateColumn,5) + '-' + convert(varchar(2),a.DateColumn, 108)
First output rows:
dateString Amount
09-12-15-14 1
09-12-15-15 1
09-12-15-16 1
09-12-15-17 3
09-12-15-18 1
09-12-15-22 3
09-12-15-23 2
As you can see here there is no data for the times from 19.00 to 21.00. This is how I want the data to be displayed:
dateString Amount
09-12-15-14 1
09-12-15-15 1
09-12-15-16 1
09-12-15-17 3
09-12-15-18 1
09-12-15-19 0
09-12-15-20 0
09-12-15-21 0
09-12-15-22 3
09-12-15-23 2
Normally, this would be approached with left join rather than outer apply. The logic is simple: keep all rows in the first table along with any matching information from the second. This means put the dates table first:
select tt.DateString, count(t.DateColumn) as Amount
from #tempTable tt left join
DbTable t
on convert(varchar(8), t.DateColumn, 5) + '-' + convert(varchar(2), t.DateColumn, 108) = tt.DateString and
t.SomeParameter = 'test'
where tt.Date >= #StartDateProc and
tt.Date <= #EndDate
group by tt.DateString;
In addition, your comparison for the dates seems overly complex, but if it works for you, it works.
The best bet here would be to use DATETIME type itself and not to lose the opportunity to use indexes:
Declare #d datetime = GETDATE()
;WITH cte1 AS(SELECT TOP 25 -1 + ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) h
FROM master..spt_values),
cte2 AS(SELECT DATEADD(hh, -h, #d) AS startdate,
DATEADD(hh, -h + 1, #d) AS enddate
FROM cte1)
SELECT c.startdate, c.enddate, count(*) as amount
FROM cte2 c
LEFT JOIN DbTable a ON a.DateColumn >= c.startdate AND
a.DateColumn < c.enddate AND
a.SomeParameter = 'test'
GROUP BY c.startdate, c.enddate

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

running total for each employee

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