SQL Query to retrieve messages from all user's friends - mysql

I have 3 databases setup for a Twitter-clone website.
Table 'users':
email | firstname | lastname | hash_password
Link Table 'friends':
email | friend
This lists all users and their friends. Friendships are 1-way (like Twitter's "Following"/"Followers").
friends table is a link table to list all users' friends. So both friends.user and friends.friend are foreign keys to users.email.
Table 'messages':
timestamp | user | content
What is an SQL query to retrieve all messages from a user and his friends?
I've tried:
SELECT
'timestamp',
user,
content
FROM
messages
INNER JOIN friends ON messages.user = friends.friend
ORDER BY 'timestamp' DESC;
This seems to join them correctly, but how can I only get the messages for a specific user's (email address) friends. Right now this just returns all messages.
Thanks.

The quoting of timestamp should be with backticks, otherwise MySQL will assume a string literal value.
Otherwise, to return messages from both the user and all friends, you may use a UNION. One half returns the user's messages, the other half returns friends' messages. You need to add the user's email to your join condition:
/* First part of the UNION returns friends messages */
SELECT
`timestamp`,
user,
content
FROM
messages
/* The join ON clause specifies the main user email */
INNER JOIN friends
ON messages.user = friends.friend
AND friends.email = 'email#example.com'
UNION ALL
/* Other part of the UNION just gets all messages for the main user by email addr */
SELECT
`timestamp`,
user,
content
FROM
messages
WHERE user = 'email#example.com'
/* ORDER BY applies to everything */
ORDER BY `timestamp` DESC;
And if you want to join in the users info (firstname/lastname) here, the easiest way to go about it would be to wrap the entire thing in a subquery and join.
SELECT
users.*,
messages_sub.content,
messages_sub.`timestamp`
FROM
users
JOIN (/* The entire thing from above */) AS messages_sub ON users.email = messages_sub.user
ORDER BY `timestamp`
It can also be done with a UNION of the literal email address you want to find and the friend list, producing only one outer query. This is a little trickier, but may be ultimately faster. It will also be less confusing to bring in other columns from the users table here. I'll add the names:
SELECT
`timestamp`,
user,
firstname,
lastname,
content
FROM
messages
INNER JOIN (
/* Union query produces a list of email addrs -
the person you're looking for plus all his friends
Starts with string literal for the main user
*/
SELECT 'email#example.com' AS email
UNION
/* Plus all his friends into one list joined against messages */
SELECT friend AS email FROM friends WHERE email = 'email#example.com'
) user_and_friends ON messages.user = user_and_friends.email
/* Join against the users table for name info */
INNER JOIN users ON user_and_friends.email = users.email
ORDER BY `timestamp` DESC

Related

Querying a large table using mysql

I manage a property website. I have a table with banned users (small table) and a table called advert_views which keeps track of each listing that each user views (currently 1.3m lines and growing). The advert_views table alsio takes note of the IP address for every advert viewed).
I want to get the IP addresses used by the banned users and check if any of these banned users have opened new accounts. I ran the following query:
SELECT adviews.user_id AS 'banned user_id',
adviews.client_ip AS 'IPs used by banned users',
adviews2.user_id AS 'banned users that opened a new account'
FROM banned_users
LEFT JOIN users on users.email_address = banned_users.email_address #since I don't store the user_id in banned_users
LEFT JOIN advert_views adviews ON adviews.user_id = users.id AND adviews.user_id IS NOT NULL # users may view listings when not logged in but they have restricted access to the information on the listing
LEFT JOIN (SELECT client_ip,
user_id
FROM advert_views
WHERE user_id IS NOT NULL
) adviews2
ON adviews2.client_ip = adviews.client_ip
WHERE banned_users.rec_status = 1 and adviews.user_id <> adviews2.user_id
GROUP BY adviews2.user_id
I applied an index on the advert_views table and the users table as per below:
enter image description here
My query takes half an hour to execute. Is there a way how to improve my query speed?
Thanks!
Chris
First of all: Why do you outer join the tables? Or better: Why do you try to outer join the tables? A left join is meant to get data from a table even when there is no match. But then your results could contain rows with all values null. (That doesn't happen though, because adviews.user_id <> adviews2.user_id in your where clause dismisses all outer-joined rows.) Don't give the DBMS more work to do than necessary. If you want inner joins, then don't outer join. (Though the difference in execution time won't be huge.)
Next: You select from banned_users, but you only use it to check existence. You shouldn't do this. Use an EXISTS or IN clause instead. (This is mainly for readability and in order not to produce duplicate results. This probably won't speed things up.)
SELECT av1.user_id AS 'banned user_id',
av2.client_ip AS 'IPs used by banned users',
av2.user_id AS 'banned users that opened a new account'
FROM adviews av1
JOIN adviews av2 ON av2.client_ip = av1.client_ip AND av2.user_id <> av1.user_id
WHERE av1.user_id IN
(
SELECT user_id
FROM users
WHERE email_address IN (select email_address from banned_users where rec_status = 1)
)
GROUP BY av2.user_id;
You may replace the inner IN clause with a join. It's mostly a matter of personal preference, but it is also that in the past MySQL sometimes didn't perform well on IN clauses, so many people made it a habit to join instead.
WHERE av1.user_id IN
(
SELECT u.user_id
FROM users u
JOIN banned_users bu ON bu.email_address = u.email_address
WHERE bu.rec_status = 1
)
At last consider removing the GROUP BY clause. It reduces your results to one row per reusing user_id, showing one of its related banned user_ids (arbitrarily chosen in case there is more than one). I don't know your tables. Are you getting many records per reusing user_id? If not, remove the clause.
As to indexes I suggest:
banned_users(rec_status, email_address)
users(email_address, user_id)
adviews(user_id, client_ip)
adviews(client_ip, user_id)

