SQL Server Retrieving Recurring Appointments By Date - sql-server

I'm working on a system to store appointments and recurring appointments. My schema looks like this
Appointment
-----------
ID
Start
End
Title
RecurringType
RecurringEnd
RecurringTypes
---------------
Id
Name
I've keeped the Recurring Types simple and only support
Week Days,
Weekly,
4 Weekly,
52 Weekly
If RecurringType is null then that appointment does not recur, RecurringEnd is also nullable and if its null but RecurringType is a value then it will recur indefinatly. I'm trying to write a stored procedure to return all appointments and their dates for a given date range.
I've got the stored procedure working for non recurring meetings but am struggling to work out the best way to return the recurrences this is what I have so far
ALTER PROCEDURE GetAppointments
(
#StartDate DATETIME,
#EndDate DATETIME
)
AS
SELECT
appointment.id,
appointment.title,
appointment.recurringType,
appointment.recurringEnd,
appointment.start,
appointment.[end]
FROM
mrm_booking
WHERE
(
Start >= #StartDate AND
[End] <= #EndDate
)
I now need to add in the where clauses to also pick up the recurrences and alter what is returned in the select to return the Start and End Dates for normal meetings and the calculated start/end dates for the recurrences.
Any pointers on the best way to handle this would be great. I'm using SQL Server 2005

you need to store the recurring dates as each individual row in the schedule. that is, you need to expand the recurring dates on the initial save. Without doing this it is impossible to (or extremely difficult) to expand them on the fly when you need to see them, check for conflicts, etc. this will make all appointments work the same, since they will all actually have a row in the table to load, etc. I would suggest that when a user specifies their recurring date, you make them pick an actual number of recurring occurrences. When you go to save that recurring appointment, expand them all out as individual rows in the table. You could use a FK to a parent appointment row and link them like a linked list:
Appointment
-----------
ID
Start
End
Title
RecurringParentID FK to ID
sample data:
ID .... RecurringParentID
1 .... null
2 .... 1
3 .... 2
4 .... 3
5 .... 4
if in the middle of the recurring appointments schedule run, say ID=3, they decide to cancel them, you can follow the chain and delete the remaining ID=3,4,5.
as for expanding the dates, you could use a CTE, numbers table, while loop, etc. if you need help doing that, just ask. the key is to save them as regular rows in the table so you don't need to expand them on the fly every time you need to display or evaluate them.

