Weird SQL Issue with Multiple Joins - mysql

I am currently in the process of building an API for an application and am having some trouble with a particular query.
This is my original query:
SELECT `users`.`id`,
unclaimed_users.id AS unclaimed_id,
`users`.`firstname`,
`users`.`lastname`,
`unclaimed_users`.`mobile_number`,
`group_members`.`status`,
`user_images`.`profile_image_main`
FROM `group_members`
LEFT JOIN `users`
ON `users`.`id` = `group_members`.`user_id`
AND `group_members`.`unclaimed_user` = 0
LEFT JOIN `user_images`
ON `user_images`.`id` = `group_members`.`user_id`
AND `group_members`.`unclaimed_user` = 0
LEFT JOIN `unclaimed_users`
ON `unclaimed_users`.`id` = `group_members`.`user_id`
AND `group_members`.`unclaimed_user` = 1
WHERE `group_members`.`group_id` = 1
ORDER BY `group_members`.`created_at` ASC
When I execute the above query, I am receiving the two records I expect, however, any field extracted from the unclaimed_user table is returning an empty string (not null), instead of returning the value.
See here:
+------+--------------+-----------+----------+---------------+---------+--------------------+
| id | unclaimed_id | firstname | lastname | mobile_number | status | profile_image_main |
+------+--------------+-----------+----------+---------------+---------+--------------------+
| 1 | NULL | Ben | Carey | NULL | active | NULL |
| NULL | 0 | NULL | NULL | | pending | NULL |
+------+--------------+-----------+----------+---------------+---------+--------------------+
However, when I replace the SELECT with SELECT *, it returns all the fields expected, with the expected values. For instance mobile_number returns the mobile number associated with the relevant unclaimed user.
What am I doing wrong? I cannot work out is happening and why the values are not being returned.
I have tried removing all the fields and only including one field from the unclaimed users table e.g.
SELECT unclaimed_users.id FROM etc...
The above returned 0, but it should return 1...
It is important to note that both id and mobile_number are present on the users table and the unclaimed_users table.
Update
I am currently in the process of putting a fiddle together but it is proving harder than I thought as I am yet to successfully recreate the issue...
In the meantime, I have simplified the query which is still not returning what it is meant to:
SELECT `unclaimed_users`.`mobile_number`
FROM `group_members`
LEFT JOIN `unclaimed_users`
ON `unclaimed_users`.`id` = `group_members`.`user_id`
AND `group_members`.`unclaimed_user` = 1
WHERE `group_members`.`group_id` = 1
The above returns the following
+---------------+
| mobile_number |
+---------------+
| NULL |
| |
+---------------+
When it should return:
+---------------+
| mobile_number |
+---------------+
| NULL |
| +447777779999 |
+---------------+
If I replace the SELECT unclaimed_users.mobile_number with SELECT *, the correct data is returned so the join is working. I cannot see why I am unable to reference the field mobile_number
Update 2
I have noticed that it works when I execute the following:
SELECT `unclaimed_users`.`mobile_number`
FROM `group_members`
LEFT JOIN `unclaimed_users`
ON `unclaimed_users`.`id` = `group_members`.`user_id`
AND `group_members`.`unclaimed_user` = 1
WHERE `group_members`.`unclaimed_user` = 1 -- This is the bit I changed
Because the above is ignoring the first record in the table (the one where unclaimed_user=0, it seems to work...

As you didn't provide too much to test...
SELECT `users`.`id`,
unclaimed_users.id AS unclaimed_id,
`users`.`firstname`,
`users`.`lastname`,
`unclaimed_users`.`mobile_number`,
`group_members`.`status`,
`group_members`.`user_id`,
`group_members`.`unclaimed_user`,
`user_images`.`profile_image_main`
FROM `group_members`
LEFT JOIN `users`
ON `users`.`id` = `group_members`.`user_id`
AND `group_members`.`unclaimed_user` = 0
LEFT JOIN `user_images`
ON `user_images`.`id` = `group_members`.`user_id`
AND `group_members`.`unclaimed_user` = 0
LEFT JOIN `unclaimed_users`
ON `unclaimed_users`.`id` = `group_members`.`user_id`
AND `group_members`.`unclaimed_user` = 1
WHERE `group_members`.`group_id` = 1
ORDER BY `group_members`.`created_at` ASC
I'm not sure if a good answer, I just can't add a comment.

Related

MySQL fetch rows if every columns equals the same thing

I have a table like this :
---------------------------------------
| Actions |
---------------------------------------
| action_id | user_id | action_active |
---------------------------------------
| 1 | 1 | 0 |
---------------------------------------
| 2 | 2 | 1 |
---------------------------------------
| 3 | 1 | 0 |
---------------------------------------
| 4 | 2 | 0 |
---------------------------------------
I want to retrieve all rows where a user has all of his rows as action_active = 0. If he has just one action_active as 1, don't retrieve it.
In this example, it should only retrieve the row 1 and 3, since the user 1 has all of his rows at action_active = 0.
I thought about something like this, but I'm not sure about how right it is :
SELECT *
FROM Actions AS a
WHERE action_active = ALL (SELECT action_active FROM actions as s WHERE action_active = 0 where a.idx_user = s.idx_user)
I'm not sure my query is right.
Thanks !
Calculate sum in a sub-query to find users with all zero values and join that with main select
SELECT a.*
FROM actions a
JOIN (SELECT user_id, SUM(action_active) AS sum
FROM actions
GROUP BY user_id) AS sum_a ON sum_a.user_id = a.user_id
WHERE sum = 0
Use NOT EXISTS:
SELECT a.*
FROM actions a
WHERE NOT EXISTS (SELECT 1
FROM actions a2
WHERE a2.user_id = a.user_id AND
a2.action_active <> 0
);
This should have better performance than a solution using group by -- and this makes direct use use of an index on actions(user_id, action_active).
You can also phrase this using a LEFT JOIN:
SELECT a.*
FROM actions a LEFT JOIN
actions a2
ON a2.user_id = a.user_id AND a2.action_active <> 0
WHERE a2.user_id IS NULL;

left outer join on the same table with condition

I have a table in a mysql 5.7 DB called "mytable" that is made like so:
--------------------
| title | flag |
|--------------------|
| first | 0 |
| first | 1 |
| second | 0 |
--------------------
From this table I need to select only the rows with flag = 0, and from those remove all the ones that has the same title of one with flag = 1. Ending up with a result like this:
--------------------
| title | flag |
|--------------------|
| second | 0 |
--------------------
How can i write my query? Thanks.
Just use a sub-query and combine it with MySQL's IN() function. The following will work:
SELECT title,
flag
FROM mytable
WHERE flag = 0
AND title NOT IN( SELECT title FROM mytable WHERE flag = 1 );
SQL Fiddle
Doing a LEFT OUTER JOIN
SELECT a.*
FROM mytable a
LEFT OUTER JOIN mytable b
ON a.title = b.title
AND b.flag = 1
WHERE a.flag = 0
AND b.flag IS NULL
Do the LEFT OUTER JOIN, then check for NULL in one of the joined columns to ensure that there is no matching record found.

Filter multiple records on right table into 1 record of left table

So I have a translations table which holds various representations of a place name. I join this table on the placeId, where this table serves as the right side (the left side contains info about the place).
However, joining on placeId results in possibly more translations, see table below. The contents of preferredName/shortName/historicName are all 0 or 1. There is no rule though that each translation should have at least 1 record with preferredName=1.
So what I end up with is: how can I select only 1 of these translations, in particular:
if record exists with preferredName=1: use this
else if record exists with shortName=1: use this
else if neither is true (so both are 0) then pick that record
.
+---------------+---------+------------------+---------------+-----------+--------------+
| translationId | placeId | alternateName | preferredName | shortName | historicName |
+---------------+---------+------------------+---------------+-----------+--------------+
| 4832 | 554 | 'New York' | 1 | 0 | 0 |
| 4833 | 554 | 'NY' | 0 | 1 | 0 |
| 4834 | 554 | 'New York City' | 0 | 0 | 0 |
+---------------+---------+------------------+---------------+-----------+--------------+
Any clue? It basically boils down to filtering multiple matches on the right table to 1 record on the left table.
You could join 3 times with different conditions and COALESCE the results:
select
a.*,
coalesce(name1.alternateName, name2.alternateName, name3.alternateName) as alternateName
from _table1_ as a
left join _table2_ as name1 on (a.placeId = name1.placeId and name1.preferredName=1)
left join _table2_ as name2 on (a.placeId = name2.placeId and name2.shortName=1)
left join _table2_ as name3 on (a.placeId = name3.placeId and name3.preferredName=0 and name3.shortName=0)
group by a.placeId
Following my previous comment, you can make your JOIN request on a subrequest.
try this :
SELECT * FROM mytable WHERE placeId = 554 ORDER BY preferredName DESC, shortName DESC;
it will order results exactly as you want, with your preferrence. Juste add a limit 1 ant you are right !
SELECT * FROM mytable WHERE placeId = 554 ORDER BY preferredName DESC, shortName DESC LIMIT 1;
so, just add you first part of request and make your join on this ;)
EDIT : the following is relative to a comment
LEFT OUTER JOIN
(SELECT * FROM alternatename
WHERE isoLanguage="NL" AND geonameid = a1.geonameid
ORDER BY isPreferredName DESC, isShortName DESC
LIMIT 1
)
AS alternate10
ON
alternate10.geonameid = a1.geonameid

