MySQL query booking system - remove entry if room is already booked - mysql

I have been banging my head on this (im a newbie in MySQL, be patient) for two days and i cannot get around this query i want to make... First the data:
I have two tables, one for the rooms like this:
table ROOMS
id_room | tipe_room | view_room
1.1 | Single | Sea
1.2 | Single | Country
1.3 | Double | Sea
1.4 | Double | Country
1.5 | Single | Sea
table RESERVATIONS
id_reservation | client_number | n_room | check_in | check_out
1 | 1 | 1.1 | 2012-02-20 | 2012-02-24
If a person wants to book a room with tipe_room=Single and view_room=Sea and check_in date=2012-02-15 and check_out date=2012-02-20 i want it to return the id_room available (1.1 and 1.5).
Now if a person wants to book a room with tipe_room=Single and view_room=Sea and check_in date=2012-02-19 and check_out date=2012-02-23 i want it to return the id_room available (only 1.5).
I have been trying around JOIN conditions but when conditions are met it excludes all rooms...
How can I do this, please?
Best Regards, Alex

This query should check for all four cases (A,B,C and D) of occupancy:
check_in check_out
|=TEST=PERIOD=|
|--A--| |--B--| |--C--| <- check_in<period_end AND check_out>period_start
|--------D--------| <- check_in<period_start AND check_out>period_end
This is complete query:
SELECT `id_room` FROM `rooms`
WHERE `tipe_room`='Single' AND `view_room`='Sea'
AND NOT EXISTS
(
SELECT * FROM `reservations` WHERE `n_room`=`rooms`.`id_room`
AND ((`check_in`<'2012-02-20' AND `check_out`>'2012-02-15')
OR (`check_in`<'2012-02-15' AND `check_out`>'2012-02-20'))
)

Try this Query
select * from room where room.view = :view and room.type = :type
and (select count(*) from reservation r where r.room_id = room.id and
(r.checkout > :checkin and r.checkin < :checkout) or
(r.checkin < :checkin and r.checkout > :checkin)) = 0
The main idea here is to select the room and then check that it is not already booked in the sub select.

Related

Mysql group by dates in weekends

I have the next database:
What I want to get is the name of the city, and the number of tours grouped by weekends
I was trying to do something like this
select `cities`.`name`,
SUM( CASE DAYOFWEEK(DATE(date)) WHEN 7 OR 1 THEN 1 ELSE 0 END)AS TOURS
from `cities`
left join `tours` on `cities`.`id` = `tours`.`city_id`
group by `cities`.`name`;
So if I have tours on the dates 2019/03/02 (Saturday), 2019/03/03 (Sunday) in the city X I would return X and 1 tour, as it groups the dates by the weekend. and if it had the dates 2019/03/09, 2019/03/10, I would return the Name of city X and 2 tours, since it has the dates of 2 and 3 as 1; and 9 and 10 as another tour.
the query I did only counts on Saturday and Sunday without distinguishing between different weeks.
Any idea?
UPDATE
I have added a fiddle
Fiddle
The desired result is
| name | TOURS |
| ----------- | ----- |
| Amatitlan | 0 |
| Guatemala | 3 |
| Villa Nueva | 1 |
I Added
count(distinct week(`tours`.`date`, 1))AS TOURS
And complete query
select `cities`.`name`,
count(distinct week(`tours`.`date`, 1))AS TOURS
from `cities`
left join `tours` on `cities`.`id` = `tours`.`city_id`
group by `cities`.`name`;
fiddle with the desired result
if i understand correctly you want your count of tour by different weeks only for weekend.
I think below is solution. correct me if i am wrong.
select `cities`.`name`,
SUM( CASE DAYOFWEEK(DATE(date)) WHEN 7 OR 1 THEN 1 ELSE 0 END)AS TOURS,
WEEKS(date),
from `cities`
left join `tours` on `cities`.`id` = `tours`.`city_id`
group by `cities`.`name`,WEEKS(date);

SQL consecutive occurrences for availability based query

