Join multiple tables, keeping NULLs - mysql

I have 4 tables user, posts, follows, notifications. The posts table has a field called privacy in which a value of 1 means "anyone can see" and 2 means "only friends can see".
My notifications table looks like this:
id user_id tonotify_id notification post_id
1 2 3 --- 1
2 3 2 --- 2
3 2 4 --- 3
And my follows table looks like this:
id user_id tofollow_id status fstatus
1 1 2 1 1
2 1 3 1 1
The field fstatus value of 1 means that they are also friends.
Now I am trying to get all notifications sent out by people who a certain user is following (lets say a user with id 1). Assuming 1 is following user 2 and 3, I need to get all notifications from the above table. However, before this I need to make sure that the post (posted by the users in the tonotify_id column) is available. That is, I need to check the privacy setting. I have the following code:
SELECT
u.username AS sender,
ux.username AS receiver,
p.id
FROM notifications n
JOIN follows f ON (n.user_id = f.tofollow_id)
JOIN follows fr ON (n.tonotify_id = fr.tofollow_id)
JOIN user u ON (u.id = n.user_id)
JOIN user ux ON (ux.id = n.tonotify_id)
LEFT JOIN posts p ON (n.posts_id = p.id)
WHERE f.user_id = 1
AND fr.user_id = 1
AND f.status = 1
AND p.privacy = 1 OR (p.privacy = 2 AND fr.fstatus = 1)
ORDER BY n.id DESC
It seems to be working except for one glitch. Since user 1 is not following user 4 all notifications aimed at 4, even if the posts are public don't show up. This my guess has to do with JOIN follows fr ON (n.tonotify_id = fr.tofollow_id) because, since the user 1 hasn't followed 4, there are no rows matching this join. Any suggestions to solving this?

I did try [the outer join], but the output is the same.
When you use an outer join, and then use one of the "outer" columns in an equality check in the WHERE clause, you convert your outer join to an inner join. This is because your condition that checks post's privacy requires the post to be there:
AND p.privacy = 1 OR (p.privacy = 2 AND fr.fstatus = 1)
When an outer join is about to produce a row that corresponds to a notification without a post, it would check the above condition. Since the post is not there, p.privacy would evaluate to NULL, "contaminating" both sides of the OR, and eventually making the whole condition evaluate to false.
Moving this condition into the ON condition of the join will fix the problem:
SELECT
u.username AS sender,
ux.username AS receiver,
p.id
FROM notifications n
JOIN follows f ON (n.user_id = f.tofollow_id)
JOIN follows fr ON (n.tonotify_id = fr.tofollow_id)
JOIN user u ON (u.id = n.user_id)
JOIN user ux ON (ux.id = n.tonotify_id)
LEFT JOIN posts p ON (n.posts_id = p.id)
AND (p.privacy = 1 OR (p.privacy = 2 AND fr.fstatus = 1))
WHERE f.user_id = 1
AND fr.user_id = 1
AND f.status = 1
ORDER BY n.id DESC
Another way to fix this would be adding an IS NULL condition to your OR, like this:
SELECT
u.username AS sender,
ux.username AS receiver,
p.id
FROM notifications n
JOIN follows f ON (n.user_id = f.tofollow_id)
JOIN follows fr ON (n.tonotify_id = fr.tofollow_id)
JOIN user u ON (u.id = n.user_id)
JOIN user ux ON (ux.id = n.tonotify_id)
LEFT JOIN posts p ON (n.posts_id = p.id)
WHERE f.user_id = 1
AND fr.user_id = 1
AND f.status = 1
AND (p.privacy IS NULL OR p.privacy = 1 OR (p.privacy = 2 AND fr.fstatus = 1))
ORDER BY n.id DESC

You need to use an outer join such as LEFT JOIN instead of an inner join using just JOIN. An outer join will always return rows from one "side" (which is why it is called LEFT [OUTER] JOIN and RIGHT [OUTER] JOIN) even if the other "side" does not match anything.

Related

How to use IF statement before FROM Clause

I have three tables:
Users and companies can add feeds. The flag columns means
0 = user
1 = company
How can I perform this SQL query correctly? Thanks in advance for your help!
SELECT
feeds.feeds_id,
feeds.title
IF feeds.flag = 0
FROM user
INNER JOIN
feeds.user_or_company = user.user_id
ELSEif feeds.flag = 1
INNER JOIN
feeds.user_or_company = company.company_id
One way to solve this is to LEFT JOIN to both user and company tables and then select the appropriate name value based on feeds.flag:
SELECT f.feeds_id, f.title,
CASE f.flag WHEN 0 THEN u.name ELSE c.name END AS name
FROM feeds f
LEFT JOIN user u ON u.user_id = f.user_or_company
LEFT JOIN company c ON c.company_id = f.user_or_company
Output (for your sample data)
feeds_id title name
2 This title added twice by user User-1
3 This title added by company Company-2
1 This title added by user User-3
Demo on dbfiddle