Can I be selective on what rows I join on in MySQL

Suppose I have two tables, people and emails. emails has a person_id, an address, and an is_primary:
people:
id
emails:
person_id
address
is_primary
To get all email addresses per person, I can do a simple join:
select * from people join emails on people.id = emails.person_id
What if I only want (at most) one row from the right table for each row in the left table? And, if a particular person has multiple emails and one is marked as is_primary, is there a way to prefer which row to use when joining?
So, if I have
people: emails:
------ -----------------------------------------
| id | | id | person_id | address | is_primary |
------ -----------------------------------------
| 1 | | 1 | 1 | a#b.c | true |
| 2 | | 2 | 1 | b#b.c | false |
| 3 | | 3 | 2 | c#b.c | true |
| 4 | | 4 | 4 | d#b.c | false |
------ -----------------------------------------
is there a way to get this result:
------------------------------------------------
| people.id | emails.id | address | is_primary |
------------------------------------------------
| 1 | 1 | a#b.c | true |
| 2 | 3 | c#b.c | true | // chosen over b#b.c because it's primary
| 3 | null | null | null | // no email for person 3
| 4 | 4 | d#b.c | false | // no primary email for person 4
------------------------------------------------
You got it a bit wrong, how left/right joins work.
This join
select * from people join emails on people.id = emails.person_id
will get you every column from both tables for all records that match your ON condition.
The left join
select * from people left join emails on people.id = emails.person_id
will give you every record from people, regardless if there's a corresponding record in emails or not. When there's not, the columns from the emails table will just be NULL.
If a person has multiple emails, multiple records will be in the result for this person. Beginners often wonder then, why the data has duplicated.
If you want to restrict the data to the rows where is_primary has the value 1, you can do so in the WHERE clause when you're doing an inner join (your first query, although you ommitted the inner keyword).
When you have a left/right join query, you have to put this filter in the ON clause. If you would put it in the WHERE clause, you would turn the left/right join into an inner join implicitly, because the WHERE clause would filter the NULL rows that I mentioned above. Or you could write the query like this:
select * from people left join emails on people.id = emails.person_id
where (emails.is_primary = 1 or emails.is_primary is null)
EDIT after clarification:
Paul Spiegel's answer is good, therefore my upvote, but I'm not sure if it performs well, since it has a dependent subquery. So I created this query. It may depend on your data though. Try both answers.
select
p.*,
coalesce(e1.address, e2.address) AS address
from people p
left join emails e1 on p.id = e1.person_id and e1.is_primary = 1
left join (
select person_id, address
from emails e
where id = (select min(id) from emails where emails.is_primary = 0 and emails.person_id = e.person_id)
) e2 on p.id = e2.person_id
Use a correlated subquery with LIMIT 1 in the ON clause of the LEFT JOIN:
select *
from people p
left join emails e
on e.person_id = p.id
and e.id = (
select e1.id
from emails e1
where e1.person_id = e.person_id
order by e1.is_primary desc, -- true first
e1.id -- If e1.is_primary is ambiguous
limit 1
)
order by p.id
sqlfiddle

