Mysql query for hotel room availability - mysql

I am trying to create an online room reservation system for a small hotel. One of the tables of the database
is supposed to hold the bookings. It has an autonumber field, customer data fields, two date fields for arrival and departure, and a number field for the number of rooms booked.
A search page submits the arrival and departure dates to a result page which is then supposed to tell the customer how many rooms are available within the period if any. This is where it all goes wrong.
I just can't get an accurate count of the number of rooms already booked within the period requested.
guest | arrive | depart |booked
Smith | 2002-06-11 | 2002-06-18 | 1
Jones | 2002-06-12 | 2002-06-14 | 2
Brown | 2002-06-13 | 2002-06-16 | 1
White | 2002-06-15 | 2002-06-17 | 2
If the hotel has 9 rooms, here is a day-by-day listing of the number of available rooms.
I want the result like this.
date available status
2002-06-10 | 9 | Hotel is empty
2002-06-11 | 8 | Smith checks in
2002-06-12 | 6 | Jones checks in
2002-06-13 | 5 | Brown checks in
2002-06-14 | 7 | Jones checks out
2002-06-15 | 5 | White checks in
2002-06-16 | 6 | Brown checks out
2002-06-17 | 8 | White checks out
2002-06-18 | 9 | Smith checks out
Please help me to find a solution

A calendar table isn't strictly necessary for problems of this nature, but they can help to conceptualise the problem in a quick and easy manner. So I have a calendar table holding dates from 1900 until 4000 and something...
DROP TABLE IF EXISTS my_table;
CREATE TABLE my_table
(booking_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY
,guest VARCHAR(12) NOT NULL
,arrive DATE NOT NULL
,depart DATE NOT NULL
,booked INT NOT NULL
,UNIQUE KEY(guest,arrive)
);
INSERT INTO my_table (guest,arrive,depart,booked) VALUES
('Smith','2002-06-11','2002-06-18',1),
('Jones','2002-06-12','2002-06-14',2),
('Brown','2002-06-13','2002-06-16',1),
('White','2002-06-15','2002-06-17',2);
SELECT x.dt
, 9 - COALESCE(SUM(booked),0) available
FROM calendar x
LEFT
JOIN my_table y
ON x.dt >= y.arrive AND x.dt < y.depart
WHERE x.dt BETWEEN '2002-06-10' AND '2002-06-20'
GROUP
BY dt;
+------------+-----------+
| dt | available |
+------------+-----------+
| 2002-06-10 | 9 |
| 2002-06-11 | 8 |
| 2002-06-12 | 6 |
| 2002-06-13 | 5 |
| 2002-06-14 | 7 |
| 2002-06-15 | 5 |
| 2002-06-16 | 6 |
| 2002-06-17 | 8 |
| 2002-06-18 | 9 |
| 2002-06-19 | 9 |
| 2002-06-20 | 9 |
+------------+-----------+
11 rows in set (0.00 sec)

Related

MySQL query to find number of unique new visitors per week