Get rows that are not logged in a joined table

I got this 3 tables as shown aboved.
My goal is to get all bits that user 1 (jm) did not put a reaction to.
Currently I have this MySQL code:
select * from bit b LEFT JOIN bit_reaction br ON (br.bitId=b.id AND br.userId != 1)
The problem here is that bit_reaction.id = 2 is being returned since br.userId is not equal to 1. The correct behavior is that it would only return bits with id 2 and 3.
Thanks for the tip!
select b.*
from bit b
left join bit_reaction br ON br.bitId = b.id
AND br.userId = 1
WHERE br.bitId is null
I assume that you want every user to react every bit. So the more generic answer will be...
SELECT *
FROM bit AS tBit
LEFT JOIN user AS tUser ON 1 = 1
LEFT JOIN bit_reaction AS tReaction ON tBit.id = tReaction.bitID
AND tUser.id = tReaction.userID
WHERE tReaction.userID IS NULL

select within if add block code

I have a selection and I wanted to search a column of a table by left join only if the User is enabled with the pre condition to essar search as a condition that adds tables I was a little confused, I do not know if I separate searches or I do everything at once ....
He says it's for me to look at the mysql system, but I see so
if .condition... then ... end if;
My code
select user. * from
u user
if (u.visible == 1,
left join houseUser hu on u.id = h.id_user
left join h house on hu.id_house = h.id_house
)
where
u.age> 30
if visible add that code to can see in what house the user is.
You can just incorporate the condition into the on clauses:
select u.*, hu.*, h.*
from user u left join
houseUser hu
on u.id = h.id_user and u.visible = 1 left join
h house
on hu.id_house = h.id_house and u.visible = 1
where u.age > 30;
The columns will come back, but they will be NULL when visible is not 1.

MySQL do INNER JOIN if specific value is 1

I have this query:
SELECT
e.*, u.name AS event_creator_name
FROM `edu_events` e
LEFT JOIN `edu_users` u
ON u.user_id = e.event_creator
INNER JOIN `edu_event_participants`
ON participant_event = e.event_id && participant_user = 1
WHERE
MONTH(e.event_date_start) = 6
AND YEAR(e.event_date_start) = 2013
It works perfect, however, I only want to do the INNER JOIN if the value: e.event_type equals 1. If not, it should ignore the INNER JOIN.
I have tried for some time to figure it out, but the solutions seems difficult to implment for my proposes (as it is only for select/specific values).
I'm thinking about something like:
SELECT
e.*, u.name AS event_creator_name
FROM `edu_events` e
LEFT JOIN `edu_users` u ON u.user_id = e.event_creator
if(e.event_type == 1) {
INNER JOIN `edu_event_participants` ON participant_event = e.event_id && participant_user = 1
}
WHERE MONTH(e.event_date_start) = 6
AND YEAR(e.event_date_start) = 2013
I have edited the below following further feedback from #Matthias
-- This will get all events for a given user plus all globals
SELECT
e.*,
u.name AS event_creator_name
FROM `edu_users` u
-- in the events
INNER JOIN `edu_events` e 
ON (
-- Get all the ones that the user is participant
e.event_creator = u.user_id
-- Or where event_type is 1
OR
e.event_type = 1
)
AND e.event_date_start BETWEEN DATE('2013-06-01') AND DATE('2013-07-01')
    
-- Add in event participants even though it doesn't seem to be used?
INNER JOIN `edu_event_participants` AS eep
ON eep.participant_event = e.event_id
AND eep.participant_user = 1
    
