MySQL Remove repeating clauses in subqueries - mysql

I'm trying to simplify my query so that it only contains the session ID (SID) once.
The abstract structure of the Users table is:
+----+------+----------+
| ID | Name | Username |
+----+------+----------+
The Friends table has an abstract structure like:
+----+-----------------+----------+--------+---------+
| ID | UserID | FriendID | Hidden | Deleted |
| | (Foreign key | | | |
| | of ID in Users) | | | |
+----+-----------------+----------+--------+---------+
The abstract structure of the Sessions table:
+----+-----------------+-----+
| ID | UserID | SID |
| | (Foreign key | |
| | of ID in Users) | |
+----+-----------------+-----+
I have the following query, which has been adapted from the answer of a previous question of mine. As you can see, the session ID (SID) is repeated 4 times, is it possible to condense the query as a whole so that the SID is only required once?
SELECT *
,CASE
WHEN D.ID IS NULL
THEN "Wants to be your friend"
ELSE "Friends"
END AS STATUS
FROM (
SELECT DISTINCT A.ID
,A.NAME
,E.Hidden
FROM Users A
INNER JOIN Friends E ON A.ID = E.UserID
WHERE A.ID IN (
SELECT A.UserID
FROM Friends A
INNER JOIN Sessions S ON A.FriendID = S.UserID
WHERE S.SID = "1234"
AND Deleted = 'No'
)
) C
LEFT JOIN (
SELECT DISTINCT B.ID
,B.NAME
,F.Hidden
FROM Users B
INNER JOIN Friends F ON B.ID = F.FriendID
WHERE B.ID IN (
SELECT A.FriendID
FROM Friends A
INNER JOIN Sessions S ON A.UserID = S.UserID
WHERE S.SID = "1234"
AND Deleted = 'No'
)
) D ON C.ID = D.ID
UNION
DISTINCT
SELECT *
,CASE
WHEN C.ID IS NULL
THEN "Request Sent"
ELSE "Friends"
END AS STATUS
FROM (
SELECT DISTINCT A.ID
,A.NAME
,E.Hidden
FROM Users A
INNER JOIN Friends E ON A.ID = E.UserID
WHERE A.ID IN (
SELECT A.UserID
FROM Friends A
INNER JOIN Sessions S ON A.FriendID = S.UserID
WHERE S.SID = "1234"
AND Deleted = 'No'
)
) C
RIGHT JOIN (
SELECT DISTINCT B.ID
,B.NAME
,F.Hidden
FROM Users B
INNER JOIN Friends F ON B.ID = F.FriendID
WHERE B.ID IN (
SELECT A.FriendID
FROM Friends A
INNER JOIN Sessions S ON A.UserID = S.UserID
WHERE S.SID = "1234"
AND Deleted = 'No'
)
) D ON C.ID = D.ID
A basic way of explaining the system is that if two users are friends, then there is two records within the database. One from the first user to the second and another record from the second user to the first.
A friend request has been sent if there is a record from the current user to another, and a friend request has been received if there is a record from one user to the current one.
Here is a vann diagram of how it works:
SQL Fiddle - http://sqlfiddle.com/#!2/c5587/1

Sql fiddle : http://sqlfiddle.com/#!2/06e08/68/0
This return Friends and Request Sent :
SELECT
f.FriendID,
u.Name,
f.Hidden,
CASE
WHEN reqs.FriendID IS NULL
THEN "Request Sent"
WHEN reqs.FriendID = f.UserID
THEN "Friends"
END AS Status
FROM
Friends AS f
INNER JOIN
Sessions AS s
ON f.UserId = s.UserID
INNER JOIN
Users AS u
ON u.ID = f.FriendID
LEFT JOIN
Friends AS reqs
ON reqs.FriendID = f.UserID
AND reqs.UserID = f.FriendID
WHERE
s.SID = "sid1"
If you want Also Request Received, append this :
UNION
SELECT
f.UserID,
u.Name,
f.Hidden,
"Request Received" AS Status
FROM
Friends AS f
INNER JOIN
Sessions AS s
ON f.FriendID = s.UserID
INNER JOIN
Users AS u
ON u.ID = f.UserID
WHERE
f.UserID NOT IN
(
SELECT
ff.FriendID
FROM
Friends AS ff
INNER JOIN
Sessions AS ss
ON ff.UserID = ss.UserID
WHERE ss.SID = "sid1"
)
AND s.SID = "sid1"
Can't figure out how to optimise the last part. Since it's a SELF JOIN it's a damn mind twister.
I understand this is not what you expected but, i can't get ride of all the SID, but this request should be faster than the one you currently use