I ended up doing this by creating a temp table of everyday between the start and end date along with their respective day of the week. I limited the recurrence intervals to weekdays and a set amount of weeks and added where clauses like this
--Check Week Days Reoccurrence
(
mrm_booking.repeat_type_id = 1 AND
#ValidWeeklyDayOfWeeks.dow IN (1,2,3,4,5)
) OR
--Check Weekly Reoccurrence
(
mrm_booking.repeat_type_id = 2 AND
DATEPART(WEEKDAY, mrm_booking.start_date) = #ValidWeeklyDayOfWeeks.dow
) OR
--Check 4 Weekly Reoccurences
(
mrm_booking.repeat_type_id = 3 AND
DATEDIFF(d,#ValidWeeklyDayOfWeeks.[Date],mrm_booking.start_date) % (7*4) = 0
) OR
--Check 52 Weekly Reoccurences
(
mrm_booking.repeat_type_id = 4 AND
DATEDIFF(d,#ValidWeeklyDayOfWeeks.[Date],mrm_booking.start_date) % (7*52) = 0
)
In case your interested I built up a table of the days between the start and end date using this
INSERT INTO #ValidWeeklyDayOfWeeks
--Get Valid Reoccurence Dates For Week Day Reoccurences
SELECT
DATEADD(d, offset - 1, #StartDate) AS [Date],
DATEPART(WEEKDAY,DATEADD(d, offset - 1, #StartDate)) AS Dow
FROM
(
SELECT ROW_NUMBER() OVER(ORDER BY s1.id) AS offset
FROM syscolumns s1, syscolumns s2
) a WHERE offset <= DATEDIFF(d, #StartDate, DATEADD(d,1,#EndDate))
Its not very elegant and probably very specific to my needs but it does the job I needed it to do.

Related

Group dates by considering look ahead days

I am using SQL Server 2017. I have a table Requests, to make things simple, there's only one column RequestDate. For example,
RquestDate
4/11
4/12
4/13
4/16
4/18
I need to group by RequestDate by considering look ahead days. If look ahead day is 0, the result should be the same as raw table.
If look ahead day is 1, it means when I look at 4/11, I need to check if 4/12 exists, if so, group 4/12 into 4/11.
The result is:
4/11 --it groups 4/12
4/13
4/16
4/18
If look ahead day is 2, when looking at 4/11, it groups 4/12, 4/13 into it.
The result is:
4/11 -- group 4/12 and 4/13.
4/16 -- group 4/18
So this problem is different from the typical gap and island problem. Because when group dates, there could be gap there, e.g, when look ahead day is 2, 4/16 groups 4/17 and 4/18.
I tried some ways but can't find a decent solution.
A recursive common table expression could work.
Select start request date using a min() function.
Use that same date as the grouping start date.
Step 1 and 2 make up the recursion anchor / start row.
Recursively go looking for the next request date. This date is higher than the previous date (r.RequestDate > c.RequestDate) and does not have another row
that follow the same criteria before it (not exists ... r2.RequestDate < r.RequestDate).
If the current request date (from step 3) falls within the look ahead interval length, then maintain the grouping start date (then c.RequestGroupDate), otherwise start a new group on the current request date (else r.RequestDate).
Step 3 and 4 make up the recursive part of the CTE.
After the recursion every request date as a corresponding request grouping date. The group by r.RequestGroupDate clause reduces the result output to the distinct values.
Sample data
create table Requests
(
RequestDate date
);
insert into Requests (RequestDate) values
('2021-04-11'),
('2021-04-12'),
('2021-04-13'),
('2021-04-16'),
('2021-04-18');
Solution
declare #lookAhead int = 1; -- look ahead days parameter
with rcte as
(
select min(r.RequestDate) as RequestDate,
min(r.RequestDate) as RequestGroupDate
from Requests r
union all
select r.RequestDate,
case
when datediff(day, c.RequestGroupDate, r.RequestDate) <= #lookAhead
then c.RequestGroupDate
else r.RequestDate
end
from rcte c
join Requests r
on r.RequestDate > c.RequestDate
where not exists ( select 'x'
from Requests r2
where r2.RequestDate > c.RequestDate
and r2.RequestDate < r.RequestDate )
)
select r.RequestGroupDate
from rcte r
group by r.RequestGroupDate;
Result
For #lookAhead = 1:
RequestGroupDate
----------------
2021-04-11
2021-04-13
2021-04-16
2021-04-18
For #lookahead = 2:
RequestGroupDate
----------------
2021-04-11
2021-04-16
Fiddle to see things in action.

Count by days, with all days

I need to count records by days, even if in the day were no records.
Count by days, sure, easy.
But how i can make it to print information, that 'in day 2018-01-10 was 0 records)
Should I use connect by level? Please, any help would be good. Can't use plsql, just oracle sql
First you generate every date that you want in an inline view. I chose every date for the current year because you didn't specify. Then you left outer join on date using whichever date field you have in that table. If you count on a non-null field from the source table then it will count 0 rows on days where there is no join.
select Dates.r, count(tablename.id)
from (select trunc(sysdate,'YYYY') + level - 1 R
from dual
connect by level <= trunc(add_months(sysdate,12),'YYYY') - trunc(sysdate,'YYYY')) Dates
left join tablename
on trunc(tablename.datefield) = Dates.r
group by Dates.r

Factoring public holidays in to a SQL code

Apologies if this is a simple one. I'm looking for some help with the following:
SELECT *
FROM (
SELECT TOP 7
RIGHT (CONVERT (VARCHAR, CompletedDate, 108), 8) AS Time,
WorkType
FROM Table
WHERE WorkType = 'WorkType1'
OR DATEPART (DW, CompletedDate) IN ('7','1')
AND WorkType = 'WorkType2'
ORDER BY CompletedDate DESC) Table
ORDER BY CompletedDate ASC
Multiple events run every day, and the above searches for the last one scheduled to run each day, and pulls the time from it for the past 7 days. This time marks the end of the day's events, and is the value I'm after.
Events run at a different order on weekends, so I search for a different WorkType. WorkType 1 is unique to weekdays. WorkType2 is run both at weekdays and weekends, however it is not the final event on a weekday, so I don't search for it then.
However, this kind of falls apart when public holidays such as bank holidays come into play, as they use the weekend timings. I still need to capture these times, but the above skips over them. If I were to remove or expand the DATEPART, I would end up with duplicate values for each day that don't mark the final job of the day.
What changes can I make to this to capture these lost holiday timings, without manually going through and checking every holiday? Is there a way that I can return a value for JobType2, if JobType1 does not appear on a day?
I suggest a materialized calendar table with one row per date along with the desired WorkType for that day. That will allow you to simply join on to the calendar table to determine the proper WorkType value without embedding the logic in the query itself.
With this table loaded with all dates for your reporting domain:
CREATE TABLE dbo.WorkTypeCalendar(
CalendarDate date NOT NULL
CONSTRAINT PK_Calendar PRIMARY KEY CLUSTERED
, WorkType varchar(10) NOT NULL
);
GO
The query can be refactored as below:
SELECT *
FROM ( SELECT TOP 7
RIGHT(CONVERT (varchar, CompletedDate, 108), 8) AS Time
, WorkType
FROM Table1 AS t
JOIN WorkTypeCalendar AS c ON t.WorkType = c.WorkType
AND t.CompletedDate >= c.CalendarDate
AND t.CompletedDate < DATEADD(DAY,
1,
c.CalendarDate)
ORDER BY CompletedDate DESC
) Table1
ORDER BY CompletedDate ASC
You also might consider making this a generalized utility calendar table. See http://www.dbdelta.com/calendar-table-and-datetime-functions/ for an complete example of such a table and script to load US holidays you can adjust for your needs and locale.

large update statement with case and multiple insert / updates in each case

I am not sure how to ask this question but here goes. I am trying to write a procedure to run each night that checks all unpaid invoices for a business and then adds service charge if needed. I need to query unpaid invoices, then check datediff() between creation and current date and then at certain values like 15 or 30 days I need to do several insert and updates to other tables to add the service charge and update balances. From what I read a loop is not the way to go but I am not sure how to keep track of current invoice or how to do inserts while I am inside a large update statement. Here is some psuedocode of what I need
select * from invoice where ispaid = 0
set days = currentdate - invoicecreationdate
switch (days)
case 30
insert servicecharge
update invoice
update balance
case 60
insert servicecharge
update invoice
update balance due
case 90
insert servicecharge
update invoice
update balance
I know this isn't much to go on but I will take any help I can get. I am not sure how this can work without a loop because I have several statements to run within each case that need to know what invoice we are currently dealing with
A loop wouldn't be so bad in your case. Each pass through the loop adds 30 days to the "past due" window and it appears that you intend to process all of the applicable rows for each window as a set. That's goodness.
Alternatively, you could use something like this to generate the appropriate date ranges:
declare #Today as Date = GetDate();
select DateAdd( day, -( AgingDays + 30 ), #Today ) as StartDate,
DateAdd( day, -( AgingDays + 1 ), #Today ) as EndDate, PenaltyPercent
from ( values ( 30, 2 ), ( 60, 5 ), ( 90, 10 ) ) as PastDueIntervals( AgingDays, PenaltyPercent )
It can be easily extended to carry additional data for each range. By JOINing this with your Invoice table you can process all the applicable invoices at once.
Depending on the size of you tables it may make sense to generate a temporary table that contains the invoice id, past due interval and any other applicable data. That table can then be used to supply the information used to update all three tables.
A useful trick is to include a CASE in UPDATE statements, e.g.:
update I
set WatchList = case when Aging >= 60 then 1 else WatchList end,
...
from Invoices as I inner join
#PastDueInvoices as PDI on PDI.InvoiceId = I.InvoiceId
This will set the watch list flag if the temporary table indicates that the invoice is 60 days or more past due, otherwise leave it unchanged.

Query to create records between two dates

I have a table with the following fields (among others)
TagID
TagType
EventDate
EventType
EventType can be populated with "Passed Inspection", "Failed Inspection" or "Repaired" (there are actually many others, but simplifies to this for my issue)
Tags can go many months between a failed inspection and the ultimate repair... in this state they are deemed to be "awaiting repair". Tags are still inspected each month even after they have been identified as having failed. (and just to be clear, a “failed inspection” doesn’t mean the item being inspected doesn’t work at all… it still works, just not at 100% capacity…which is why we still do inspections on it).
I need to create a query that counts, by TagType, Month and Year the number of Tags that are awaiting repair. The end result table would look like this, for example
TagType EventMonth EventYear CountofTagID
xyz 1 2011 3
abc 1 2011 2
xyz 2 2011 2>>>>>>>>>>>>indicating a repair had been made since 1/2011
abc 2 2011 2
and so on
The "awaiting repair" status should be assessed on the last day of the month
This is totally baffling me...
One thought that I had was to develop a query that returned:
TagID,
TagType,
FailedInspectionDate, and
NextRepairDate,
then try and do something that stepped thru the months in between the two dates, but that seems wildly inefficient.
Any help would be much appreciated.
Update
A little more research, and a break from the problem to think about it differently gave me the following approach. I'm sure its not efficient or elegant, but it works. Comments to improve would be appreciated.
declare #counter int
declare #FirstRepair date
declare #CountMonths as int
set #FirstRepair = (<Select statement to find first repair across all records>)
set #CountMonths = (<select statement to find the number of months between the first repair across all records and today>)
--clear out the scratch table
delete from dbo.tblMonthEndDate
set #counter=0
while #counter <=#CountMonths --fill the scratch table with the date of the last day of every month from the #FirstRepair till today
begin
insert into dbo.tblMonthEndDate(monthenddate) select dbo.lastofmonth(dateadd(m,#counter, #FirstRepair))
set #counter = #counter+1
end
--set up a CTE to get a cross join between the scratch table and the view that has the associated first Failed Inspection and Repair
;with Drepairs_CTE (FacilityID, TagNumber, CompType, EventDate)
AS
(
SELECT dbo.vwDelayedRepairWithRepair.FacilityID, dbo.vwDelayedRepairWithRepair.TagNumber, dbo.vwDelayedRepairWithRepair.CompType,
dbo.tblMonthEndDate.MonthEndDate
FROM dbo.vwDelayedRepairWithRepair INNER JOIN
dbo.tblMonthEndDate ON dbo.vwDelayedRepairWithRepair.EventDate <= dbo.tblMonthEndDate.MonthEndDate AND
dbo.vwDelayedRepairWithRepair.RepairDate >= dbo.tblMonthEndDate.MonthEndDate
)
--use the CTE to build the final table I want
Select FacilityID, CompType, Count(TagNumber), MONTH(EventDate), YEAR(EventDate), 'zzz' as EventLabel
FROM Drepairs_CTE
GROUP BY FacilityID, CompType, MONTH(EventDate), YEAR(EventDate)`
Result set ultimately looks like this:
FacilityID CompType Count Month Year Label
1 xyz 2 1 2010 zzz
1 xyz 1 2 2010 zzz
1 xyz 1 7 2009 zzz
Here is a recursive CTE which generates table of last dates of months in interval starting with minimum date in repair table and ending with maximum date.
;with tableOfDates as (
-- First generation returns last day of month of first date in repair database
-- and maximum date
select dateadd (m, datediff (m, 0, min(eventDate)) + 1, 0) - 1 startDate,
max(eventDate) endDate
from vwDelayedRepairWithRepair
union all
-- Last day of next month
select dateadd (m, datediff (m, 0, startDate) + 2, 0) - 1,
endDate
from tableOfDates
where startDate <= endDate
)
select *
from tableOfDates
-- If you change the CTE,
-- Set this to reasonable number of months
-- to prevent recursion problems. 0 means no limit.
option (maxrecursion 0)
EndDate column from tableOfDates is to be ignored, as it serves as upper bound only. If you create UDF which returns all the dates in an interval, omit endDate in select list or remove it from CTE and replace with a parameter.
Sql Fiddle playground is here.

Resources