Avoiding clashing appointments Access - sql

I have three tables:
Employee(Id,name etc)
Appointment(Id,date,time,employee id, clientid etc)
Client(Id,name etc)
The Employee table has a 1 to many with the Appointment table as does the Client table.
However what I'm trying to achieve is to allow for the system to prevent duplicate or conflicting appointments but cant quite get my head around how to go about this. Would I need an additional table with say available time slots and some how link it all together? Or for example an employee availability table in addition?
Or could I achieve what I need with what I already have and just by manipulation of queries?
Many thanks

I think an appointments table is going to be necessary. It will allow you to include only available slots and also to analyse employees workload and availability. The table would include 15 minute slots for each day for each employee. You may wish to add a further table for holidays / sick days / generally unavailable for a chunk of time.
EDIT
I had envisioned something on the lines of:
Timeslots:
EmployeeID ) Primary key
TimeSlot )
JobID - Foreign key
Status ) And so forth
Notes )
"I want an early appointment with E1"
SELECT TimeSlots.EmployeeID, TimeSlots.TimeSlot, TimeSlots.JobID
FROM TimeSlots
WHERE TimeSlots.EmployeeID=1
AND TimeSlots.TimeSlot Between #2/9/2012 9:0:0# And #2/9/2012 11:30:0#
"I want an appointment at 9:00am"
SELECT TimeSlots.EmployeeID, TimeSlots.TimeSlot, TimeSlots.JobID
FROM TimeSlots
WHERE TimeSlots.TimeSlot Between #2/9/2012 9:0:0# And #2/9/2012 9:30:0#

To prevent collisions, the logic here is quite simple:
A collision occurs when:
RequestStartDate <= EndDate
and
RequestEndDate >= StartDate
The above is thus a rather simply query. If any collision occurs, the
above will return records and you simply don't allow the booking.
The above of course can EASY be extended for time, and even a particular room.
Eg:
RequestStartTime <= EndTime
and
RequestEndTime >= StartTime
And
RequestDate = BookingDate
And in fact in access since you can store a date + time column, then we are quite much back to the first example above (and as such a booking can span multiple days if you do this). And as noted, if this was for a particular room, then just add a room condition to the above.
Access 2010 does have table triggers and store procedures, but since you need a UI for the user, then code like this normally does the trick:
dim strWhere as string
dim dtRequeestStartDate as date
dim dtRequestEndDate as date
dtRequestStartDate = inputbox("Enter start Date")
dtRequestEndDate = inputbox("Enter end date")
strWhere="#" & format(dtRequestStartDate,"mm/dd/yyyy") & "# <= EndDate" & _
" and #" & format(dtRequestEndDate,"mm/dd/yyyy") & "# >= StartDate"
if dcount("*","tableBooking",strWhere) > 0 then
msgbox "sorry, you cant book
...bla bla bla....
The above is just an example, and I am sure you would build a nice form that
prompts the user for the booking dates.
So the above simple conditions above does return ANY collisions. And having written so many reservation systmes, I strongly recommend you do NOT create blank records ahead of time, but use the above correct logic and thus ONLY add records to the booking system and not have to write tons of code to create a bunch of blank records with time slots etc.
The above simply query will prevent collisions.

Related

SQL Select/From/Where Run Speed