Related

How to exclude rows when using a LEFT JOIN (MySQL)

I have users with many posts. I want to build an SQL query that would do the following in 1 query (no subquery), and hopefully no unions if possible. I know I can do this with union but I want to learn if this can be done using only joins.
I want to get a list of distinct active users who:
have no posts
have no approved posts
Here's what I have so far:
SELECT DISTINCT u.*
FROM users u
LEFT JOIN posts p
ON p.user_id = u.id
LEFT JOIN posts p2
ON p2.user_id = u.id
WHERE u.status = 'active'
AND (p.status IS NULL
OR p2.status != 'approved');
The problem is when a user has multiple posts and one is active. This will still return the user which I do not want. If a user has an active post, he should be removed from the result set. Any ideas?
Here's what the data looks like:
mysql> select * from users;
+----+---------+
| id | status |
+----+---------+
| 1 | active |
| 2 | pending |
| 3 | pending |
| 4 | active |
| 5 | active |
+----+---------+
5 rows in set (0.00 sec)
mysql> select * from posts;
+----+---------+----------+
| id | user_id | status |
+----+---------+----------+
| 1 | 1 | approved |
| 2 | 1 | pending |
| 3 | 4 | pending |
+----+---------+----------+
3 rows in set (0.00 sec)
The answer here should be only users 4 and 5. 4 doesn't have an approved post and 5 doesn't have a post. It should not include 1, which has an approved post.
Not exists:
SELECT u.*
FROM users u
WHERE NOT EXISTS (
SELECT 1
FROM posts p
WHERE p.user_id = u.id AND p.status = 'approved');
Or equivalent LEFT JOIN
SELECT u.*
FROM users u
LEFT JOIN posts p
ON p.user_id = u.id AND p.status = 'approved'
WHERE p.user_id IS NULL;
Taking your requirements and translating them literally to SQL, I get this:
SELECT users.id,
COUNT(posts.id) as posts_count,
COUNT(approved_posts.id) as approved_posts_count
FROM users
LEFT JOIN posts ON posts.user_id = users.id
LEFT JOIN posts approved_posts
ON approved_posts.status = 'approved'
AND approved_posts.user_id = users.id
WHERE users.status = "active"
GROUP BY users.id
HAVING (posts_count = 0 OR approved_posts_count = 0);
For your test data above, this returns:
4|1|0
5|0|0
i.e. users with ids 4 and 5, the first of which has 1 post but no approved posts and the second of which has no posts.
However, it seems to me that this can be simplified since any user that has no approved posts will also have no posts, so the union of conditions is unnecessary.
In that case, the SQL is simply:
SELECT users.id,
COUNT(approved_posts.id) as approved_posts_count
FROM users
LEFT JOIN posts approved_posts
ON approved_posts.status = 'approved'
AND approved_posts.user_id = users.id
WHERE users.status = "active"
GROUP BY users.id
HAVING approved_posts_count = 0;
This also returns the same two users. Am I missing something?
Please explain why you don't want JOINs or UNIONs. If it is because of performance, then consider the following:
CREATE TABLE t ( PRIMARY KEY(user_id) )
SELECT user_id, MIN(status) AS z
FROM Posts
GROUP BY user_id;
SELECT u.id AS user,
IFNULL(z, 'no_posts') AS status
FROM users u
WHERE u.status = 'active'
LEFT JOIN t ON t.user_id = u.id
HAVING status != 'approved';
It will make only one pass over each table, thereby being reasonably efficient (considering the complexity of the query).
This one may help:
SELECT DISTINCT u.*
FROM users u
LEFT JOIN posts p ON 1=1
-- matches only if user has any post
AND p.user_id = u.id
-- matches only if user has any active post
AND p.status = 'approved'
WHERE 1=1
-- matches only active users
AND u.status = 'active'
-- matches only users with no matches on the LEFT JOIN
AND p.status IS NULL
;
I think this should be easy.
SELECT u.`id`, u.`status` FROM `users` u
LEFT OUTER JOIN `post` p ON p.`user_id` = u.`id` AND p.`status` = 'approved'
WHERE u.`status` = 'active' AND p.`id` IS NULL
Gives a result of 4 & 5.
[Edit] Just wanted to add why this works:
u.status = 'active'
This results into exclusion of all users that are not active.
p.status = 'approved'
This excludes all posts that are approved.
Hence, by using these two lines, we have excluded all users that qualify as approved for your criteria.
[Edit 2]
If you also need to know how many pending and how many approved, here is an updated version:
SELECT u.`id`, u.`status`, SUM(IF(p.`status` = 'approved', 1, 0)) AS `Approved_Posts`, SUM(IF(p.`status` = 'pending', 1, 0)) AS `Pending_Posts`
FROM `test_users` u
LEFT OUTER JOIN `test_post` p ON p.`user_id` = u.`id`
WHERE u.`status` = 'active'
GROUP BY u.`id`
HAVING SUM(IF(p.`id` IS NOT NULL, 1, 0))
Try this
SELECT DISTINCT u.*
FROM users u LEFT JOIN posts p
ON p.user_id = u.id
WHERE p.status IS NULL
OR p.status != 'approved';
Can you try with the below query:
SELECT DISTINCT u.*
FROM users u
LEFT JOIN posts p
ON p.user_id = u.id
WHERE
u.status = 'active' AND (
p.user_id IS NULL
OR p.status != 'approved');
EDIT
As per the updated question, the above query will include User 1. If we want to prevent that, and don't want to use inner query, we can use group_concat function of MySQL to get all the (distinct) statuses and see if it contains 'active' status, below query should give the desired output:
SELECT u.id, group_concat(distinct p.status) as statuses
FROM users u
LEFT JOIN posts p
ON u.id = p.user_id
WHERE
u.status = 'active'
group by u.id
having (statuses is null or statuses not like '%approved%');

