Joining tables when certain one table has no values? - mysql

I have these tables:
USER TABLE
uid | name | role
| |
1 | bob | package 1
2 | jill | package 2
3 | pam | package 1
NODE TABLE
nid | uid | type
| |
1 | 1 | car
2 | 1 | car
3 | 1 | car
4 | 2 | page
5 | 1 | car
6 | 3 | car
If I do:
select u.uid, u.name, count(nid) as totalNodes from USER as u left join NODE on n.uid = u.uid where n.type = 'car' group by u.uid
I end up with:
uid | name | totalNodes
| |
1 | bob | 4
3 | pam | 1
In other words, Jill is excluded. Why? And how can I avoid this? I.e. I want Jill to also appear in the list, but with totalNodes as 0 (or even NULL).

You need to perform your aggregate before attempting to join the tables as what you are currently doing is left joining, then restricting the data (at which point Jill is excluded) then grouping. If you do the count and restriction in a subquery you can then left join these results to the user table for the output you want:
SELECT u.uid, u.name, IFNULL(c.nodecount,0) AS `count`
FROM USER u LEFT JOIN (
SELECT uid, `type` , COUNT(nid) AS nodecount
FROM node
WHERE TYPE = 'car'
GROUP BY uid, type
) AS c ON u.uid = c.uid

use RIGHT JOIN instead left,
try :
select u.uid, u.name, count(nid) as totalNodes from USER as u
right join NODE on n.uid = u.uid where n.type IS NULL or n.type = 'car' group by n.uid
see this excellent post a visual explanation of joins :
http://www.codinghorror.com/blog/2007/10/a-visual-explanation-of-sql-joins.html
mysql syntax of join :
http://dev.mysql.com/doc/refman/5.0/en/join.html

Related

Select COUNT with sub-query or separate query

I'm using DataTables to display the data. It requires the total count of rows, So which approach is better for this case?
1- Sub-query:
SELECT u.id, u.name, (SELECT COUNT(id) FROM users WHERE active = 1) AS total_count FROM employees e JOIN users u ON u.id = e.user_id WHERE u.active = 1
This would return:
___________________________
| id | name | total_count |
|____|______|_____________|
| 1 | John | 7 |
| 2 | Mark | 7 |
| .. | .. | 7 |
|____|______|_____________|
2- Separate query:
SELECT u.id, u.name FROM employees e JOIN users u ON u.id = e.user_id WHERE u.active = 1
SELECT COUNT(id) FROM users WHERE active = 1
This 1st query would return:
____________
| id | name |
|____|______|
| 1 | John |
| 2 | Mark |
| .. | .. |
|____|______|
The 2nd one would return:
_____________
| COUNT(id) |
|___________|
| 7 |
|___________|
If the dataset is small then the 1st query wouldn't harm.The only drawback is since inline view is part of select statement it will get executed for each record being returned by the join between employees and users.
Here is a better option from performance standpoint,the inline view represents a table and the total_count can now be part of select statement :
SELECT u.id, u.name,total_count.tot_count
FROM (SELECT COUNT(id) tot_count FROM users WHERE active = 1) AS total_count,
employees e JOIN users u
ON u.id = e.user_id WHERE u.active = 1;

Improve query to join other tables depending on value without union