I have a program that is pulling data from a Visual FoxPro table and dumping into a Dataset with VB.net. My connection string works great, and the query I'm using usually runs with respectable speed. As I've ran it more, however, I've learned that there is a large amount of "bad" data in my table. So now, I'm trying to refine my query to buffer against the "bad" data, but what I thought would be a very small tweak has yielded massive performance losses, and I'm not particularly sure why.
My original query is:
'Pull desired columns for orders that have not "shipped" and were received in past 60 days.
'To "ship", an order must qualify with both an updated ship date and Sales Order #.
sqlSelect = "SELECT job_id,cust_id,total_sale,received,due,end_qty,job_descr,shipped,so "
sqlFrom = "FROM job "
sqlWhere = "WHERE fac = 'North Side' AND shipped < {12/30/1899} AND so = '' AND received >= DATE()-60;"
sql = sqlSelect & sqlFrom & sqlWhere
This has a run-time of about 20 seconds; while I'd prefer it to be quicker, it's not a problem. In my original testing (and occasional debugging), I replaced sqlWhere with sqlWhere = "WHERE job_id = 127350". This runs pretty much instantaneously.
Now the problem block: Once I replaced sqlWhere with
'Find jobs that haven't "shipped" OR were received within last 21 days.
'Recently shipped items are desired in results.
sqlWhere = "WHERE fac = 'North Side' AND ((shipped < {12/30/1899} AND so = '') OR received >= DATE()-21);"
My performance jumped to about 3 min 40 sec. This time is almost exactly the same as the time to run with sqlWhere = "WHERE received >= DATE();".
I'm not the moderator of these tables; I'm merely pulling from them to create a series of reports for our users. My best guess is that the received field is not indexed, this is the cause of my performance drop-off. But while my first search returns about 100 records, pulling the jobs only from today returns about 5, and still takes about 11x as long.
So my question is three part:
1) Would someone be able to explain the phenomenon I'm experiencing right now? I feel like I'm somewhat on the right track, but my knowledge of SQL has been limited to circumstantial use within other languages...
2) Is there something I'm missing, or some better way to obtain the results I need? There are a large volume of records that haven't "shipped", but simply because the user only input a shipped date or s/o, and didn't do the other. I need a way to view very recent orders (regardless of "shipped" status), and then also view less recent orders that have "bad" data, so I can get the user in the habit of cleaning up the data.
3) Is it bad SQL practice to overconstrain a WHERE clause? If I run fifteen field comparisons, joined together with nested ANDs/ORs, am I wasting my time when I could be doing something much cleaner?
Many thanks,
B
If you are looking for a non-indexed record in your WHERE string, the SQL engine must do a table scan, i.e. - look at every record in the table.
The difference between the two queries is having the OR instead of the AND. When you have a non-indexed column in an AND, the SQL engine can use the indexes to narrow down the number of records it has to look at for the non-indexed column. When you have an OR, it now must look at every record in the table and compare on that column.
Adding an index on the Received column would probably fix the performance issue.
In general, there are two things you don't want to have happen in your WHERE clause.
1. A primary condition on an non-indexed column
2. Using a calculation on a column. For example, doing WHERE Shipped-2 < date() is often worse than doing Shipped < Date() + 2, because the former doesn't typically allow the index to be used.
Refining your query through multiple WHERE clauses is generally a good thing. The fewer records you need to return to your application the better your performance will be, but you need to have appropriate indexing in place.

SQL Selecting records where one date range doesn't intersect another

