Implement a stored procedure with conditions and update - sql-server

The objective here is to update the table: BOOK_LOANS, column: #Status to a string (See code below) when I execute the Stored procedure. When the loan time goes past the Due_date adds 1 #Warning to the table READERS. When #Warnings = 3, SET #Eligibility to 'NOT ELIGIBLE'. How can I write this, this syntax is a mess.
IF SELECT DATEDIFF(day, #Due_date, CONVERT(date, getdate())) = 5
BEGIN
UPDATE BOOK_LOANS
SET #Status = 'DELAYED'
RETURN;
END
IF DATEDIFF(day, #Due_date, CONVERT(date, getdate())) = 3
BEGIN
UPDATE BOOK_LOANS
SET #Status = 'URGENT RETURN'
RETURN;
END
IF DATEDIFF(day, #Due_date, CONVERT(date, getdate())) = -1
BEGIN
UPDATE BOOK_LOANS
SET #Status = 'OUTDATED'
UPDATE READERS
SET #Warnings = #Warnings + 1
RETURN;
END
IF (#Warnings > 3)
BEGIN
UPDATE READERS
SET #Eligibility = 'NOT ELIGIBLE'
END

I think here is what you are trying to do :
UPDATE BOOK_LOANS
SET
status = CASE DATEDIFF(day, Due_date, CONVERT(date, getdate()))
WHEN 5 THEN 'DELAYED'
WHEN 3 THEN 'URGENT RETURN'
WHEN -1 THEN 'OUTDATED'
ELSE status
END
, warning = CASE WHEN DATEDIFF(day, Due_date, CONVERT(date, getdate())) = -1
THEN warning + 1
ELSE warning
END
GO
UPDATE r
SET Eligibility = CASE WHEN l.warning > 3
THEN 'NOT ELIGIBLE'
ELSE Eligibility
END
FROM READERS r
JOIN BOOK_LOANS l
on l.reader_id = l.reader_is -- here I assume you need to link two tables based on the PK, FKs

Thank you so much for the time. After some time learning a lit bit more I came to this final code. For some reason I couldn't apply the code above but nevertheless helped me to reach my final code.
UPDATE book_loans
SET loan_status = CASE
WHEN Datediff(day, CONVERT(DATE, Getdate()), due_date) =
5 THEN
'RETURN SOON'
WHEN Datediff(day, CONVERT(DATE, Getdate()), due_date) =
3 THEN
'URGENT RETURN'
WHEN Datediff(day, CONVERT(DATE, Getdate()), due_date) <=
-1
THEN 'OUTDATED'
ELSE loan_status
END;
-- Create temp table
SELECT r.card_no,
b.due_date,
Warnings = 0
INTO #temp3
FROM readers AS r
JOIN book_loans AS b
ON r.card_no = b.card_no
SELECT *
FROM #temp3
-- Calculate warnings
UPDATE #temp3
SET warnings = CASE
WHEN Datediff(day, CONVERT(DATE, Getdate()), due_date) <= -1
THEN
warnings + 1
ELSE warnings
END;
UPDATE readers
SET readers.warnings = readers.warnings + p.total
FROM (SELECT card_no,
Sum(warnings) AS total
FROM #temp3
GROUP BY card_no) p
WHERE readers.card_no = p.card_no
UPDATE readers
SET eligibility = CASE
WHEN warnings > 3 THEN 'NOT ELIGIBLE'
ELSE eligibility
END

Related

Datediff with join returning not expected result [duplicate]

How can I calculate the number of work days between two dates in SQL Server?
Monday to Friday and it must be T-SQL.
For workdays, Monday to Friday, you can do it with a single SELECT, like this:
DECLARE #StartDate DATETIME
DECLARE #EndDate DATETIME
SET #StartDate = '2008/10/01'
SET #EndDate = '2008/10/31'
SELECT
(DATEDIFF(dd, #StartDate, #EndDate) + 1)
-(DATEDIFF(wk, #StartDate, #EndDate) * 2)
-(CASE WHEN DATENAME(dw, #StartDate) = 'Sunday' THEN 1 ELSE 0 END)
-(CASE WHEN DATENAME(dw, #EndDate) = 'Saturday' THEN 1 ELSE 0 END)
If you want to include holidays, you have to work it out a bit...
In Calculating Work Days you can find a good article about this subject, but as you can see it is not that advanced.
--Changing current database to the Master database allows function to be shared by everyone.
USE MASTER
GO
--If the function already exists, drop it.
IF EXISTS
(
SELECT *
FROM dbo.SYSOBJECTS
WHERE ID = OBJECT_ID(N'[dbo].[fn_WorkDays]')
AND XType IN (N'FN', N'IF', N'TF')
)
DROP FUNCTION [dbo].[fn_WorkDays]
GO
CREATE FUNCTION dbo.fn_WorkDays
--Presets
--Define the input parameters (OK if reversed by mistake).
(
#StartDate DATETIME,
#EndDate DATETIME = NULL --#EndDate replaced by #StartDate when DEFAULTed
)
--Define the output data type.
RETURNS INT
AS
--Calculate the RETURN of the function.
BEGIN
--Declare local variables
--Temporarily holds #EndDate during date reversal.
DECLARE #Swap DATETIME
--If the Start Date is null, return a NULL and exit.
IF #StartDate IS NULL
RETURN NULL
--If the End Date is null, populate with Start Date value so will have two dates (required by DATEDIFF below).
IF #EndDate IS NULL
SELECT #EndDate = #StartDate
--Strip the time element from both dates (just to be safe) by converting to whole days and back to a date.
--Usually faster than CONVERT.
--0 is a date (01/01/1900 00:00:00.000)
SELECT #StartDate = DATEADD(dd,DATEDIFF(dd,0,#StartDate), 0),
#EndDate = DATEADD(dd,DATEDIFF(dd,0,#EndDate) , 0)
--If the inputs are in the wrong order, reverse them.
IF #StartDate > #EndDate
SELECT #Swap = #EndDate,
#EndDate = #StartDate,
#StartDate = #Swap
--Calculate and return the number of workdays using the input parameters.
--This is the meat of the function.
--This is really just one formula with a couple of parts that are listed on separate lines for documentation purposes.
RETURN (
SELECT
--Start with total number of days including weekends
(DATEDIFF(dd,#StartDate, #EndDate)+1)
--Subtact 2 days for each full weekend
-(DATEDIFF(wk,#StartDate, #EndDate)*2)
--If StartDate is a Sunday, Subtract 1
-(CASE WHEN DATENAME(dw, #StartDate) = 'Sunday'
THEN 1
ELSE 0
END)
--If EndDate is a Saturday, Subtract 1
-(CASE WHEN DATENAME(dw, #EndDate) = 'Saturday'
THEN 1
ELSE 0
END)
)
END
GO
If you need to use a custom calendar, you might need to add some checks and some parameters. Hopefully it will provide a good starting point.
All Credit to Bogdan Maxim & Peter Mortensen. This is their post, I just added holidays to the function (This assumes you have a table "tblHolidays" with a datetime field "HolDate".
--Changing current database to the Master database allows function to be shared by everyone.
USE MASTER
GO
--If the function already exists, drop it.
IF EXISTS
(
SELECT *
FROM dbo.SYSOBJECTS
WHERE ID = OBJECT_ID(N'[dbo].[fn_WorkDays]')
AND XType IN (N'FN', N'IF', N'TF')
)
DROP FUNCTION [dbo].[fn_WorkDays]
GO
CREATE FUNCTION dbo.fn_WorkDays
--Presets
--Define the input parameters (OK if reversed by mistake).
(
#StartDate DATETIME,
#EndDate DATETIME = NULL --#EndDate replaced by #StartDate when DEFAULTed
)
--Define the output data type.
RETURNS INT
AS
--Calculate the RETURN of the function.
BEGIN
--Declare local variables
--Temporarily holds #EndDate during date reversal.
DECLARE #Swap DATETIME
--If the Start Date is null, return a NULL and exit.
IF #StartDate IS NULL
RETURN NULL
--If the End Date is null, populate with Start Date value so will have two dates (required by DATEDIFF below).
IF #EndDate IS NULL
SELECT #EndDate = #StartDate
--Strip the time element from both dates (just to be safe) by converting to whole days and back to a date.
--Usually faster than CONVERT.
--0 is a date (01/01/1900 00:00:00.000)
SELECT #StartDate = DATEADD(dd,DATEDIFF(dd,0,#StartDate), 0),
#EndDate = DATEADD(dd,DATEDIFF(dd,0,#EndDate) , 0)
--If the inputs are in the wrong order, reverse them.
IF #StartDate > #EndDate
SELECT #Swap = #EndDate,
#EndDate = #StartDate,
#StartDate = #Swap
--Calculate and return the number of workdays using the input parameters.
--This is the meat of the function.
--This is really just one formula with a couple of parts that are listed on separate lines for documentation purposes.
RETURN (
SELECT
--Start with total number of days including weekends
(DATEDIFF(dd,#StartDate, #EndDate)+1)
--Subtact 2 days for each full weekend
-(DATEDIFF(wk,#StartDate, #EndDate)*2)
--If StartDate is a Sunday, Subtract 1
-(CASE WHEN DATENAME(dw, #StartDate) = 'Sunday'
THEN 1
ELSE 0
END)
--If EndDate is a Saturday, Subtract 1
-(CASE WHEN DATENAME(dw, #EndDate) = 'Saturday'
THEN 1
ELSE 0
END)
--Subtract all holidays
-(Select Count(*) from [DB04\DB04].[Gateway].[dbo].[tblHolidays]
where [HolDate] between #StartDate and #EndDate )
)
END
GO
-- Test Script
/*
declare #EndDate datetime= dateadd(m,2,getdate())
print #EndDate
select [Master].[dbo].[fn_WorkDays] (getdate(), #EndDate)
*/
My version of the accepted answer as a function using DATEPART, so I don't have to do a string comparison on the line with
DATENAME(dw, #StartDate) = 'Sunday'
Anyway, here's my business datediff function
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE FUNCTION BDATEDIFF
(
#startdate as DATETIME,
#enddate as DATETIME
)
RETURNS INT
AS
BEGIN
DECLARE #res int
SET #res = (DATEDIFF(dd, #startdate, #enddate) + 1)
-(DATEDIFF(wk, #startdate, #enddate) * 2)
-(CASE WHEN DATEPART(dw, #startdate) = 1 THEN 1 ELSE 0 END)
-(CASE WHEN DATEPART(dw, #enddate) = 7 THEN 1 ELSE 0 END)
RETURN #res
END
GO
Another approach to calculating working days is to use a WHILE loop which basically iterates through a date range and increment it by 1 whenever days are found to be within Monday – Friday. The complete script for calculating working days using the WHILE loop is shown below:
CREATE FUNCTION [dbo].[fn_GetTotalWorkingDaysUsingLoop]
(#DateFrom DATE,
#DateTo   DATE
)
RETURNS INT
AS
     BEGIN
         DECLARE #TotWorkingDays INT= 0;
         WHILE #DateFrom <= #DateTo
             BEGIN
                 IF DATENAME(WEEKDAY, #DateFrom) IN('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday')
                     BEGIN
                         SET #TotWorkingDays = #TotWorkingDays + 1;
                 END;
                 SET #DateFrom = DATEADD(DAY, 1, #DateFrom);
             END;
         RETURN #TotWorkingDays;
     END;
GO
Although the WHILE loop option is cleaner and uses less lines of code, it has the potential of being a performance bottleneck in your environment particularly when your date range spans across several years.
You can see more methods on how to calculate work days and hours in this article:
https://www.sqlshack.com/how-to-calculate-work-days-and-hours-in-sql-server/
DECLARE #TotalDays INT,#WorkDays INT
DECLARE #ReducedDayswithEndDate INT
DECLARE #WeekPart INT
DECLARE #DatePart INT
SET #TotalDays= DATEDIFF(day, #StartDate, #EndDate) +1
SELECT #ReducedDayswithEndDate = CASE DATENAME(weekday, #EndDate)
WHEN 'Saturday' THEN 1
WHEN 'Sunday' THEN 2
ELSE 0 END
SET #TotalDays=#TotalDays-#ReducedDayswithEndDate
SET #WeekPart=#TotalDays/7;
SET #DatePart=#TotalDays%7;
SET #WorkDays=(#WeekPart*5)+#DatePart
RETURN #WorkDays
(I'm a few points shy of commenting privileges)
If you decide to forgo the +1 day in CMS's elegant solution, note that if your start date and end date are in the same weekend, you get a negative answer. Ie., 2008/10/26 to 2008/10/26 returns -1.
my rather simplistic solution:
select #Result = (..CMS's answer..)
if (#Result < 0)
select #Result = 0
RETURN #Result
.. which also sets all erroneous posts with start date after end date to zero. Something you may or may not be looking for.
For difference between dates including holidays I went this way:
1) Table with Holidays:
CREATE TABLE [dbo].[Holiday](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](50) NULL,
[Date] [datetime] NOT NULL)
2) I had my plannings Table like this and wanted to fill column Work_Days which was empty:
CREATE TABLE [dbo].[Plan_Phase](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Id_Plan] [int] NOT NULL,
[Id_Phase] [int] NOT NULL,
[Start_Date] [datetime] NULL,
[End_Date] [datetime] NULL,
[Work_Days] [int] NULL)
3) So in order to get "Work_Days" to later fill in my column just had to:
SELECT Start_Date, End_Date,
(DATEDIFF(dd, Start_Date, End_Date) + 1)
-(DATEDIFF(wk, Start_Date, End_Date) * 2)
-(SELECT COUNT(*) From Holiday Where Date >= Start_Date AND Date <= End_Date)
-(CASE WHEN DATENAME(dw, Start_Date) = 'Sunday' THEN 1 ELSE 0 END)
-(CASE WHEN DATENAME(dw, End_Date) = 'Saturday' THEN 1 ELSE 0 END)
-(CASE WHEN (SELECT COUNT(*) From Holiday Where Start_Date = Date) > 0 THEN 1 ELSE 0 END)
-(CASE WHEN (SELECT COUNT(*) From Holiday Where End_Date = Date) > 0 THEN 1 ELSE 0 END) AS Work_Days
from Plan_Phase
Hope that I could help.
Cheers
Here is a version that works well (I think). Holiday table contains Holiday_date columns that contains holidays your company observe.
DECLARE #RAWDAYS INT
SELECT #RAWDAYS = DATEDIFF(day, #StartDate, #EndDate )--+1
-( 2 * DATEDIFF( week, #StartDate, #EndDate ) )
+ CASE WHEN DATENAME(dw, #StartDate) = 'Saturday' THEN 1 ELSE 0 END
- CASE WHEN DATENAME(dw, #EndDate) = 'Saturday' THEN 1 ELSE 0 END
SELECT #RAWDAYS - COUNT(*)
FROM HOLIDAY NumberOfBusinessDays
WHERE [Holiday_Date] BETWEEN #StartDate+1 AND #EndDate
I know this is an old question but I needed a formula for workdays excluding the start date since I have several items and need the days to accumulate correctly.
None of the non-iterative answers worked for me.
I used a defintion like
Number of times midnight to monday, tuesday, wednesday, thursday and friday is passed
(others might count midnight to saturday instead of monday)
I ended up with this formula
SELECT DATEDIFF(day, #StartDate, #EndDate) /* all midnights passed */
- DATEDIFF(week, #StartDate, #EndDate) /* remove sunday midnights */
- DATEDIFF(week, DATEADD(day, 1, #StartDate), DATEADD(day, 1, #EndDate)) /* remove saturday midnights */
This is basically CMS's answer without the reliance on a particular language setting. And since we're shooting for generic, that means it should work for all ##datefirst settings as well.
datediff(day, <start>, <end>) + 1 - datediff(week, <start>, <end>) * 2
/* if start is a Sunday, adjust by -1 */
+ case when datepart(weekday, <start>) = 8 - ##datefirst then -1 else 0 end
/* if end is a Saturday, adjust by -1 */
+ case when datepart(weekday, <end>) = (13 - ##datefirst) % 7 + 1 then -1 else 0 end
datediff(week, ...) always uses a Saturday-to-Sunday boundary for weeks, so that expression is deterministic and doesn't need to be modified (as long as our definition of weekdays is consistently Monday through Friday.) Day numbering does vary according to the ##datefirst setting and the modified calculations handle this correction with the small complication of some modular arithmetic.
A cleaner way to deal with the Saturday/Sunday thing is to translate the dates prior to extracting a day of week value. After shifting, the values will be back in line with a fixed (and probably more familiar) numbering that starts with 1 on Sunday and ends with 7 on Saturday.
datediff(day, <start>, <end>) + 1 - datediff(week, <start>, <end>) * 2
+ case when datepart(weekday, dateadd(day, ##datefirst, <start>)) = 1 then -1 else 0 end
+ case when datepart(weekday, dateadd(day, ##datefirst, <end>)) = 7 then -1 else 0 end
I've tracked this form of the solution back at least as far as 2002 and an Itzik Ben-Gan article. (https://technet.microsoft.com/en-us/library/aa175781(v=sql.80).aspx) Though it needed a small tweak since newer date types don't allow date arithmetic, it is otherwise identical.
EDIT:
I added back the +1 that had somehow been left off. It's also worth noting that this method always counts the start and end days. It also assumes that the end date is on or after the start date.
None of the functions above work for the same week or deal with holidays. I wrote this:
create FUNCTION [dbo].[ShiftHolidayToWorkday](#date date)
RETURNS date
AS
BEGIN
IF DATENAME( dw, #Date ) = 'Saturday'
SET #Date = DATEADD(day, - 1, #Date)
ELSE IF DATENAME( dw, #Date ) = 'Sunday'
SET #Date = DATEADD(day, 1, #Date)
RETURN #date
END
GO
create FUNCTION [dbo].[GetHoliday](#date date)
RETURNS varchar(50)
AS
BEGIN
declare #s varchar(50)
SELECT #s = CASE
WHEN dbo.ShiftHolidayToWorkday(CONVERT(varchar, [Year] ) + '-01-01') = #date THEN 'New Year'
WHEN dbo.ShiftHolidayToWorkday(CONVERT(varchar, [Year]+1) + '-01-01') = #date THEN 'New Year'
WHEN dbo.ShiftHolidayToWorkday(CONVERT(varchar, [Year] ) + '-07-04') = #date THEN 'Independence Day'
WHEN dbo.ShiftHolidayToWorkday(CONVERT(varchar, [Year] ) + '-12-25') = #date THEN 'Christmas Day'
--WHEN dbo.ShiftHolidayToWorkday(CONVERT(varchar, [Year]) + '-12-31') = #date THEN 'New Years Eve'
--WHEN dbo.ShiftHolidayToWorkday(CONVERT(varchar, [Year]) + '-11-11') = #date THEN 'Veteran''s Day'
WHEN [Month] = 1 AND [DayOfMonth] BETWEEN 15 AND 21 AND [DayName] = 'Monday' THEN 'Martin Luther King Day'
WHEN [Month] = 5 AND [DayOfMonth] >= 25 AND [DayName] = 'Monday' THEN 'Memorial Day'
WHEN [Month] = 9 AND [DayOfMonth] <= 7 AND [DayName] = 'Monday' THEN 'Labor Day'
WHEN [Month] = 11 AND [DayOfMonth] BETWEEN 22 AND 28 AND [DayName] = 'Thursday' THEN 'Thanksgiving Day'
WHEN [Month] = 11 AND [DayOfMonth] BETWEEN 23 AND 29 AND [DayName] = 'Friday' THEN 'Day After Thanksgiving'
ELSE NULL END
FROM (
SELECT
[Year] = YEAR(#date),
[Month] = MONTH(#date),
[DayOfMonth] = DAY(#date),
[DayName] = DATENAME(weekday,#date)
) c
RETURN #s
END
GO
create FUNCTION [dbo].GetHolidays(#year int)
RETURNS TABLE
AS
RETURN (
select dt, dbo.GetHoliday(dt) as Holiday
from (
select dateadd(day, number, convert(varchar,#year) + '-01-01') dt
from master..spt_values
where type='p'
) d
where year(dt) = #year and dbo.GetHoliday(dt) is not null
)
create proc UpdateHolidaysTable
as
if not exists(select TABLE_NAME from INFORMATION_SCHEMA.TABLES where TABLE_NAME = 'Holidays')
create table Holidays(dt date primary key clustered, Holiday varchar(50))
declare #year int
set #year = 1990
while #year < year(GetDate()) + 20
begin
insert into Holidays(dt, Holiday)
select a.dt, a.Holiday
from dbo.GetHolidays(#year) a
left join Holidays b on b.dt = a.dt
where b.dt is null
set #year = #year + 1
end
create FUNCTION [dbo].[GetWorkDays](#StartDate DATE = NULL, #EndDate DATE = NULL)
RETURNS INT
AS
BEGIN
IF #StartDate IS NULL OR #EndDate IS NULL
RETURN 0
IF #StartDate >= #EndDate
RETURN 0
DECLARE #Days int
SET #Days = 0
IF year(#StartDate) * 100 + datepart(week, #StartDate) = year(#EndDate) * 100 + datepart(week, #EndDate)
--same week
select #Days = (DATEDIFF(dd, #StartDate, #EndDate))
- (CASE WHEN DATENAME(dw, #StartDate) = 'Sunday' THEN 1 ELSE 0 END)
- (CASE WHEN DATENAME(dw, #EndDate) = 'Saturday' THEN 1 ELSE 0 END)
- (select count(*) from Holidays where dt between #StartDate and #EndDate)
ELSE
--diff weeks
select #Days = (DATEDIFF(dd, #StartDate, #EndDate) + 1)
- (DATEDIFF(wk, #StartDate, #EndDate) * 2)
- (CASE WHEN DATENAME(dw, #StartDate) = 'Sunday' THEN 1 ELSE 0 END)
- (CASE WHEN DATENAME(dw, #EndDate) = 'Saturday' THEN 1 ELSE 0 END)
- (select count(*) from Holidays where dt between #StartDate and #EndDate)
RETURN #Days
END
Using a date table:
DECLARE
#StartDate date = '2014-01-01',
#EndDate date = '2014-01-31';
SELECT
COUNT(*) As NumberOfWeekDays
FROM dbo.Calendar
WHERE CalendarDate BETWEEN #StartDate AND #EndDate
AND IsWorkDay = 1;
If you don't have that, you can use a numbers table:
DECLARE
#StartDate datetime = '2014-01-01',
#EndDate datetime = '2014-01-31';
SELECT
SUM(CASE WHEN DATEPART(dw, DATEADD(dd, Number-1, #StartDate)) BETWEEN 2 AND 6 THEN 1 ELSE 0 END) As NumberOfWeekDays
FROM dbo.Numbers
WHERE Number <= DATEDIFF(dd, #StartDate, #EndDate) + 1 -- Number table starts at 1, we want a 0 base
They should both be fast and it takes out the ambiguity/complexity. The first option is the best but if you don't have a calendar table you can allways create a numbers table with a CTE.
DECLARE #StartDate datetime,#EndDate datetime
select #StartDate='3/2/2010', #EndDate='3/7/2010'
DECLARE #TotalDays INT,#WorkDays INT
DECLARE #ReducedDayswithEndDate INT
DECLARE #WeekPart INT
DECLARE #DatePart INT
SET #TotalDays= DATEDIFF(day, #StartDate, #EndDate) +1
SELECT #ReducedDayswithEndDate = CASE DATENAME(weekday, #EndDate)
WHEN 'Saturday' THEN 1
WHEN 'Sunday' THEN 2
ELSE 0 END
SET #TotalDays=#TotalDays-#ReducedDayswithEndDate
SET #WeekPart=#TotalDays/7;
SET #DatePart=#TotalDays%7;
SET #WorkDays=(#WeekPart*5)+#DatePart
SELECT #WorkDays
CREATE FUNCTION x
(
#StartDate DATETIME,
#EndDate DATETIME
)
RETURNS INT
AS
BEGIN
DECLARE #Teller INT
SET #StartDate = DATEADD(dd,1,#StartDate)
SET #Teller = 0
IF DATEDIFF(dd,#StartDate,#EndDate) <= 0
BEGIN
SET #Teller = 0
END
ELSE
BEGIN
WHILE
DATEDIFF(dd,#StartDate,#EndDate) >= 0
BEGIN
IF DATEPART(dw,#StartDate) < 6
BEGIN
SET #Teller = #Teller + 1
END
SET #StartDate = DATEADD(dd,1,#StartDate)
END
END
RETURN #Teller
END
I took the various examples here, but in my particular situation we have a #PromisedDate for delivery and a #ReceivedDate for the actual receipt of the item. When an item was received before the "PromisedDate" the calculations were not totaling correctly unless I ordered the dates passed into the function by calendar order. Not wanting to check the dates every time, I changed the function to handle this for me.
Create FUNCTION [dbo].[fnGetBusinessDays]
(
#PromiseDate date,
#ReceivedDate date
)
RETURNS integer
AS
BEGIN
DECLARE #days integer
SELECT #days =
Case when #PromiseDate > #ReceivedDate Then
DATEDIFF(d,#PromiseDate,#ReceivedDate) +
ABS(DATEDIFF(wk,#PromiseDate,#ReceivedDate)) * 2 +
CASE
WHEN DATENAME(dw, #PromiseDate) <> 'Saturday' AND DATENAME(dw, #ReceivedDate) = 'Saturday' THEN 1
WHEN DATENAME(dw, #PromiseDate) = 'Saturday' AND DATENAME(dw, #ReceivedDate) <> 'Saturday' THEN -1
ELSE 0
END +
(Select COUNT(*) FROM CompanyHolidays
WHERE HolidayDate BETWEEN #ReceivedDate AND #PromiseDate
AND DATENAME(dw, HolidayDate) <> 'Saturday' AND DATENAME(dw, HolidayDate) <> 'Sunday')
Else
DATEDIFF(d,#PromiseDate,#ReceivedDate) -
ABS(DATEDIFF(wk,#PromiseDate,#ReceivedDate)) * 2 -
CASE
WHEN DATENAME(dw, #PromiseDate) <> 'Saturday' AND DATENAME(dw, #ReceivedDate) = 'Saturday' THEN 1
WHEN DATENAME(dw, #PromiseDate) = 'Saturday' AND DATENAME(dw, #ReceivedDate) <> 'Saturday' THEN -1
ELSE 0
END -
(Select COUNT(*) FROM CompanyHolidays
WHERE HolidayDate BETWEEN #PromiseDate and #ReceivedDate
AND DATENAME(dw, HolidayDate) <> 'Saturday' AND DATENAME(dw, HolidayDate) <> 'Sunday')
End
RETURN (#days)
END
If you need to add work days to a given date, you can create a function that depends on a calendar table, described below:
CREATE TABLE Calendar
(
dt SMALLDATETIME PRIMARY KEY,
IsWorkDay BIT
);
--fill the rows with normal days, weekends and holidays.
create function AddWorkingDays (#initialDate smalldatetime, #numberOfDays int)
returns smalldatetime as
begin
declare #result smalldatetime
set #result =
(
select t.dt from
(
select dt, ROW_NUMBER() over (order by dt) as daysAhead from calendar
where dt > #initialDate
and IsWorkDay = 1
) t
where t.daysAhead = #numberOfDays
)
return #result
end
As with DATEDIFF, I do not consider the end date to be part of the interval.
The number of (for example) Sundays between #StartDate and #EndDate is the number of Sundays between an "initial" Monday and the #EndDate minus the number of Sundays between this "initial" Monday and the #StartDate. Knowing this, we can calculate the number of workdays as follows:
DECLARE #StartDate DATETIME
DECLARE #EndDate DATETIME
SET #StartDate = '2018/01/01'
SET #EndDate = '2019/01/01'
SELECT DATEDIFF(Day, #StartDate, #EndDate) -- Total Days
- (DATEDIFF(Day, 0, #EndDate)/7 - DATEDIFF(Day, 0, #StartDate)/7) -- Sundays
- (DATEDIFF(Day, -1, #EndDate)/7 - DATEDIFF(Day, -1, #StartDate)/7) -- Saturdays
Best regards!
I borrowed some ideas from others to create my solution. I use inline code to ignore weekends and U.S. federal holidays. In my environment, EndDate may be null, but it will never precede StartDate.
CREATE FUNCTION dbo.ufn_CalculateBusinessDays(
#StartDate DATE,
#EndDate DATE = NULL)
RETURNS INT
AS
BEGIN
DECLARE #TotalBusinessDays INT = 0;
DECLARE #TestDate DATE = #StartDate;
IF #EndDate IS NULL
RETURN NULL;
WHILE #TestDate < #EndDate
BEGIN
DECLARE #Month INT = DATEPART(MM, #TestDate);
DECLARE #Day INT = DATEPART(DD, #TestDate);
DECLARE #DayOfWeek INT = DATEPART(WEEKDAY, #TestDate) - 1; --Monday = 1, Tuesday = 2, etc.
DECLARE #DayOccurrence INT = (#Day - 1) / 7 + 1; --Nth day of month (3rd Monday, for example)
--Increment business day counter if not a weekend or holiday
SELECT #TotalBusinessDays += (
SELECT CASE
--Saturday OR Sunday
WHEN #DayOfWeek IN (6,7) THEN 0
--New Year's Day
WHEN #Month = 1 AND #Day = 1 THEN 0
--MLK Jr. Day
WHEN #Month = 1 AND #DayOfWeek = 1 AND #DayOccurrence = 3 THEN 0
--G. Washington's Birthday
WHEN #Month = 2 AND #DayOfWeek = 1 AND #DayOccurrence = 3 THEN 0
--Memorial Day
WHEN #Month = 5 AND #DayOfWeek = 1 AND #Day BETWEEN 25 AND 31 THEN 0
--Independence Day
WHEN #Month = 7 AND #Day = 4 THEN 0
--Labor Day
WHEN #Month = 9 AND #DayOfWeek = 1 AND #DayOccurrence = 1 THEN 0
--Columbus Day
WHEN #Month = 10 AND #DayOfWeek = 1 AND #DayOccurrence = 2 THEN 0
--Veterans Day
WHEN #Month = 11 AND #Day = 11 THEN 0
--Thanksgiving
WHEN #Month = 11 AND #DayOfWeek = 4 AND #DayOccurrence = 4 THEN 0
--Christmas
WHEN #Month = 12 AND #Day = 25 THEN 0
ELSE 1
END AS Result);
SET #TestDate = DATEADD(dd, 1, #TestDate);
END
RETURN #TotalBusinessDays;
END
That's working for me, in my country on Saturday and Sunday are non-working days.
For me is important the time of #StartDate and #EndDate.
CREATE FUNCTION [dbo].[fnGetCountWorkingBusinessDays]
(
#StartDate as DATETIME,
#EndDate as DATETIME
)
RETURNS INT
AS
BEGIN
DECLARE #res int
SET #StartDate = CASE
WHEN DATENAME(dw, #StartDate) = 'Saturday' THEN DATEADD(dd, 2, DATEDIFF(dd, 0, #StartDate))
WHEN DATENAME(dw, #StartDate) = 'Sunday' THEN DATEADD(dd, 1, DATEDIFF(dd, 0, #StartDate))
ELSE #StartDate END
SET #EndDate = CASE
WHEN DATENAME(dw, #EndDate) = 'Saturday' THEN DATEADD(dd, 0, DATEDIFF(dd, 0, #EndDate))
WHEN DATENAME(dw, #EndDate) = 'Sunday' THEN DATEADD(dd, -1, DATEDIFF(dd, 0, #EndDate))
ELSE #EndDate END
SET #res =
(DATEDIFF(hour, #StartDate, #EndDate) / 24)
- (DATEDIFF(wk, #StartDate, #EndDate) * 2)
SET #res = CASE WHEN #res < 0 THEN 0 ELSE #res END
RETURN #res
END
GO
Create function like:
CREATE FUNCTION dbo.fn_WorkDays(#StartDate DATETIME, #EndDate DATETIME= NULL )
RETURNS INT
AS
BEGIN
DECLARE #Days int
SET #Days = 0
IF #EndDate = NULL
SET #EndDate = EOMONTH(#StartDate) --last date of the month
WHILE DATEDIFF(dd,#StartDate,#EndDate) >= 0
BEGIN
IF DATENAME(dw, #StartDate) <> 'Saturday'
and DATENAME(dw, #StartDate) <> 'Sunday'
and Not ((Day(#StartDate) = 1 And Month(#StartDate) = 1)) --New Year's Day.
and Not ((Day(#StartDate) = 4 And Month(#StartDate) = 7)) --Independence Day.
BEGIN
SET #Days = #Days + 1
END
SET #StartDate = DATEADD(dd,1,#StartDate)
END
RETURN #Days
END
You can call the function like:
select dbo.fn_WorkDays('1/1/2016', '9/25/2016')
Or like:
select dbo.fn_WorkDays(StartDate, EndDate)
from table1
Create Function dbo.DateDiff_WeekDays
(
#StartDate DateTime,
#EndDate DateTime
)
Returns Int
As
Begin
Declare #Result Int = 0
While #StartDate <= #EndDate
Begin
If DateName(DW, #StartDate) not in ('Saturday','Sunday')
Begin
Set #Result = #Result +1
End
Set #StartDate = DateAdd(Day, +1, #StartDate)
End
Return #Result
End
I found the below TSQL a fairly elegant solution (I don't have permissions to run functions). I found the DATEDIFF ignores DATEFIRST and I wanted my first day of the week to be a Monday. I also wanted the first working day to be set a zero and if it falls on a weekend Monday will be a zero. This may help someone who has a slightly different requirement :)
It does not handle bank holidays
SET DATEFIRST 1
SELECT
,(DATEDIFF(DD, [StartDate], [EndDate]))
-(DATEDIFF(wk, [StartDate], [EndDate]))
-(DATEDIFF(wk, DATEADD(dd,-##DATEFIRST,[StartDate]), DATEADD(dd,-##DATEFIRST,[EndDate]))) AS [WorkingDays]
FROM /*Your Table*/
One approach is to 'walk the dates' from start to finish in conjunction with a case expression which checks if the day is not a Saturday or a Sunday and flagging it(1 for weekday, 0 for weekend). And in the end just sum flags(it would be equal to the count of 1-flags as the other flag is 0) to give you the number of weekdays.
You can use a GetNums(startNumber,endNumber) type of utility function which generates a series of numbers for 'looping' from start date to end date. Refer http://tsql.solidq.com/SourceCodes/GetNums.txt for an implementation. The logic can also be extended to cater for holidays(say if you have a holidays table)
declare #date1 as datetime = '19900101'
declare #date2 as datetime = '19900120'
select sum(case when DATENAME(DW,currentDate) not in ('Saturday', 'Sunday') then 1 else 0 end) as noOfWorkDays
from dbo.GetNums(0,DATEDIFF(day,#date1, #date2)-1) as Num
cross apply (select DATEADD(day,n,#date1)) as Dates(currentDate)

SQL Server if condition based on date

I need help with, giving the if exists and if not exists condition.
I want to write an if condition, based on date, such that, if any row exists in table1 on current date TARGET_DT for a particular job, then do some action
else do other action.
IF EXISTS(SELECT 1 FROM COD_BLU_OPENING_VOLUME
WHERE TARGET_DT = CONVERT(DATETIME, GETDATE(), 101) AND JOBID = #OPENJOB)
BEGIN
IF(SELECT COUNT(1) FROM COD_BLU_INVENTORY WITH(NOLOCK)
WHERE (JOBID = #JOB AND STATUS = 'A' AND TARGETDT = CONVERT(DATETIME, #TARGETDT, 101))) = 0
BEGIN
INSERT INTO COD_BLU_INVENTORY(JOBID, TARGETDT, CARRYOVER, FRESHVOL, TOTALVOL, STATUS, UPDATEDBY, UPDATEDT, OPENINGVOL)
VALUES(#JOB, #TARGETDT, #CARRYOVERVOL, #FRESHVOL, #TOTALVOL, 'A', #EMPCODE, GETDATE(), #CARRYOVERVOL)
SELECT '1'
END
ELSE
BEGIN
SELECT'0'
END
END
ELSE IF NOT EXISTS(SELECT 1 FROM COD_BLU_OPENING_VOLUME
WHERE TARGET_DT = CONVERT(DATETIME, GETDATE(), 101)
AND TARGET_DT < CONVERT(DATETIME, GETDATE(), 101)
AND JOBID = #OPENJOB)
BEGIN
INSERT INTO COD_BLU_OPENING_VOLUME(TARGET_DT, JOBID, OPENING_COUNT, UPDATED_BY, UPDATED_DT, PRE_ASSIGNEDCOUNT)
VALUES(#OPENTARDT, #OPENJOB, #OPENCNT, #OPENEMPCODE, GETDATE(), #PREOPENCNT)
IF (SELECT COUNT(1) FROM COD_BLU_INVENTORY WITH(NOLOCK)
WHERE (JOBID = #JOB AND STATUS = 'A'
AND TARGETDT = CONVERT(DATETIME, #TARGETDT, 101))) = 0
BEGIN
INSERT INTO COD_BLU_INVENTORY(JOBID, TARGETDT, CARRYOVER, FRESHVOL, TOTALVOL, STATUS, UPDATEDBY, UPDATEDT, OPENINGVOL)
VALUES(#JOB, #TARGETDT, #CARRYOVERVOL, #FRESHVOL, #TOTALVOL, 'A', #EMPCODE, GETDATE(), #CARRYOVERVOL)
SELECT '1'
END
ELSE
BEGIN
SELECT '0'
END
END
but the problem is that if the TARGET_DT is given as future date, it executes the else condition, I want it to execute the if condition, the above is the code which I tired. Any help in solving this will be appreciated. Please help.
IF EXISTS(SELECT 1 FROM COD_BLU_OPENING_VOLUME WHERE ((TARGET_DT=CONVERT(DATETIME,GETDATE(),101) OR TARGET_DT>CONVERT(DATETIME,GETDATE(),101)) AND JOBID=#OPENJOB))
BEGIN
IF(SELECT COUNT(1) FROM COD_BLU_INVENTORY WITH(NOLOCK) WHERE (JOBID=#JOB AND STATUS='A' AND (TARGET_DT=CONVERT(DATETIME,GETDATE(),101) OR TARGET_DT>CONVERT(DATETIME,GETDATE(),101))))=0
BEGIN
INSERT INTO COD_BLU_INVENTORY(JOBID,TARGETDT,CARRYOVER,FRESHVOL,TOTALVOL,STATUS,UPDATEDBY,UPDATEDT,OPENINGVOL)
VALUES(#JOB,#TARGETDT,#CARRYOVERVOL,#FRESHVOL,#TOTALVOL,'A',#EMPCODE,GETDATE(),#CARRYOVERVOL)
SELECT '1'
END
ELSE
BEGIN
SELECT'0'
END
END
ELSE IF NOT EXISTS(SELECT 1 FROM COD_BLU_OPENING_VOLUME WHERE ((TARGET_DT=CONVERT(DATETIME,GETDATE(),101) OR TARGET_DT<CONVERT(DATETIME,GETDATE(),101)) AND JOBID=#OPENJOB))
BEGIN
INSERT INTO COD_BLU_OPENING_VOLUME(TARGET_DT,JOBID,OPENING_COUNT,UPDATED_BY,UPDATED_DT,PRE_ASSIGNEDCOUNT)
VALUES(#OPENTARDT,#OPENJOB,#OPENCNT,#OPENEMPCODE,GETDATE(),#PREOPENCNT)
IF (SELECT COUNT(1) FROM COD_BLU_INVENTORY WITH(NOLOCK) WHERE (JOBID=#JOB AND STATUS='A' AND TARGETDT = CONVERT(DATETIME,#TARGETDT,101)))=0
BEGIN
INSERT INTO COD_BLU_INVENTORY(JOBID,TARGETDT,CARRYOVER,FRESHVOL,TOTALVOL,STATUS,UPDATEDBY,UPDATEDT,OPENINGVOL)
VALUES(#JOB,#TARGETDT,#CARRYOVERVOL,#FRESHVOL,#TOTALVOL,'A',#EMPCODE,GETDATE(),#CARRYOVERVOL)
SELECT '1'
END
ELSE
BEGIN
SELECT'0'
END
END
END

Add Hours to Current Date But it excludes the holidays and weekends

I have a task to create the T-SQL function to add the 12 hours with current date and time but it should not include any holidays/weekends.
ALTER FUNCTION [dbo].[GetNextWorkingDay_Custom] (#givenDate DATETIME)
RETURNS DATE
AS
BEGIN
DECLARE #StartDate DATETIME
DECLARE #EndDate DATETIME
DECLARE #DiffDate INT
DECLARE #workingDate DATETIME
IF (DATENAME(dw , #givenDate) = 'Friday')
BEGIN
SET #workingDate = DATEADD(day, 3, #givenDate)
END
ELSE IF (DATENAME(dw , #givenDate) = 'Saturday')
BEGIN
SET #workingDate = DATEADD(day, 2, #givenDate)
END
ELSE
BEGIN
SET #workingDate = DATEADD(day, 1, #givenDate)
END
SELECT #StartDate = START_DATE
,#EndDate = END_DATE
FROM special_time WHERE START_DATE = #workingDate AND IS_HOLIDAY = 1
-- Select count(*) from tblHolidays where holdate = #workingDate
while ((select count(*) from special_time WHERE START_DATE = #workingDate AND IS_HOLIDAY = 1) > 0)
begin
set #DiffDate = DATEDIFF(day,#StartDate,#EndDate)
set #workingDate = dateadd(dd,#DiffDate,#WorkingDate)
end
-- if adding a day makes it a Saturday, add 2 more to get to Monday (and test to make sure the week doesn't start with a holiday)
IF (DATENAME(dw , #workingDate) = 'Saturday')
BEGIN
SET #workingDate = DATEADD(day, 2, #workingDate)
END
RETURN #workingDate
END
Create a static dates lookup table (you can find many guides on how to do this via google, such as https://www.mssqltips.com/sqlservertip/4054/creating-a-date-dimension-or-calendar-table-in-sql-server/) and then add a column that signifies whether or not that date is a working day or a holiday.
Your function is then as simple as:
select min(DateValue) as NextWorkingDay
from DatesLookupTable
where DateValue > #GivenDate
and WorkingDay = 1

T-SQL query to compare date and find if column entry exists for a month

I have a two columns named "Statement Date" and "Account Number". For some account numbers statements are uploaded monthly, for some quarterly and for some annually and the date on which these statements are uploaded is saved in "statement date"
I want to check if for a particular a statement is uploaded or not.
For e.g- "account number"-2345426576 "statement date"-5/30/2016, I have to check if there is entry for previous month(4/30/2016) and next month(6/30/2016). If entry is absent then it should return missing for that month.
If dates are to be compared of different tables and then find if a statement is missing or not.i.e if t1.date>t2.date then check t1.date is how many month greater. Same goes if t1.date
Create a function for this:
CREATE FUNCTION [dbo].[IsUploaded]
(
#AccountNo int,
#Date datetime
)
RETURNS bit
AS
BEGIN
DECLARE #MonthStart datetime
DECLARE #NextMonthStart datetime
-- current month
SET #MonthStart = DATEADD(month, DATEDIFF(month, 0, #Date), 0)
SET #NextMonthStart = DATEADD(month, 1, #MonthStart)
IF EXISTS (SELECT 1 From UploadedStatememtsTable WHERE UploadDate >= #MonthStart AND UploadDate < #NextMonthStart AND AccountNo = #AccountNo)
RETURN 1
-- next month
SET #MonthStart = #NextMonthStart
SET #NextMonthStart = DATEADD(month, 1, #MonthStart)
IF EXISTS (SELECT 1 From UploadedStatememtsTable WHERE UploadDate >= #MonthStart AND UploadDate < #NextMonthStart AND AccountNo = #AccountNo)
RETURN 1
-- previous month
SET #MonthStart = DATEADD(month, -2, #MonthStart)
SET #NextMonthStart = DATEADD(month, 1, #MonthStart)
IF EXISTS (SELECT 1 From UploadedStatememtsTable WHERE UploadDate >= #MonthStart AND UploadDate < #NextMonthStart AND AccountNo = #AccountNo)
RETURN 1
RETURN 0
END
Try this
SELECT
T.accountnumber,
T.statementdate,
CASE
WHEN
EXISTS
(
SELECT TOP 1 1 FROM Tbl tmp
WHERE
tmp.accountnumber = T.accountnumber AND
tmp.statementdate = DATEADD(mm, 1, T.statementdate) -- Next month
) AND
EXISTS
(
SELECT TOP 1 1 FROM Tbl tmp
WHERE
tmp.accountnumber = T.accountnumber AND
tmp.statementdate = DATEADD(mm, -1, T.statementdate)-- Priv month
)
THEN 'There is entry'
ELSE 'Missing' END
FROM
Tbl T

Handling multiple records in a MS SQL trigger

I am having to use triggers in MSSQL for the first time, well triggers in general. Having read around and tested this myself I realise now that a trigger fires per command and not per row inserted, deleted or updated.
The entire thing is some statistics for an advertising system. Our main stat table is rather large and doesn't contain the data in a way that makes sense in most cases. It contains one row per advert clicked, viewed and etc. As a user one is more inclined to want to view this as day X has Y amount of clicks and Z amount of views and so forth. We have done this purely based on a SQL query so far, getting this sort of report from the main table, but as the table has grown so does the time for that query to execute. Because of this we have opted for using triggers to keep another table updated and hence making this a bit easier on the SQL server.
My issue is now to get this working with multiple records. What I have done is to create 2 stored procedures, one for handling the operation of an insert, and one for a delete. My insert trigger (written to work with a single record) then graps the data off the Inserted table, and sends it off to the stored procedure. The delete trigger works in the same way, and (obviously?) the update trigger does the same as a delete + an insert.
My issue is now how to best do this with multiple records. I have tried using a cursor, but as far as I have been able to read and see myself, this performs really badly. I have considered writing some "checks" as well - as in checking to see IF there are multiple records in the commands and then go with the cursor, and otherwise simply just avoid this. Anyhow, here's my solution with a cursor, and im wondering if there's a way of doing this better?
CREATE TRIGGER [dbo].[TR_STAT_INSERT]
ON [iqdev].[dbo].[Stat]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
DECLARE #Date DATE
DECLARE #CampaignId BIGINT
DECLARE #CampaignName varchar(500)
DECLARE #AdvertiserId BIGINT
DECLARE #PublisherId BIGINT
DECLARE #Unique BIT
DECLARE #Approved BIT
DECLARE #PublisherEarning money
DECLARE #AdvertiserCost money
DECLARE #Type smallint
DECLARE InsertCursor CURSOR FOR SELECT Id FROM Inserted
DECLARE #curId bigint
OPEN InsertCursor
FETCH NEXT FROM InsertCursor INTO #curId
WHILE ##FETCH_STATUS = 0
BEGIN
SELECT #Date = [Date], #PublisherId = [PublisherCustomerId], #Approved = [Approved], #Unique = [Unique], #Type = [Type], #AdvertiserCost = AdvertiserCost, #PublisherEarning = PublisherEarning
FROM Inserted
WHERE Id = #curId
SELECT #CampaignId = T1.CampaignId, #CampaignName = T2.Name, #AdvertiserId = T2.CustomerId
FROM Advert AS T1
INNER JOIN Campaign AS T2 on T1.CampaignId = T2.Id
WHERE T1.Id = (SELECT AdvertId FROM Inserted WHERE Id = #curId)
EXEC ProcStatInsertTrigger #Date, #CampaignId, #CampaignName, #AdvertiserId, #PublisherId, #Unique, #Approved, #PublisherEarning, #AdvertiserCost, #Type
FETCH NEXT FROM InsertCursor INTO #curId
END
CLOSE InsertCursor
DEALLOCATE InsertCursor
END
The stored procedure is rather big and intense and I do not think there's a way of having to avoid looping through the records of the Inserted table in one way or another (ok, maybe there is, but I'd like to be able to read the code too :p), so I'm not gonna bore you with that one (unless you like to think otherwise). So pretty much, is there a better way of doing this, and if so, how?
EDIT: Well after request, here's the sproc
CREATE PROCEDURE ProcStatInsertTrigger
#Date DATE,
#CampaignId BIGINT,
#CampaignName varchar(500),
#AdvertiserId BIGINT,
#PublisherId BIGINT,
#Unique BIT,
#Approved BIT,
#PublisherEarning money,
#AdvertiserCost money,
#Type smallint
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
IF #Approved = 1
BEGIN
DECLARE #test bit
SELECT #test = 1 FROM CachedStats WHERE [Date] = #Date AND CampaignId = #CampaignId AND CustomerId = #PublisherId
IF #test IS NULL
BEGIN
INSERT INTO CachedStats ([Date], CustomerId, CampaignId, CampaignName) VALUES (#Date, #PublisherId, #CampaignId, #CampaignName)
END
SELECT #test = NULL
DECLARE #Clicks int
DECLARE #TotalAdvertiserCost money
DECLARE #TotalPublisherEarning money
DECLARE #PublisherCPC money
DECLARE #AdvertiserCPC money
SELECT #Clicks = Clicks, #TotalAdvertiserCost = AdvertiserCost + #AdvertiserCost, #TotalPublisherEarning = PublisherEarning + #PublisherEarning FROM CachedStats
WHERE [Date] = #Date AND CustomerId = #PublisherId AND CampaignId = #CampaignId
IF #Type = 0 -- If click add one to the calculation
BEGIN
SELECT #Clicks = #Clicks + 1
END
IF #Clicks > 0
BEGIN
SELECT #PublisherCPC = #TotalPublisherEarning / #Clicks, #AdvertiserCPC = #TotalAdvertiserCost / #Clicks
END
ELSE
BEGIN
SELECT #PublisherCPC = 0, #AdvertiserCPC = 0
END
IF #Type = 0
BEGIN
UPDATE CachedStats SET
Clicks = #Clicks,
UniqueClicks = UniqueClicks + #Unique,
PublisherEarning = #TotalPublisherEarning,
AdvertiserCost = #TotalAdvertiserCost,
PublisherCPC = #PublisherCPC,
AdvertiserCPC = #AdvertiserCPC
WHERE [Date] = #Date AND CustomerId = #PublisherId AND CampaignId = #CampaignId
END
ELSE IF #Type = 1 OR #Type = 4 -- lead or coreg
BEGIN
UPDATE CachedStats SET
Leads = Leads + 1,
PublisherEarning = #TotalPublisherEarning,
AdvertiserCost = #TotalAdvertiserCost,
AdvertiserCPC = #AdvertiserCPC,
PublisherCPC = #AdvertiserCPC
WHERE [Date] = #Date AND CustomerId = #PublisherId AND CampaignId = #CampaignId
END
ELSE IF #Type = 3 -- Isale
BEGIN
UPDATE CachedStats SET
Leads = Leads + 1,
PublisherEarning = #TotalPublisherEarning,
AdvertiserCost = #TotalAdvertiserCost,
AdvertiserCPC = #AdvertiserCPC,
PublisherCPC = #AdvertiserCPC,
AdvertiserOrderValue = #AdvertiserCost,
PublisherOrderValue = #PublisherEarning
WHERE [Date] = #Date AND CustomerId = #PublisherId AND CampaignId = #CampaignId
END
ELSE IF #Type = 2 -- View
BEGIN
UPDATE CachedStats SET
[Views] = [Views] + 1,
UniqueViews = UniqueViews + #Unique,
PublisherEarning = #TotalPublisherEarning,
AdvertiserCost = #TotalAdvertiserCost,
PublisherCPC = #PublisherCPC,
AdvertiserCPC = #AdvertiserCPC
WHERE [Date] = #Date AND CustomerId = #PublisherId AND CampaignId = #CampaignId
END
END
END
After help, here's my final result, posted in case others have a similiar issue
CREATE TRIGGER [dbo].[TR_STAT_INSERT]
ON [iqdev].[dbo].[Stat]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON
-- insert all missing "CachedStats" rows
INSERT INTO
CachedStats ([Date], AdvertId, CustomerId, CampaignId, CampaignName)
SELECT DISTINCT
CONVERT(Date, i.[Date]), i.AdvertId, i.[PublisherCustomerId], c.Id, c.Name
FROM
Inserted i
INNER JOIN Advert AS a ON a.Id = i.AdvertId
INNER JOIN Campaign AS c ON c.Id = a.CampaignId
WHERE
i.[Approved] = 1
AND NOT EXISTS (
SELECT 1
FROM CachedStats as t
WHERE
[Date] = CONVERT(Date, i.[Date])
AND CampaignId = c.Id
AND CustomerId = i.[PublisherCustomerId]
AND t.AdvertId = i.AdvertId
)
-- update all affected records at once
UPDATE
CachedStats
SET
Clicks =
Clicks + (
SELECT COUNT(*) FROM Inserted s
WHERE s.Approved = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
AND s.[Type] = 0
),
UniqueClicks =
UniqueClicks + (
SELECT COUNT(*) FROM Inserted s
WHERE s.Approved = 1
AND s.[Unique] = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
AND s.[Type] = 0
),
[Views] =
[Views] + (
SELECT COUNT(*) FROM Inserted s
WHERE s.Approved = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
AND s.[Type] = 2
),
UniqueViews =
UniqueViews + (
SELECT COUNT(*) FROM Inserted s
WHERE s.Approved = 1
AND s.[Unique] = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
AND s.[Type] = 2
),
Leads =
Leads + (
SELECT COUNT(*) FROM Inserted s
WHERE s.Approved = 1
AND s.[Unique] = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
AND s.[Type] IN (1,3,4)
),
PublisherEarning =
CachedStats.PublisherEarning + ISNULL((
SELECT SUM(PublisherEarning) FROM Inserted s
WHERE s.Approved = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
), 0),
AdvertiserCost =
CachedStats.AdvertiserCost + ISNULL((
SELECT SUM(AdvertiserCost) FROM Inserted s
WHERE s.Approved = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
), 0),
PublisherOrderValue =
PublisherOrderValue + ISNULL((
SELECT SUM(PublisherEarning) FROM Inserted s
WHERE s.Approved = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
AND s.[Type] = 3
), 0),
AdvertiserOrderValue =
AdvertiserOrderValue + ISNULL((
SELECT SUM(AdvertiserCost) FROM Inserted s
WHERE s.Approved = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
AND s.[Type] = 3
), 0),
PublisherCPC =
CASE WHEN (Clicks + (
SELECT COUNT(*) FROM Inserted s
WHERE s.Approved = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
AND s.[Type] = 0
)) > 0 THEN
(CachedStats.PublisherEarning + ISNULL((
SELECT SUM(PublisherEarning) FROM Inserted s
WHERE s.Approved = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
), 0)) -- COST ^
/ (
Clicks + (
SELECT COUNT(*) FROM Inserted s
WHERE s.Approved = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
AND s.[Type] = 0
)
) --- Clicks ^
ELSE
0
END,
AdvertiserCPC =
CASE WHEN (Clicks + (
SELECT COUNT(*) FROM Inserted s
WHERE s.Approved = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
AND s.[Type] = 0
)) > 0 THEN
(CachedStats.AdvertiserCost + ISNULL((
SELECT SUM(AdvertiserCost) FROM Inserted s
WHERE s.Approved = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
), 0)) -- COST ^
/ (
Clicks + (
SELECT COUNT(*) FROM Inserted s
WHERE s.Approved = 1
AND s.PublisherCustomerId = i.PublisherCustomerId
AND CONVERT(Date, s.[Date]) = CONVERT(Date, i.[Date])
AND s.AdvertId = i.AdvertId
AND s.[Type] = 0
)
) --- Clicks ^
ELSE
0
END
FROM
Inserted i
WHERE
i.Approved = 1 AND
CachedStats.Advertid = i.AdvertId AND
CachedStats.[Date] = Convert(Date, i.[Date]) AND
CachedStats.CustomerId = i.PublisherCustomerId
SET NOCOUNT OFF
END
It looks slightly different now because I had to index it per advertisement too - but thanks alot for the help - sped everything up from 30hour+ to 30 sec to generate the CachedStats from my own development Stat table :)
The trick with these kinds of situations is to turn the sequential operation (for each record do xyz) into a set-based operation (an UPDATE statement).
I have analyzed your stored procedure and merged your separate UPDATE statements into a single one. This single statement can then be transformed into a version that can be applied to all inserted records at once, eliminating the need for a stored procedure and thereby the need for a cursor.
EDIT: Below is the code that we finally got working. Execution time for the whole operation went down from "virtually forever" (for the original solution) to something under one second, according to the OP's feedback. Overall code size also decreased quite noticeably.
CREATE TRIGGER [dbo].[TR_STAT_INSERT]
ON [iqdev].[dbo].[Stat]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON
-- insert all missing "CachedStats" rows
INSERT INTO
CachedStats ([Date], AdvertId, CustomerId, CampaignId, CampaignName)
SELECT DISTINCT
CONVERT(Date, i.[Date]), i.AdvertId, i.PublisherCustomerId, c.Id, c.Name
FROM
Inserted i
INNER JOIN Advert a ON a.Id = i.AdvertId
INNER JOIN Campaign c ON c.Id = a.CampaignId
WHERE
i.Approved = 1
AND NOT EXISTS (
SELECT 1
FROM CachedStats
WHERE Advertid = i.AdvertId AND
CustomerId = i.PublisherCustomerId AND
[Date] = CONVERT(DATE, i.[Date])
)
-- update all affected records at once
UPDATE
CachedStats
SET
Clicks = Clicks + i.AddedClicks,
UniqueClicks = UniqueClicks + i.AddedUniqueClicks,
[Views] = [Views] + i.AddedViews,
UniqueViews = UniqueViews + i.AddedUniqueViews,
Leads = Leads + i.AddedLeads,
PublisherEarning = PublisherEarning + ISNULL(i.AddedPublisherEarning, 0),
AdvertiserCost = AdvertiserCost + ISNULL(i.AddedAdvertiserCost, 0),
PublisherOrderValue = PublisherOrderValue + ISNULL(i.AddedPublisherOrderValue, 0),
AdvertiserOrderValue = AdvertiserOrderValue + ISNULL(i.AddedAdvertiserOrderValue, 0)
FROM
(
SELECT
AdvertId,
CONVERT(DATE, [Date]) [Date],
PublisherCustomerId,
COUNT(*) NumRows,
SUM(CASE WHEN Type IN (0) THEN 1 ELSE 0 END) AddedClicks,
SUM(CASE WHEN Type IN (0) AND [Unique] = 1 THEN 1 ELSE 0 END) AddedUniqueClicks,
SUM(CASE WHEN Type IN (2) THEN 1 ELSE 0 END) AddedViews,
SUM(CASE WHEN Type IN (2) AND [Unique] = 1 THEN 1 ELSE 0 END) AddedUniqueViews,
SUM(CASE WHEN Type IN (1,3,4) AND [Unique] = 1 THEN 1 ELSE 0 END) AddedLeads,
SUM(PublisherEarning) AddedPublisherEarning,
SUM(AdvertiserCost) AddedAdvertiserCost,
SUM(CASE WHEN Type IN (3) THEN PublisherOrderValue ELSE 0 END) AddedPublisherOrderValue,
SUM(CASE WHEN Type IN (3) THEN AdvertiserOrderValue ELSE 0 END) AddedAdvertiserOrderValue
FROM
Inserted
WHERE
Approved = 1
GROUP BY
AdvertId,
CONVERT(DATE, [Date]),
PublisherCustomerId
) i
INNER JOIN CachedStats cs ON
cs.Advertid = i.AdvertId AND
cs.CustomerId = i.PublisherCustomerId AND
cs.[Date] = i.[Date]
SET NOCOUNT OFF
END
The operations involving the CachedStats table will greatly benefit from one multiple-column index over (Advertid, CustomerId, [Date]) (as confirmed by the OP).
Depending on what version of MSSQL you are running, you should also consider using Indexed Views for this as well. That could very well be a simpler approach than your triggers, depending on what the report query looks like. See here for more info.
Also, in your trigger, you should try to write your updates to the materialized results table as a set based operation, not a cursor. Writing a cursor based trigger could potentially just be moving your problem from the report query to your table inserts instead.
First thing I would do is use a FAST_FORWARD cursor instead. As you are only going from one record to the next and not doing any updates this will be much better for performance.
DECLARE CURSOR syntax
You can slightly optimize your cursor variation by doing FAST_FORWARD, READ_ONLY and LOCAL options on the cursor. Also, you're pulling the Id into your cursor, and then looping back to get the values. Either use CURRENT_OF or throw them all into variables. But, I wouldn't expect these changes to buy you much.
You really need to move to a set based approach. That stored proc is definitely doable in a set based model - though it may take 3 or 4 different update statements. But even 3 or 4 different triggers (1 for views, 1 for clicks, etc.) would be better than the cursor approach.
Your best bet is to move to a set based operation in the trigger. I'm not going write this for you 100% but let me get you started, and we can see where we go from there. Keep in mind I am writting this without tables / schemas and so I'm not going validate. Expect Typos:-)
Let's look at your update statements first, From what I can tell you are updating the same table with the same where clause the only difference is the columns. You can consolidate this to look like:
UPDATE CachedStats SET
/* Basically we are going to set the counts based on the type inline in the update clause*/
Leads= CASE WHEN (#Type = 1 OR #Type = 4 OR #Type=3 ) THEN Leads + 1 ELSE LEADS END,
Clicks=CASE WHEN (#Type=0) THEN Clicks+1 ELSE Clicks END,
Views=CASE WHEN (#Type=4) THEN Views+1 ELSE Views END,
PublisherEarning = #PublisherEarning + PublisherEarning,
AdvertiserCost = #AdvertiserCost +AdvertiserCost,
FROM CachedStats CS
INNER JOIN Inserted INS
ON CS.Date=Inserted.Date AND CS.CustomerId=Ins.PublisherId AND CS.CampaignId=Ins.CampaignId
I do aggree with you that this could get ugly but that's a decision you'll have to make.
As for your insert clause, I would handle that the same way you already are just insert into the table from the Inserted table whatever doesn't already exist.

Resources