Left join sum, group trouble

I have this query
SELECT
`from_id` as user_id,
MAX(`createdon`) as updated_at,
SUM(`unread`) as new,
u.username,
p.sessionid,
s.access
FROM (
SELECT `from_id`, `createdon`, `unread`
FROM `modx_messenger_messages`
WHERE `to_id` = {$id}
UNION
SELECT `to_id`, `createdon`, 0
FROM `modx_messenger_messages`
WHERE `from_id` = {$id}
ORDER BY `createdon` DESC
) as m
LEFT JOIN `modx_users` as u ON (u.id = m.from_id)
LEFT JOIN `modx_user_attributes` as p ON (p.internalKey = m.from_id)
LEFT JOIN `modx_session` as s ON (s.id = p.internalKey)
GROUP BY `from_id`
ORDER BY `new` DESC, `createdon` DESC;
table
id | message | createdon | from_id | to_id | unread
1 | test | NULL | 5 | 6 | 0
2 | test2 | NULL | 6 | 5 | 1
3 | test3 | NULL | 6 | 5 | 1
result new = 28. Why?
If remove joins new = 2, correctly.
Though it depends on the actual database, pure SQL says that a statement using GROUP BY requires all non-aggregated columns to be in the GROUP BY. Without including all columns, weird stuff can happen, which might explain why you get different results. If you know that the other columns are going to be the same within the user_id, you could do MAX(u.username) or something similar (again, depending on your database server). So I'd try and clean up the SQL statement first.