-- Add the user ID into the WHERE
WHERE u.user_id = 1;
This just might not make too much sence as it feels as though edu_event_participants has too much information in. event_creator should really be stored against the event itself, and then event_participants just containing an event id, user id, and user type.
If you are looking to get all users on an event, it may be better to do a seperate query for that event to select all users based off an event_id
The note on your use of MONTH() and YEAR(). This will trigger a table scan, as MySQL will need to apply the MONTH() and YEAR() functions to all rows to determine which match that WHERE statement. If you instead calculate the upper and lower limits (i.e. 2013-06-01 00:00:00 <= e.event_date_start < 2013-07-01 00:00:00) then MySQL can use a far more efficient range scan on an index (assuming one exists on e.event_date_start)
If I understand correctly you only want the results where there is an entry on edu_event_participants with the same event_id and participant_user = 1 but only if event_type = 1, but you don't really want to get any information from the edu_event_participants table. If that is the case:
SELECT
e.*, u.name AS event_creator_name
FROM `edu_events` e
LEFT JOIN `edu_users` u
ON u.user_id = e.event_creator
WHERE
-- as Simon at mso.net suggested
WHERE e.event_date_start BETWEEN DATE('2013-06-01') AND DATE('2013-07-01')
-- MONTH(e.event_date_start) = 6
-- AND YEAR(e.event_date_start) = 2013
AND (
-- either event is public
e.event_type = 1 or
-- or the user is in the participants table
exists
(select 1 from `edu_event_participants`
where participant_event = e.event_id
AND participant_user = 1)
)
Maybe what you're after is displaying the left table value even if there's no matching data from right table? On that case you can use outer join like so:
LEFT OUTER JOIN `edu_event_participants` ON participant_event = e.event_id && participant_user = 1 AND e.event_type = 1

MySQL join & search

I have a problem with joining some tables, heres my structure:
tbl_imdb:
fldID fldTitle fldImdbID
1 Moviename 0000001
tbl_genres:
fldID fldGenre
1 Action
2 Drama
tbl_genres_rel:
fldID fldMovieID fldGenreID
1 1 1
2 1 2
What I’m trying to do is a query that will find all movies that is both an action movie and drama, is this possible to do without a subquery, if so, how?
What I'm trying right now is:
SELECT tbl_imdb.*
FROM tbl_imdb
LEFT JOIN tbl_imdb_genres_rel ON ( tbl_imdb.fldID = tbl_imdb_genres_rel.fldMovieID )
LEFT JOIN tbl_imdb_genres ON ( tbl_imdb_genres_rel.fldGenreID = tbl_imdb_genres.fldID )
WHERE tbl_imdb_genres.fldGenre = 'Drama'
AND tbl_imdb_genres.fldGenre = 'Action';
But this dosnt work, however it does work if I only keep one of the two WHERE's, but thats not what I want.
Two ways to do it:
1
SELECT tbl_imdb.*
FROM tbl_imdb
INNER JOIN tbl_genres_rel rel_action
ON tbl_imdb.fldID = rel_action.fldMovieID
INNER JOIN tbl_genres genre_action
ON rel_action.fldGenreId = genre_action.fldID
AND 'Action' = genre_action.fldGenre
INNER JOIN tbl_genres_rel rel_drama
ON tbl_imdb.fldID = rel_drama.fldMovieID
INNER JOIN tbl_genres genre_drama
ON rel_drama.fldGenreId = genre_drama.fldID
AND 'Drama' = genre_drama.fldGenre
This method is on the same path as your original solution. 2 differences:
The join should be inner, not left because you're trying to get movies that certainly have the corresponding genre entry
Since you want to find 2 different generes, you'll have to do the join with tbl_genres_rel and tbl_genres twice, once for each particular genre you're interested in.
2
SELECT tbl_imdb.*
FROM tbl_imdb
INNER JOIN tbl_genres_rel
ON tbl_imdb.fldID = tbl_genres_rel.fldMovieID
INNER JOIN tbl_genres
ON tbl_genres_rel.fldGenreId = tbl_genres.fldID
AND tbl_genres.fldGenre IN ('Action', 'Drama')
GROUP BY tbl_imdb.fldID
HAVING COUNT(*) = 2
Again, the basic join plan is the same. Difference here is that we join to the tbl_genres_rel and tbl_genres path just once. This on itself fetches all genres for one film, and then filters for the one's you're interested in. The ones that qualify will now have 2 rows for each distinct value of tbl_imdb.fldId. The GROUP BY aggregates on that, flattening that into one row. By asserting in the HAVING clause that we have exactly 2 rows, we ensure that we keep only those rows that have both the genres.
(Note that this assumes that there is a unique constraint on tbl_genres_rel over {fldMovieID, fldGenreID}. If such a constraint is not present, you should consider adding it.)
LEFT JOIN is not applicable in your case because records should exist on both tables. And you need to count the instances of the movie
SELECT *
FROM tbl_imdb a
INNER JOIN tbl_genres_rel b
on a.fldID = fldMovieID
INNER JOIN tbl_genres c
on c.fldGenreID = b.fldID
WHERE c.fldGenre IN ('Drama', 'Action')
GROUP BY a.Moviename
HAVING COUNT(*) > 1