get best sales rep weekly SQL - 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;

Related

Selecting count of consecutives dates before and after a specified date based on start/end

I'm trying to determine the number of records with consecutive dates (previous record ends on the same date as the start date of the next record) before and after a specified date, and ignore any consecutive records as soon as there is a break in the chain.
If I have the following data:
-- declare vars
DECLARE #dateToCheck date = '2020-09-20'
DECLARE #numRecsBefore int = 0
DECLARE #numRecsAfter int = 0
DECLARE #tempID int
-- temp table
CREATE TABLE #dates
(
[idx] INT IDENTITY(1,1),
[startDate] DATETIME ,
[endDate] DATETIME,
[prevEndDate] DATETIME
)
-- insert temp table
INSERT INTO #dates
( [startDate], [endDate] )
VALUES ( '2020-09-01', '2020-09-04' ),
( '2020-09-04', '2020-09-10' ),
( '2020-09-10', '2020-09-16' ),
( '2020-09-17', '2020-09-19' ),
( '2020-09-19', '2020-09-20' ),
--
( '2020-09-20', '2020-09-23' ),
( '2020-09-25', '2020-09-26' ),
( '2020-09-27', '2020-09-28' ),
( '2020-09-28', '2020-09-30' ),
( '2020-10-01', '2020-09-05' )
-- update with previous records endDate
DECLARE #maxRows int = (SELECT MAX(idx) FROM #dates)
DECLARE #intCount int = 0
WHILE #intCount <= #maxRows
BEGIN
UPDATE #dates SET prevEndDate = (SELECT endDate FROM #dates WHERE idx = (#intCount - 1) ) WHERE idx=#intCount
SET #intCount = #intCount + 1
END
-- clear any breaks in the chain?
-- number of consecutive records before this date
SET #numRecsBefore = (SELECT COUNT(idx) FROM #dates WHERE startDate = prevEndDate AND endDate <= #dateToCheck)
-- number of consecutive records after this date
SET #numRecsAfter = (SELECT COUNT(idx) FROM #dates WHERE startDate = prevEndDate AND endDate >= #dateToCheck)
-- return & clean up
SELECT * FROM #dates
SELECT #numRecsBefore AS numBefore, #numRecsAfter AS numAfter
DROP TABLE #dates
With the specified date being '2020-09-20, I would expect #numRecsBefore = 2 and #numRecsAfter = 1. That is not what I am getting, as its counting all the consecutive records.
There has to be a better way to do this. I know the loop isn't optimal, but I couldn't get LAG() or LEAD() to work. I've spend all morning trying different methods and searching, but everything I find doesn't deal with two dates, or breaks in the chain.
This reads like a gaps-and-island problem. Islands represents rows whose date ranges are adjacent, and you want to count how many records preceed of follow a current date in the same island.
You could do:
select
max(case when #dateToCheck > startdate and #dateToCheck <= enddate then numRecsBefore end) as numRecsBefore,
max(case when #dateToCheck >= startdate and #dateToCheck < enddate then numRecsAfter end) as numRecsAfter
from (
select d.*,
count(*) over(partition by grp order by startdate) as numRecsBefore,
count(*) over(partition by grp order by startdate desc) as numRecsAfter
from (
select d.*,
sum(case when startdate = lag_enddate then 0 else 1 end) over(order by startdate) as grp
from (
select d.*,
lag(enddate) over(order by startdate) as lag_enddate
from #dates d
) d
) d
) d
This uses lag() and a cumulative sum() to define the islands. The a window count gives the number and preceding and following records on the same island. The final step is conditional aggrgation; extra care needs to be taken on the inequalities to take in account various possibilites (typically, the date you search for might not always match a range bound).
Demo on DB Fiddle
I think this is what you are after, however, this does not give the results in your query; I suspect that is because they aren't the expected results? One of the conditional aggregated may also want to be a >= or <=, but I don't know which:
WITH CTE AS(
SELECT startDate,
endDate,
CASE startDate WHEN LAG(endDate) OVER (ORDER BY startDate ASC) THEN 1 END AS IsSame
FROM #dates d)
SELECT COUNT(CASE WHEN startDate < #dateToCheck THEN IsSame END) AS numBefore,
COUNT(CASE WHEN startDate > #dateToCheck THEN IsSame END) AS numAfter
FROM CTE;

How to calculate MTD given daily account balance in SQL Server?

I have a table with columns [accountid], [DateEnding], and [AccountBalance].
I need to calculate MTD using the balance of the current month and subtracting the account balance from the last day of the previous month for each accountid.
So far I have this:
SELECT [accountid]
,[DateEnding]
,[AccountBalance]
,[AccountBalance MTD Last] = AccountBalance - FIRST_VALUE(AccountBalance) OVER (PARTITION BY accountid, YEAR(DATEADD(mm,-1,[DateEnding])), MONTH(DATEADD(mm,-1,[DateEnding])) ORDER BY [DateEnding] DESC)
FROM [test]
ORDER BY accountid, DateEnding;
Here, for each distinct account, we find the latest record available according to DateEnding
we then find the last day of the last month by taking a number of days away equal to the current day number. e.g 23rd April 2019 we subtract 23 days to get 1st March 2019
we can then find the balance on that day.
Then put the calculation together in the SELECT
SELECT Q1.accountid,
Q2.DateEnding ,
Q3.EOMbalance,
Q2.LatestBalance,
Q2.LatestBalance - Q3.EOMbalance EOM
FROM (
SELECT Distinct t1.accountid FROM test t1
) Q1
CROSS APPLY (
SELECT TOP 1 t2.AccountBalance LatestBalance, t2.[DateEnding]
FROM test t2
WHERE t2.[accountid] = Q1.accountid
ORDER BY t2.[DateEnding] DESC
) Q2
CROSS APPLY (
SELECT Top 1 t3.AccountBalance EOMbalance
FROM test t3
WHERE t3.[accountid] = Q1.accountid
AND t3.[DateEnding]
= dateadd(day,0 - DAY(q2.dateending), q2.dateending)
ORDER BY t3.[DateEnding] DESC
) Q3
The first answer seems a little complicated for this problem (Cross Apply isn't necessary here).
The following may be easier for you:
I first look at the current day's account balances in subquery 'a'.
Then I look at the account balances from the last day of last month's data, in subquery 'b'.
Then it's just a matter of subtracting the two to show the MTD delta:
select a.accountid,
a.DateEnding,
a.AccountBalance as [Current AccountBalance],
b.AccountBalance as [EOM prior AccountBalance], --added for clarity
a.AccountBalance-b.AccountBalance as [AccountBalance MTD Last]
from
(select accountid, DateEnding, AccountBalance
from #test
where DateEnding = cast(getdate() as date)
/* getdate() returns today's date, so this query will also be with respect to today */
) a
left join
(select *
from #test
where DateEnding = DATEADD(MONTH, DATEDIFF(MONTH, -1, GETDATE())-1, -1)
/*this returns the last day of last month, always*/
) b
on a.accountid = b.accountid
Here is the SQL that makes this sample data and #test table. Simply execute it to have your own '#test' table to run against:
/*drop table #test
drop table #dates */
create table #test ([accountid] varchar(255),[DateEnding] date, [AccountBalance] decimal(16,2))
create table #dates (rnk int,dt date)
insert into #dates (dt)
values (cast('20180101' as date))
DECLARE
#basedate DATE,
#d INT
SELECT
#basedate = '20180101',
#d = 1
WHILE #d < (select datediff(day,cast('20180101' as date),getdate())+2) --select datediff(day,getdate(),cast('20180101' as datetime))
BEGIN
INSERT INTO #dates (dt)
values (DATEADD(day, 1, (select max(dt) from #dates)))
set #d = #d+1
END
update a
set a.rnk = b.rnk
from #dates a
left join (select rank() over (order by dt) rnk,dt from #dates) b on a.dt = b.dt
declare #a int
set #a = 1
declare #i int
set #i = 1
while #a <20
begin
while #i < (select max(rnk) from #dates)
begin
insert into #test
values (#a,(select dt from #dates where rnk = #i),cast(rand()*1000.0+#i as decimal(16,2)))
set #i=#i+1
end
set #a=#a+1
set #i = 1
end

How to list all months in specific period of time which doesn't have any order at that month

I want to know how to list all months in specific period of time which doesn't have any order. If you can help me.
I have Order Table has OrderDate column
I just make this:
select distinct month(Order.OrderDate) from Order where year(Order.OrderDate) = 1997
the result will show me the months that have order in specific year only
what should i do to complete this query
You need to retrieve the months in which no orders are placed for that we can use below query
;WITH months(MonthNumber) AS
(
SELECT 1
UNION ALL
SELECT MonthNumber+1
FROM months
WHERE MonthNumber < 12
)
SELECT DATENAME( month , DATEADD( month ,MonthNumber , 0 ) )
FROM months
EXCEPT
SELECT DISTINCT month([Order].OrderDate)
FROM [Order]
WHERE YEAR([Order].OrderDate) = 1997
You can try using left join like below
DEMO
select * from
(
VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),(12)
) AS M(val)
left join t1 on month(OrderDate)=val
and year(orderdate)=1997
where month(OrderDate) is null
The problem here is that if we we have a date range, then we may go beyond single year, for example 01-Jul-2017 to 30-Jun-2018 has 2 years, therefore creating a month range may NOT work in this scenario. The possible solution is to have a list of all the months in range along with the year, so that when we search an order, we'll search by the month and year both.
-- this is test order table, just to test the output
declare #order table(OrderDate date);
insert into #order(OrderDate) values('2018-01-01')
declare #dateRange table(d datetime not null primary key);
-- date range input parameter
declare #startDate date = '2017-06-01';
declare #endDate date = '2018-06-30';
-- modifying date range so that we go from start
-- of the month to the end of the month in the range
set #startDate = cast(year(#startDate) as varchar(100)) + '-' + cast(month(#startDate) as varchar(100)) + '-1';
set #endDate = dateadd(day, -1, dateadd(month, 1, cast(year(#endDate) as varchar(100)) + '-' + cast(month(#endDate) as varchar(100)) + '-1'));
-- creating dates for every month
declare #d date = #startDate;
while(#d <= #endDate)
begin
insert into #dateRange(d) values(#d);
set #d = dateadd(month, 1, #d);
end
-- selecting all the months in the range where
-- order does not exists
select cast(year(t.d) as varchar(100)) + '-' + DATENAME(month, t.d) as [Month]
from #dateRange as t
where not exists(
select 1
from #order as x
where month(x.OrderDate) = month(t.d) and year(x.OrderDate) = year(t.d)
)
order by t.d
Output: (notice that 2018-January is missing from result because it has an order)
Month
------------------
2017-June
2017-July
2017-August
2017-September
2017-October
2017-November
2017-December
2018-February
2018-March
2018-April
2018-May
2018-June
You are looking for a LEFT JOIN
CREATE TABLE Orders
(
OrderDate DATE
);
INSERT INTO Orders VALUES
('2018-01-01'),
('2018-03-01'),
('2018-05-15');
DECLARE #MND DATE = (SELECT MIN(OrderDate) FROM Orders);
DECLARE #MXD DATE = (SELECT MAX(OrderDate) FROM Orders);
WITH CTE AS
(
SELECT #MND OrderDate
UNION ALL
SELECT DATEADD(Month, 1, CTE.OrderDate)
FROM CTE
WHERE CTE.OrderDate <= DATEADD(Month, -1, #MXD)
)
SELECT MONTH(CTE.OrderDate) [Months]
FROM CTE LEFT JOIN Orders O ON MONTH(CTE.OrderDate) = MONTH(O.OrderDate)
AND
YEAR(CTE.OrderDate) = YEAR(O.OrderDate)
WHERE O.OrderDate IS NULL;
-- Add extra conditions here to filter the period needed
Returns:
+--------+
| Months |
+--------+
| 2 |
| 4 |
+--------+
Demo

Based on day fetch all dates - sql

I have start date, end date and name of days. How can fetch all dates between those two dates of that specific days in sql?
example data:
start_date:4/11/2018
end_date: 5/11/2018
days: monday, thursday
expected output: all dates between start and end date which comes on monday and thursday and store them in table
updated
my present code(not working)
; WITH CTE(dt)
AS
(
SELECT #P_FROM_DATE
UNION ALL
SELECT DATEADD(dw, 1, dt) FROM CTE
WHERE dt < #P_TO_DATE
)
INSERT INTO Table_name
(
ID
,DATE_TIME
,STATUS
,CREATED_DATE
,CREATED_BY
)
SELECT #P_ID
,(SELECT dt FROM CTE WHERE DATENAME(dw, dt) In ('tuesday','friday',null))
,'NOT SENT'
,CAST(GETDATE() AS DATE)
,#USER_ID
Another approach for generating dates between ranges can be like following query. This will be faster compared to CTE or WHILE loop.
DECLARE #StartDate DATETIME = '2018-04-11'
DECLARE #EndDate DATETIME = '2018-05-15'
SELECT #StartDate + RN AS DATE FROM
(
SELECT (ROW_NUMBER() OVER (ORDER BY (SELECT NULL)))-1 RN
FROM master..[spt_values] T1
) T
WHERE RN <= DATEDIFF(DAY,#StartDate,#EndDate)
AND DATENAME(dw,#StartDate + RN) IN('Monday','Thursday')
Note:
If the row count present in master..[spt_values] is not sufficient for the provided range, you can make a cross join with the same to get a bigger range like following.
SELECT (ROW_NUMBER() OVER (ORDER BY (SELECT NULL)))-1 RN
FROM master..[spt_values] T1
CROSS JOIN master..[spt_values] T2
By this you will be able to generate date between a range with gap of 6436369 days.
You can use a recursive common table expression (CTE) to generate a list of days. With datepart(dw, ...) you can filter for specific days of the week.
An example that creates a list of Mondays and Thursdays between March 1st and today:
create table ListOfDates (dt date);
with cte as
(
select cast('2018-03-01' as date) as dt -- First day of interval
union all
select dateadd(day, 1, dt)
from cte
where dt < getdate() -- Last day of interval
)
insert into ListOfDates
(dt)
select dt
from cte
where datepart(dw, dt) in (2, 5) -- 2=Monday and 5=Thursday
option (maxrecursion 0)
See it working at SQL Fiddle.
This will work for you:
DECLARE #table TABLE(
ID INT IDENTITY(1,1),
Date DATETIME,
Day VARCHAR(50)
)
DECLARE #Days TABLE(
ID INT IDENTITY(1,1),
Day VARCHAR(50)
)
INSERT INTO #Days VALUES ('Monday')
INSERT INTO #Days VALUES ('Thursday')
DECLARE #StartDate DATETIME='2018-01-01';
DECLARE #EndDate DATETIME=GETDATE();
DECLARE #Day VARCHAR(50)='Friday';
DECLARE #TempDate DATETIME=#StartDate;
WHILE CAST(#TempDate AS DATE)<=CAST(#EndDate AS DATE)
BEGIN
IF EXISTS (SELECT 1 FROM #Days WHERE DAY IN (DATENAME(dw,#TempDate)))
BEGIN
INSERT INTO #table
VALUES (
#TempDate, -- Date - datetime
DATENAME(dw,#TempDate) -- Day - varchar(50)
)
END
SET #TempDate=DATEADD(DAY,1,#TempDate)
END
SELECT * FROM #table
INSERT INTO TargetTab(dateCOL)
SELECT dateCOL
FROM tab
WHERE dateCOL >= startdate AND dateCOL <= enddate
AND (DATENAME(dw,dateCOL) ='Thursday' OR DATENAME(dw,dateCOL) = 'Monday')
Try this query to get your result.
Use a recursive CTE to generate your dates, then filter by week day.
SET DATEFIRST 1 -- 1: Monday, 7 Sunday
DECLARE #StartDate DATE = '2018-04-11'
DECLARE #EndDate DATE = '2018-05-15'
DECLARE #WeekDays TABLE (WeekDayNumber INT)
INSERT INTO #WeekDays (
WeekDayNumber)
VALUES
(1), -- Monday
(4) -- Thursday
;WITH GeneratingDates AS
(
SELECT
GeneratedDate = #StartDate,
WeekDay = DATEPART(WEEKDAY, #StartDate)
UNION ALL
SELECT
GeneratedDate = DATEADD(DAY, 1, G.GeneratedDate),
WeekDay = DATEPART(WEEKDAY, DATEADD(DAY, 1, G.GeneratedDate))
FROM
GeneratingDates AS G -- Notice that we are referencing a CTE that we are also declaring
WHERE
G.GeneratedDate < #EndDate
)
SELECT
G.GeneratedDate
FROM
GeneratingDates AS G
INNER JOIN #WeekDays AS W ON G.WeekDay = W.WeekDayNumber
OPTION
(MAXRECURSION 30000)
Try this:
declare #start date = '04-11-2018'
declare #end date = '05-11-2018'
declare #P_ID int = 1
declare #USER_ID int = 11
;with cte as(
select #start [date]
union all
select dateadd(DAY, 1, [date]) from cte
where [date] < #end
)
--if MY_TABLE doesn't exist
select #P_ID,
[date],
'NOT SENT',
cast(getdate() as date),
#USER_ID
into MY_TABLE
from cte
--here you can specify days: 1 - Sunday, 2 - Monday, etc.
where DATEPART(dw,[date]) in (2, 5)
option (maxrecursion 0)
--if MY_TABLE does exist
--insert into MY_TABLE
--select #P_ID,
-- [date],
-- 'NOT SENT',
-- cast(getdate() as date),
-- #USER_ID
--from cte
--where DATEPART(dw,[date]) in (2, 5)
--option (maxrecursion 0)

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