I am a bit stuck trying to create a pretty complex on SQL, and more specifically MySQL.
The database deals with car rentals, and the main table of what is a snowflake patters looks a bit like:
id | rent_start | rent_duration | rent_end | customerID | carId
-----------------------------------------------------------------------------------
203 | 2016-10-03 | 5 | 2016-11-07 | 16545 | 4543
125 | 2016-10-20 | 9 | 2016-10-28 | 54452 | 5465
405 | 2016-11-01 | 2 | 2016-01-02 | 43565 | 346
My goal is to create a query that allows given
1) A period range like, for example: from 2016-10-03 to 2016-11-03
2) A number of days, for example: 10
allows me to retrieve the cars that are actually available for at least 10 CONSECUTIVE days between the 10th of October and the 11th.
A list of IDs for those cars is more than enough... I just don't really know how to setup a query like that.
If it can help: I do have a list of all the car IDs in another table.
Either way, thanks!
I think it is much simpler to work with availability, rather than rentals, for this purpose.
So:
select r.car_id, r.rent_end as avail_start,
(select min(r2.rent_start
from rentals r2
where r2.car_id = r.car_id and r2.rent_start > r.rent_start
) as avail_end
from rentals r;
Then, for your query, you need at least 10 days. You can use a having clause or subquery for that purpose:
select r.*
from (select r.car_id, r.rent_end as avail_start,
(select min(r2.rent_start
from rentals r2
where r2.car_id = r.car_id and r2.rent_start > r.rent_start
) as avail_end
from rentals r
) r
where datediff(avail_end, avail_start) >= $days;
And finally, you need for that period to be during the dates you specify:
select r.*
from (select r.car_id, r.rent_end as avail_start,
(select min(r2.rent_start
from rentals r2
where r2.car_id = r.car_id and r2.rent_start > r.rent_start
) as avail_end
from rentals r
) r
where datediff(avail_end, avail_start) >= $days and
( (avail_end > $end and avail_start < $start) or
(avail_start <= $start and avail_end >= $start + interval 10 day) or
(avail_start > $start and avail_start + interval 10 day <= $end)
);
This handles the various conditions where the free period covers the entire range or starts/ends during the range.
There are no doubt off-by-one errors in this logic (is a car available the same date it returns). The this should give you a solid approach for solving the problem.
By the way, you should also include cars that have never been rented. But that is not possible with the tables you describe in the question.

Select each month even though the month not exist in mysql table

Let say I have these two tables in mysql.
table1:
date staff_no
2016-06-10 1
2016-06-09 1
2016-05-09 1
2016-04-09 1
table2:
staff_no name
1 David
Then, I have this query to get analysis for the staff for each month:
SELECT DATE_FORMAT(table1.date,'%b %Y') as month,COUNT(table1.date) as total_records,table2.name as name
FROM table1 as table1
LEFT JOIN table2 as table2 on table2.staff_no = table1.staff_no
WHERE table1.staff_no = "1" and date(table1.date) between = "2016-04-01" and "2016-06-30"
GROUP BY table2.name,DATE_FORMAT(table1.date,'%Y-%m')
ORDER BY DATE_FORMAT(table1.date,'%Y-%m-%d')
This query will output:
month total_records name
Apr 2016 1 David
May 2016 1 David
Jun 2016 2 David
But, if I replace the date between "2016-04-01" and "2016-07-31" from the query,it wont show me the July record because it is not exist in table1 which is not what I want. I still want to get result like this:
month total_records name
Apr 2016 1 David
May 2016 1 David
Jun 2016 2 David
Jul 2016 0 David
Anyone expert on this? Kindly help me into this. Thanks!
Consider the following schema with the 3rd table being the year/month Helper Table mentioned. Helper tables are very common and can be re-used throughout your code naturally. I will leave it to you to load it up with substantial date data. Note however the way the end date for each month was put together for those of us that want to do less work, while allowing the db engine to figure out leap years for us.
You could have just one column in that helper table. But that would require the use of function calls for end dates in some of your functions and that means more slowness. We like fast.
Schema
create table workerRecords
( id int auto_increment primary key,
the_date date not null,
staff_no int not null
);
-- truncate workerRecords;
insert workerRecords(the_date,staff_no) values
('2016-06-10',1),
('2016-06-09',1),
('2016-05-09',1),
('2016-04-09',1),
('2016-03-02',2),
('2016-07-02',2);
create table workers
( staff_no int primary key,
full_name varchar(100) not null
);
-- truncate workers;
insert workers(staff_no,full_name) values
(1,'David Higgins'),(2,"Sally O'Riordan");
Helper table below
create table ymHelper
( -- Year Month helper table. Used for left joins to pick up all dates.
-- PK is programmer's choice.
dtBegin date primary key, -- by definition not null
dtEnd date null
);
-- truncate ymHelper;
insert ymHelper (dtBegin,dtEnd) values
('2015-01-01',null),('2015-02-01',null),('2015-03-01',null),('2015-04-01',null),('2015-05-01',null),('2015-06-01',null),('2015-07-01',null),('2015-08-01',null),('2015-09-01',null),('2015-10-01',null),('2015-11-01',null),('2015-12-01',null),
('2016-01-01',null),('2016-02-01',null),('2016-03-01',null),('2016-04-01',null),('2016-05-01',null),('2016-06-01',null),('2016-07-01',null),('2016-08-01',null),('2016-09-01',null),('2016-10-01',null),('2016-11-01',null),('2016-12-01',null),
('2017-01-01',null),('2017-02-01',null),('2017-03-01',null),('2017-04-01',null),('2017-05-01',null),('2017-06-01',null),('2017-07-01',null),('2017-08-01',null),('2017-09-01',null),('2017-10-01',null),('2017-11-01',null),('2017-12-01',null),
('2018-01-01',null),('2018-02-01',null),('2018-03-01',null),('2018-04-01',null),('2018-05-01',null),('2018-06-01',null),('2018-07-01',null),('2018-08-01',null),('2018-09-01',null),('2018-10-01',null),('2018-11-01',null),('2018-12-01',null),
('2019-01-01',null),('2019-02-01',null),('2019-03-01',null),('2019-04-01',null),('2019-05-01',null),('2019-06-01',null),('2019-07-01',null),('2019-08-01',null),('2019-09-01',null),('2019-10-01',null),('2019-11-01',null),('2019-12-01',null);
-- will leave as an exercise for you to add more years. Good idea to start, 10 in either direction, at least.
update ymHelper set dtEnd=LAST_DAY(dtBegin); -- data patch. Confirmed leap years.
alter table ymHelper modify dtEnd date not null; -- there, ugly patch above worked fine. Can forget it ever happened (until you add rows)
-- show create table ymHelper; -- this confirms that dtEnd is not null
So that is a helper table. Set it up once and forget about it for a few years
Note: Don't forget to run the above update stmt
Quick Test for your Query
SELECT DATE_FORMAT(ymH.dtBegin,'%b %Y') as month,
ifnull(COUNT(wr.the_date),0) as total_records,#soloName as full_name
FROM ymHelper ymH
left join workerRecords wr
on wr.the_date between ymH.dtBegin and ymH.dtEnd
and wr.staff_no = 1 and wr.the_date between '2016-04-01' and '2016-07-31'
LEFT JOIN workers w on w.staff_no = wr.staff_no
cross join (select #soloName:=full_name from workers where staff_no=1) xDerived
WHERE ymH.dtBegin between '2016-04-01' and '2016-07-31'
GROUP BY ymH.dtBegin
order by ymH.dtBegin;
+----------+---------------+---------------+
| month | total_records | full_name |
+----------+---------------+---------------+
| Apr 2016 | 1 | David Higgins |
| May 2016 | 1 | David Higgins |
| Jun 2016 | 2 | David Higgins |
| Jul 2016 | 0 | David Higgins |
+----------+---------------+---------------+
It works fine. The first mysql table is the Helper table. A left join to bring in the worker records (allowing for null). Let's pause here. That was afterall the point of your question: missing data. Finally the worker table in a cross join.
The cross join is to initialize a variable (#soloName) that is the worker's name. Whereas the null status of missing dates as you requested is picked up fine via the ifnull() function returning 0, we don't have that luxury for a worker's name. That forces the cross join.
A cross join is a cartesian product. But since it is a single row, we don't suffer from the normal problems one gets with cartesians causing way to many rows in the result set. Anyway, it works.
But here is one problem: it is too hard to maintain and plug in values in 6 places as can be seen. So consider below a stored proc for it.
Stored Proc
drop procedure if exists getOneWorkersRecCount;
DELIMITER $$
create procedure getOneWorkersRecCount
(pStaffNo int, pBeginDt date, pEndDt date)
BEGIN
SELECT DATE_FORMAT(ymH.dtBegin,'%b %Y') as month,ifnull(COUNT(wr.the_date),0) as total_records,#soloName as full_name
FROM ymHelper ymH
left join workerRecords wr
on wr.the_date between ymH.dtBegin and ymH.dtEnd
and wr.staff_no = pStaffNo and wr.the_date between pBeginDt and pEndDt
LEFT JOIN workers w on w.staff_no = wr.staff_no
cross join (select #soloName:=full_name from workers where staff_no=pStaffNo) xDerived
WHERE ymH.dtBegin between pBeginDt and pEndDt
GROUP BY ymH.dtBegin
order by ymH.dtBegin;
END$$
DELIMITER ;
Test the stored proc a number of times
call getOneWorkersRecCount(1,'2016-04-01','2016-06-09');
call getOneWorkersRecCount(1,'2016-04-01','2016-06-10');
call getOneWorkersRecCount(1,'2016-04-01','2016-07-01');
call getOneWorkersRecCount(2,'2016-02-01','2016-11-01');
Ah, much easier to work with (in PHP, c#, Java, you name it). Choice is yours, stored proc or not.
Bonus Stored Proc
drop procedure if exists getAllWorkersRecCount;
DELIMITER $$
create procedure getAllWorkersRecCount
(pBeginDt date, pEndDt date)
BEGIN
SELECT DATE_FORMAT(ymH.dtBegin,'%b %Y') as month,ifnull(COUNT(wr.the_date),0) as total_records,w.staff_no,w.full_name
FROM ymHelper ymH
cross join workers w
left join workerRecords wr
on wr.the_date between ymH.dtBegin and ymH.dtEnd
and wr.staff_no = w.staff_no and wr.the_date between pBeginDt and pEndDt
-- LEFT JOIN workers w on w.staff_no = wr.staff_no
-- cross join (select #soloName:=full_name from workers ) xDerived
WHERE ymH.dtBegin between pBeginDt and pEndDt
GROUP BY ymH.dtBegin,w.staff_no,w.full_name
order by ymH.dtBegin,w.staff_no;
END$$
DELIMITER ;
Quick test of it
call getAllWorkersRecCount('2016-03-01','2016-08-01');
+----------+---------------+----------+-----------------+
| month | total_records | staff_no | full_name |
+----------+---------------+----------+-----------------+
| Mar 2016 | 0 | 1 | David Higgins |
| Mar 2016 | 1 | 2 | Sally O'Riordan |
| Apr 2016 | 1 | 1 | David Higgins |
| Apr 2016 | 0 | 2 | Sally O'Riordan |
| May 2016 | 1 | 1 | David Higgins |
| May 2016 | 0 | 2 | Sally O'Riordan |
| Jun 2016 | 2 | 1 | David Higgins |
| Jun 2016 | 0 | 2 | Sally O'Riordan |
| Jul 2016 | 0 | 1 | David Higgins |
| Jul 2016 | 1 | 2 | Sally O'Riordan |
| Aug 2016 | 0 | 1 | David Higgins |
| Aug 2016 | 0 | 2 | Sally O'Riordan |
+----------+---------------+----------+-----------------+
The Takeaway
Helper Tables have been used for decades. Don't be afraid or embarrassed to use them. In fact, trying to get some specialty work done without them is nearly impossible at times.
You can build an inline set of variables representing all the dates you want by using any other table in your system that has AT LEAST the number of months you are trying to represent even though the data does not have to have dates. Just has records that you can put a limit on.
TRY the following statement that uses MySql variables. The FROM clause declares a variable inline to the SQL statement "#Date1". I am starting it with MARCH 1 of 2016. Now, the select fields list takes that variable and keeps adding 1 month at a time to it. Since it is combined with the "AnyTableWithAtLeast12Records" (literally any table in your system with at least X records), it will create a result showing the dates. This is one way of forcing a calendar type of list.
But notice the SECOND column in this select does not change the #Date1 via the := assignment. So, it takes the date as it now stands and adds another month to it for the END Date. If you need a smaller or larger date range, just change the limit of records to create the calendar spread...
select
#Date1 := date_add( #Date1, interval 1 month ) StartDate,
date_add( #Date1, interval 1 month ) EndDate
from
AnyTableWithAtLeast12Records,
( select #Date1 := '2016-03-01' ) sqlvars
limit 12;
The result is something like...
StartDate EndDate
2016-04-01 2016-05-01
2016-05-01 2016-06-01
2016-06-01 2016-07-01
2016-07-01 2016-08-01
2016-08-01 2016-09-01
2016-09-01 2016-10-01
2016-10-01 2016-11-01
2016-11-01 2016-12-01
2016-12-01 2017-01-01
2017-01-01 2017-02-01
2017-02-01 2017-03-01
2017-03-01 2017-04-01
Now you have your dynamic "Calendar" completed in one simple query. Now, use that as a basis for all the records you need counts for and format as you had. So take the entire query above as a JOIN to find records within those date ranges... No other queries or stored procedures required. Now, a simple LEFT JOIN will keep all dates, but only show those with staff when WITHIN the between range of per start/end. So ex: greater or equal to 04/01/2016, but LESS THEN 05/01/2016 which includes 04/30/2016 # 11:59:59pm.
SELECT
DATE_FORMAT(MyCalendar.StartDate,'%b %Y') as month,
COALESCE(COUNT(T1.Staff_no),0) as total_records,
COALESCE(T2.name,"") as name
FROM
( select #Date1 := date_add( #Date1, interval 1 month ) StartDate,
date_add( #Date1, interval 1 month ) EndDate
from
AnyTableWithAtLeast12Records,
( select #Date1 := '2016-03-01' ) sqlvars
limit 12 ) MyCalendar
LEFT JOIN table1 T1
ON T1.Date >= MyCalendar.StartDate
AND T1.Date < MyCalendar.EndDate
AND T1.Staff_No = 1
LEFT JOIN table2 T2
ON T1.staff_no = T2.StaffNo
GROUP BY
T2.name,
DATE_FORMAT(MyCalendar.StartDate,'%Y-%m')
ORDER BY
DATE_FORMAT(MyCalendar.StartDate,'%Y-%m-%d')
I would say you need to have RIGHT JOIN here to include staff from second table

SQL query to check if column includes some data

I need to select all free rooms from hotel DB and I think I can do it with two steps:
bookings = select * from booking where booking.startDate>=selectedStartDate and booking.endDate=<selectedEndDate.
pseudo query: select * from room where room.room_id not includes bookings.room_id.
I wrote my second query like pseudo query because I can't find how I can do what I want to do. How to check that bookings doesn't includes room_id's?
My booking table looks like:
+-----------+------------+------------+---------+---------+
| bookingId | startDate | endDate | room_id | guestId |
+-----------+------------+------------+---------+---------+
| 1 | 2016-03-12 | 2016-03-22 | 1 | 1 |
| 2 | 2016-03-12 | 2016-03-22 | 2 | 2 |
+-----------+------------+------------+---------+---------+
You could transform the first query to a subquery of the second query by using the not in operator:
SELECT *
FROM room
WHERE room.room_id NOT IN (SELECT room_id
FROM booking
WHERE startDate >= selectedEndDate AND
endDate <= selectedStartDate)
If you want rooms free during a period of time, use not exists. The correct logic is:
select r.*
from room r
where not exists (select 1
from booking b
where $startdate <= b.enddate and $enddate >= b.startdate
);
Two periods overlap when one starts before the second ends and the first ends after the second starts.
Note that the <= and >= might be strict inequalities, depending on whether the first and last dates are included in the period.
You can use a more optimized query
SELECT * FROM room JOIN booking ON room.room_id = booking.room_id
WHERE booking.startDate >= selectedStartDate AND booking.endDate <= selectedEndDate

Join derived table with itself

In short words:
I have events table:
[tbl]
time | newState
1200 | 1
1300 | 2
1400 | 5
I need to transform this table into intervals table [intervals]:
t0 | t1 | state
1200 | 1300 | 1
1300 | 1400 | 2
RESTRICTION: SQL COMPACT EDITION
the query:
SELECT leftPart.time AS t0, min(rightPart.time) AS t1, leftPart.newState
FROM tbl AS leftPart
LEFT OUTER JOIN tbl As rightPart
ON leftPart.time<rightPart.time
GROUP BY leftPart.Time,leftPart.newState
It works perfect when [tbl] is permanent table in database, but in my case [tbl] is derived from another select subquery! like this:
(SELECT time,newState
from ...) AS derivedTb
So, when i try something like this:
SELECT derivedTbl.time As t0,derivedTbl.state,min(rigthTblPart.time) FROM
(SELECT time,newState
from ...) AS derivedTbl
LEFT OUTER JOIN with derivedTbl AS rigthTblPart
ON derivedTbl.Time<rightTblPart.Time ...
It throws error: "derivedTbl does not exist"...
It seems that the derived table under it's alias is not visible to higher level query (thanks, google translator! ))...
Is there any way to store derived tables in query and using them in different parts of query? SQL CE doesn't support temporary tables, views and common table expressions...
task details (if interesting):
i have 2 tables of events:
[states1]
time | state1
1200 | 1
1300 | 2
1400 | 3
[states2]
time | state2
1200 | 0
1230 | 10
1330 | 20
1430 | 30
I need convert them in intervals table:
[intervals]
t0 t1 state1 state2
1200 1230 1 0
1230 1300 1 10
1300 1330 2 10
1330 1400 2 20
1400 1430 3 20
1430 NULL 3 30
stages of convertion:
1. overall timeline
(SELECT Time FROM States1
UNION
SELECT Time FROM States2) AS timetbl
join states1 table
join states2 table
for this moment all goes well:
SELECT states12tbl.time, states12tbl.state1, states2tbl.State AS state2
FROM (SELECT states12tbl_1.time, states12tbl_1.state1, MAX(states2tbl.Time) AS states2time
FROM (SELECT timetbl.time, states1tbl.State AS state1
FROM (SELECT timetbl_1.Time AS time, MAX(States1tbl.Time) AS state1time
FROM (SELECT Time
FROM States1
UNION
SELECT Time
FROM States2) AS timetbl_1 LEFT OUTER JOIN
States1 AS States1tbl ON States1tbl.Time <= timetbl_1.Time
GROUP BY timetbl_1.Time) AS timetbl INNER JOIN
States1 AS states1tbl ON timetbl.state1time = states1tbl.Time
join table with itself... and here is problem, i need to join code(3) with itself, because sql ce can't remember temp tables... If you have some better idea, please, share :)
Create a VIEW based on your first SELECT.
Onotole, I think that this WITH is making the trouble.
rewrite this query like
SELECT
derivedTbl.time As t0,
derivedTbl.state,
min(rigthTblPart.time)
FROM
(SELECT time,newState from ...) AS derivedTbl
LEFT OUTER JOIN derivedTbl AS rigthTblPart
ON derivedTbl.Time<rightTblPart.Time
....