Scalar Function with CTE Fails - sql

This function return is a single float value, but it always is null. Why?
Function to calculate the total working hour per employee:
ALTER FUNCTION GetTotalWorkingHour
(
#StartDate datetime,
#EndDate datetime,
#EmpID nvarchar(6) = null
)
RETURNS float
AS
BEGIN
DECLARE #Result float;
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]), #EndDate) * 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 #Result =
(CTE_Start.SumStart - ISNULL(CTE_End.SumEND, 0) / 60.0) --AS SumHours
FROM
CTE_End
LEFT JOIN CTE_Start ON CTE_Start.EmpID = CTE_End.EmpID
RETURN #Result
END
GO
the above code run in a correct way and give me the expected result if i use it in a single query(not in function), so what's wrong?

i found the solution, it was a small bug, i must add:
SELECT #Result =
(SumEnd - ISNULL(SumStart, 0)) / 60.0 --AS SumHours
FROM
CTE_End
LEFT JOIN CTE_Start ON CTE_Start.EmpID = CTE_End.EmpID
RETURN #Result
instead of:
SELECT #Result =
(CTE_Start.SumStart - ISNULL(CTE_End.SumEND, 0) / 60.0) --AS SumHours
FROM
CTE_End
LEFT JOIN CTE_Start ON CTE_Start.EmpID = CTE_End.EmpID
RETURN #Result

Related

Count of rows by month within period

