MySQL Booking system: Getting available rooms - mysql

I have a problem with my query for locating available rooms in a simple hotel booking system.
My table structure looks like the following:
hotels
hotelroomtypes (intersects a hotel, with a room and a roomtype)
bookings
orders
There is more to it, but the use of the other tables sums themselves up in the query, which looks like the following:
SELECT DISTINCT
hotels.HotelName
,hotels.HotelID
,hotels.Address
,hotels.Description
,images.URL
,roomtypes.Price
,roomtypes.RoomtypeName
,roomtypes.RoomtypeID
,COUNT(DISTINCT hotelroomtypes.HRID) AS AvailableRooms
FROM hotelroomtypes
INNER JOIN hotels ON (
hotelroomtypes.HotelID = hotels.HotelID
AND
hotels.CountryID = 1 // e.g. United States
)
INNER JOIN roomtypes ON (
hotelroomtypes.RoomtypeID = roomtypes.RoomtypeID
AND
roomtypes.RoomtypeID = 1 // e.g. Suite
)
RIGHT OUTER JOIN bookings ON (
hotelroomtypes.HRID NOT IN (
SELECT HRID FROM bookings
WHERE bookings.From BETWEEN '2015-06-13' and '2015-06-16'
OR bookings.To BETWEEN '2015-06-13' and '2015-06-16'
)
)
INNER JOIN images ON (
hotels.ImageID = images.ImageID
)
GROUP BY
hotels.HotelName
HAVING COUNT(hotelroomtypes.HRID) > 0
ORDER BY hotels.HotelName ASC, AvailableRooms ASC
Now my problems lay with getting the correct number of booked rooms, and also hiding the hotel from the query if all the rooms are taken.
I used to do this by having an INNER JOIN on bookings, but when I then had an empty bookings table, the query failed 100% and didn't show any hotels.
With LEFT joins, I had some false positives at times, but currently using these types of joins all return EVERY room the hotel has or none at all.
INNER naturally returns no hotels if there are no bookings present, as expected. Which is what I was using earlier.
How can I correcly structure this query to give me an exact amount of available rooms on each hotel, as well as only returning hotels which have available rooms, even if there are no bookings to search through?
edit for table structures:
Hotels:
_________________________________________________________________
| hotelid | hotelname | countryid | description | otherfieldshere |
|_________|___________|___________|_____________|_________________|
| 1 | example | 1 | something | something |
|_________|___________|___________|_____________|_________________|
Hotelroomtypes:
______________________________________
| hrid | hotelid | roomtypeid | roomid |
|______|_________|____________|________|
| 1 | 1 | 1 | 1 |
|______|_________|____________|________|
Bookings:
_______________________________________________
| bookingid | from | to | orderid |
|___________|____________|____________|_________|
| 1 | 2015-05-13 | 2015-05-16 | 1 |
|___________|____________|____________|_________|
Orders:
________________________________________
| orderid | reference | email |
|_________|___________|__________________|
| 1 | 12345 | some#example.com |
|_________|___________|__________________|
Rooms:
_____________________
| roomid | roomnumber |
|________|____________|
| 1 | 500 |
|________|____________|

Unfortunately it seems alcohol and problem solving isn't really an option when you've been working for 7 hours straight without a break.
I restructured my query to the following:
SELECT DISTINCT
hotels.HotelName
,hotels.HotelID
,hotels.Address
,hotels.Description
,images.URL
,roomtypes.Price
,roomtypes.RoomtypeName
,roomtypes.RoomtypeID
,COUNT(DISTINCT hotelroomtypes.HRID) AS AvailableRooms
FROM hotels
INNER JOIN hotelroomtypes ON (
hotelroomtypes.HotelID = hotels.HotelID
AND
hotels.CountryID = ?
AND
hotelroomtypes.HRID NOT IN (
SELECT HRID FROM bookings
WHERE bookings.From BETWEEN ? and ?
OR bookings.To BETWEEN ? and ?
)
)
INNER JOIN roomtypes ON (
hotelroomtypes.RoomtypeID = roomtypes.RoomtypeID
AND
roomtypes.RoomtypeID = ?
)
INNER JOIN images ON (
hotels.ImageID = images.ImageID
)
GROUP BY
hotels.HotelName
HAVING COUNT(hotelroomtypes.HRID) > 0
ORDER BY hotels.HotelName ASC, AvailableRooms ASC
Which actually solved my problem.

Related

Match only if all elements of a column are in another table