mySQL SELECT help. IF or EXISTS?

I have this DB structure
* user
user_id
name
* client
client_id
name
* user_client
user_client_id
user_id
client_id
* message
message_id
client_id
description
If there are entries on user_client then the user has permissions restricted to the specific clients listed for his id on the table. If there are no entries, then the user has access to any client.
How can I select only messages that the user can read?
I'm trying to do an IF on the WHERE clause to check if any entries on the user_client table but I don't know where to go from there. It needs to select all messages from any client if no entries on user_client or only messages for client_id specified on user_client table
Thanks for the help!
I would suggest doing two different queries: one for the superusers and the other for the restricted users. Then you can join the two results with a UNION.
SELECT M.message_id,
M.client_id,
M.description
FROM message M
INNER JOIN user_client UC ON (UC.client_id = M.client_id)
INNER JOIN user U ON (UC.user_id = U.id)
WHERE U.id = :user_id
UNION
SELECT M.message_id,
M.client_id,
M.description
FROM message M
WHERE NOT EXISTS (
SELECT *
FROM user_client
WHERE user_id = :user_id
)
You can obtain the same result with other queries but IMHO this one is clearer and more maintainable.
Edit: If you want to ensure that the user exists you should join the second query with the user table.
SELECT M.message_id,
M.client_id,
M.description
FROM message M
JOIN user U
WHERE U.id = :user_id
AND NOT EXISTS (
SELECT *
FROM user_client
WHERE user_id = :user_id
)
One way the do this could be to use two different queries to create a set of the messages users can see and filter according to your needs; something like this should work:
select * from (
select u.user_id, u.name, c.name client, m.message_id, m.description
from user u
join user_client uc on u.user_id = uc.user_id
join client c on uc.client_id = c.client_id
join message m on c.client_id = m.client_id
union all
select u.user_id, u.name, c.name client, m.message_id, m.description
from user u
cross join client c
join message m on c.client_id = m.client_id
where user_id not in (select user_id from user_client)
) x
where x.user_id = 1;
Here users present in the user_client table are restricted to the set of messages that they have access to (the first set in the union), while users not present in the user_client table can see all messages (the second set in the union).
Sample SQL Fiddle
If I am understanding your question correctly, such as
1) Administrative user... They can look at EVERYTHING since they would have no records in the user_client table.
2) Client Supervisor... Such a person who's primary responsibility is to a specific client (or multiple clients). Therefore, the user DOES have a record in the user_client table. If so, then only allow the user to see records for those clients they DO have associations with.
select
m.message_id,
m.client_id,
m.description,
c.name as clientName
from
( select count(*) as HasClients
from user_client
where user_id = TheUserYouWant ) ClientCheck,
message m
left join user_client uc
on m.client_id = uc.client_id
AND uc.user_id = TheUserYouWant
join client c
on m.client_id = c.client_id
where
ClientCheck.HasClients = 0
OR NOT uc.user_id IS NULL
The query looks at the user_client table TWICE. The first time is to just get a count of those records that DO exist for the given user, regardless of which client associated with. The query will always come back with 1 row and it will either be 0 (no such records), or greater than 1 (however many they are associated with).
The second instance is a LEFT-JOIN to the user_client table, JUST IN CASE the person IS restricted to only looking at their own client messages.
The WHERE clause now comes in and says... if the underlying count of clients was zero, then ok to give me all messages. If any other value, then the user ID in the user_client table (as left-joined to the messages on both the CLIENT AND THE USER you want) MUST EXIST (via NOT a NULL value for the user_id column).
Now, you probably don't want to query EVERY message as it could get quite large as your database grows, but you could put whatever other criteria in the WHERE clause, such as date restrictions and/or client(s) you are interested in.