I'm working on a T-SQL function to return the count of two categories (enrolls and verifies) for each month within a specific period. The function below works only when there is at least a count greater than 1 for each month. What I'd like to do is given a start date of the lowest date found in the source table, an end date of today, and a given number of months, return the counts of enrolls, and the count of verifies for a given user id between the start and end period of months.
Ideally I'd like the range-period data be formatted as MMM-YYYY. For any months during this period where there are no counts, I'd like those values to show a count of 0.
RELEASED_DATETIME is a DATETIME column in the INTERACTION_SESSION_T table.
Valid Data for a period of 7 Months
RANGEPERIOD | ENROLLS | VERIFIES
------------+---------+---------
Nov-2017 | 15 | 15
Dec-2017 | 150 | 2582
Jan-2018 | 0 | 0
Feb-2018 | 0 | 98
Mar-2018 | 10 | 0
Apr-2018 | 8 | 0
May-2018 | 12 | 85
My code:
CREATE FUNCTION [dbo].[SVE_GET_ENROL_VERIFY_COUNT_MONTH_R]
(#DEV_ID INT, #NUM_MONTHS INT)
RETURNS #D TABLE (RANGEPERIOD VARCHAR(10),
TOTAL_ENROLLMENTS INT,
TOTAL_VERIFICATIONS INT)
AS
BEGIN
DECLARE #cur VARCHAR(12); -- stores the current record date the cursor points to
DECLARE #sdate DATETIME; -- start date of range
DECLARE #edate DATETIME; -- end date of range
DECLARE #tot_enrls INT; -- stores the #cur date's enrollment count
DECLARE #tot_verfs INT; -- stores the #cur date's verification count
SELECT #sdate = MIN(RELEASED_DATETIME)
FROM SVE_INTERACTION_SESSION_T
WHERE (DEVELOPER_OWNER_ID = #DEV_ID OR DEVELOPER_USER_ID = #DEV_ID)
AND RELEASED_DATETIME >= DATEADD(MONTH, -#NUM_MONTHS + 1, GETDATE())
SELECT #edate = GETDATE();
INSERT INTO #D (RANGEPERIOD)
(SELECT
RANGEPERIOD = CONCAT(SUBSTRING(datename(month, I.RELEASED_DATETIME), 1, 3 ), '-', YEAR(I.RELEASED_DATETIME))
FROM
SVE_INTERACTION_SESSION_T AS I WITH (NOLOCK)
INNER JOIN
SVE_APPLICATIONS_T AS A WITH (NOLOCK) ON I.APPLICATION_ID = A._ID
WHERE
(I.DEVELOPER_OWNER_ID = #DEV_ID OR I.DEVELOPER_USER_ID = #DEV_ID)
AND I.RELEASED_CODE = 'C'
AND A.IS_DELETED = 0
AND I.RELEASED_DATETIME >= #sdate
AND I.RELEASED_DATETIME < DATEADD(day, 1, #edate)
GROUP BY
YEAR(I.RELEASED_DATETIME), MONTH(I.RELEASED_DATETIME),
DATENAME(MONTH, I.RELEASED_DATETIME)
);
-- Start of cursor
DECLARE DTY CURSOR FOR
SELECT RANGEPERIOD FROM #d
OPEN DTY
-- Fetch the first row
FETCH NEXT FROM DTY INTO #cur
WHILE ##FETCH_STATUS = 0
BEGIN
SET #tot_enrls = (SELECT COUNT(DISTINCT I._ID) AS TOT_ENROLLS
FROM SVE_INTERACTION_SESSION_T AS I WITH (NOLOCK)
INNER JOIN SVE_ENROLL_SESSION_T AS E WITH (NOLOCK) ON E.INTERACTION_SESSION_ID = I._ID
INNER JOIN SVE_APPLICATIONS_T AS A WITH (NOLOCK) ON I.APPLICATION_ID = A._ID
WHERE
(I.DEVELOPER_OWNER_ID = #DEV_ID OR I.DEVELOPER_USER_ID = #DEV_ID)
AND E.INSTANCE_ID = 0
AND E.COMPLETION_STATUS = 'T'
AND I.RELEASED_CODE = 'C'
AND A.IS_DELETED = 0
AND CONCAT( SUBSTRING( datename( month, I.RELEASED_DATETIME ), 1, 3 ), '-', YEAR( I.RELEASED_DATETIME ) ) = #cur
);
SET #tot_verfs = (
SELECT
COUNT( DISTINCT I._ID ) AS TOT_VERIFYS
FROM
SVE_INTERACTION_SESSION_T AS I WITH (NOLOCK) INNER JOIN SVE_VERIFY_SESSION_T AS V WITH (NOLOCK)
ON
V.INTERACTION_SESSION_ID = I._ID INNER JOIN SVE_APPLICATIONS_T AS A WITH (NOLOCK)
ON
I.APPLICATION_ID = A._ID
WHERE
( I.DEVELOPER_OWNER_ID = #DEV_ID OR I.DEVELOPER_USER_ID = #DEV_ID ) AND
V.INSTANCE_ID = 0 AND
V.COMPLETION_STATUS = 'S' AND
I.RELEASED_CODE = 'C' AND
A.IS_DELETED = 0 AND
CONCAT( SUBSTRING( datename( month, I.RELEASED_DATETIME ), 1, 3 ), '-', YEAR( I.RELEASED_DATETIME ) ) = #cur
);
-- Updates the fields in the return table
UPDATE
#d
SET
TOTAL_ENROLLMENTS = #tot_enrls,
TOTAL_VERIFICATIONS = #tot_verfs
WHERE
RANGEPERIOD = #cur
-- Fetch the next row and repeat the above process
FETCH NEXT FROM DTY INTO #cur
END
-- Updates the return table by setting all null values to 0 for better visuals
UPDATE #d SET TOTAL_ENROLLMENTS = 0, TOTAL_VERIFICATIONS = 0 WHERE TOTAL_ENROLLMENTS IS NULL
RETURN
END
It might be worth noting that I already do something similar for a range of days, and this works flawlessly. The only difference is I use a calendar table for the day range function. I know the secret sauce is in the temporary table that gets built from the calendar table, which i attempted to modify the month function to suit, but I'm at this point flustered
Here's the day function which works great
ALTER FUNCTION [dbo].[SVE_GET_ENROL_VERIFY_COUNT_R]( #DEV_ID INT, #NUM_DAYS INT )
RETURNS #D TABLE
(
RANGEDATE DATE,
TOTAL_ENROLLMENTS INT,
TOTAL_VERIFICATIONS INT
)
AS
BEGIN
DECLARE #s DATE; -- start date of range
DECLARE #e DATE; -- end date of range
DECLARE #cur DATE; -- stores the current record date the cursor points to
DECLARE #tot_enrls INT; -- stores the #cur date's enrollment count
DECLARE #tot_verfs INT; -- stores the #cur date's verification count
SET #e = getdate(); -- stores today's date as the end date
SET #s = DATEADD( dd, -( #NUM_DAYS-1 ), #e ); -- Subtract 1 from the incoming day and setup the accurate range of days
INSERT INTO #d ( RANGEDATE )
(
SELECT
RANGEDATE = c.d
FROM
Calendar AS c LEFT OUTER JOIN SVE_INTERACTION_SESSION_T as I WITH (NOLOCK)
ON
I.RELEASED_DATETIME >= #s AND I.RELEASED_DATETIME < = #e AND
c.d = CONVERT( DATE, I.RELEASED_DATETIME )
WHERE
c.d >= #s AND c.d <= #e
GROUP BY c.d
);
-- Start of cursor
DECLARE DTY CURSOR FOR
SELECT RANGEDATE FROM #d
OPEN DTY
-- Fetch the first row
FETCH NEXT FROM DTY INTO #cur
WHILE ##FETCH_STATUS = 0
BEGIN
-- Gets the developer's enrollment count for the current record's day
SET #tot_enrls = (
SELECT
COUNT( DISTINCT I._ID ) AS TOT_ENROLLS
FROM
SVE_INTERACTION_SESSION_T AS I WITH (NOLOCK) INNER JOIN SVE_ENROLL_SESSION_T AS E WITH (NOLOCK)
ON
E.INTERACTION_SESSION_ID = I._ID INNER JOIN SVE_APPLICATIONS_T AS A WITH (NOLOCK)
ON
I.APPLICATION_ID = A._ID
WHERE
( I.DEVELOPER_OWNER_ID = #DEV_ID OR I.DEVELOPER_USER_ID = #DEV_ID ) AND
E.INSTANCE_ID = 0 AND
E.COMPLETION_STATUS = 'T' AND
I.RELEASED_CODE = 'C' AND
A.IS_DELETED = 0 AND
I.RELEASED_DATETIME >= #cur AND
I.RELEASED_DATETIME < dateadd(day,1,#cur)
);
-- Gets the developer's verification count for the current record's day
SET #tot_verfs = (
SELECT
COUNT( DISTINCT I._ID ) AS TOT_VERIFYS
FROM
SVE_INTERACTION_SESSION_T AS I WITH (NOLOCK) INNER JOIN SVE_VERIFY_SESSION_T AS V WITH (NOLOCK)
ON
V.INTERACTION_SESSION_ID = I._ID INNER JOIN SVE_APPLICATIONS_T AS A WITH (NOLOCK)
ON
I.APPLICATION_ID = A._ID
WHERE
( I.DEVELOPER_OWNER_ID = #DEV_ID OR I.DEVELOPER_USER_ID = #DEV_ID ) AND
V.INSTANCE_ID = 0 AND
V.COMPLETION_STATUS = 'S' AND
I.RELEASED_CODE = 'C' AND
A.IS_DELETED = 0 AND
I.RELEASED_DATETIME >= #cur AND
I.RELEASED_DATETIME < dateadd(day,1,#cur)
);
-- Updates the fields in the return table
UPDATE
#d
SET
TOTAL_ENROLLMENTS = #tot_enrls,
TOTAL_VERIFICATIONS = #tot_verfs
WHERE
RANGEDATE = #cur
-- Fetch the next row and repeat the above process
FETCH NEXT FROM DTY INTO #cur
END
-- Updates the return table by setting all null values to 0 for better visuals
UPDATE #d SET TOTAL_ENROLLMENTS = 0, TOTAL_VERIFICATIONS = 0 WHERE TOTAL_ENROLLMENTS IS NULL
RETURN
END
This is what my modification looks like for the month period to use the calendar table but its an epic fail, as it only returns one row.
INSERT INTO #d ( RANGEPERIOD )
(
SELECT
RANGEPERIOD = CONCAT( SUBSTRING( datename( month, c.d ), 1, 3 ), '-', YEAR( c.d ) )
FROM
Calendar AS c LEFT OUTER JOIN SVE_INTERACTION_SESSION_T as I WITH (NOLOCK)
ON
(YEAR(I.RELEASED_DATETIME) >= YEAR(#s) AND MONTH(I.RELEASED_DATETIME) >= MONTH(#s)) AND (YEAR(I.RELEASED_DATETIME) <= YEAR(#e) AND MONTH(I.RELEASED_DATETIME) <= MONTH(#e)) AND
c.d = CONVERT( DATE, I.RELEASED_DATETIME )
WHERE
(YEAR(c.d) >= YEAR(#s) AND MONTH(c.d) >= MONTH(#s)) AND (YEAR(c.d) <= YEAR(#e) AND MONTH(c.d) <= MONTH(#e))
GROUP BY c.d
);
Can someone point me in the right direction, or provide an assist?
I cannot give you a specific point but I can give you some guidelines to resolve your problem.
I assume that you need the results in this format. (MMM-YYYY)
In your modification query, you can remove SVE_INTERACTION_SESSION_T table.
Because
You didn't select column from this table.
You were using LEFT JOIN then your result don't get the effect from your condition. The result still comes from whole table of calendar
SELECT RANGEPERIOD = CONCAT(SUBSTRING(datename(MONTH, c.d), 1, 3), '-', YEAR(c.d))
FROM Calendar AS c
WHERE (YEAR(c.d) >= YEAR(#s) AND MONTH(c.d) >= MONTH(#s))
AND (YEAR(c.d) <= YEAR(#e) AND MONTH(c.d) <= MONTH(#e))
GROUP BY c.d
If anyone is interested in a solution to this see my code below. It might not be the most efficient, but at the moment I cannot think of another solution.
CREATE FUNCTION [dbo].[SVE_GET_ENROL_VERIFY_COUNT_MONTH_R]( #DEV_ID INT, #NUM_MONTHS INT )
RETURNS #M TABLE
(
RANGEPERIOD VARCHAR(10),
TOTAL_ENROLLMENTS INT,
TOTAL_VERIFICATIONS INT
)
AS
BEGIN
-- We need to declare a temporary table to hold the days from the period
DECLARE #D TABLE(
RANGEDATE DATETIME,
TOTAL_ENROLLMENTS INT,
TOTAL_VERIFICATIONS INT
)
DECLARE #cur VARCHAR(12); -- stores the current record date the cursor points to
DECLARE #sdate DATETIME; -- start date of range
DECLARE #edate DATETIME; -- end date of range
DECLARE #tot_enrls INT; -- stores the #cur date's enrollment count
DECLARE #tot_verfs INT; -- stores the #cur date's verification count
SELECT #sdate = DATEADD( MONTH, -( #NUM_MONTHS-1 ), GETDATE() )
SELECT #edate = GETDATE();
-- First fill the temp day table with dates from the calendar
INSERT INTO #D ( RANGEDATE ) (
SELECT
RANGEDATE = CONVERT( DATE, c.d )
FROM
Calendar AS c
WHERE
c.d >= #sdate AND c.d <= #edate
GROUP BY c.d
)
-- Next we fill our return table with our grouped by formatted dates
INSERT INTO #M ( RANGEPERIOD ) (
SELECT
RANGEPERIOD = CONCAT( SUBSTRING( datename( month, D.RANGEDATE ), 1, 3 ), '-', YEAR( D.RANGEDATE ) )
FROM
#D AS D
GROUP BY year( D.RANGEDATE ), month( D.RANGEDATE ), datename( month, D.RANGEDATE )
)
-- Start of cursor
DECLARE DTY CURSOR FOR
SELECT RANGEPERIOD FROM #M
OPEN DTY
-- Fetch the first row
FETCH NEXT FROM DTY INTO #cur
WHILE ##FETCH_STATUS = 0
BEGIN
SET #tot_enrls = (
SELECT
COUNT( DISTINCT I._ID ) AS TOT_ENROLLS
FROM
SVE_INTERACTION_SESSION_T AS I WITH (NOLOCK) INNER JOIN SVE_ENROLL_SESSION_T AS E WITH (NOLOCK)
ON
E.INTERACTION_SESSION_ID = I._ID INNER JOIN SVE_APPLICATIONS_T AS A WITH (NOLOCK)
ON
I.APPLICATION_ID = A._ID
WHERE
( I.DEVELOPER_OWNER_ID = #DEV_ID OR I.DEVELOPER_USER_ID = #DEV_ID ) AND
E.INSTANCE_ID = 0 AND
E.COMPLETION_STATUS = 'T' AND
I.RELEASED_CODE = 'C' AND
A.IS_DELETED = 0 AND
CONCAT( SUBSTRING( datename( month, I.RELEASED_DATETIME ), 1, 3 ), '-', YEAR( I.RELEASED_DATETIME ) ) = #cur
);
SET #tot_verfs = (
SELECT
COUNT( DISTINCT I._ID ) AS TOT_VERIFYS
FROM
SVE_INTERACTION_SESSION_T AS I WITH (NOLOCK) INNER JOIN SVE_VERIFY_SESSION_T AS V WITH (NOLOCK)
ON
V.INTERACTION_SESSION_ID = I._ID INNER JOIN SVE_APPLICATIONS_T AS A WITH (NOLOCK)
ON
I.APPLICATION_ID = A._ID
WHERE
( I.DEVELOPER_OWNER_ID = #DEV_ID OR I.DEVELOPER_USER_ID = #DEV_ID ) AND
V.INSTANCE_ID = 0 AND
V.COMPLETION_STATUS = 'S' AND
I.RELEASED_CODE = 'C' AND
A.IS_DELETED = 0 AND
CONCAT( SUBSTRING( datename( month, I.RELEASED_DATETIME ), 1, 3 ), '-', YEAR( I.RELEASED_DATETIME ) ) = #cur
);
-- Updates the fields in the return table
UPDATE
#M
SET
TOTAL_ENROLLMENTS = #tot_enrls,
TOTAL_VERIFICATIONS = #tot_verfs
WHERE
RANGEPERIOD = #cur
-- Fetch the next row and repeat the above process
FETCH NEXT FROM DTY INTO #cur
END
-- Updates the return table by setting all null values to 0 for better visuals
UPDATE #M SET TOTAL_ENROLLMENTS = 0, TOTAL_VERIFICATIONS = 0 WHERE TOTAL_ENROLLMENTS IS NULL
RETURN
END

How to avoid not to query tables or views in scalar functions?

I have scalar functions( 4 functions) in my View. It drastically reduces the view's performance. I believe the reason for that is I use SELECT queries in my scalar functions.
EG:
CREATE FUNCTION [dbo].[udf_BJs_GENERAL]
(
#TankSystemId int,
#TimeStamp datetime2(7)
)
RETURNS varchar(10)
AS
BEGIN
DECLARE #leakChk varchar(10);
DECLARE #allowableVariance float;
DECLARE #GallonsPumped int;
DECLARE #DailyOverOrShort float;
DECLARE #TimePeriod datetime2(7);
DECLARE #ReportDate datetime2(7)
SELECT TOP 1 #TimePeriod = Date
FROM [bjs].udv_DailySiraData
where TankSystemId=#TankSystemId ORDER BY Date DESC
SET #ReportDate=#TimePeriod
IF( #TimeStamp <= #TimePeriod)
SET #ReportDate=#TimeStamp
SELECT #GallonsPumped = SUM(GallonsPumped)
FROM [bjs].[udv_DailySiraData]
where TankSystemId=#TankSystemId
and Date <=#ReportDate and Date >= DATEADD(mm, DATEDIFF(mm,0,#ReportDate), 0)
SELECT #DailyOverOrShort = SUM(DailyVar)
FROM [bjs].[udv_DailySiraData]
where TankSystemId=#TankSystemId
and Date <=#ReportDate and Date >= DATEADD(mm, DATEDIFF(mm,0,#ReportDate), 0)
SELECT #allowableVariance= (#GallonsPumped/100) + 130
SET #leakChk='FAIL'
IF (#allowableVariance > ABS(#DailyOverOrShort))
SET #leakChk = 'PASS'
RETURN #leakChk;
How can i avoid such situations? Is there a way to do select queries in my View and pass that result to my scalar function?
Try this:
create function dbo.udf_BJs_GENERAL(
#TankSystemId int,
#TimeStamp datetime2(7)
) returns varchar(10) as
with dates as (
select top 1
ReportDate = case when #TimeStamp <= Date then #TimeStamp else Date
from bjs.udv_DailySiraData
where TankSystemId=#TankSystemId
order by Date desc
),
gallons as (
select
allowableVariance = ( sum(GallonsPumped)/100) + 130,
DailyOverOrShort = sum(DailyVar)
from bjs.udv_DailySiraData data
join dates
on data.Date <= dates.ReportDate
and date.Date >= dateadd(mm, datediffmm, 0, dates.ReportDate), 0)
where TankSystemId = #TankSystemId
)
select
leakChk = cast( case when allowableVariance > ABS(DailyOverOrShort))
then 'PASS' else 'FAIL' end as varchar(10) )
from gallons
your case is special, your have a special input parameter,assue the timestamp parameter is on Day level
This view will return check result of each TankSystemId on every day.
Then join will your query with TankSystemId and Day.
But if the input parameter is more detail. I think it is difficult to convert this function to view
CREATE view [dbo].[uvw_BJs_GENERAL]
AS
BEGIN
/*
SET #ReportDate=#TimePeriod
IF( #TimeStamp <= #TimePeriod)
SET #ReportDate=#TimeStamp
*/
SELECT TankSystemId,b.[Date]
,GallonsPumped = SUM(GallonsPumped),DailyOverOrShort = SUM(DailyVar)
,leakChk=CASE WHEN (SUM(GallonsPumped)/100) + 130)> ABS(SUM(DailyVar)) THEN 'PASS' ELSE 'FAIL' END
FROM [bjs].[udv_DailySiraData] AS a
INNER JOIN (
SELECT CONVERT(DATE,[Date]) AS [Date] FROM [bjs].[udv_DailySiraData] GROUP BY TankSystemId, CONVERT(DATE,[Date])
) b ON a.TankSystemId=b.TankSystemId AND DATEDIFF(d,a.[Date],b.[Date])>=0
-- and Date <=#ReportDate and Date >= DATEADD(mm, DATEDIFF(mm,0,#ReportDate), 0)
GROUP BY TankSystemId,b.[Date]
END

return data even if they are errors

I have a stored procedure, in which return a table containing date,presence,total working hour if present...
Total Working Hour is calculated in a separate function that return a decimal value, But sometimes employees have a mistakes in data, like a duplicate punches or punch (out or in) for twice,
if any of the above scenario exist the function returned an error, and therefore The SP return error...
My request is when any error is occurred, i want to return 0 or -1 or whatever instead of the error, to forces the SP to return data even if they are errors..
The Error generating every time is :
SQL Server Subquery returned more than 1 value.
This is not permitted when the subquery follows =, !=, <, <= , >, >=
So I want in every time when this error occurred, to return a value instead of it,
the SP Code is:
ALTER PROCEDURE [dbo].[SM_GetAttendance]
(
#StartDate date ,
#EndDate date ,
#EmployeeID NVARCHAR(6)
)
AS
BEGIN
SET NOCOUNT ON;
DECLARE dte_Cursor CURSOR FOR
WITH T(date)
AS
(
SELECT #StartDate
UNION ALL
SELECT DateAdd(day,1,T.date) FROM T WHERE T.date < #EndDate
)
SELECT date FROM T OPTION (MAXRECURSION 32767);
DECLARE #date NVARCHAR(20);
CREATE TABLE #datetable(date DATETIME,Status NVARCHAR(50),nbOfWorkingHour DECIMAL(36,2))
--SELECT date FROM T
set #date = ''
OPEN dte_Cursor
FETCH NEXT FROM dte_Cursor INTO #date
WHILE ##FETCH_STATUS = 0
BEGIN
insert #datetable
SELECT
cast((select distinct Convert(Nvarchar(12),date,102) from Attendance where date = #date
and employeeid =#EmployeeID ) as nvarchar(30))
date
,CASE WHEN EXISTS (select 1 from Attendance
where employeeid=#EmployeeID and date = #date)
then 'Present'
else 'absent'
end Status
,dbo.GetWorkingHourPerDay(#date,#EmployeeID) as numberOFWorkingHour
FETCH NEXT FROM dte_Cursor INTO #date
END
CLOSE dte_Cursor;
DEALLOCATE dte_Cursor;
end
And The Part of the Function Code that get the error is:
SET #From = (SELECT Time from #Tbl where date = #Date AND (EmployeeID=#employeeID OR ISNULL( #employeeID, '') = '') and funckey = 'EMPIN')
set #to = (CASE WHEN EXISTS(SELECT Times from #Tbl where dates = #Date AND (EmployeeID=#employeeID OR ISNULL( #employeeID, '') = '') and funckey = 'EMPOUT' )
then (SELECT Time from #Tbl where date = #Date AND (EmployeeID=#employeeID OR ISNULL( #employeeID, '') = '') and funckey = 'EMPOUT' )
else (SELECT Top 1 Time from #Tbl where date = dateadd(day,1,#Date) AND (EmployeeID=#employeeID OR ISNULL( #employeeID, '') = '') and funckey = 'EMPOUT')
end)
return ROUND(CAST(DATEDIFF(#From,#to) AS decimal)/ 60,2)
the following piece of code that get the error :
(CASE WHEN EXISTS(SELECT Times from #Tbl where dates = #Date AND (EmployeeID=#employeeID OR ISNULL( #employeeID, '') = '') and funckey = 'EMPOUT' )
IN this example the employee have two out punches like the below :
EMPID Date Time Status
123 2015-10-22 06:54:42 AM OUT
123 2015-10-22 04:35:02 PM OUT
So how to how to handle this state?
You can just change
(CASE WHEN EXISTS(SELECT Times from #Tbl where dates = #Date AND (EmployeeID=#employeeID OR ISNULL( #employeeID, '') = '') and funckey = 'EMPOUT' )
To this
(CASE WHEN EXISTS(SELECT top 1 Times from #Tbl where dates = #Date AND (EmployeeID=#employeeID OR ISNULL( #employeeID, '') = '') and funckey = 'EMPOUT' )

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

How to use IF statement in SQL Server 2005?

This is the scenario I would like to have in my INSERT in a stored procedure.
Tables:
tblRate
RateID (pk)
Rate money
Days int
isDailyRate bit
tblBooking
Totals money
In my vb app this is the statement. How would I translate this into T-SQL?
if !isDaily = True then
!Totals = (!Days * !Rate)
else
!Totals = !Rate
end if
This is my stored procedure:
Create PROCEDURE [dbo].[sp_tblBooking_Add]
(
#RateID bigint,
#Rate money,
#Days int,
#CheckOUT datetime
)
AS
BEGIN
--Below is the logic I want. I can't get the right syntax
--Declare #myTotals as money
--Declare #myCheckOut as DateTime
--if (Select isDailyRate FROM tblRates WHERE (RateID = #RateID)) = True THEN
-- set myTotals = (#Rate * #Days)
-- set #CheckOUT = DATEADD(DAY, DATEDIFF(DAY, 0, GETDATE()) + #Days, '12:00')
--Else
-- set myTotals = #Rate
-- set #CheckOUT = GETDATE()
--End if
INSERT INTO tblBooking(Totals, CheckOUT)
VALUES(#myTotals, #myCheckOut);
END
Use the CASE expression:
INSERT INTO tblBooking (Totals, CheckOUT)
SELECT
CASE
WHEN idDailyRate = 1 THEN #Rate * #Days
ELSE #rate
END,
CASE
WHEN idDailyRate = 1 THEN DATEADD(DAY,
DATEDIFF(DAY, 0, GETDATE()) + #Days,
'12:00')
ELSE GETDATE()
END
FROM tblRates
WHERE RateID = #RateID;
Or, if they are scalar values, then you can select them into a variables and insert them instead of INSERT ... INTO ... SELECT.
Update 1
Like this:
Declare #myTotals as money;
Declare #myCheckOut as DateTime;
SELECT
#myTotals = CASE
WHEN idDailyRate = 1 THEN #Rate * #Days
ELSE #rate
END,
#myCheckOut = CASE
WHEN idDailyRate = 1 THEN DATEADD(DAY,
DATEDIFF(DAY, 0, GETDATE()) + #Days,
'12:00')
ELSE GETDATE()
END
FROM tblRates
WHERE RateID = #RateID;
INSERT INTO tblBooking (Totals, CheckOUT) VALUES(#myTotals, #myCheckOut );
But this will give you an error, if there is more than value returned from this table tblRates into those variables.