I have 3 tables in MySQL:
1) page (id, title)
2) visitor (id, name)
3) page_visit (page_id, visitor_id, timestamp_of_visit)
Visitors can visit pages multiple times, across several days. Hence, while we will have one row for a page, and one row for a visitor, we can have several page_visit rows, each with a timestamp of the visit.
I'm trying to find the number of unique visitors, by week. I know how to get the 'by week count' query for non-uniques (i.e. 'how many visitors did I see each week'). I'm not sure how to pick the unique visitors by week, though, with the visitor showing up on the list ONLY the first time they are ever seen.
----------- ----------- ----------------------------
| page | | visitor | | page_visit |
----------- ----------- ----------------------------
|id |title| |id |name | |pid|vid|timestamp of visit|
----------- ----------- ----------------------------
| 1 | p1 | | 1 | v1 | | 1 | 1 | 02-18-2016:08:30 |
| 2 | p2 | | 2 | v2 | | 1 | 1 | 02-18-2016:10:00 |
| 3 | p3 | | 3 | v3 | | 1 | 3 | 02-20-2016:23:45 |
| 4 | p4 | | 4 | v4 | | 2 | 3 | 02-22-2016:07:30 |
| 5 | p5 | | 5 | v5 | | 3 | 1 | 02-23-2016:08:30 |
| 6 | p6 | | 6 | v6 | | 3 | 6 | 02-24-2016:09:30 |
What the result set should show:
------------------------
| results |
------------------------
| Week of | Net new |
------------------------
| 02-15-2016 | 2 |
| 02-22-2016 | 1 |
As mentioned, I can figure out how to show ALL visitors by week. I'm not sure how to get the unique visitors.
I tried doing a min(timestamp of visit), but, based on where I tried it, it returned the lowest timestamp across all rows (understandably...).
Any help would be much appreciated!
This is a tricky question when you first encounter it. It requires two levels of aggregation. The first gets the first visit for each visitor, the second summarizes by time. The following does the summary by day:
select date(minvd), count(*) as numvisitors
from (select vid, min(visitdate) as minvd
from page_visit pv
group by vid
) v
group by date(minvd)
order by date(minvd);
Translating to weeks is always a bit tricky -- do they begin on Mondays? End on Saturdays? On Fridays? (I've seen all of these.) However, the above is additive, so you can just add all the values for a given week to get your value.
In case you want to do this without a subquery:
SELECT
<week>,
COUNT(DISTINCT PV.vid)
FROM
Page_Visit PV
LEFT OUTER JOIN Page_Visit PV2 ON
PV2.vid = PV.vid AND
PV2.visit_date < PV.visit_date
WHERE
PV2.vid IS NULL
GROUP BY
<week>
As Gordon mentions, how you determine the week can be tricky. Just add in that calculation where you see <week>. Personally, I like to use a Calendar table for that kind of functionality, but it's up to you. You can run any expressions directly against PV.visit_date to determine it.

MySQL complex date calculation in inner join

I am reading the MySql tutorial in the docs and have the following tables and SQL statements:
Event table:
+----------+------------+----------+------------------------------+
| name | date | type | remark |
+----------+------------+----------+------------------------------+
| Fluffy | 1995-05-15 | litter | 4 kittens, 3 females, 1 male |
| Buffy | 1993-06-23 | litter | 5 puppies, 2 female, 3 male |
| Buffy | 1994-06-19 | litter | 3 puppies, 3 female |
| Chirpy | 1999-03-21 | vet | needed beak streightened |
| Slim | 1997-08-03 | vet | broken rib |
| Bowser | 1991-10-12 | kennel | NULL |
| Fang | 1991-10-12 | kennel | NULL |
| Fang | 1998-08-28 | birthday | Gave him new chew toy |
| Claws | 1998-03-17 | birthday | Gave him a flea collar |
| Whistler | 1998-12-09 | birthday | First birthday |
+----------+------------+----------+------------------------------+
Pet table:
+----------+--------+---------+------+------------+------------+
| name | owner | species | sex | birth | death |
+----------+--------+---------+------+------------+------------+
| Fluffy | Harold | cat | f | 1993-02-04 | NULL |
| Claws | Gwen | cat | m | 1994-03-17 | NULL |
| Buffy | Harold | dog | f | 1989-05-13 | NULL |
| Fang | Benny | dog | m | 1990-08-27 | NULL |
| Bowser | Diane | dog | m | 1989-03-31 | 1995-07-29 |
| Chirpy | Gwen | bird | f | 1998-09-11 | NULL |
| Whistler | Gwen | bird | NULL | 1997-12-09 | NULL |
| Slim | Benny | snake | m | 1996-04-29 | NULL |
| Puffball | Diane | hamster | f | 1999-03-30 | NULL |
| Jenny | Robert | dog | f | 2004-01-01 | 2014-05-04 |
+----------+--------+---------+------+------------+------------+
SQL:
select pet.name,
( YEAR(date) - YEAR(birth) ) - ( RIGHT(date,5) < RIGHT(birth,5) ) AS age, remark
from pet inner join event
on pet.name = event.name
where event.type = 'litter';
I understand the SQL statement except for this one:
( YEAR(date) - YEAR(birth) ) - ( RIGHT(date,5) < RIGHT(birth,5) )
A step by step explanation would greatly help. I know that the YEAR() function is used to extract the year from a date.
As you've mentioned YEAR() gets the year from the date.
We will use Fluffy as an example with birth = 1993-02-04 and date = 1995-05-15
Step 1:
Subtract the extracted year from both dates ( YEAR(date) - YEAR(birth) )
You now have 1995 - 1993 which is equal to 2
Step 2:
( RIGHT(date,5) < RIGHT(birth,5) ) this will actually read the date and birth string 5 paces from right to left, so if you perform RIGHT(date,5) you will get the value 0, and if you perform RIGHT(birth,5) you will also get a value of 0.
Step 3:
Now we get on to the < operator, this returns a boolean value of 1 or 0 if it satisfies the condition. Since 0 = 0, the statement is false, so it will return to 0.
The whole function actually checks if the day part of your date is less than the day on your birthdate which will determine if you have a sort of remaining days before a whole year. And if you do, it will return 1 which will be subtracted from the current year - year operation you performed earlier.
But in our case, since the < will return 0, we can definitely say that Fluffy's age is 2 - 0, which is 2.
If, however, Fluffy's birthdate is say, 1993-12-04, this will yield a 1 value for the < operation meaning that the year is not yet complete which will bring a result of 2 - 1, which is 1.
Sorry if its a bit messy.
If you were born in 1980 and I know that right now it's 2015, then I I can almost compute your age by computing YEAR(date) - YEAR(birth) = 2015 - 1980 = 35. The sticking point is that your birthday might not have happened yet. So how can I tell if your birthday has happened? Lets say you were born on July 20, 1980. Then in MySQL date format your birthday would look like 2015-06-20. If I look at the last 5 characters of your birthday, the right-most 5 characters, or RIGHT(birth, 5) I'd get 06-20. If the the last 5 characters of today's date, 06-07 is less than the last 5 characters of your birthday then your birthday hasn't happened yet. Here, by "less than" we mean only that it would "sort before" in normal string order. So, if RIGHT(date, 5) < RIGHT(birth, 5) then your birthday hasn't happened yet. And, luckily, MySQL treats a "true" as the number 1 and a "false" as the number 0. So, the above will subtract 1 from our date computation only if your birthday hasn't happened yet.
It's a clever(?) way of checking if the month and day of the date of birth happened before or after the date from the event (litter) in order to properly calculate the number of years between the events (as the year part in itself isn't enough).
In this part:
( YEAR(date) - YEAR(birth) ) - ( RIGHT(date,5) < RIGHT(birth,5) )
the RIGHT(date,5) returns the month and day part like (03-30) and does a boolean less than comparison which returns either 0 or 1 depending on the result. This is then subtracted from the YEAR(date) - YEAR(birth) calculation so that the years between the events get adjusted correctly.

