Count of rows by month within period - sql

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

Related

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' )

Scalar Function with CTE Fails

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

How to optimize a cycle in stored procedure [T-SQL]

I have this stored procedure in T-SQL for splitting a DATETIME into shifts.
I would divide the DATETIME START and DATETIME END into shifts (for example, one shift for hours or one shift every 15 minutes). I should this shift into a temporary table for use it in another query.
So for this I have create this cycle:
BEGIN
SET #DATASTART = '2014-11-28 06:00:00'
SET #DATAEND = '2014-11-28 21:00:00'
--DICHIARO DUE VARIABILI DATA CHE UTILIZZO
--PER MANTENERE I DUE SHIFT
DECLARE #DataFirstShift as DATETIME
DECLARE #DataLastShift as DATETIME
--DICHIARO UN CONTATORE PER POPOLARE IL CAMPO ID
DECLARE #Contatore as INT
SET #Contatore = 0
--SETTO LA DATA FIRSTSHIFT A DATASTART
SET #DataFirstShift = #DATA_START
WHILE(#DataFirstShift <= #DATA_END)
BEGIN
--POPOLO LA DATA LAST CON UN ORA IN PIU RISPETTO ALLA PRIMA DATA
IF #Shift LIKE 'All'
BEGIN
SET #DataLastShift = DATEADD(HOUR,1,#DataFirstShift)
END
ELSE
BEGIN
SET #DataLastShift = DATEADD(MINUTE,15,#DataFirstShift)
END
INSERT INTO #TemporaryTable2 (ID,DATASTART,DATAEND)
VALUES (#Contatore,#DataFirstShift,#DataLastShift)
SET #DataFirstShift=#DataLastShift
--INCREMENTO IL CONTATORE
SET #Contatore+=1
END
END
This method works but I this cycle is slow. I want to know if exist a method faster than it.
Can someone help me? Regards
Try below :
--param of SP
DECLARE #DATASTART DATETIME = '2014-11-28 06:00:00'
DECLARE #DATAEND DATETIME = '2014-11-28 21:00:00'
DECLARE #Shift VARCHAR(50) = 'All'
--body
DECLARE #TemporaryTable2 TABLE
(
id INT,
startdate DATETIME,
enddate DATETIME
)
DECLARE #id INT = 0
IF #Shift LIKE 'All'
BEGIN
INSERT INTO #TemporaryTable2
SELECT N.number + 1 AS id,
startdate,
enddate
FROM master..spt_values N
CROSS apply (SELECT Dateadd(dd, N.number, #DATASTART),
Dateadd(dd, N.number + 1, #DATASTART)) AS D(startdate, enddate)
WHERE N.number BETWEEN 0 AND Datediff(dd, #DATASTART, #DATAEND)
AND N.type = 'P'
END
ELSE
BEGIN
INSERT INTO #TemporaryTable2
SELECT N.number + 1 AS id,
startdate,
enddate
FROM master..spt_values N
CROSS apply (SELECT Dateadd(MINUTE, N.number * 15, #DATASTART),
Dateadd(MINUTE, ( N.number + 1 ) * 15, #DATASTART)) AS D(startdate, enddate)
WHERE N.number BETWEEN 0 AND ( Datediff(MINUTE, #DATASTART, #DATAEND) / 15 )
AND N.type = 'P'
END
SELECT *
FROM #TemporaryTable2
A similar question is present here. The only limitation of this method is spt_values ranges from 0 to 2047 for type = 'p'. Hence it can return maximum 2047 rows.

remove duplicates in sql query

I've been working on a query that should :
business logic - show all members who attended the gym(selected from dropdown) within a specified date range.
Problem encountered -I realised that results shows duplicates of which mustn't , may you please assist.
Below is the query I used.
Declare #StartDate datetime = '29 May 2014'
Declare #EndDate datetime = '29 May 2014'
DEclare #SiteID INT =14
Declare #StartTime datetime = '05:00 AM'
Declare #EndTime datetime = '10:00 PM'
Declare #Start datetime = cast (#StartDate + ' '+ #StartTime as datetime)
Declare #End datetime = cast(#EndDate + ' ' + #EndTime as datetime )
SELECT isnull(atc.totalaccepted,0) TotalAcceptedVisits,ISNULL(att.TotalOverrideVisits, 0) AS TotalOverrides, isnull(att1.TotalOverrideDenieds,0) as TotalDenies, ast.Name as VisitiedSite, c.FirstName + ' ' + c.LastName as Name, md.MemRefNo
FROM Contacts c
inner JOIN Attendance a on a.ContactGUID = c.GUID
inner JOIN MemberDetail md on md.ContactGUID = c.GUID
inner JOIN Sites ast on a.SiteID = ast.ID
OUTER APPLY
(
SELECT a1.contactguid,COUNT(*) totalaccepted
FROM Attendance a1
WHERE a1.contactguid = c.guid
AND (a1.IsSwipeSuccessful = 1)
AND a1.accessoverridereasonid IS NULL
AND a1.AttendDate BETWEEN #Start AND #End
group by a1.contactguid
)atc
outer APPLY
(
SELECT a2.contactguid, COUNT(*) TotalOverrideVisits
FROM Attendance a2
WHERE a2.contactguid = c.guid
And (a.IsSwipeSuccessful = 1)
AND a2.accessoverridereasonid IS NOT NULL
AND a2.AttendDate BETWEEN #Start AND #End
and (a2.SiteID = #SiteID OR #SiteID = 0)
group by a2.contactguid
) att
OUTER APPLY
(
SELECT a3.contactguid,COUNT(*) TotalOverrideDenieds
FROM Attendance a3
WHERE a3.contactguid = c.guid
AND a3.IsSwipeSuccessful = 0
AND a3.accessoverridereasonid IS NULL
AND a3.AttendDate BETWEEN #Start AND #end
group by a3.contactguid
) att1
where (a.SiteID = #SiteID OR #SiteID = 0)
and a.AttendDate BETWEEN #Start AND #End
order by md.MemRefNo
Results: as you can see below there are members repeating themselves, I only need to see one row of each member with the number of total visits if any, overrides if any and denied visits if any.
TotalAcceptedVisits TotalOverrides TotalDenies VisitiedSite Name memRefNo
1 0 0 Groblersdal Jean T G0030
1 1 1 Groblersdal Koky Bakkes G0032
1 0 1 Groblersdal Koky Bakkes G0032
1 1 1 Groblersdal Koky Bakkes G0032
1 0 0 Groblersdal Naomi Fisher G0035
1 0 0 Groblersdal Arthur Bart G0089
1 0 0 Groblersdal Tulinda Swi G0034
1 1 0 Groblersdal Devon Mooi G0008
1 1 0 Groblersdal Devon Mooi G0008
I think you will need something like this:
Declare #StartDate datetime = '29 May 2014'
Declare #EndDate datetime = '29 May 2014'
DEclare #SiteID INT =14
Declare #StartTime datetime = '05:00 AM'
Declare #EndTime datetime = '10:00 PM'
Declare #Start datetime = cast (#StartDate + ' '+ #StartTime as datetime)
Declare #End datetime = cast(#EndDate + ' ' + #EndTime as datetime )
SELECT CASE WHEN TotalAcceptedVisits > 0 THEN 1 ELSE 0 END AS TotalAcceptedVisits,
CASE WHEN TotalOverrides > 0 THEN 1 ELSE 0 END AS TotalOverrides,
CASE WHEN TotalDenies > 0 THEN 1 ELSE 0 END AS TotalDenies,
VisitiedSite, Name, MemRefNo
FROM
(SELECT SUM(isnull(atc.totalaccepted,0)) TotalAcceptedVisits, SUM(ISNULL(att.TotalOverrideVisits, 0)) AS TotalOverrides, SUM(isnull(att1.TotalOverrideDenieds,0)) as TotalDenies, ast.Name as VisitiedSite, c.FirstName + ' ' + c.LastName as Name, md.MemRefNo
FROM Contacts c
inner JOIN Attendance a on a.ContactGUID = c.GUID
inner JOIN MemberDetail md on md.ContactGUID = c.GUID
inner JOIN Sites ast on a.SiteID = ast.ID
OUTER APPLY
(
SELECT a1.contactguid,COUNT(*) totalaccepted
FROM Attendance a1
WHERE a1.contactguid = c.guid
AND (a1.IsSwipeSuccessful = 1)
AND a1.accessoverridereasonid IS NULL
AND a1.AttendDate BETWEEN #Start AND #End
group by a1.contactguid
)atc
outer APPLY
(
SELECT a2.contactguid, COUNT(*) TotalOverrideVisits
FROM Attendance a2
WHERE a2.contactguid = c.guid
And (a.IsSwipeSuccessful = 1)
AND a2.accessoverridereasonid IS NOT NULL
AND a2.AttendDate BETWEEN #Start AND #End
and (a2.SiteID = #SiteID OR #SiteID = 0)
group by a2.contactguid
) att
OUTER APPLY
(
SELECT a3.contactguid,COUNT(*) TotalOverrideDenieds
FROM Attendance a3
WHERE a3.contactguid = c.guid
AND a3.IsSwipeSuccessful = 0
AND a3.accessoverridereasonid IS NULL
AND a3.AttendDate BETWEEN #Start AND #end
group by a3.contactguid
) att1
where (a.SiteID = #SiteID OR #SiteID = 0)
and a.AttendDate BETWEEN #Start AND #End
GROUP BY atc.totalaccepted, att.TotalOverrideVisits, att1.TotalOverrideDenieds, ast.Name, c.FirstName + ' ' + c.LastName, md.MemRefNo) TB
ORDER BY MemRefNo
Since, you don't want the SUM of the values, then you can do something as above which basically embeds your SELECT as a subquery and then has a CASE statement to see whether the Total Values is greater than 0, if so return 1 else 0.