I have a table containing the following data:
+-----------------+--------------+----------------------+------------+--------------------+-----------+------+---------+---------+-----+------------------------+---------------------+
| notification_id | from_user_id | from_user_auth_level | to_user_id | to_user_auth_level | status_id | type | subject | message | url | timestamp_inserted_utc | timestamp_read_utc |
+-----------------+--------------+----------------------+------------+--------------------+-----------+------+---------+---------+-----+------------------------+---------------------+
| 1 | NULL | NULL | 1 | 1 | 0 | 2 | test | test | url | 2010-10-10 00:00:00 | 2011-10-10 00:00:00 |
| 2 | 2 | 5 | 1 | 1 | 0 | 2 | test | test | url | 2010-10-10 00:00:00 | 2011-10-10 00:00:00 |
| 3 | 3 | 5 | 1 | 1 | 0 | 2 | test | test | url | 2010-10-10 00:00:00 | 2011-10-10 00:00:00 |
| 4 | 2295 | 4 | 1 | 1 | 0 | 2 | test | test | url | 2010-10-10 00:00:00 | 2011-10-10 00:00:00 |
| 5 | 10 | 1 | 1 | 1 | 0 | 2 | test | test | url | 2010-10-10 00:00:00 | 2011-10-10 00:00:00 |
+-----------------+--------------+----------------------+------------+--------------------+-----------+------+---------+---------+-----+------------------------+---------------------+
And then I have some other tables like 'users', 'companies', 'organizations', ... etc.
I need to be able to get the username, gender and image of every notification (based on the from_user_id and from_user_auth_level).
But the problem resides in the fact, that this info resides in different places, depending on what the user_auth_level is.
For example: if my user is a "regular" user, his auth_level will be 1. And the image will reside in my "users" table, and the gender is applicable.
But if the user has auth_level == 5, it means he is an organization. In this case, gender is not applicable, and the image resides in the "organization" table, this needs to be linked via users to user_roles and then to the organization.
And this goes on for every user type, they all require different joins.
I have created a working query, but this uses UNION's everywhere, and I have read that it is not the best to use for performance reasons, so i'm hoping someone can guide me to improving this query with performance in mind:
SELECT n.*, NULL as username, NULL as gender, NULL as picture
FROM notification as n
WHERE n.from_user_auth_level IS NULL
AND n.to_user_id = $userid
UNION
SELECT n.*, u.username, u.gender as gender, u.profile_picture as picture
FROM notification as n
LEFT JOIN users AS u ON n.from_user_id = u.user_id
WHERE n.from_user_auth_level = 1
AND n.to_user_id = $userid
UNION
SELECT n.*, u.username, NULL as gender, c.logo as picture
FROM notification as n
LEFT JOIN users AS u ON n.from_user_id = u.user_id
LEFT JOIN user_companies AS uc on u.user_id = uc.user_id
LEFT JOIN company as c on uc.company_id = c.company_id
WHERE n.from_user_auth_level = 4
AND n.to_user_id = $userid
UNION
SELECT n.*, u.username, NULL as gender, o.logo as picture
FROM notification as n
LEFT JOIN users AS u ON n.from_user_id = u.user_id
LEFT JOIN user_roles as ur on u.user_id = ur.user_id
LEFT JOIN organization as o on ur.org_id = o.org_id
WHERE n.from_user_auth_level = 5
AND n.to_user_id = $userid
UNION
SELECT n.*, u.username, NULL as gender, o.logo as picture
FROM notification as n
LEFT JOIN users AS u ON n.from_user_id = u.user_id
LEFT JOIN user_roles as ur on u.user_id = ur.user_id
LEFT JOIN organization as o on ur.org_id = o.org_id
WHERE n.from_user_auth_level = 7
AND n.to_user_id = $userid
UNION
SELECT n.*, u.username, NULL as gender, NULL as picture
FROM notification as n
LEFT JOIN users AS u ON n.from_user_id = u.user_id
WHERE n.from_user_auth_level = 9
AND n.to_user_id = $userid"
After I get this result, I use PHP to order the results based on the timestamp_inserted_utc, since it's not possible to get the correct results with the UNION.
I'd use the notification table as base and used conditional outer joins as:
select
n.*,t1.gender, t2.orgNo
from
notifications n
left outer join table1 t1 on (n.auth=1 and more join)
left outer join table2 t2 on (n.auth=2 and more..)
You will have more columns but their name would make sense and you may merge at application level.
Suggested indexes (most are "covering"):
n: (from_user_auth_level, to_user_id, from_user_id)
u: (user_id, username, profile_picture, gender)
o: (org_id, logo)
ur: (org_id, user_id)
c: (company_id, logo)
uc: (company_id, user_id)
(They may even speed up Teson's Answer.)

Count rows from one tables of users in another table

I want to create a query for project listings that would give the number of registered applications, excluding the ones for which the user does not exist.
In this case, considering user 10 does not exist, I should have the query results as folows:
RESULTS
+----+------------+--------------+
| id | project | applications |
+----+------------+--------------+
| 1 | MyProject1 | 3 |
| 2 | MyProject2 | 0 |
| 3 | MyProject3 | 0 |
+----+------------+--------------+
TABLES
Projects
+----+------------+
| id | name |
+----+------------+
| 1 | MyProject1 |
| 2 | MyProject2 |
| 3 | MyProject3 |
+----+------------+
applications
+----+------+------------+
| id | user | project_id |
+----+------+------------+
| 1 | 3 | 1 |
| 2 | 4 | 1 |
| 3 | 5 | 1 |
| 4 | 10 | 1 |
+----+------+------------+
users
+----+---------+
| id | Name |
+----+---------+
| 1 | Smith |
| 2 | John |
| 3 | Paul |
| 4 | Chris |
| 5 | Gabriel |
+----+---------+
The below query is not excluding the non-existing users:
SELECT `projects` . * , (
SELECT COUNT( * )
FROM `applications`
WHERE `applications`.`project_id` = `projects`.`id`
AND EXISTS (
SELECT `applications`.`id`
FROM `applications` , `users`,`project`
WHERE `application`.`user` = `users`.`id` AND `applications`.`project_id` = `project`.`id`
)
) AS `applications`
FROM `projects` ORDER BY `id` DESC LIMIT 30
I think you want left join and group by:
select p.id, p.name, count(u.id)
from projects p left join
applications a
on p.id = a.project_id left join
users u
on a.user_id = u.id
group by p.id, p.name;
However, you might want to think about fixing the data. It seems like there should be foreign key relationships between applications and projects and applications and users. The ability to have an invalid user means that there is no valid foreign key relationship to users.
Your query looks overly complicated. This should do:
select
id,
name as project,
(
select count(*)
from applications a
where a.project_id = p.id
and a.user in (select id from users)
) as applications
from projects p;
Based on previous solution
select p.id, p.name, count(u.id)
from projects p left join
applications a
on p.id = a.project_id left join
users u
on a.user = u.id
where u.id is not null
group by p.id, p.name;
When you do a left join, if the search value doesn't exists, it returns null. Then filtering by excluding null users, will give you the result.
Please find a sqlfiddle to illustrate it : http://www.sqlfiddle.com/#!9/cbfec6/3
But easiest solution would be
select p.id, p.name, count(u.id)
from projects p,applications a, users u
where a.user = u.id
and p.id = a.project_id
group by p.id, p.name;

Union and Intersection of data

I have three tables:
user
id | name
------------------
1 | Foo
2 | Bar
3 | Baz
group_type
id | name
------------------
1 | Group 1
2 | Group 2
3 | Group 3
4 | Group 4
5 | Group 5
user_group
id | user_id | group_type_id | [..]
------------------------------------
1 | 1 | 1 | [..]
2 | 1 | 3 | [..]
3 | 2 | 1 | [..]
4 | 1 | 5 | [..]
5 | 2 | 3 | [..]
6 | 3 | 3 | [..]
Well, currently, I can find all users from a specified list of groups with union, which is like a "or" clause:
SELECT u.*
FROM user u,
user_group ug
WHERE ug.user_id = u.id
AND ug.group_type_id IN( 1, 3, 5 )
Resulting:
id | name
------------------
1 | Foo
2 | Bar
3 | Baz
Now, I need to intersect the gorup, find all users which have groups of type 1 AND 3, resulting:
id | name
------------------
1 | Foo
2 | Bar
I have tried some queries, but don't imagine a way of doing this correctly.
SELECT u.id, u.name
FROM user u
INNER JOIN user_group g
ON u.id = g.user_id
WHERE ug.group_type_id IN (1,3)
GROUP BY u.id, u.name
HAVING count(distinct ug.group_type_id) = 2
Not as clean as the normal case, but it's certainly possible.
Try to use INTERSECT query. The syntax for the SQL INTERSECT query is:
select field1, field2, ... field_n
from tabl,tab2...
INTERSECT
select field1, field2, ... field_n
from tablel,table2...
I'm not sure if my syntax is perfect, but I'd reccomend self-joining user_group onto itself using user_id and forcing one of the selected entries (ug1 and ug2) to have ug1.group_type_id=1 and the other ug2.group_type_id=3. This gives you all user_id's with 1 AND 3 as their group_type_id. Now that you have that, you can do another join onto your user table, giving you all of the results that you were looking for.
SELECT u.*
FROM user u
JOIN (SELECT ug1.user_id
FROM user_group ug1 JOIN user_group ug2
ON ug1.user_id=ug2.user_id
WHERE ug1.group_type_id=1 and ug2.group_type_id=3) ug
ON u.id=ug.user_id

JOIN and GROUP_CONCAT with three tables

I have three tables:
users: sports: user_sports:
id | name id | name id_user | id_sport | pref
---+-------- ---+------------ --------+----------+------
1 | Peter 1 | Tennis 1 | 1 | 0
2 | Alice 2 | Football 1 | 2 | 1
3 | Bob 3 | Basketball 2 | 3 | 0
3 | 1 | 2
3 | 3 | 1
3 | 2 | 0
The table user_sports links users and sports with an order of preference (pref).
I need to make a query that returns this:
id | name | sport_ids | sport_names
---+-------+-----------+----------------------------
1 | Peter | 1,2 | Tennis,Football
2 | Alice | 3 | Basketball
3 | Bob | 2,3,1 | Football,Basketball,Tennis
I have tried with JOIN and GROUP_CONCAT but I get weird results.
Do I need to do a nested query?
Any ideas?
Its not particularly difficult.
Join the three tables using the JOIN clause.
Use Group_concat on the fields you're interested in.
Don't forget the GROUP BY clause on the fields you're not concatenating or weird things will happen
SELECT u.id,
u.Name,
Group_concat(us.id_sport order by pref) sport_ids,
Group_concat(s.name order by pref) sport_names
FROM users u
LEFT JOIN User_Sports us
ON u.id = us.id_user
LEFT JOIN sports s
ON US.id_sport = s.id
GROUP BY u.id,
u.Name
DEMO
Update LEFT JOIN for when the user doesn't have entries in User_Sports as per comments
I think this is just a simple join and aggregation:
select u.id, u.name, group_concat(s.name order by pref separator ',')
from user_sports us join
users u
on us.id_user = u.id join
sports s
on us.id_sport = s.id
group by u.id, u.name