How to find the most common value after joining three tables - mysql

I have to write a SQL query that, for every user, will return the name of the room that was the most frequently reserved by the user.
I created one of the three tables:
SELECT User.Name as user_name, Room.Name as room_reser
FROM Reservation
INNER JOIN User ON User.Id = Reservation.UserId
INNER JOIN Room ON Room.Id = Reservation.RoomId
Table of 3:
Name room_rese name common_room
Jack room_1
Anna room_2 I need => Jack room_1
Jack room_1
Anna room_1 Anna room_2
Jack room_2
Anna room_2
I tried something like this but I don't know how to use it in this case :
SELECT DISTINCT r.user_name, (
select b.room_reser
from Reservation b
where b.user_name = r.user_name
group by b.user_name, b.roo_reser
order by count(*) desc
limit 1
) as roo_reser from Reservation r)`

If you are running a database that supports window functions, you can do this with aggregation and window function rank():
select user_name, room_name
from (
select
us.name as user_name,
ro.name as room_name,
rank() over(partition by re.userid order by count(*) desc) rn
from reservation re
inner join user us on us.id = re.userid
inner join room ro on ro.id = re.roomid
group by re.userid, re.roomid, us.name, ro.name
) t
where rn = 1
The inner query aggregates by user name and room, and ranks rooms per user. The outer query filters on the top room per user. If there are ties (ie the two most reserved room of a user have the same number of reservations), then both will be displayed - if you want a single record even if there are ties, you can add another sorting criteria to break the tie.
If your database does not support window function, you could try and filter in the having clause with an aggregate correlated subquery:
select
us.name as user_name,
ro.name as room_name
from reservation re
inner join user us on us.id = re.userid
inner join room ro on ro.id = re.roomid
group by us.name, ro.name
having count(*) = (
select count(*)
from reservation re1
where re1.userid = re.userid
group by re1.roomid
order by count(*) desc
limit 1
)

SELECT DISTINCT User.Name as user_name, Room.Name as room_reser
FROM Reservation
INNER JOIN User ON User.Id = Reservation.UserId
INNER JOIN Room ON Room.Id = Reservation.RoomId
GROUP BY user_name, room_reser
ORDER BY COUNT(room_reser)

Related

Subquery left join refer to parent ID

I am trying to make a query to fetch the newest car for each user:
select * from users
left join
(select cars.* from cars
where cars.userid=users.userid
order by cars.year desc limit 1) as cars
on cars.userid=users.userid
It looks like it says Unknown column "users.userid" in where clause
I tried to remove cars.userid=users.userid part, but then it only fetches 1 newest car, and sticks it on to each user.
Is there any way to accomplish what I'm after? thanks!!
For this purpose, I usually use row_number():
select *
from users u left join
(select c.* , row_number() over (partition by c.userid order by c.year desc) as seqnum
from cars c
) c
on c.userid = u.userid and c.seqnum = 1;
One option is to filter the left join with a subquery:
select * -- better enumerate the columns here
from users u
left join cars c
on c.userid = u.userid
and c.year = (select max(c1.year) from cars c1 where c1.userid = c.userid)
For performance, consider an index on car(userid, year).
Note that this might return multiple cars per user if you have duplicate (userid, year) in cars. It would be better to have a real date rather than just the year.
Maybe there are better and more efficient way to query this. Here is my solution;
select users.userid, cars.*
from users
left join cars on cars.userid = users.userid
join (SELECT userid, MAX(year) AS maxDate
FROM cars
GROUP BY userid) as sub on cars.year = sub.maxDate;

How to connect 2 queries SQL

How to connect these 2 queries to receive one that gives such a response:
user_name room_name_reserved room_name_biggest_time
user_1 room_1 room_2
room_name_reserved - most often reserved room by user
room_name_biggest_time - room when user spent the most time
1 query:
SELECT
us.name as user_name
,ro.name as room_name_reserved
FROM reservation re
INNER JOIN user us on us.id = re.userid
INNER JOIN room ro on ro.id = re.roomid
GROUP BY us.name, ro.name
HAVING COUNT(room_name_reser) = (SELECT COUNT(*) FROM reservation re1
WHERE re1.userid = re.userid
GROUP BY re1.roomid
ORDER BY COUNT(*) DESC
LIMIT 1
)
2 query :
SELECT
us.name as user_name
,ro.name as room_name_biggest_time
from Reservation re
INNER JOIN user us on us.id = re.userid
INNER JOIN room ro on ro.id = re.roomid
WHERE (strftime('%s', re.EndsAt) - strftime('%s', re.StartsAt)) = (SELECT MAX((strftime('%s', re1.EndsAt) - strftime('%s', re1.StartsAt))) as time FROM Reservation re1 where re1.UserId = re.userid
ORDER BY time DESC
LIMIT 1 )
Use the two query as subquery in join
select t1.user_name, t1.room_name_reserved , t2.room_name_biggest_time
from (
select
us.name as user_name,
ro.name as room_name_reserved
from reservation re
inner join user us on us.id = re.userid
inner join room ro on ro.id = re.roomid
group by us.name, ro.name
having count(room_name_reser) = (
select count(*)
from reservation re1
where re1.userid = re.userid
group by re1.roomid
order by count(*) desc
limit 1)
) t1
left join (
select
us.name as user_name,
ro.name as room_name_biggest_time
from Reservation re
inner join user us on us.id = re.userid
inner join room ro on ro.id = re.roomid
Where (strftime('%s', re.EndsAt) - strftime('%s', re.StartsAt)) = (
SELECT Max((strftime('%s', re1.EndsAt) - strftime('%s', re1.StartsAt))) as time
from Reservation re1
where re1.UserId = re.userid
order by time desc limit 1
)
) t2 on t1.user_name = t2.user_name
Assuming you are using MySql, here's a little hack that can work:
SELECT
user_name,
SUBSTRING_INDEX(group_concat(room_name ORDER BY n_reservations DESC), ',', 1) as room_name_reserved,
SUBSTRING_INDEX(group_concat(room_name ORDER BY total_stay DESC), ',', 1) as room_name_biggest_time
FROM (
SELECT
us.name as user_name,
ro.name as room_name,
count(*) as n_reservations,
sum(strftime('%s', re.EndsAt) - strftime('%s', re.StartsAt)) as total_stay
FROM user u
JOIN reservation re on us.id = re.userid
JOIN room ro on ro.id = re.roomid
GROUP BY us.name, ro.name
) t
GROUP BY user_name
Explanation:
With the subquery, aliased as t, we extract the total number of reservations (count(*)) and the total staying time (sum(...)) for each user/room, producing an intermediate output as follows:
user room n_reservations total_stay
--------------------------------------
user1 room1 1 6000
user1 room2 2 3000
user1 room3 1 4000
user2 room2 1 9000
user2 room4 5 4000
With the outer query we GROUP BY user, and for each user we first group concat the room name using a sorting based on counts and sums made before (biggest value first), producing an intermediate output as follow:
user group_concat#1 group_concat#2
---------------------------------------
user1 room2,room1,room3 room1,room3,room2
user2 room4,room2 room2,room4
Last step, we split by comma the output of the two group_concat mantaining only the first value, so final result will be:
user room_name_reserved room_name_biggest_time
-----------------------------------------------
user1 room2 room1
user2 room4 room2

Select distinct record from mapping table with condition

In my MySQL database I have these tables:
I want to select count of users who only own birds and no other pet.
So far I've came up with this:
SELECT COUNT(DISTINCT(user_id)) FROM users_pets_map WHERE pet_id IN (SELECT id FROM pets WHERE animal = 'bird')
but it doesn't satisfy the requirement of not owning other animals.
You can do aggregation :
select m.user_id, count(*)
from user_pets_map m inner join
pets p
on p.id = m.pet_id
group by m.user_id
having sum( p.animal <> 'bird' ) = 0;
In other way, you can also do :
select m.user_id, count(*)
from user_pets_map m inner join
pets p
on p.id = m.pet_id
group by m.user_id
having min(p.animal) = max(p.animal) and min(p.animal) = 'bird';
EDIT : If you want only Users count then you can do :
select count(distinct m.user_id)
from user_pets_map m
where not exists (select 1 from user_pets_map m1 where m1.user_id = m.user_id and m1.pet_id <> 3);
You can modify your query as below:
SELECT COUNT(DISTINCT(user_id)) FROM users_pets_map WHERE pet_id IN (SELECT id
FROM pets WHERE animal = 'bird') AND user_id NOT IN (SELECT user_id FROM
users_pets_map WHERE pet_id IN (SELECT id FROM pets WHERE animal <> 'bird'))
The last sub-query will fetch the pet_id who are not birds, the query outside it will fetch users who have animal other than birds. Finally combined your current query it will fetch you the users who does not have any other animals as well as have bird. Although the above query is not the best possible solution in terms of time complexity, but it's one of many solutions as well as easier to understand.
You can use GROUP BY AND HAVING
SELECT COUNT(DISTINCT(user_id)) FROM users_pets_map
WHERE pet_id IN (SELECT id FROM pets WHERE animal = 'bird')
GROUP BY pet_id HAVING COUNT(distinct pet_id)=1

Order data by data from another table

I have two tables:
rooms (all the rooms)
id | title | ...
-----------------
1 |Room 1 |
2 |Room 2 |
3 |Room 3 |
user_rooms (in which room is every user, column user is user's id and it's primary column)
user | room | ...
------------------
20 | 3 |
14 | 1 |
35 | 3 |
So I want to select all the rooms from the 'rooms' table but to order them in that way to show the rooms with the most users in them and after that the rooms with less and less users. For example, I want to show room 3 first (because 2 users are in it), then room 1 (one user in it), and finally room 2 (since no users are in it). How to achieve that?
SELECT aa.id, aa.title
FROM rooms AS aa
LEFT JOIN (
SELECT room, COUNT(*) AS total_count
FROM user_rooms
GROUP BY room
) AS _aa
ON aa.id = _aa.room
ORDER BY _aa.total_count;
This would often be done without a subquery:
select r.id, r.title, count(ur.room) as numusers
from rooms r left join
user_rooms ur
on r.id = ur.room
group by r.id, r.title
order by numusers desc;
This would often be more efficient than a version using a subquery in the from clause because it can take advantage of an index on the join key.
Interestingly, the same index would be used for a correlated subquery in the select, which is an alternative approach:
select r.id, r.title,
(select count(*)
from user_rooms ur
where r.id = ur.room
) as numusers
from rooms r
order by numusers desc;
This might be the most efficient approach, because it removes the aggregation in the outer query.
select r.id, r.title, coalesce(t.cnt,0)
from rooms r left join
(select room, count(*) as cnt
from user_rooms
group by room) t on t.room = r.id
order by t.cnt desc
This will give you only rooms with users
SELECT title, count(user) As MostUsers
FROM Rooms R
INNER JOIN user_rooms U
ON R.?field? = U.?field?
GROUP BY title
ORDER BY MostUsers
You need to complete the query inserting the names of the fields that you can use to Join the tables
If you want all rooms you can use a Left Join:
SELECT title, count(user) As MostUsers
FROM Rooms R
LEFT JOIN user_rooms U
ON R.?field? = U.?field?
GROUP BY title
ORDER BY MostUsers
Please try the following query:
select * from rooms r
order by (select count(1)
from userroom ur
where ur.roomid = r.roomid ) desc

Query on two tables with belongs_to/has_many relation

One table is Users with id and email columns.
Another table is Payments with id, created_at, user_id and foo columns.
User has many Payments.
I need a query that returns each user's email, his last payment date and this last payment's foo value. How do I do that? What I have now is:
SELECT users.email, MAX(payments.created_at), payments.foo
FROM users
JOIN payments ON payments.user_id = users.id
GROUP BY users.id
This is wrong, because foo value does not necessarily belong to user's most recent payment.
Try this :
select users.email,foo,create_at
from users
left join(
select a.* from payments a
inner join (
select id,user_id,max(create_at)
from payments
group by id,user_id
)b on a.id = b.id
) payments on users.id = payments.user_id
If users has no payment yet, then foo and create_at would return NULL. if you want to exclude users who has no payment, then use INNER JOIN.
One approach would be to use a MySQL version of rank over partition and then select only those rows with rank = 1:
select tt.email,tt.created_at,tt.foo from (
select t.*,
case when #cur_id = t.id then #r:=#r+1 else #r:=1 end as rank,
#cur_id := t.id
from (
SELECT users.id,users.email, payments.created_at, payments.foo
FROM users
JOIN payments ON payments.user_id = users.id
order by users.id asc,payments.created_at desc
) t
JOIN (select #cur_id:=-1,#r:=0) r
) tt
where tt.rank =1;
This would save hitting the payments table twice. Could be slower though. Depends on your data!