I've made a little database in SQL that as 2 tables Product (Name, Ingredient and Available (Ingredient):
| Product | Available |
| Name | Ingredient | Ingredient |
| 1 | a | a |
| 1 | b | c |
| 2 | a |
| 2 | c |
I want the name of a product only if ALL its ingredients are inside the Available table.
For the previous example, the result should be: Product "2"
and not Product "1", because I don't have the ingredient "b" in the Available table.
Thanks for the help
You can try with left join (to figure out which Products don't have necessary Ingredients) and group by + having to filter Products that have at least one missing Ingredient:
select p.Name
from Products p
left join Available a on a.Ingredient = p.Ingredient
group by p.Name
having sum(a.Ingredient is null) = 0
You can try something like this also:
WITH TEMP_PRODUCTS AS
(
SELECT NAME, COUNT(1) AS NUMBER_OF_INGREDIENTS
FROM PRODUCT
GROUP BY PRODUCT
)
SELECT PRD.NAME, COUNT(1) AS NUMBER_OF_AVAILABLE_INGREDIENTS
FROM PRODUCT PRD
JOIN TEMP_PRODUCTS TMP ON PRD.NAME = TMP.NAME
WHERE EXISTS (SELECT 1
FROM INGREDIENT ING
WHERE ING.INGREDIENT = PRD.INGREDIENT)
GROUP BY PRD.NAME
HAVING COUNT(1) = TMP.NUMBER_OF_INGREDIENTS;

Multiple joins in mysql tables with union

In mysql, I am having an issue trying to get the right data. I think I have to use union to get all the results from both tables, but not sure how to do it.
Description of tables:
Order holds order numbers
Employee holds the employee
Zone holds the zones names
Actual time has the zone id, the order id and the amount of hours it should take to deliver
Deliver details contains the employee id, the zone the employee delivered and the amount of hours it took to deliver
Order
| id | number |
|----|--------|
| 1 | 0001 |
employees
| id | name |
|----|------|
| 1 | Jon |
zones
| id | name |
|----|-------|
| 1 | ZoneA |
| 2 | ZoneB |
actual_times
| id | zone_id | eta_hours | order_id |
|----|---------|-----------|----------|
| 1 | 1 | 5 | 1 |
| 2 | 2 | 4 | 1 |
deliver_details
| id | order_id | employee_id | zone_id | hours |
|----|----------|-------------|---------|-------|
| 1 | 1 | 1 | 1 | 3 |
| 2 | 1 | 1 | 1 | 1 |
What I am hoping to get is the zone name, the amount of hours it takes to deliver and the sum of hours the employee took deliver. If the employee did not deliver to that zone then show 0
Expected output
| zone_name | hours | eta_hours | employee_name |
|-----------|-------|-----------|---------------|
| ZoneA | 4 | 5 | Jon |
| ZoneB | 0 | 4 | Jon |
I tried making a union all on the actual time but I am not getting it right.
This is something I tried (note that this was just to get the right zones with deliver times and actual times).
SELECT deliver_details.zone_id, actual_times.zone_id, zones.zone_name FROM actual_times
RIGHT JOIN deliver_details ON actual_times.order_id = deliver_details.order_id
INNER JOIN zones ON zones.id = deliver_details.zone_id
WHERE deliver_details.order_id = 1
GROUP BY deliver_details.zone_id
UNION ALL
SELECT deliver_details.zone_id, actual_times.zone_id, zones.zone_name FROM actual_times
LEFT JOIN deliver_details ON actual_times.order_id = deliver_details.order_id
INNER JOIN zones ON zones.id = actual_times.zone_id
WHERE actual_times.order_id = 1
group by actual_times.zone_id
I am pretty much trying to get all of this in one query. Is there a way to do this?
Please note that this is a simplification to a more complex problem I am having. If you need more explanation or something does not make sense, please let me know.
No need to use UNION.
Start from table employees, then CROSS JOIN with zones and actual_times to get a simple cartesian products. Then search the deliver_details for deliveries performed by each employee on each zone ; use a LEFT JOIN for that. If an epmplyee did not deliver on a given zone, use COALESCE to return 0 instead of NULL.
Query :
select
z.name,
coalesce(sum(dd.hours), 0),
at.eta_hours,
e.name
from
employees e
cross join zones z
inner join actual_times at on at.zone_id = z.id
left join deliver_details dd on dd.employee_id = e.id and dd.id = at.zone_id
group by
z.name, at.eta_hours, e.name
I came up with a simillar solution to GMB, but using UNION to get rows with 0 hours...:
SELECT z.name, sum(dd.hours), at.eta_hours, e.name
FROM zones z JOIN deliver_details dd ON z.id = dd.zone_id
JOIN actual_times at ON z.id = at.zone_id
JOIN employees e ON dd.employee_id = e.id
GROUP BY z.name, at.eta_hours, e.name
UNION
SELECT z.name, 0, at.eta_hours, e.name
FROM zones z JOIN actual_times at ON z.id = at.zone_id,
employees e
WHERE e.id NOT IN (SELECT employee_id FROM deliver_details WHERE zone_id = z.id)
If you have only one employee for each zone the following query should work for you:
SELECT Z.name AS zone_name
,DT.hours_total
,ACT.eta_hours_total
,E.name AS employee_name
FROM zones Z
INNER JOIN (SELECT zone_id
, SUM(eta_hours) eta_hours_total
FROM actual_times
GROUP BY zone_id) ACT ON Z.zone_id = ACT.zone_id
INNER JOIN (SELECT zone_id
, employee_id
, SUM(hours) hours_total
FROM deliver_details
GROUP BY zone_id, employee_id) DT ON Z.zone_id = DT.zone_id
INNER JOIN employees E ON DT.employee_id = E.employee_id

MySQL - Get records from INNER JOIN not between dates

I have two tables
Accounts:
+------------+--------+
| accountsid | name |
+------------+--------+
| 1 | Bob |
| 2 | Rachel |
| 3 | Mark |
+------------+--------+
Sales Orders
+--------------+------------+------------+--------+
| salesorderid | accountsid | so_date | amount |
+--------------+------------+------------+--------+
| 1 | 1 | 2015-12-16 | 50 |
| 2 | 1 | 2016-01-13 | 20 |
| 3 | 2 | 2015-12-14 | 10 |
| 4 | 3 | 2016-02-14 | 35 |
+--------------+------------+------------+--------+
As you can see, is a 1-N relation where Accounts has many Salesorders and Salesorder has 1 Account.
I need to retrieve "old" Accounts where are not active anymore. For example, If some Account dont have Salesorder in 2016 is an inactive Account.
So, in this example the result will be ONLY Rachel.
How can i retrieve this? I think its the "opposite" of between but I cant figure how to do it...
Thanks.
PS. Despite the title I can get this without INNER JOIN.
You're looking to effect an anti-join, for which there are three possibilities in MySQL:
Using NOT IN:
SELECT a.*
FROM Accounts a
WHERE a.accountsid NOT IN (
SELECT so.accountsid
FROM `Sales Orders` so
WHERE so.so_date >= '2016-01-01'
)
Using NOT EXISTS:
SELECT a.*
FROM Accounts a
WHERE NOT EXISTS (
SELECT *
FROM `Sales Orders` so
WHERE so.accountsid = a.accountsid
AND so.so_date >= '2016-01-01'
)
Using an outer JOIN:
SELECT a.*
FROM Accounts a LEFT JOIN `Sales Orders` so
ON so.accountsid = a.accountsid
AND so.so_date >= '2016-01-01'
WHERE so.accountsid IS NULL
why do you need to use only inner join? inner join is for cases you have data matching on two tables but in this case you don't you need to be using a subquery with either "not in" or "not exists"
What you want is to get the ids that didn´t make any order, so get the ids that made some order and the rest of them are the ones that didn´t make orders.
It should be something like this SELECT * FROM Accounts WHERE accountsid NOT IN (SELECT accountsid FROM Sales Orders WHERE so_date > your_date)

join one row to all row and returning all row

can I get data like this from my table
| id_outlet| date | count(msisdn) |
| 34.10.1 | 2014-08 | 0 |
| 34.10.1 | 2014-09 | 3 |
| 34.10.1 | 2014-10 | 2 |
| 34.10.2 | 2014-08 | 1 |
| 34.10.2 | 2014-09 | 0 |
| 34.10.2 | 2014-10 | 0 |
So I have 2 tables
1. table outlet (unique)
2. table sales (detail of table outlet)
As u see in my second table there are 3 periode (2014-08, 2014-09, 2014-10)
I want join that periode with id_outlet in first table like that example.
Can I?
Please Help me
Using a CROSS JOIN:-
SELECT
o.id_outlet,
s_main.periode,
o.branch,
count(msisdn)
FROM
(
SELECT DISTINCT SUBSTRING(date,1,7) AS periode
FROM sales
) s_main
CROSS JOIN outlet o
LEFT OUTER JOIN sales s
ON s_main.periode = SUBSTRING(s.date,1,7)
AND o.id_outlet = s.id_outlet
WHERE (o.STATUS LIKE 'STREET%')
GROUP BY s_main.periode, o.branch, o.id_outlet
If you have a table of dates then you can just use that rather than the sub query to get the dates (which also avoids the potential problem of not having a date in the results for a month where there has been zero sales for any outlet).
Don't worry, be happy!
SELECT
o.id_outlet,
SUBSTRING(s.date,1,7) AS periode,
o.branch
FROM outlet o LEFT JOIN sales s ON o.id_outlet = s.id_outlet
WHERE (o.STATUS LIKE 'STREET%')
ORDER BY o.id_outlet, YEAR(s.DATE), MONTH(s.DATE), branch
You need this query:
SELECT
o.id_outlet,
d.period AS periode,
o.branch,
count(msisdn)
FROM dates d LEFT JOIN outlet o ON d.period = SUBSTRING(o.date,1,7) LEFT JOIN sales s ON o.id_outlet = s.id_outlet
WHERE (o.STATUS LIKE 'STREET%')
GROUP BY CONCAT(d.period, '#', s.id_outlet)
ORDER BY o.id_outlet, d.period, branch

mysql subquery not producing all results

I have two tables: contacts and client_profiles. A contact has many client_profiles, where client_profiles has foreign key contact_id:
contacts:
mysql> SELECT id,first_name, last_name FROM contacts;
+----+-------------+-----------+
| id | first_name | last_name |
+----+-------------+-----------+
| 10 | THERESA | CAMPBELL |
| 11 | donato | vig |
| 12 | fdgfdgf | gfdgfd |
| 13 | some random | contact |
+----+-------------+-----------+
4 rows in set (0.00 sec)
client_profiles:
mysql> SELECT id, contact_id, created_at FROM client_profiles;
+----+------------+---------------------+
| id | contact_id | created_at |
+----+------------+---------------------+
| 6 | 10 | 2014-10-09 17:17:43 |
| 7 | 10 | 2014-10-10 11:38:01 |
| 8 | 10 | 2014-10-10 12:20:41 |
| 9 | 10 | 2014-10-10 12:24:19 |
| 11 | 12 | 2014-10-10 12:35:32 |
+----+------------+---------------------+
I want to get the latest client_profiles for each contact. That means There should be two results. I want to use subqueries to achieve this. This is the subquery I came up with:
SELECT `client_profiles`.*
FROM `client_profiles`
INNER JOIN `contacts`
ON `contacts`.`id` = `client_profiles`.`contact_id`
WHERE (client_profiles.id =
(SELECT `client_profiles`.`id` FROM `client_profiles` ORDER BY created_at desc LIMIT 1))
However, this is only returning one result. It should return client_profiles with id 9 and 11.
What is wrong with my subquery?
It looks like you were trying to filter twice on the client_profile table, once in the JOIN/ON clause and another time in the WHERE clause.
Moving everything in the where clause looks like this:
SELECT `cp`.*
FROM `contacts`
JOIN (
SELECT
`client_profiles`.`id`,
`client_profiles`.`contact_id`,
`client_profiles`.`created_at`
FROM `client_profiles`
ORDER BY created_at DESC
LIMIT 1
) cp ON `contacts`.`id` = `cp`.`contact_id`
Tell me what you think.
Should be something like maybe:
SELECT *
FROM `client_profiles`
INNER JOIN `contacts`
ON `contacts`.`id` = `client_profiles`.`contact_id`
GROUP BY `client_profiles`.`contact_id`
ORDER BY created_at desc;
http://sqlfiddle.com/#!2/a3f21b/9
You need to prequery the client profiles table grouped by each contact.. From that, re-join to the client to get the person, then again to the client profiles table based on same contact ID, but also matching the max date from the internal prequery using max( created_at )
SELECT
c.id,
c.first_name,
c.last_name,
IDByMaxDate.maxCreate,
cp.id as clientProfileID
from
( select contact_id,
MAX( created_at ) maxCreate
from
client_profiles
group by
contact_id ) IDByMaxDate
JOIN contacts c
ON IDByMaxDate.contact_id = c.id
JOIN client_profiles cp
ON IDByMaxDate.contact_id = cp.contact_id
AND IDByMaxDate.maxCreate = cp.created_at