I'm trying to write a simple reservation program for a campground.
I have a table for campsites (one record for every site available at the campground).
I have a table for visitors which uses the campsite table's id as a foreign key, along with a check in date and check out date.
What I need to do is gather a potential check in and check out date from the user and then gather all the campsites that are NOT being used at any point in that range of dates.
I think I'm close to the solution but there's one piece I seem to be missing.
I'm using 2 queries.
1) Gather all the campsites that are occupied during that date range.
2) Gather all campsites that are not in query 1.
This is my first query:
SELECT Visitors.CampsiteID, Visitors.CheckInDate, Visitors.CheckOutDate
FROM Visitors
WHERE (((Visitors.CheckInDate)>=#CHECKINDATE#
And (Visitors.CheckInDate)<=#CHECKOUTDATE#)
Or ((Visitors.CheckOutDate)>=#CHECKINDATE#
And (Visitors.CheckOutDate)<=CHECKOUTDATE));
I think I'm missing something. If the #CHECKINDATE# and #CHECKOUTDATE# both occur between someone else's Check-in and Check-out dates, then this doesn't catch it.
I know I could split this between two queries, where one is dealing with just the #CHECKINDATE# and the second is dealing with the #CHECKOUTDATE#, but I figure there's a cleaner way to do this and I'm just not coming up with it.
This is my second one, which I think is fine the way it is:
SELECT DISTINCT Campsites.ID, qryCampS_NotAvailable.CampsiteID
FROM Campsites LEFT JOIN qryCampS_NotAvailable
ON Campsites.ID = qryCampS_NotAvailable.CampsiteID
WHERE (((qryCampS_NotAvailable.CampsiteID) Is Null));
Thanks,
Charles
To get records that overlap with the requested time period, use this simple logic. Two time periods overlap when one starts before the other ends and the other ends after the first starts:
SELECT v.CampsiteID, v.CheckInDate, v.CheckOutDate
FROM Visitors v
WHERE v.CheckInDate <= #CHECKOUTDATE# and
v.CheckOutDate >= #CHECKINDATE# ;

Hotel Room Booking Statement

Essentially i'm making a basic hotel booking system for my computing a-level coursework, and have hit a stumbling block (doesn't help having a migraine either!)
I'm in the process of checking if a room is available for then the user to book (the application is all run and used by staff by the way) but trying to get my head around how to do it.
At the moment I was thinking I could just search the current booking database for the room number and dates I want to book for and if a match came back that meant that certain room was booked. That's all fine and dandy, but how do I tell it the range of dates? (sorry bad question) I was thinking an pseudo code version of the sql statement on the lines of:
If 'check in date of booking already on the database' is before
'check in date of new booking' AND 'check out date of booking already
on the database' is after 'check in date of new booking' then room is
already booked.
Basically if it returns a record then I know that room is booked. But I just need to figure out how to use SQL to search for records based on my check in/out dates.
But that'll allow double bookings, agh it's driving me mad :/ Any ideas?
It may not seem totally obvious at first, but you can actually simplify the testing of a full or partial date overlap with just two conditions.
Let's say you've got BOOKING table with existing bookings and a new booking with check in date #NEW_CHECK_IN and check out date #NEW_CHECK_OUT. To find all bookings that overlap these dates (assuming the end points are inclusive) you just need this kind of where clause:
--Select overlapping bookings
-- ...
WHERE BOOKING.CHECK_IN <= #NEW_CHECK_OUT
AND BOOKING.CHECK_OUT >= #NEW_CHECK_IN
-- ...
I know it looks too simple, but try it out with the scenarios that drf illustrated (plus the three that were missed, (1) where the new booking is after the existing booking, (2) the new booking is strictly within the existing booking and (3) the new booking is equal to the existing booking). You'll see that this simple set of conditions covers all of these scenarios.
The pseudocode suggested only considers one possibility of conflict between an existing booking and a new booking. To illustrate, consider a room with one booking.
|======================|
Check-in date Check out date
Suppose we want to create a new booking, and have 4 potential new bookings.
|======================| Existing booking
|-----------------------------| New booking (Scenario 1)
|----------| New booking (Scenario 2)
|-------------| New booking (Scenario 3)
|---| New booking (Scenario 4)
Of these, only Scenario 4 does not overlap an existing booking; the others conflict with the existing booking. While your pseudocode addresses Scenario 1, it does not detect Scenarios 2 or 3 and will thus allow double-bookings.
Effectively, your pseudocode might look something like this.
Let E = booking already on the database
N = new booking,
CID = check-in date,
COD = check-out date
For a new booking N, N conflicts with an existing booking iff there exists a record E where:
(CID of E is between CID of N and COD of N), or
(COD of E is between CID of N and COD of N), or
(CID of N < CID of E and COD of N > COD of E)
In SQL, depending on your schema, the query might resemble something like this:
-- assume #new_cid is the new checkin date and #new_cod is the new checkout date
select count(*) from bookings
where
#new_cid between bookings.checkindate and bookings.checkoutdate or
#new_cod between bookings.checkindate and bookings.checkoutdate or
(#new_cid <= bookings.checkindate AND #new_cod > bookings.checkoutdate)

SQL Scheduling - Overbooked Report

I need a way to view a given resource (in this case rooms/beds) that are overbooked. Here's my table structure. Sorry about the Hungarian notation:
tblRoom
--RoomID
tblBooking
--BookingID
--BeginDate
--EndDate
--AssignedRoomID (foreign key)
I don't have any non-working SQL to post here because I really don't know where to start. I'm using MS Access but I'm looking for a database agnostic solution if possible. It's OK to have to have to change some of the keywords to match the dialect of a given SQL engine but I'd like avoid using other features that are proprietary or only available in one RDBMS.
I realize that it's best to avoid overbooking from the beginning but that's not the point of this question.
In case it's helpful, I posted a related question a couple days ago about how to find resources that are not yet booked for a given data range. You can see that question here.
Edit1:
In reply to the answer below, I've modified your SQL slightly to make it work in Access as well as to be more accurate when it comes to detecting conflicts. If I err not your solution posted below allows some conflicts to go unnoticed but also shows conflicts when a given Booking's EndDate and a different Booking's BeginDate fall on the same day, which is actually allowable and should not show as a conflict. Am I understanding this correctly or am I missing something here?
SELECT
*
FROM
tblBooking AS booking
INNER JOIN
tblBooking AS conflict
ON [conflict].AssignedRoomID = [booking].AssignedRoomID
AND (([conflict].BeginDate >= DateAdd("d", -1, [booking].BeginDate) AND [conflict].BeginDate < [booking].EndDate)
OR ([conflict].EndDate > [booking].BeginDate AND [conflict].EndDate < [booking].EndDate))
AND [conflict].BookingID <> [booking].BookingID
So, what you're looking for is any record in tblBooking for which there is another record with the same AssignRoomID for an overlapping period?
A naive solution would be...
SELECT
*
FROM
tblBooking [booking]
INNER JOIN
tblBooking [conflict]
ON [conflict].AssignedRoomID = [booking].AssignedRoomID
AND [conflict].BeginDate <= [booking].EndDate
AND [conflict].EndDate >= [booking].BeginDate
AND [conflict].BookingID != [booking].BookingID
The last condition stops a booking from being it's own conflict. It can also be changed to AND [conflict].BookingID > [booking].BookingID so that you don't get the conflict repeating. (If A conflicts with B, you only get A,B and not B,A.)
EDIT
The issue with the above solution is that it does not scale very well. When searching for a Conflict, all bookings for that room Before the booking's EndDate are found, then filtered based on the EndDate. After a few years, that first search (hopefully using an Index) will return many, many records.
One optimisation is to have a maximum booking length, and only look that many days back in time for a conflict...
INNER JOIN
tblBooking [conflict]
ON [conflict].AssignedRoomID = [booking].AssignedRoomID
AND [conflict].BeginDate <= [booking].EndDate
AND [conflict].BeginDate >= [booking].BeginDate - 7 -- Or however long the max booking length is
AND [conflict].EndDate >= [booking].BeginDate
AND [conflict].BookingID != [booking].BookingID
By having wrapped a >= AND a <= around the [conflict].BeginDate, an index search can now quickly return a reasonably limitted number of records.
For bookings longer than the maximum booking length, they can be entered into the database as multiple bookings. That's where the art of optimisation comes in, it's often all about trade-offs and compromises :)
EDIT
Another option, giving different details, would be to join the bookings against a calendar table. (Having, for example, one record per day.)
SELECT
[room].RoomID,
[calendar].Date,
COUNT(*) AS [total_bookings],
MIN([booking].BookingID) AS [min_booking_id],
MAX([booking].BookingID) AS [max_booking_id]
FROM
[calendar]
CROSS JOIN
tblRoom [room]
INNER JOIN
tblBooking [booking]
ON [booking].AssignedRoomID = [room].RoomID
AND [booking].BeginDate <= [calendar].Date
AND [booking].EndDate >= [calendar].Date
GROUP BY
[room].RoomID,
[calendar].Date
HAVING
COUNT(*) > 1

