How to avoid repetitive CASE statements in SQL WHILE loop doing INSERT - sql

Is there a way to simplify this script so that the CASE statements are not duplicated? It can look acceptable in this shortened example but in reality the CASE statement is much longer as I have cases for "every 2 weeks, "every 4 weeks", "monthly", etc. I am using SQL Server and a WHILE statement for performance reason. Would a CTE or MERGE help?
DECLARE #theStartDate DATE
DECLARE #Interval INT
DECLARE #eventCharges TABLE
(
[EventDate] [datetime],
Person_Id int
)
SET #today = GETDATE()
SET #Interval = 0
-- delete event charges from previous user
DELETE FROM #eventCharges
-- Insert the calculated transactions
WHILE #Interval < 100
BEGIN
SET #Interval = #Interval + 1
INSERT INTO #eventCharges
SELECT
CASE
WHEN pcc.Recurrence = 'Daily'
THEN DATEADD(DAY, #Interval, #theStartDate)
WHEN pcc.Recurrence = 'Weekly'
THEN DATEADD(WEEK, #Interval, #theStartDate)
END AS EventDate
,pcc.Person_Id
FROM #personChargeCurrent pcc
WHERE CASE
WHEN pcc.Recurrence = 'Daily'
THEN DATEADD(DAY, #Interval, #theStartDate)
WHEN pcc.Recurrence = 'Weekly'
THEN DATEADD(WEEK, #Interval, #theStartDate)
END <= #today
AND NOT EXISTS(SELECT 1 FROM dbo.PersonChargeTransaction pct
WHERE pct.Person_Id = pcc.Person_Id
AND pct.PersonCharge_Id = pcc.Id
AND pct.TransactionDate =
CASE
WHEN pcc.Recurrence = 'Daily'
THEN DATEADD(DAY, #Interval, #theStartDate)
WHEN pcc.Recurrence = 'Weekly'
THEN DATEADD(WEEK, #Interval, #theStartDate)
END)
ORDER BY StartDate
END

You can wrap this in a function:
Create Function dbo.IntervalEnd(
#recurrence varchar(10),
#interval int,
#startDate date -- or whatever data type you're using for dates
) returns date as
begin
return case
when #recurrence = 'Daily' then dateadd(day, #interval, #startDate)
when #recurrence = 'Weekly' then dateadd(week, #interval, #startDate)
end
end
Then
Insert Into #eventCharges
Select
dbo.IntervalEnd(pcc.Recurrence, #Interval, #theStartDate) as EventDate,
pcc.Person_Id
From
#personChargeCurrent pcc
Where
dbo.IntervalEnd(pcc.Recurrence, #Interval, #theStartDate) <= #today And
Not Exists (
Select
1
From
dbo.PersonChargeTransaction pct
Where
pct.Person_Id = pcc.Person_Id And
pct.PersonCharge_Id = pcc.Id And
pct.TransactionDate
= dbo.IntervalEnd(pcc.Recurrence, #Interval, #theStartDate)
)
There are overheads for using a function. You'll have to decide if the slightly reduced performance is worth the tradeoff for increased readibility.

Yes, a CTE should help. Try changing your INSERT statement to:
WITH cte as
(SELECT CASE
WHEN Recurrence = 'Daily'
THEN DATEADD(DAY, #Interval, #theStartDate)
WHEN Recurrence = 'Weekly'
THEN DATEADD(WEEK, #Interval, #theStartDate)
END AS EventDate,
p.*
FROM #personChargeCurrent p)
INSERT INTO #eventCharges
SELECT cte.EventDate, cte.Person_Id
FROM cte
WHERE cte.EventDate <= #today AND
NOT EXISTS
(SELECT 1
FROM dbo.PersonChargeTransaction pct
WHERE pct.Person_Id = cte.Person_Id AND
pct.PersonCharge_Id = cte.Id AND
pct.TransactionDate = cte.EventDate)
ORDER BY StartDate

You could create a temporary table in which to insert the calculated fields and then use that table with a join to insert the data / apply the conditions

Related

Is there an efficient way to break a date range into hours per day?

In SQL Server I am attempting to break a date range into hours per day and have the following bit of code which is OK for a short time frame, but rather inefficient for longer periods of time. Could anyone suggest a more efficient approach?
DECLARE #StartDate datetime = '2015-01-27 07:32:35.000',
#EndDate datetime = '2015-04-29 14:39:35.000',
#TempDate datetime = '';
SET #TempDate = #StartDate;
DECLARE #dateTimeTable TABLE (dt datetime, minCol INT);
WHILE #TempDate < #EndDate
BEGIN
INSERT INTO #dateTimeTable VALUES (CONVERT(date,#TempDate), 1)
SET #TempDate = DATEADD(minute,1,#TempDate)
END
Select dt,
FORMAT(SUM(minCol) / 60.0,'F') as Hours
from #dateTimeTable
GROUP BY dt
Thanks,
Carl
The best way would be to use recursive cte :
DECLARE #StartDate datetime = '2015-01-27 07:32:35.000',
#EndDate datetime = '2015-04-29 14:39:35.000';
WITH cte AS (
SELECT CAST(#StartDate AS DATE) startdate,DATEDIFF(minute, #StartDate, DATEADD(DAY, 1, CAST(#StartDate AS DATE) ) ) / 60.0 hours
UNION ALL
SELECT DATEADD(DAY,1, startdate), DATEDIFF(minute, DATEADD(DAY,1, startdate), CASE WHEN DATEADD(DAY,2, startdate) > #EndDate
THEN #enddate ELSE DATEADD(DAY,2, startdate) END) / 60.0
FROM cte
WHERE startdate <> CAST(#EndDate AS DATE)
)
SELECT * FROM cte
db<>fiddle here

Difference in Days Between Two Dates with Years, Months, and Days

I'm not a SQL expert, would you know if it's possible to have a query provide Days Between Two Dates to count days outstanding with today's date and output: Years, Months, Days and outstanding days providing 30 Days, 60 Days, 90 Days?
If I Declare "Years, Months, Days" from "Check_Date" and "Created_Date".
SQL provides a separate window, and, I only see Declared "FromDate" time
with "ToDate" displaying total Years, Months, Days. I am looking for a way
to have results provide a roll BY "Created_Date" records and include:
Years, Months, Days and outstanding days providing 30 Days, 60 Days, 90 Days? Can you suggest something with a similar result?
Scrip Code:
DECLARE #FromDate DATETIME = '2015-01-01 00:00:00',
#ToDate DATETIME = '2019-09-18 00:00:00',
#Years INT, #Months INT, #Days INT, #tmpFromDate DATETIME
SET #Years = DATEDIFF(YEAR, #FromDate, #ToDate)
- (CASE WHEN DATEADD(YEAR, DATEDIFF(YEAR, #FromDate, #ToDate),
#FromDate) > #ToDate THEN 1 ELSE 0 END)
SET #tmpFromDate = DATEADD(YEAR, #Years , #FromDate)
SET #Months = DATEDIFF(MONTH, #tmpFromDate, #ToDate)
- (CASE WHEN DATEADD(MONTH,DATEDIFF(MONTH, #tmpFromDate, #ToDate),
#tmpFromDate) > #ToDate THEN 1 ELSE 0 END)
SET #tmpFromDate = DATEADD(MONTH, #Months , #tmpFromDate)
SET #Days = DATEDIFF(DAY, #tmpFromDate, #ToDate)
- (CASE WHEN DATEADD(DAY, DATEDIFF(DAY, #tmpFromDate, #ToDate),
#tmpFromDate) > #ToDate THEN 1 ELSE 0 END)
SELECT #FromDate FromDate, #ToDate ToDate,
#Years Years, #Months Months, #Days Days
SELECT DISTINCT
ge.Name, --table columns
ge.Entity_Type,
ge.Entity_Number,
bc.Super_Entity_ID,
ch.Check_Date, --check created
ch.Created_Date, --if payment was received
ch.Check_Number,
ch.Amount,
vn.Vendor_Name
Check_Date,Created_Date,DATEDIFF("DAY",Check_Date,Created_Date) AS DAY
FROM dbo.gl_entities AS ge
INNER JOIN
dbo.super_entity AS se
ON ge.Super_Entity_ID = se.Super_Entity_ID
INNER JOIN
dbo.bank_codes AS bc
ON se.Super_Entity_ID = bc.Super_Entity_ID
INNER JOIN
dbo.checks AS ch
ON bc.Bank_Code_ID = ch.Bank_Code_ID
INNER JOIN
dbo.vendors AS vn
ON ch.Vendor_ID = vn.Vendor_ID
WHERE
ge.Active = 1 and vn.active = 1 and (ge.IS_Shadow = 1 OR se.IS_Tiered = 0)
AND CHECK_DATE > '20150101 00:00:00'
AND CHECK_DATE< '20190918 00:00:00'
ORDER BY ch.Check_Date, ch.Created_Date
To simply repeat them in their own columns in the query:
DECLARE #FromDate DATETIME = '2015-01-01 00:00:00',
#ToDate DATETIME = '2019-09-18 00:00:00',
#Years INT, #Months INT, #Days INT, #tmpFromDate DATETIME
SET #Years = DATEDIFF(YEAR, #FromDate, #ToDate)
- (CASE WHEN DATEADD(YEAR, DATEDIFF(YEAR, #FromDate, #ToDate),
#FromDate) > #ToDate THEN 1 ELSE 0 END)
SET #tmpFromDate = DATEADD(YEAR, #Years , #FromDate)
SET #Months = DATEDIFF(MONTH, #tmpFromDate, #ToDate)
- (CASE WHEN DATEADD(MONTH,DATEDIFF(MONTH, #tmpFromDate, #ToDate),
#tmpFromDate) > #ToDate THEN 1 ELSE 0 END)
SET #tmpFromDate = DATEADD(MONTH, #Months , #tmpFromDate)
SET #Days = DATEDIFF(DAY, #tmpFromDate, #ToDate)
- (CASE WHEN DATEADD(DAY, DATEDIFF(DAY, #tmpFromDate, #ToDate),
#tmpFromDate) > #ToDate THEN 1 ELSE 0 END)
SELECT DISTINCT
ge.Name, --table columns
ge.Entity_Type,
ge.Entity_Number,
bc.Super_Entity_ID,
ch.Check_Date, --check created
ch.Created_Date, --if payment was received
ch.Check_Number,
ch.Amount,
vn.Vendor_Name,
Check_Date,Created_Date,DATEDIFF("DAY",Check_Date,Created_Date) AS DAY,
#FromDate FromDate, #ToDate ToDate, #Years Years, #Months Months, #Days Days
FROM
. . .
If you run into problems with the DISTINCT clause, you can always use a subquery. If you want Years, Months, Days to be different based on one of the columns, you'll have to elaborate and then we can move everything you did in the SET statements into the SELECT statement.
Without your tables and data I cannot test very well.
pseudocode to try and demonstrate moving set statements to select statements
declare #begin date = '2018-01-01'
declare #end date = '2019-01-01'
declare #middletest int = datediff("dd", #begin, #end)/2
declare #middledate date = dateadd("dd", #middletest, #begin)
declare #middlemonth int = month(#middledate)
select #middlemonth half_month, #middledate middle_date
-- ,other_columns here
from table
where something
Start replacing.
#middlemonth is in the select statement so replace it with month(#middledate).
declare #begin date = '2018-01-01'
declare #end date = '2019-01-01'
declare #middletest int = datediff("dd", #begin, #end)/2
declare #middledate date = dateadd("dd", #middletest, #begin)
select month(#middledate) half_month, #middledate middle_date
-- ,other_columns here
from table
where something
#middledate is now in the select statement in 2 places so replace it with dateadd("dd", #middletest, #begin) each time.
declare #begin date = '2018-01-01'
declare #end date = '2019-01-01'
declare #middletest int = datediff("dd", #begin, #end)/2
select month(dateadd("dd", #middletest, #begin)) half_month,
dateadd("dd", #middletest, #begin) middle_date
-- ,other_columns here
from table
where something
Continue along.
declare #begin date = '2018-01-01'
declare #end date = '2019-01-01'
select month(dateadd("dd", datediff("dd", #begin, #end)/2, #begin)) half_month,
dateadd("dd", datediff("dd", #begin, #end)/2, #begin) middle_date
-- ,other_columns here
from table
where something
And you can, as desired, replace things with values from the table. If the table has a column start_date and you want that to be the begin and then use the current date as the end:
select month(dateadd("dd", datediff("dd", start_date, getdate())/2, start_date)) half_month,
dateadd("dd", datediff("dd", start_date, getdate())/2, start_date) middle_date
-- ,other_columns here
from table
where something

Add a date range to SQL query

I have simple SQL Server view that I need to make amends to:
CREATE VIEW [dbo].[ApplicantStat]
AS SELECT ISNULL(CONVERT(VARCHAR(50), NEWID()), '') AS Pkid,
AVG(ApplicationTime) AS 'AvgApplicationTime',
AVG(ResponseTime) AS 'AvgResponseTime',
CAST(ROUND(100.0 * count(case when [IsAccepted] = 1 then 1 end) / count(case when [IsValid] = 1 then 1 end), 0) AS int) AS 'AcceptRate'
FROM [Application]
It works as planned, but I need to add a date range to it. It's not quite as simple as Where > this date and < that date, instead I need to create a range.
Suppose I have a 'CreatedOn' date in my Application table. I want to be able to include all rows from the last full day (yesterday) and work back 30 days (inclusive).
I'm using SQL Server 2014.
Use :
where CreatedOn between cast(getdate()-30 as date) and cast(getdate()-1 as date)
Please notice CAST is used, it is because to get the full day ignoring the time part.
Something like this:
where MyColumn between dateadd(dd, -1, convert(date, getdate())) and dateadd(dd, -30, convert(date, getdate()))
It's a bit beyond the scope of this question, but maybe useful to some. I like this way of creating a table with date range, to use in queries:
USE MyDataBase
DECLARE #StartDate DATE
DECLARE #EndDate DATE
SET #StartDate = '2014-01-01' -- << user input >> --
SET #EndDate = '2036-12-31' -- << user input >> --
IF OBJECT_ID ('TEMPDB..#Date') IS NOT NULL DROP TABLE #Date
IF OBJECT_ID ('TEMPDB..#Date') IS NULL CREATE TABLE #Date (DateOne DATE)
INSERT INTO #Date VALUES (#StartDate)
WHILE #StartDate < #EndDate
BEGIN
INSERT INTO #Date
SELECT DATEADD (DD, 1, #StartDate) AS Date
SET #StartDate = DATEADD (DD, 1, #StartDate)
END
SELECT * FROM #Date
You should be able to just stick a WHERE with a BETWEEN clause on the end.
CREATE VIEW [dbo].[ApplicantStat]
AS SELECT ISNULL(CONVERT(VARCHAR(50), NEWID()), '') AS Pkid,
AVG(ApplicationTime) AS 'AvgApplicationTime',
AVG(ResponseTime) AS 'AvgResponseTime',
CAST(ROUND(100.0 * count(case when [IsAccepted] = 1 then 1 end) / count(case when [IsValid] = 1 then 1 end), 0) AS int) AS 'AcceptRate'
FROM [Application]
WHERE CreatedOn BETWEEN GETDATE()-1 AND GETDATE()-30

Converting SQL Server UDF to inline table-valued function

I am new here and new to SQL. I got this tip to create a scalar function that extends the functionality of the built-in DateAdd function (namely to exclude weekends and holidays). It is working fine for a single date but when I use it on a table, it is extremely slow.
I have seen some recommendation to use inline table-valued function instead. Would anyone be so kind to point me in the direction, how I would go about converting the below to inline table-valued function? I greatly appreciate it.
ALTER FUNCTION [dbo].[CalcWorkDaysAddDays]
(#StartDate AS DATETIME, #Days AS INT)
RETURNS DATE
AS
BEGIN
DECLARE #Count INT = 0
DECLARE #WorkDay INT = 0
DECLARE #Date DATE = #StartDate
WHILE #WorkDay < #Days
BEGIN
SET #Count = #Count - 1
SET #Date = DATEADD(DAY, #Count, #StartDate)
IF NOT (DATEPART(WEEKDAY, #Date) IN (1,7) OR
EXISTS (SELECT * FROM RRCP_Calendar WHERE Is_Holiday = 1 AND Calendar_Date = #Date))
BEGIN
SET #WorkDay = #WorkDay + 1
END
END
RETURN #Date
END
This should do the trick...
CREATE FUNCTION dbo.tfn_CalcWorkDaysAddDays
(
#StartDate DATETIME,
#Days INT
)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
SELECT
TheDate = MIN(x.Calendar_Date)
FROM (
SELECT TOP (#Days)
c.Calendar_Date
FROM
dbo.RRCP_Calendar c
WHERE
c.Calendar_Date < #StartDate
AND c.Is_Holiday = 0
AND c.is_Weekday = 1 -- this should be part of your calendar table. do not calculate on the fly.
ORDER BY
c.Calendar_Date DESC
) x;
GO
Note: for best performance, you'll want a unique, filtered, nonclustered index on on your calendar table...
CREATE UNIQUE NONCLUSTERED INDEX uix_RRCPCalendar_CalendarDate_IsHoliday_isWeekday ON dbo.RRCP_Calendar (
Calendar_Date, Is_Holiday, is_Weekday)
WHERE Is_Holiday = 0 AND is_Weekday = 1;
Try this and see if it returns the same values as your function, just without the loop:
SELECT WorkDays =
DATEADD(WEEKDAY, #Days, #StartDate) -
(SELECT COUNT(*)
FROM RRCP_Calendar
WHERE Is_Holiday = 1
AND Calendar_Date >= #StartDate
AND Calendar_Date <= DATEADD(DAY, #Days, #StartDate)
)
And yes, you can sometimes get substantially better performance with a non-procedural table-valued-function, but you have to set it up right. Look up SARGability and non-procedural table-valued-functions for more info, but if the above query works, this should do the trick:
CREATE FUNCTION dbo.SelectWorkDaysAddDays(#StartDate DATE, #Days INT)
RETURNS TABLE
AS
RETURN
SELECT WorkDays =
DATEADD(WEEKDAY, #Days, #StartDate) -
(SELECT COUNT(*)
FROM RRCP_Calendar
WHERE Is_Holiday = 1
AND Calendar_Date >= #StartDate
AND Calendar_Date <= DATEADD(DAY, #Days, #StartDate)
)
GO
And then you call the function by using an OUTER APPLY join:
SELECT y.foo
, y.bar
, dt.WorkDays
FROM dbo.YourTable y
OUTER APPLY dbo.SelectWorkDaysAddDays(#StartDate, #Days) dt
Say [dbo].[CalcWorkDaysAddDays], getdate(), 2 would return Sept 8,
2017 since it is adding two days. This function is similar to DateAdd
but it is excluding weekends and holidays
The code you've posted doesn't do this.
But if you want the result described, the function can be smth like this:
alter FUNCTION [dbo].[CalcWorkDaysAddDays_inline](#StartDate As DateTime,#Days AS INT)
returns table
as return
with cte as
(
select *,
ROW_NUMBER() over(order by Calendar_Date) as rn
from RRCP_Calendar
where Calendar_Date > #StartDate and #Days > 0
and not (DATEPART(WEEKDAY,Calendar_Date) IN (1,7) or Is_Holiday = 1)
union ALL
select *,
ROW_NUMBER() over(order by Calendar_Date desc) as rn
from RRCP_Calendar
where Calendar_Date < #StartDate and #Days < 0
and not (DATEPART(WEEKDAY,Calendar_Date) IN (1,7) or Is_Holiday = 1)
)
select cast(Calendar_Date as date) as dt
from cte
where rn = abs(#Days);

how view result date time datediff dateadd , but out-of-range value

i try compare date now and start date working from master_employee.
but i failed...
if at line i write
select #date = date_start
from Master_Employee
where id = '2'
its succes.
but i hope, can view all result in table Master_Employee.
can you help me ?
thank's very much..
DECLARE #date DATETIME
,#tmpdate DATETIME
,#years INT
,#months INT
,#days INT
SELECT #date = date_Start
FROM Master_Employee
SELECT #tmpdate = #date
SELECT #years = DATEDIFF(yyyy, #tmpdate, GETDATE()) - CASE
WHEN (MONTH(#date) > MONTH(GETDATE()))
OR (
MONTH(#date) = MONTH(GETDATE())
AND DAY(#date) > DAY(GETDATE())
)
THEN 1
ELSE 0
END
SELECT #tmpdate = DATEADD(yyyy, #years, #tmpdate)
SELECT #months = DATEDIFF(mm, #tmpdate, GETDATE()) - CASE
WHEN DAY(#date) > DAY(GETDATE())
THEN 1
ELSE 0
END
SELECT #tmpdate = DATEADD(mm, #months, #tmpdate)
SELECT #days = DATEDIFF(dd, #tmpdate, GETDATE())
SELECT #years AS Years
,#months AS Months
,#days AS Dayss
,GETDATE() AS Date_Now
This will give you how many days, months, years have passed in aggregate for all employees, As far as I can tell this is what you are tying to do.
DECLARE #Today as datetime = CONVERT(Date,GETDATE())
SELECT SUM(DATEDIFF(day,ISNULL(convert(datetime,#date),Today),#Today)) [Days]
,SUM(DATEDIFF(MONTH,ISNULL(convert(datetime,#date),Today),#Today)) [Months]
,SUM(DATEDIFF(Year,ISNULL(convert(datetime,#date),Today(,#Today)) [Years]
FROM Master_Employee
The reason that
SELECT #date = date_Start
FROM Master_Employee
is failing is because you are trying to assign all the start dates to the same variable.
If you want separate lines for each employee try:
DECLARE #Today as datetime = CONVERT(Date,GETDATE())
SELECT Id
,SUM(DATEDIFF(day,ISNULL(convert(datetime,#date),Today),#Today)) [Days]
,SUM(DATEDIFF(MONTH,ISNULL(convert(datetime,#date),Today),#Today)) [Months]
,SUM(DATEDIFF(Year,ISNULL(convert(datetime,#date),Today),#Today)) [Years]
FROM Master_Employee
GROUP BY ID
Be careful, month and year can be misleading, if the person started 12/31/14 and you ran this on 1/1/15 you will see 1 day, 1 month, 1 year. You might be better off using only days and figuring your own math for how long that is...