Troubles conceptualizing a query

I have a 'Course' table and an 'Event' table.
I would like to have all the courses that actually take place, i.e. they are not cancelled by an event.
I have done this by a simple request for all the course and a script analysis (basically some loops), but this request take a time that I believe too long. I think what I want is possible in one query and no loops to optimize this request.
Here are the details :
'Course' c have the fields 'date', 'duration' and a many to many relation with the 'Grade' table
'Event' e have the fields 'begin', 'end', 'break' and a many to many relation with the 'Grade' table
A course is cancelled by an event if they occur at the same time and if the event is a break (e.break = 1)
A course is cancelled by an event if all the grades of the course are in the events that occurs at the same time (many events can occurs, I have to sum up the grades of these events and compare them to the grades of the courses). This is the part I'm doing with a loop, I have some trouble to conceptualize that.
Any help is welcome,
Thanks in advance,
PS : I'm using mysql
EDIT : Tables details
-Course
+-----------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------+-------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| date | datetime | NO | | NULL | |
| duration | time | NO | | NULL | |
| type | int(11) | NO | | NULL | |
+-----------+-------------+------+-----+---------+----------------+
+-------+---------------------+----------+------+
| id | date | duration | type |
+-------+---------------------+----------+------+
| 1 | 2013-12-10 10:00:00 | 02:00:00 | 0 |
| 2 | 2013-12-11 10:00:00 | 02:00:00 | 0 |
+-------+---------------------+----------+------+
-Event
+-------------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+-------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| begin | datetime | NO | | NULL | |
| end | datetime | YES | | NULL | |
| break | tinyint(1) | NO | | NULL | |
+-------------+-------------+------+-----+---------+----------------+
+----+---------------------+---------------------+-------+
| id | begin | end | break |
+----+---------------------+---------------------+-------+
| 1 | 2013-12-10 00:00:00 | 2013-12-11 23:59:00 | 1 |
+----+---------------------+---------------------+-------+
-course_grade
+-----------+----------+
| course_id | grade_id |
+-----------+----------+
| 1 | 66 |
| 2 | 65 |
| 2 | 66 |
+-----------+----------+
-event_grade
+----------+----------+
| grade_id | event_id |
+----------+----------+
| 66 | 1 |
+----------+----------+
So here, only the course 2 should appear, because course 1 has only one grade, and this grade has an event.
I like riddles, this is a nice one, has many solutions, I think
As you say 'Any help is welcome', I give an answer altough its not the solution (and it does not fit into a comment)
I dont know, if you just want (A) the naked statement (over and out), or if you want (B) to understand how to get to the solution, I take (B)
I start with 'what would I change' before starting about the solution:
you are mixing date,datetime,start,end and duration, try to use only one logic (if it is your model ofcourse) ie.
an event/course has a start and an end time (or start/duration)
duration should (IMHO) not be a time
try to find a smallest timeslice for events/course (are there 1 sec events? or is a granularity of 5' (ie. 10:00, 10:05, 10:10 ans so on) a valid compromise?
My solution, a prgmatic one not academic
(sounds funny, but does work good in a simillar prob I had see annotation)
Create a table (T_TIME_OF_DAY) having all from 00:00, 00:05, .. 23:55
Create a Table (T_DAYS) in a valid and usefull range (a year?)
the carthesian product - call it points in time - (ie. select date, time from T_DAYS,T_TIME_OF_DAY no condition) of them (days x times) 300*24*12 ~ 100.000 rows if you need to look at a whole year (and 5' are ok for you) - thats not much and no prob
the next step is to join the curses fitting to your points in time (and the rows are down to <<100.000)
if you next join these with your events (again using point in time) you should get what you want.
simplyfied quarters of a day:
12 12 12 12 12 12 12 12
08 09 10 11 12 13 14 15
|...|...|...|...|...|...|...|...
grade 65 (C).............2..................
grade 66 (C).........1...2..................
grade 65 (E)................................
grade 66 (e)........1111..................
(annotation: I use this logic to calculate the availabillity of services regarding to their downtimes per Month / Year, and could use the already in timeslices prepared data for display)
(second answer, because it is a totaly different and mor3 standard aproach)
I made an SQLFiddle for you
so what to do:
and thats the a solution:
step one (in mind) select course,grades (lets call them C)
step two (in mind) select events, grades (lets call them E)
and - tada -
select all from C where there a no rows in E that have the same grade and the same date(somehow) and eventtype='break'
so your solution:
select
id, date start_time, date+duration end_time, grade_id
from Course c join course_grade cg on c.id=cg.course_id
where not exists (
select grade_id, begin start_time, end end_time
from event_grade eg join event e on eg.event_id=e.id
where
eg.grade_id=cg.grade_id
and e.break=1
and
(
(e.begin<=c.date and e.end >=c.date+c.duration)
or e.begin between c.date and c.date+c.duration
or e.end between c.date and c.date+c.duration
)
)
I did take no attention to optimize here

Data Entry Tracking (Database Design)

I have developed a website (PHP) that allow staffs to add records on to our system.
Staffs will be adding thousands of records into our database.
I need a way to keep track of what record have been done and the process/status of record.
Here a number of Teams I could think of:
Data Entry Team
Proof Reading Team
Admin Team
When staff (Data Entry Team) completed a record - he/she will then click on the Complete button. Then somehow it should asssign to 'Proof Reading Team' automatically.
A record need to be checked twice from a Proof Reading Team. If StaffB finish proof reading then another member from Proof Reading Team need to check it again.
When Proof reading is done, Admin Team will then assign "Record Completed"
In a few months later record might need to be updated (spelling mistake, price change, etc) - Admin might to assign record to Data entry team.
Is this good data entry management solution? How do I put this into Database Design perspective?
Here what I tried:
mysql> select * from records;
+----+------------+----------------------+
| id | name | address |
+----+------------+----------------------+
| 1 | Bill Gates | Text 1 Text Text 1 |
| 2 | Jobs Steve | Text 2 Text 2 Text 2 |
+----+------------+----------------------+
mysql> select * from staffs;
+----+-----------+-----------+---------------+
| id | username | password | group |
+----+-----------+-----------+---------------+
| 1 | admin1 | admin1 | admin |
| 2 | DEntryA | DEntryA | data_entry |
| 3 | DEntryB | DEntryB | data_entry |
| 4 | PReadingA | PReadingA | proof_reading |
| 5 | PReadingB | PReadingB | proof_reading |
+----+-----------+-----------+---------------+
mysql> select * from data_entry;
+----+------------+-----------+------------------------+
| id | records_id | staffs_id | record_status |
+----+------------+-----------+------------------------+
| 1 | 2 | 3 | data_entry_processiing |
| 2 | 2 | 3 | data_entry_completed |
| 3 | 2 | 4 | proof_read_processing |
| 4 | 2 | 4 | proof_read_completed |
| 5 | 2 | 5 | proof_read_processing |
| 6 | 2 | 5 | proof_read_completed |
+----+------------+-----------+------------------------+
Is there alternative better solution of database design?
i think design it's well done. but may be you want to separate group into groups table, and record_status into status table. If you're storing a lot of records you would store a lot of useless information, at least create an enum type for record_status field and group field
table: groups
id - name 1 - admin 2 - data_entry 3 - proof_reading
...
table: status
id - name 1 - data_entry_processing ...
and if you want the users to be in different groups at a time, you could create users_group table
table: user_groups
group_id - user_id 1 - 1 2 - 1 1 - 4 3 -
4 4 - 4 ....
Hope this helps

MySQL - move from flat table to first normal form

I am building an application that will allow a user to record weekly activity over a 6 week period. Each week has 3 benchmarks to record against, here is an example:
Week 1
+------------+-----------+------------+-----------+
| Day | Minutes | Location | Miles |
+------------+-----------+------------+-----------+
| Monday | | | |
+------------+-----------+------------+-----------+
| Tuesday | | | |
+------------+-----------+------------+-----------+
| Wednesday | | | |
+------------+-----------+------------+-----------+
| Thursday | | | |
+------------+-----------+------------+-----------+
| Friday | | | |
+------------+-----------+------------+-----------+
| Saturday | | | |
+------------+-----------+------------+-----------+
| Sunday | | | |
+------------+-----------+------------+-----------+
This is repeated for each week up to 6.
In my flat table I have the following:
UserID | Username | Week 1 Day 1 Minutes | Week 1 Day 1 Location | Week 1 Day 1 Miles | Week 1 Day 2 Minutes | Week 1 Day 2 Location | Week 1 Day 2 Miles ETC...
X 7 for a week and then X 6 for the 6 weeks.
I am trying to figure out where my eliminations are, and what my separate tables would be. So far I have the following:
User Table
+------------+-----------+
| UserID | Username |
+------------+-----------+
| | |
+------------+-----------+
Activity Table
+------------+-----------+------------+-----------+------------+-----------+
| UserID | WeekID | Day | Minutes | Location | Miles |
+------------+-----------+------------+-----------+------------+-----------+
| | | | | | |
+------------+-----------+------------+-----------+------------+-----------+
Weeks Table
+------------+-----------+------------+
| UserID | WeekID | Week_No |
+------------+-----------+------------+
| | | |
+------------+-----------+------------+
I think I am getting along the right lines, but the Weeks Table doesn't seem right and I am not sure what the relationships are - I don't think I need UserID in each table, and I'm not sure what the PKs should be.
Any comments on this schema, or an efficient way to achieve the first normal form given the application requirements would be much appreciated, many thanks.
EDIT:
Thanks very much for all the answers, great stuff.
I think having a Location Table would be beneficial as I could standardize locations (could provide a list to choose from) and if I need to query based on location, I'll have consistent location names.
Revised the schema to this:
User Table - UserID PK
+------------+-----------+
| UserID | Username |
+------------+-----------+
| | |
+------------+-----------+
Activity Table - ActivityID PK
+------------+-----------+------------+-----------+------------+-------------+-----------+
| ActivityID | UserID | Week_No | Day | Minutes | LocationID | Miles |
+------------+-----------+------------+-----------+------------+-------------+-----------+
| | | | | | | |
+------------+-----------+------------+-----------+------------+-------------+-----------+
Location Table - LocationID PK
+------------+---------------+
| LocationID | Location_Name |
+------------+---------------+
| | |
+------------+---------------+
2nd EDIT:
I now have a question on 2NF and 3NF on this topic:
MySQL - moving from 1st Normal Form to 2nd and 3rd Normal Forms
Add a Location table and change Location to LocationID (PK). The Weeks table does not need UserID in it. You can find what weeks a user has by querying the Activity table.
I only see the need for a Week table if Week_No changes by user, which doesn't seem to make too much sense. Otherwise, you can just replace WeekID with WeekNo in the Activity table, and delete the Weeks table.
In the weeks table your primary key should be WeekID. I'm not sure you would need a week table though as you don't seem to be storing anything in it apart from the week that the activity took place, which could actually be in the activity table. So I would get rid of it, add Week_No to the Activity table, and have an ActivityID in the Activity table as primary, and UserID as Foriegn Key.
Don't want to tell you too much, just enough to get you on your way as you seem to want to normalise this fully own your own.
PKs:
User tbl: UserID
Weeks tbl: WeekId
Activity tbl: (UserID,WeekID)
And you don't need a UserId in Weeks Tbl
i don't think you need a week table.
activity table with userid, weekno, day(enum), minutes, location, miles, is enough for what you mentioned...