MYSQL query (with joins and virtual values)

I have 3 InnoDB tables: emails, websites and subscriptions.
emails table has id and email columns.
websites table has id and address columns.
subscriptions table has id, email_id and website_id columns.
What I'm tring to do is supply an email and return a table with columns address and subscribed. The former is a list of all the addresses in the websites table and the latter gets value 1 if the supplied email address has an occurence in the subscriptions table with website_id set to that website, or 0 otherwise. But I'm willing to retain all the websites even if the user is not found.
The point I'm stuck is where I should change the value of the virtual column subscribed from 0 to 1 when that email has that record.
Here's my query so far. Does anybody know how to do this?
SELECT `address`, "0" AS `subscribed`
/* 0 becomes 1 for the websites email has subscribed to */
FROM `websites` a
LEFT JOIN (
SELECT s.`website_id` FROM `subscriptions` s
RIGHT JOIN (
SELECT `id` AS `email_id` FROM `emails`
WHERE `email`='someone#mail.com' LIMIT 1) e
ON s.`email_id`=e.`email_id`) l
ON l.`website_id`=a.`id`
And here are the example outputs for the desired values for the subscribed column:
If email is not found in the emails table all the rows get value 0
If email is found in the emails table...
if it is not found in subscriptions table all the rows get value 0
if it is found in subscriptions table, the appropriate address rows get value 1
Let me know if I couldn't wxplain it well. Does anytbody know what I should alter in my query?
Thanks in advance.
SELECT w.address, CASE WHEN s.id IS NULL THEN 0 ELSE 1 END AS subscribed
FROM websites w
LEFT JOIN subscriptions s
INNER JOIN emails e
ON s.email_id = e.id
AND e.email = 'someone#mail.com'
ON w.id = s.website_id
You could also come up with the subscribed value this way, which is a bit more concise but also somewhat less obvious.
SELECT w.address, COALESCE(s.id/s.id, 0) AS subscribed
FROM websites w
LEFT JOIN subscriptions s
INNER JOIN emails e
ON s.email_id = e.id
AND e.email = 'someone#mail.com'
ON w.id = s.website_id