SQL not yielding expected results

I have three tables related to this particular query:
Lawson_Employees: LawsonID (pk), LastName, FirstName, AccCode (numeric)
Lawson_DeptInfo: AccCode (pk), AccCode2 (don't ask, HR set up), DisplayName
tblExpirationDates: EmpID (pk), ACLS (date), EP (date), CPR (date), CPR_Imported (date), PALS (date), Note
The goal is to get the data I need to report on all those who have already expired in one or more certification, or are going to expire in the next 90 days.
Some important notes:
This is being run as part of a vbScript, so the 90-day date is being calculated when the script is run. I'm using 2010-08-31 as a placeholder since its the result at the time this question is being posted.
All cards expire at the end of the month. (which is why the above date is for the end of August and not 90 days on the dot)
A valid EP card supersedes ACLS certification, but only the latter is required of some employees. (wasn't going to worry about it until I got this question answered, but if I can get the help I'll take it)
The CPR column contains the expiration date for the last class they took with us. (NULL if they didn't take any classes with us)
The CPR_Imported column contains the expiration date for the last class they took somewhere else. (NULL if they didn't take it elsewhere, and bravo for following policy)
The distinction between CPR classes is important for other reports. For purposes of this report, all we really care about is which one is the most current - or at least is currently current.
If I have to, I'll ignore ACLS and PALS for the time being as it is non-compliance with CPR training that is the big issue at the moment. (not that the others won't be, but they weren't mentioned in the last meeting...)
Here's the query I have so far, which is giving me good data:
SELECT
iEmp.LawsonID, iEmp.LastName, iEmp.FirstName,
dept.AccCode2, dept.DisplayName,
Exp.ACLS, Exp.EP, Exp.CPR, Exp.CPR_Imported, Exp.PALS, Exp.Note
FROM (Lawson_Employees AS iEmp
LEFT JOIN Lawson_DeptInfo AS dept ON dept.AccCode = iEmp.AccCode)
LEFT JOIN tblExpirationDates AS Exp ON iEmp.LawsonID = Exp.EmpID
WHERE iEmp.CurrentEmp = 1
AND ((Exp.ACLS <= #2010-08-31#
AND Exp.ACLS IS NOT NULL)
OR (Exp.CPR <= #2010-08-31#
AND Exp.CPR_Imported <= #2010-08-31#)
OR (Exp.PALS <= #2010-08-31#
AND Exp.PALS IS NOT NULL))
ORDER BY dept.AccCode2, iEmp.LastName, iEmp.FirstName;
After perusing the result set, I think I'm missing some expiration dates that should be in the result set. Am I missing something? This is the sucky part of being the only developer in the department... no one to ask for a little help.
I think the problem is here:
OR (Exp.CPR <= #2010-08-31#
AND Exp.CPR_Imported <= #2010-08-31#)
Null is not less than or greater than anything.
If you need the people who do not have a valid CPR, you will need to include nulls.
It may be easiest to use Nz:
OR (Nz(Exp.CPR,#2010-08-31#) <= #2010-08-31#
AND Nz(Exp.CPR_Imported,#2010-08-31#) <= #2010-08-31#)