Left Join 2 tables on 1 table - mysql

It must be pretty easy, but i can't think of any solution nor can I find an answer somewhere...
I got the table 'users'
and one table 'blogs' (user_id, blogpost)
and one table 'messages' (user_id, message)
I'd like to have the following result:
User | count(blogs) | count(messages)
Jim | 0 | 3
Tom | 2 | 3
Tim | 0 | 1
Foo | 2 | 0
So what I did is:
SELECT u.id, count(b.id), count(m.id) FROM `users` u
LEFT JOIN blogs b ON b.user_id = u.id
LEFT JOIN messages m ON m.user_id = u.id
GROUP BY u.id
It obviously doesn't work, because the second left join relates to blogs not users. Any suggestions?

First, if you only want the count value, you could do subselects:
select u.id, u.name,
(select count(b.id) from blogs where userid = u.id) as 'blogs',
(select count(m.id) from messages where userid = u.id) as 'messages'
from 'users'
Note that this is just a plain sql example, I have no mysql db here to test it right now.
On the other hand, you could do a join, but you should use an outer join to include users without blogs but with messages. That would imply that you get several users multiple times, so a group by would be helpful.

If you use an aggregate function in a select, SQL will collapse all your rows into a single row.
In order to get more than 1 row out you must use a group by clause.
Then SQL will generate totals per user.
Fastest option
SELECT
u.id
, (SELECT(COUNT(*) FROM blogs b WHERE b.user_id = u.id) as blogcount
, (SELECT(COUNT(*) FROM messages m WHERE m.user_id = u.id) as messagecount
FROM users u
Why you code does not work
SELECT u.id, count(b.id), count(m.id)
FROM users u
LEFT JOIN blogs b ON b.user_id = u.id <<-- 3 matches multiplies # of rows *3
LEFT JOIN messages m ON m.user_id = u.id <<-- 5 matches multiplies # of rows *5
GROUP BY u.id
The count will be off, because you are counting duplicate items.
Simple fix, but will be slower than option 1
If you only count distinct id's, you will get the correct counts:
SELECT u.id, count(DISTNICT b.id), count(DISTINCT m.id)
FROM users u
LEFT JOIN blogs b ON b.user_id = u.id
LEFT JOIN messages m ON m.user_id = u.id
GROUP BY u.id

Related

How to write a SQL query to find the percentage using 3 tables?

I need to find the percentage of users who only visited a mobile and the percentage of users that were on both mobile/web.
Table 1: User
user_id | page
6684 | home_page
Table 2: MobileData​
user_id | page
1210 | page_6_mobile
Table 3: WebData​
user_id | page
129 | page_1_web
To find all the users:
SELECT user_id FROM User
To find all the users who have been on mobile:
SELECT DISTINCT user_id FROM MobileData
We use DISTINCT so that we only get one row for each user.
To find all the users who have been on both mobile and web:
SELECT DISTINCT u.user_id
FROM User u
JOIN MobileData m ON m.user_id = u.user_id
JOIN WebData w ON w.user_id = u.user_id;
Now we just need to count all the results and compute the percentages:
SELECT total_users,
mobile_users / total_users * 100 AS percent_mobile,
mobile_and_web_users / total_users * 100 AS percent_both
FROM (SELECT COUNT(*) AS total_users,
(SELECT COUNT(DISTINCT user_id) FROM MobileData) AS mobile_users,
(SELECT COUNT(DISTINCT u.user_id)
FROM User u
JOIN MobileData m ON m.user_id = u.user_id
JOIN WebData w ON w.user_id = u.user_id) AS mobile_and_web_users
FROM User) u
SQLFiddle Demo
Update
Here is a much cleaner version of the query. It uses a LEFT JOIN from User to MobileData to find all users who have been on mobile (while retaining the entire list of users). Then it uses another LEFT JOIN from MobileData to WebData to find users who have been on mobile and web. By doing this we can simplify the counting to a neat COUNT(DISTINCT user_id) on each table:
SELECT total_users,
mobile_users / total_users * 100 AS percent_mobile,
mobile_and_web_users / total_users * 100 AS percent_both
FROM (SELECT COUNT(DISTINCT u.user_id) AS total_users,
COUNT(DISTINCT m.user_id) AS mobile_users,
COUNT(DISTINCT w.user_id) AS mobile_and_web_users
FROM User u
LEFT JOIN MobileData m ON m.user_id = u.user_id
LEFT JOIN WebData w ON w.user_id = m.user_id) u
SQLFiddle Demo

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%');

Get unique records based on multi-join with conditionals

I have 4 tables: posts, users, mentions, following
posts
----------------------------
id | user_id | post_text
1 1 foo
2 1 bar
3 2 hello
4 3 jason
users
------------
id | name
1 jason
2 nicole
3 frank
mentions
--------------------------
id | post_id | user_id
1 4 1
following
-------------------------------------------------
id | user_id | user_id_of_user_being_followed
1 1 2
posts includes the user_id of the user who posted some text
users has the user id and name of the user
mentions has the post id and user id of any post which has mentioned 1 or more other users
following has a the user id and the user they are following (user can follow 0 to many users)
What I'm trying to do is return all posts from users a that a given user follows, PLUS any posts that have mentioned that user (whether or not the given user is following), without returning any duplicates.
SELECT p.id, p.post, u.name,
FROM following f
JOIN posts p ON f.following = p.user_id
JOIN users u ON u.id = p.user_id
WHERE f.user_id = :user;
The above returns all posts from users that a given user is following, but I'm struggling figuring out how to include mentions as well (remember, a user does not have to follow someone to be able to see the post they've been mention in).
UPDATE:
Thanks to John R I was able to figure this out:
SELECT DISTINCT(p.id), p.post, u.name
FROM posts p
LEFT JOIN following f ON f.following = p.user_id
LEFT JOIN mentions m ON m.posts_id = p.id
JOIN users u ON u.id = p.user_id
WHERE (f.user_id = :user_id OR m.user_id = :user_id)
if i understand your querstion correctly you would want a left join to include any mentions.. but not filter out any followers/posts
if you can add some sample data to play with I can make sure its working how you want it to...
SELECT
if(p.id is not null, p.id, p1.id) as post_id,
if(p.post is not null, p.post, p1.post) as post_text,
u.username, m.id, m.user_id
FROM posts p
JOIN users u on u.id = p.user_id
JOIN following f on f.user_id_of_user_being_followed = u.id
LEFT JOIN mentions m on m.user_id = f.user_id
LEFT JOIN posts p1 on p1.id = m.post_id
WHERE f.user_id = :user or m.user_id = :user;
I left join mentions to the post made and also when the user_id in the mention table is equal to the specified user to filter out other users. the left join shouldn't change the number of rows returned.. but only include any mentions
EDIT: WORKING FIDDLE
after playing around with it I realised it was trying to put all of the data into one row.. try this:
(
SELECT p.id, p.post_text, u.name
FROM posts p
JOIN users u on u.id = p.user_id
JOIN following f on f.user_id_of_user_being_followed = u.id
WHERE f.user_id = 1
)
UNION
(
SELECT p.id, p.post_text, u.name
FROM following f
JOIN mentions m on m.user_id = f.user_id
JOIN posts p on p.id = m.post_id
join users u on u.id = p.user_id
WHERE f.user_id = 1
);
Maybe you inherited this db; but the last table is not really in line with good data normalization. The table should be the id and following_id; as set up you'll eventually run out of columns (or have to keep adding them when a user gets an error) - new users won't be able to follow anyone.

Double count within a table

I have a table users:
user_id name
1 John
2 Dan
3 Jane
4 Sophie
5 Jodie
I then have a table named associates:
user_id assoc_id
1 2
1 3
3 4
3 1
3 5
4 1
5 1
5 2
What I want to do is show how many associates each user has, or none
So, the results would show
user_id Name Number of Associates
1 John 2
2 Dan 0
3 Jane 3
4 Sophie 1
5 Jodie 2
What I'm trying works but does not show those with 0
Here's what I'm trying, how do I get the 0s?
SELECT u.user_id, u.name, count(a.user_id) as howmany from users u
join associates a on a.user_id = u.user_id
group by u.user_id order by u.user_id asc
You need a LEFT OUTER JOIN:
SELECT u.user_id, u.name, count(a.user_id) as howmany
FROM users u LEFT OUTER JOIN associates a
ON a.user_id = u.user_id
GROUP BY u.user_id
ORDER BY u.user_id ASC
The LEFT OUTER JOIN will include every rows of the first table even is they aren't present in the joined table.
Try this :
SELECT u.user_id,
u.name,
count(a.user_id) as howmany
FROM users u
LEFT OUTER JOIN associates a
ON a.user_id = u.user_id
GROUP BY u.user_id
ORDER BY u.user_id ASC
Try using a left join instead of a inner join, that should select the rows that do not have corresponding associates.
SELECT u.user_id, u.name, count(a.user_id) as howmany
from users u
left join associates a
on a.user_id = u.user_id
group by u.user_id
order by u.user_id asc
This will work with strict rules also (if i had not made any typo):
SELECT users.*, ISNULL(associatesCount.AssocCount, 0) AS AssocCount
FROM users
LEFT JOIN (
SELECT user_id, COUNT(*) AS AssocCount
FROM associates
GROUP BY user_id
) AS associatesCount
ON users.user_id = associatesCount.user_id

Mysql queries issue

I have 3 tables in my mysql DB to query.
Users_rates (fields: id,userid,raterid,rate,eventid) containing all of the rates(rate) that have been assigned to users(userid), participating to specific events(eventid), by other users(raterid)
Events_participants (fields:id,userid,eventid) containing all of the users(userid) participating to each event(eventid)
Users (fields:id,name,lastname)containing all the user relative data
I need to query those three tables to retrieve an event-specific rank for the users' rates.
Ex. John,Erik and Mark participated to 'eventid=31'.
John received 1 rate from Mark, and 2 from Erik.
Mark received 1 rate from Erik.
Nobody has rated Erik though.
I need to retrieve for each user name,lastname and the sum of the rates received for eventid=31
I tried with this:
SELECT events_participants.userid,users.name,users.lastname,
(SELECT SUM(rate)FROM users_rates WHERE users_rates.eventid=31 AND users_rates.userid=events_participants.userid)AS rate
FROM(( events_participants INNER JOIN users ON events_participants.userid=users.id)
LEFT OUTER JOIN users_rates ON events_participants.userid=users_rates.userid )
WHERE events_participants.eventid=31
But I receive:
userid | name | lastname | rate
1 | luca | silvestro | 1
3 | claudio | buricchi | 6
3 | claudio | buricchi | 6
What's the right query?
Thanks
Luca
Try this:
SELECT users.userid, users.name, users.lastname, temp.sum as rate
FROM users LEFT JOIN (
SELECT userid, SUM(rate) as sum FROM users_rates WHERE eventid = 31 GROUP BY userid
) as temp USING (userid)
It might give an error, this might work instead:
SELECT users.userid, users.name, users.lastname, temp.sum as rate
FROM users, (
SELECT userid, SUM(rate) as sum FROM users_rates WHERE eventid = 31 GROUP BY userid
) as temp WHERE users.userid = temp.userid
I don't know if I got the problem right, but maybe something like:
SELECT u.id, u.name, u.lastname, SUM(ur.rate) AS rate
FROM users AS u
INNER JOIN users_rates AS ur ON ur.userid = u.id
WHERE ur.eventid = 31
GROUP BY u.id
edit: If you want to receive a list with all users regardless of whether they have any rates at all, you could also join the users_participants table and replace the INNER JOIN of users_rates by a LEFT JOIN. The WHERE clause has to reference events_participants then (not users_rates anymore as it could be NULL):
SELECT u.id, u.name, u.lastname, SUM(ur.rate) AS rate
FROM users AS u
INNER JOIN events_participants AS ep ON ep.userid = u.id
LEFT JOIN users_rates AS ur ON ur.userid = u.id AND ur.eventid = ep.eventid
WHERE ep.eventid = 31
GROUP BY u.id