Facebook Style Messages: Efficient Query

users tbl:
id
username
first_name
last_name
email_address
messages tbl:
id
date_sent
title
content
status
message_relation tbl:
id
message_id
sender_id
receiver_id
What would be the most efficient way to query a message TO a selected user given these tables? In other words, I want to list all messages that are in userA's "inbox"
Secondly, how would you recommend handling global messages that need to be sent to everyone from the admin?
I think this is what you're looking for..
SELECT [WHATEVER] FROM
messages
INNER JOIN message_relation
ON (messages.id = message_relation.message_id AND message_relation.receiver_id = $id)
This does what you asked for efficiently. If you also want to select fields on either of the users, you can JOIN Receiver_ID to User_ID to to get that.
Suggested tables setup if you want Global/Admin messages:
Messages (Message_ID, [Fields common to all messages, e.g. Message_Content, Message_Timestamp, etc])
Global_Messages (Global_Message_ID, Message_ID, [any fields specific to Global_Messages])
User_Messages (User_Message_ID, Message_ID, [any fields specific to User_Messages])
User_Message_Relations (User_Message_Relations_ID, User_Message_ID, Sender_ID, Receiver_ID)
Then, to query an Inbox, something like:
SELECT [WHATEVER] FROM
Messages
LEFT JOIN (Global_Messages)
ON (Messages.Message_ID = Global_Messages.Message_ID)
LEFT JOIN (User_Messages)
ON (Messages.Message_ID = User_Messages.Message_ID)
LEFT JOIN (User_Message_Relations)
ON (User_Messages.User_Message_ID = User_Message_Relations.User_Message_ID AND User_Message_Relations.Receiver_ID = $uid)
That will give you a result you can loop through to get a user's entire Inbox.
SELECT
m.id, m.date_sent,m.title,m.content,m.status
FROM
messages m
INNER JOIN
message_relation mr on (mr.message_id = m.id)
INNER JOIN
users u on (u.id = mr.receiver_id);
WHERE u.id = ...your current user id ...
This is a simple way of doing it, and it will be fast and almost no load on your database

Making a user profile activity feed

I need to build an activity feed to go on each users profile page showing what they have been doing on the site.
There is three tables: comments, ratings, users
I want the feed to include the comments and ratings that the user has posted.
in the comments and ratings table it stores the user id of the user who posted it, not the username, so in for each item in the news feed it needs to select from the users table where the user id is the same to retrieve the username.
All the entries in the feed should be ordered by date.
Here is what ive got even though i know it is not correct because it is trying to match both with the same row in the users table.
SELECT comments.date, comments.url AS comment_url, comments.user_id, ratings.date, ratings.url AS rating_url, ratings.user_id, users.id, users.username
FROM comments, ratings, users
WHERE comments.user_id=%s
AND comments.user_id=users.id
AND ratings.user_id=%s
AND ratings.user_id=users.id
ORDER BY ratings.date, comments.date DESC
JOIN. It seems you know that, but here's how:
SELECT * FROM comments LEFT JOIN users ON comments.user_id = users.id
Thus, as far as I can tell, you're trying to order two separate things at the same time. The closest I think I can come up with would be something like:
(SELECT comments.date AS date, users.username AS name, comments.url AS url CONCAT('Something happened: ',comments.url) AS text
FROM comments LEFT JOIN users ON comments.user_id = users.id
WHERE users.id = %s)
UNION
(SELECT ratings.date AS date, users.username AS name, ratings.url AS url CONCAT('Something happened: ',ratings.url) AS text
FROM comments LEFT JOIN users ON comments.user_id = users.id
WHERE users.id = %s)
ORDER BY date DESC LIMIT 0,10
Note that the columns of both parts of the union match up. I'm pretty sure that that is required for something like this to work. That's why I have that CONCAT statement, which lets you build a string that works differently between ratings and comments.