MySQL query for on user to list of users

My message system is based on message, threads, and participant
a message created by a user is linked to a thread
two participants (or more) are linked to a thread
each participant is linked to one user
I got this request. It give me the number of message a user has received (= number of message where the user is not the creator)
I count all messages created in a list of threads where the user is not the creator.
I got the list of threads by find all thread_id where my user participant is linked
SELECT COUNT(m.id)
FROM message m
INNER JOIN message_thread t ON m.thread_id = t.id
WHERE m.created_by_id != :userId
AND t.id IN (
SELECT t2.id
FROM message_thread t2
INNER JOIN message_participants p ON p.thread_id = t2.id
WHERE p.user_id = :userId
)
So that is for one uniq user.
+-------------+
| count(m.id) |
+-------------+
| 7 |
+-------------+
I'm trying to do the same, but with severals users, and get for each of them, the number of message they received
+---------+-------------+
| User id | count(m.id) |
+---------+-------------+
| 12645 | 1 |
| 985 | 5 |
| 8956 | 15 |
| 37856 | 2 |
+---------+-------------+
I tried to do that with more JOIN instead of t.id IN ( and group by the result, but without success...
One method is a brute force method, where you bring in all the users you care about and use that information for the processing:
SELECT u.userid, COUNT(m.id)
FROM message m INNER JOIN
message_thread t
ON m.thread_id = t.id CROSS JOIN
(SELECT 12645 as userid UNION ALL
SELECT 985 UNION ALL
SELECT 8956 UNION ALL
SELECT 37856
) u
WHERE m.created_by_id <> u.userId AND
t.id IN (SELECT t2.id
FROM message_thread t2 INNER JOIN
message_participants p
ON p.thread_id = t2.id
WHERE p.user_id = u.userId
)
GROUP BY u.userId;
If you wanted to do this for all users, I would recommend a different approach: count all the messages a user participates in and subtract out the ones where s/he is the creator:
SELECT p.userid,
COUNT(DISTINCT CASE m.created_by_id <> p.userId THEN m.id END) as numMessages
FROM message m INNER JOIN
message_thread t
ON m.thread_id = t.id INNER JOIN
message_participants p
ON p.thread_id = t.id
GROUP BY p.userid;
You should use a group by
SELECT message_thread.your_user_id COUNT(m.id)
FROM message m
INNER JOIN message_thread t ON m.thread_id = t.id
WHERE m.created_by_id != :userId
AND t.id IN (
SELECT t2.id
FROM message_thread t2
INNER JOIN message_participants p ON p.thread_id = t2.id
)
group by message_thread.your_user_id

LEFT JOIN in MySQL across multiple tables with NULL values

I have the following table structure with data
TABLE: USER
USER ID | USER NAME
1 | Joe
2 | Mary
TABLE : USER GROUP
USER ID | GROUP ID
1 | 1
1 | 2
TABLE : GROUP
GROUP ID | GROUP NAME
1 | Company 1
2 | Company 2
TABLE : ROLE
ROLE ID | ROLE NAME
1 | Administrator
2 | Users
TABLE : USER ROLE
USER ID | ROLE ID
1 | 1
2 | 1
As you can see user #2 does not belong to any group. Roles & Groups are optional forcing me to left joint but when I run a query as below
`SELECT a.user_id,
a.user_name
GROUP_CONCAT(r.role_name) AS role_names,
GROUP_CONCAT(g.group_name) AS group_names
FROM user a
LEFT JOIN role_map m ON a.user_id = m.user_id
INNER JOIN role r ON m.role_id = r.role_id
LEFT JOIN user_group s ON a.user_id = s.user_id
INNER JOIN group g ON s.group_id = g.group_id
GROUP BY a.user_id`
I get a cartesian product in the role_names column - the result looks like this
Joe | Administrators, Administrators | Company 1, Company 2
What am I doing wrong?
The easiest way to solve this is by using DISTINCT in your GROUP_CONCAT (SQL Fiddle). Also, you will need to add GROUP BY a.user_id in order to group per user:
SELECT a.user_id,
a.user_name,
GROUP_CONCAT(DISTINCT r.role_name) AS role_names,
GROUP_CONCAT(DISTINCT g.group_name) AS group_names
FROM `user` a
LEFT JOIN `user_role` m ON a.user_id = m.user_id
LEFT JOIN `role` r ON m.role_id = r.role_id
LEFT JOIN `user_group` s ON a.user_id = s.user_id
LEFT JOIN `group` g ON s.group_id = g.group_id
GROUP BY a.user_id;

MySQL join type for records in the same table

The title is a little awkward, but I will do my best to explain what I'm trying to accomplish.
I have a table called Users and another called Friends.
The abstract structure of the Users table is:
+----+------+----------+----------+
| ID | Name | Username | Password |
+----+------+----------+----------+
The Friends table has an abstract structure like:
+----+--------+----------+--------+
| ID | UserID | FriendID | Hidden |
+----+--------+----------+--------+
Where the UserID is the ID of the user who sent the friend request and the FriendID is the recipient of the request. The hidden column will have a value of 1 if the recipient has chosen to hide the request.
I am wanting to compress this all down to one query, so far I have two separate ones which use either LEFT JOIN or RIGHT JOIN.
To find sent requests and current friends:
SELECT
*,
CASE
WHEN C.ID IS Null THEN "Request Sent"
ELSE "Friends"
END AS Status
FROM
(SELECT DISTINCT
A.ID, A.Name, E.Hidden
FROM
Users A
INNER JOIN
Friends E ON A.ID = E.UserID
WHERE
A.ID in (SELECT UserID
FROM Friends
WHERE FriendID = "1" AND Deleted='No')) C
RIGHT JOIN
(SELECT DISTINCT
B.ID, B.Name, F.Hidden
FROM
Users B
INNER JOIN
Friends F ON B.ID = F.FriendID
WHERE
B.ID in (SELECT FriendID
FROM Friends
WHERE UserID = "1" AND Deleted = 'No')) D ON C.ID = D.ID
As for friend requests received and current friends:
SELECT *, CASE WHEN D.ID IS Null THEN "Wants to be your friend" ELSE "Friends" END AS Status FROM
(SELECT DISTINCT
A.ID, A.Name, E.Hidden
FROM
Users A INNER JOIN Friends E ON A.ID=E.UserID
WHERE
A.ID in (SELECT UserID FROM Friends WHERE FriendID = "1" AND Deleted='No')) C
LEFT JOIN
(SELECT DISTINCT
B.ID, B.Name, F.Hidden
FROM
Users B INNER JOIN Friends F ON B.ID=F.FriendID
WHERE
B.ID in (SELECT FriendID FROM Friends WHERE UserID = "1" AND Deleted='No')) D
ON C.ID=D.ID
The venn diagram below may be a vital illustrative push if I haven't explained what I need.
Please note, for the purpose of constructing the querie(s) I have selected all columns, but I will only require the friends names and ID's at the end.
Do a union all or union distinct between the two queries this will return all the results in one table.
SELECT *, D.ID, CASE WHEN D.ID IS Null THEN "Wants to be your friend" ELSE "Friends" END AS Status FROM
(SELECT DISTINCT
A.ID, A.Name, E.Hidden
FROM
Users A INNER JOIN Friends E ON A.ID=E.UserID
WHERE
A.ID in (SELECT UserID FROM Friends WHERE FriendID = "1" AND Deleted='No')) C
LEFT JOIN
(SELECT DISTINCT
B.ID, B.Name, F.Hidden
FROM
Users B INNER JOIN Friends F ON B.ID=F.FriendID
WHERE
B.ID in (SELECT FriendID FROM Friends WHERE UserID = "1" AND Deleted='No')) D
ON C.ID=D.ID
union all
SELECT *, D.ID, CASE WHEN C.ID IS Null THEN "Request Sent" ELSE "Friends" END AS Status FROM
(SELECT DISTINCT
A.ID, A.Name, E.Hidden
FROM
Users A INNER JOIN Friends E ON A.ID=E.UserID
WHERE
A.ID in (SELECT UserID FROM Friends WHERE FriendID = "1" AND Deleted='No')) C
RIGHT JOIN
(SELECT DISTINCT
B.ID, B.Name, F.Hidden
FROM
Users B INNER JOIN Friends F ON B.ID=F.FriendID
WHERE
B.ID in (SELECT FriendID FROM Friends WHERE UserID = "1" AND Deleted='No')) D
ON C.ID=D.ID

Friend Table with mysql

I am working the friend table with mysql. I want to get friend userid and username form current username.
friend
=======
- id
- uid
- fid
Sample Data
===========
id uid fid
1 1 2
2 3 1
user
====
- id
- username
Sample Data
===========
id username
1 saturngod
2 snow
3 John
My current code is
SELECT `user`.id,`user`.username FROM friend
INNER JOIN User
ON user.id = friend.uid
WHERE friend.fid = ( SELECT `id` FROM `User` WHERE `username`='saturngod')
UNION
SELECT `user`.id,`user`.username FROM friend
INNER JOIN User
ON user.id = friend.fid
WHERE friend.uid = ( SELECT `id` FROM `User` WHERE `username`='saturngod')
It's working. I got the friend list. However, I feel , the sql is so long.
Can I reduce this sql and can we write without UNION in sql ?
You have already joined the tables, so the subselect is not needed.
SELECT u.id,u.username
FROM user u
INNER JOIN friend f ON (u.id = f.uid)
WHERE u.username='saturngod'
UNION ALL
SELECT u2.id,u2.username
FROM user u
INNER JOIN friend f ON (u.id = f.uid)
INNER JOIN user u2 ON (u2.id = f.fid)
WHERE u.username='saturngod'
Or you can do:
SELECT
u.id as user_id
,u.username
,u2.id as friend_id
,u2.username as friendname
FROM user u
INNER JOIN friend f ON (u.id = f.uid)
LEFT JOIN user u2 ON (u2.id = f.fid)
WHERE 'saturngod' = u.username
If you want one row per user you can do:
SELECT
u.id as user_id
,u.username
,GROUP_CONCAT(u2.id) AS friend_ids
,GROUP_CONCAT(u2.username) as friendnames
FROM user u
INNER JOIN friend f ON (u.id = f.uid)
LEFT JOIN user u2 ON (u2.id = f.fid)
WHERE 'saturngod' = u.username <<-- optional
GROUP BY u.id
SELECT CASE WHEN user_id="$id" THEN friend_id ELSE user_id END AS friendID
FROM user_friend WHERE user_id="$id" OR friend_id="$id" order by friendID ASC