Limits on one part of MySQL joins - mysql

Currently I'm using the following SQL query to find out what rooms a user is in, and what other users are in those rooms:
SELECT r1.room, r2.user
FROM
rooms r1 JOIN rooms r2 ON r2.room = r1.room
WHERE r1.user='foobar'
Sample rooms table:
room | user
a | user1
b | user1
c | user1
a | user2
a | user3
a | user4
I would like to limit the other users in same rooms part to 50 users returned per room, but not limit the amount of rooms. Is this possible in a single query?
Example response for user1 (let's say I want to limit it to 3 users returned per room):
a, user1
a, user2
a, user3
b, user1
c, user1

It is possible, with some interesting use of parameters, and nesting of derived tables:
SET #user = 'user1';
SELECT
calclist.room,
calclist.user
FROM
(
SELECT
joinedlist.room 'room'
, joinedlist.user 'user'
, IF(joinedlist.room=#room, #rownum:=#rownum+1, #rownum:=1) 'count'
, (#room:=joinedlist.room) dummy
FROM
(
SELECT
roommate_rooms.room,
roommate_rooms.user
FROM
rooms user_rooms
INNER JOIN rooms roommate_rooms
ON user_rooms.room = roommate_rooms.room
WHERE user_rooms.user = #user
ORDER BY
roommate_rooms.room,
roommate_rooms.user
) joinedlist
JOIN (SELECT #rownum:=1) rn
) calclist
WHERE calclist.count <= 3
;
It can cope with the user being in any number of rooms, with or without any roommates; any number of other users who do not share any rooms; and it can cope with the data being entered 'out of order'. Worked example on SQL Fiddler; and credit to Jimmy's Blog for how to use the dynamic parameters to do the counting.

SELECT a.*
FROM rooms a
WHERE
(
SELECT COUNT(*)
FROM rooms b
WHERE a.room = b.room AND
a.user >= b.user
) <= 3
ORDER BY a.Room, a.User
SQLFiddle Demo

Related

How to join the same table more than once to select different columns?

I need to find out users who have either made or received a booking.
I have two tables that look like this:
Users:
+----+
| id |
+----+
| 1 |
| 2 |
| 3 |
+----+
Bookings:
+----+-----+-----+
| id | rid | oid |
+----+-----+-----+
| 1 | 1 | 2 |
| 2 | 2 | 1 |
| 3 | 3 | 4 |
+----+-----+-----+
A booking has two users, a 'rider' (rid), and an 'owner' (oid).
The rider and owner can't be the same for each booking but riders can also be owners.
My output should be a list of user IDs that correspond with users who have made or received a booking.
So far I have written
select u.id, b1.rid, b2.oid
from users u
left join bookings b1
on u.id = b1.rid
left join bookings b2
on u.id = b2.oid;
And various other permutations, but I'm not getting the desired result. Any help would be appreciated.
You want all User IDs that are either in Bookings.rid or Bookdings.oid. So you could do something like:
select
users.id
from
users
where
users.id in (select bookings.rid from bookings)
or
users.id in (select bookings.oid from bookings);
You should be able to utilize a UNION clause here.
However, you don't define what the "time window" is, so I am not sure we can come up with a complete solution for you. However, try something like the following:
SELECT
users.id,
bookings.rid,
bookings.oid
FROM
users
LEFT JOIN bookings ON users.id = bookings.rid
UNION ALL
SELECT
users.id,
bookings.rid,
bookings.oid
FROM
users
LEFT JOIN bookings ON users.id = bookings.oid
My output should be a list of user IDs that correspond with users who have made or received a booking.
To do that, you only need to look at the bookings table :
SELECT DISTINCT rid id FROM bookings
UNION ALL SELECT DISTINCT oid FROM bookings
The DISTINCT removes the duplicates returned by each query, and the UNION ALL removes duplicates across both queries.
If you are looking to filter by time frame :
SELECT DISTINCT rid id FROM bookings WHERE some_date BETWEEN :start_date AND :end_date
UNION ALL SELECT DISTINCT oid FROM bookings WHERE some_date BETWEEN :start_date AND :end_date
Where some_date is the field that contains the booking date, and :start_date/end_date are the beginning and the end of the date interval.
I guess there is a name column in Users table.
If you want this too then:
select users.id, users.name from (
select rid userid from bookings
union
select oid userid from bookings
) t inner join users
on users.id = t.userid
group by users.id, users.name
See the demo
If not you only need to scan the bookings table:
select distinct userid from (
select rid userid from bookings
union
select oid userid from bookings
) t
See the demo

Count with Join in mysql

So I have this two table where it records what kind of food is the user's favorite:
users table
------------
id | country
------------
1 | US
2 | PH
3 | US
4 | US
5 | PH
food_favourites table
-----------------
food_id | user_id
-----------------
3 | 1
7 | 1
3 | 2
3 | 3
3 | 4
I want to know how many unique users from US tagged food_id 3 as their favorite.
So far I have this query:
select *, count(user_id) as total
from food_favourite
inner join users on users.id = food_favourites.user_id
where food_favourites.food_id = 3
and users.country = 'US'
group by users.id
Well This doesn't work coz it returns total to 4 instead of just 3.
I also tried doing subqueries - no luck, I think I'm missing something.
DROP TABLE IF EXISTS users;
CREATE TABLE users
(user_id SERIAL PRIMARY KEY
,country CHAR(2) NOT NULL
);
INSERT INTO users VALUES
(1,'US'),
(2,'EU'),
(3,'US'),
(4,'US'),
(5,'EU');
DROP TABLE IF EXISTS favourite_foods;
CREATE TABLE favourite_foods
(food_id INT NOT NULL
,user_id INT NOT NULL
,PRIMARY KEY(food_id,user_id)
);
INSERT INTO favourite_foods VALUES
(3,1),
(7,1),
(3,2),
(3,3),
(3,4);
SELECT COUNT(DISTINCT u.user_id) distinct_users
FROM users u
JOIN favourite_foods f
ON f.user_id = u.user_id
WHERE u.country = 'US'
AND f.food_id = 3;
+----------------+
| distinct_users |
+----------------+
| 3 |
+----------------+
First of all the answer to the above question should be 3 as id 1,3,4 all have food_id 3 as their favorite food.
To just print the query try this, it will surely work:
select count(*) as total from food_favourites
inner join users on users.id=food_favourites.user_id
where food_id=3 and country='US';
I want to know how many unique users from US tagged food_id 3 as their favorite.
You count unique values with COUNT DISTINCT:
select count(distinct ff.user_id) as total
from food_favourite ff
inner join users u on u.id = ff.user_id
where ff.food_id = 3
and u.country = 'US';
Don't group by user, because you don't want a result per user. You want one row with one number, telling you how many US users prefer food 3.
An alternative that I prefer over the join. The query reads like I would word the task: count users from US that like food 3.
select count(*) as total
from users
where country = 'US'
and id in (select user_id from food_favourites where food_id = 3);
No unnecessary join and hence no need to get back to distinct values.
The sub_query is
SELECT u.country, f.food_id, COUNT(u.id) AS 'Total users'
FROM users u
INNER JOIN food_favourites AS f ON (u.id = f.[user_id])
WHERE u.country = 'US'
GROUP BY u.country, f.food_id
select count(user_id) as total, Country
from food_favourites
inner join users on users.id = food_favourites.user_id
where food_favourites.food_id = 3
and users.country = 'US'
group by country
Untested, but I think this is what you're after? This will return results only for the US and a food id of 3. If you want something more reusable that you can simply loop through the results for ALL countries...something like this should work (once again, untested...):
select count(user_id) as total, Country, food_id
from food_favourites
inner join users on users.id = food_favourites.user_id
group by country, food_id
order by country, food_id
Try:
select count(user_id) as total
from food_favourites
inner join users on users.id = food_favourites.user_id
where food_favourites.food_id = 3
and users.country = 'US'

Selecting a count of rows having a max value

Working example: http://sqlfiddle.com/#!9/80995/20
I have three tables, a user table, a user_group table, and a link table.
The link table contains the dates that users were added to user groups. I need a query that returns the count of users currently in each group. The most recent date determines the group that the user is currently in.
SELECT
user_groups.name,
COUNT(l.name) AS ct,
GROUP_CONCAT(l.`name` separator ", ") AS members
FROM user_groups
LEFT JOIN
(SELECT MAX(added), group_id, name FROM link LEFT JOIN users ON users.id = link.user_id GROUP BY user_id) l
ON l.group_id = user_groups.id
GROUP BY user_groups.id
My question is if the query I have written could be optimized, or written better.
Thanks!
Ben
You actual query is not giving you the answer you want; at least, as far as I understand your question. John actually joined group 2 on 2017-01-05, yet it appears on group 1 (that he joined on 2017-01-01) on your results. Note also you're missing one Group 4.
Using standard SQL, I think the next query is what you're looking for. The comments in the query should clarify what each part is doing:
SELECT
user_groups.name AS group_name,
COUNT(u.name) AS member_count,
group_concat(u.name separator ', ') AS members
FROM
user_groups
LEFT JOIN
(
SELECT * FROM
(-- For each user, find most recent date s/he got into a group
SELECT
user_id AS the_user_id, MAX(added) AS last_added
FROM
link
GROUP BY
the_user_id
) AS u_a
-- Join back to the link table, so that the `group_id` can be retrieved
JOIN link l2 ON l2.user_id = u_a.the_user_id AND l2.added = u_a.last_added
) AS most_recent_group ON most_recent_group.group_id = user_groups.id
-- And get the users...
LEFT JOIN users u ON u.id = most_recent_group.the_user_id
GROUP BY
user_groups.id, user_groups.name
ORDER BY
user_groups.name ;
This can be written in a more compact way in MySQL (abusing the fact that, in older versions of MySQL, it doesn't follow the SQL standard for the GROUP BY restrictions).
That's what you'll get:
group_name | member_count | members
:--------- | -----------: | :-------------
Group 1 | 2 | Mikie, Dominic
Group 2 | 2 | John, Paddy
Group 3 | 0 | null
Group 4 | 1 | Nellie
dbfiddle here
Note that this query can be simplified if you use a database with window functions (such as MariaDB 10.2). Then, you can use:
SELECT
user_groups.name AS group_name,
COUNT(u.name) AS member_count,
group_concat(u.name separator ', ') AS members
FROM
user_groups
LEFT JOIN
(
SELECT
user_id AS the_user_id,
last_value(group_id) OVER (PARTITION BY user_id ORDER BY added) AS group_id
FROM
link
GROUP BY
user_id
) AS most_recent_group ON most_recent_group.group_id = user_groups.id
-- And get the users...
LEFT JOIN users u ON u.id = most_recent_group.the_user_id
GROUP BY
user_groups.id, user_groups.name
ORDER BY
user_groups.name ;
dbfiddle here

Select with Multiple Counts on Left Join on Same Table

I'm not certain that this can be done, I have a table of users with a related table of user activity joined on a foreign key. Activity has different types, e.g. comment, like etc. I need to get users filtered by the number of each different type of activity.
What I have so far is this:
SELECT
users.*,
COUNT(t1.id) AS comments,
COUNT(t2.id) AS likes
FROM users
LEFT JOIN activity AS t1 ON users.id = t1.user_id
LEFT JOIN activity AS t2 ON users.id = t2.user_id
WHERE t1.activity_type_id = 1 AND t2.activity_type_id = 2
GROUP BY users.id
HAVING comments >= 5 AND likes >= 5
This seems to be close but it's returning a user with a count of 5 both likes and comments, when in reality the user has 5 likes and 1 comment.
To be clear I want this query to return users who have 5 or more likes and also users who have 5 or more comments.
UPDATE:
I've created an SQL Fiddle. In this case I have 3 users:
User 1: 6 comments, 8 likes
User 2: 3 comments, 2 likes
User 3: 5 comments, 2 likes
I want the query to return only user 1 and 3, with their respective totals of likes and comments.
http://sqlfiddle.com/#!2/dcc63/4
You can use conditional summing to do the count and due to the way MySQL treats boolean expressions an expression like sum(case when et.name = 'comment' then 1 else 0 end) (the "normal" SQL syntax) can be reduced to sum(case when et.name = 'comment').
SELECT
u.id,
sum(et.name = 'comment') AS comments,
sum(et.name = 'like') AS likes
FROM users AS u
LEFT JOIN engagements AS e ON u.id = e.user_id
JOIN engagement_types AS et ON e.engagement_type_id = et.id
GROUP BY u.id
HAVING sum(et.name = 'comment') >= 5
OR sum(et.name = 'like') >= 5
Result:
| ID | COMMENTS | LIKES |
|----|----------|-------|
| 1 | 6 | 8 |
| 3 | 5 | 2 |
Sample SQL Fiddle

Group messages by latest response in conversation threading

I need a simple internal messaging system between users.
My tables:
+--------------+ +---------------------+
| messages | | users |
+----+---------+ +---------------------+
| id | message | | id | username | ...
+----+---------+ +---------------------+
+------------------------------------------------------------------------------+
| users_messages |
+------------------------------------------------------------------------------+
| id | from_usr_id | to_usr_id | msg_id | thread_id | read | sent_at | read_at |
+------------------------------------------------------------------------------+
INT 'thread_id' represents the conversation thread, its used to group messages.
BOOLEAN 'read' represents if the user opened/viewed the message or not.
I want to group messages by 'thread_id', sorted by 'sent_at' so I can show the user his latest messages by thread. I want also to count the messages in each thread.
I want to get something like this for a specific user id:
+----------------------------------------------------------------------------
| last_messages_by_conversation
+----------------------------------------------------------------------------
| message | from_username | sent_at | count_thread_msgs | count_unread_msg |
+----------------------------------------------------------------------------
TEXT 'message' is the latest message in the specific 'thread_id'
VARCHAR 'from_username' and DATETIME 'sent_at' are related to the latest message.
INT 'count_thread_msgs' and INT 'count_unread_msg' are related to the thread, representing the total number of messages and the number of unread messages in the thread.
Each row represents a thread/conversation (group by 'thread_id'), showing the last message (sorted by 'sent_at') for that specific thread.
You are looking for the groupwise maximum, which can be found by first grouping the users_messages table by thread_id and selecting MAX(sent_at), then joining the result back onto the users_messages table to find the other fields of that maximum record.
I find that NATURAL JOIN is a very handy shortcut here:
SELECT messages.message,
users.username AS from_username,
t.sent_at,
t.count_thread_msgs,
t.count_unread_msg
FROM users_messages NATURAL JOIN (
SELECT thread_id,
to_usr_id,
MAX(sent_at) AS sent_at,
COUNT(*) AS count_thread_msgs,
SUM(NOT read) AS count_unread_msg
FROM users_messages
WHERE to_usr_id = ?
GROUP BY thread_id
) t JOIN messages ON messages.id = users_messages.msg_id
JOIN users ON users.id = users_messages.from_usr_id
SELECT
users.id,
users.username,
user_messages.thread_id,
user_messages.unread ,
messages.message
FROM users
LEFT JOIN (SELECT
from_usr_id ,
msg_id,
count(thread_id)) as thread_id,
count(read_at) as unread
FROM user_messages)as user_messages on user_messages.from_usr_id = users.id
LEFT JOIN messages on messages.id = user_messages.msg_id
You can try this solution:
SELECT c.message,
d.username AS from_username,
b.sent_at,
a.count_thread_msgs,
a.count_unread_msg
FROM (
SELECT MAX(id) AS maxid,
COUNT(*) AS count_thread_msgs,
COUNT(CASE WHEN `read` = 0 AND <uid> = to_usr_id THEN 1 END) AS count_unread_msg
FROM users_messages
WHERE <uid> IN (from_usr_id, to_usr_id)
GROUP BY thread_id
) a
JOIN users_messages b ON a.maxid = b.id
JOIN messages c ON b.msg_id = c.id
JOIN users d ON b.from_usr_id = d.id
ORDER BY b.sent_at DESC
This gets the latest message in each thread that the user <uid> started or is a part of.
The latest message is based on the highest id of each thread_id.
This solution makes the following assumptions:
The id in users_messages is a unique auto-incrementing int for each new row.
Each thread contains correspondence between never more than two users.
If the thread can contain more than two users, then the query will need to be slightly adjusted so as to derive an accurate count aggregation.
Try this and let me know, change $$ for your user ID..
select u.username,msg.message,m.sent_at,
(select count(*) from user_message where read=0 and to_usr_id=$$) as count_thread_msgs,
(select count(*) from user_message where to_usr_id= $$) as count_unread_msg
from users as u join user_messages as m
on u.id=m.id where u.id=$$
join messages as msg on msg.id=m.id
group by u.id;`
Try this query -
SELECT
m.message,
u.username from_username,
um1.sent_at,
um2.count_thread_msgs,
um2.count_unread_msg
FROM users_messages um1
JOIN (
SELECT
thread_id,
MAX(sent_at) sent_at,
COUNT(*) count_thread_msgs,
COUNT(IF(`read` = 1, `read`, NULL)) count_unread_msg
FROM users_messages GROUP BY thread_id) um2
ON um1.thread_id = um2.thread_id AND um1.sent_at = um2.sent_at
JOIN messages m
ON m.id = um1.msg_id
JOIN users u
ON u.id = um1.from_usr_id
-- WHERE u.id = 100 -- specify user id here
Answers on your questions:
About last datetime: I have changed query a little, just try new one.
About specific users: Add WHERE condition to filter users - ...WHERE u.id = 100.
About many records: because you join another tables (messages and users), and there can be more then one record with the same thread_id. To avoid this you should group result set by thread_id field and use aggregate function to get single result, e.g. using GROUP_CONCAT function.