Grouping results based on link table

Posted on

Question :

I have three tables:

users

+----+-------+
| id | name  | 
+----+-------+
| 1  | dave  |
| 2  | cher  |
| 3  | meg   |
| 4  | sarah |
+----+-------+

groups

+----+-------+
| id | name  |
+----+-------+
| 1  | admin |
| 2  | super |
+----+-------+

user_groups

+----+---------+----------+
| id | user_id | group_id |
+----+---------+----------+
| 1  | 1       | 1        |
| 1  | 2       | 1        |
+----+---------+----------+

I want to create a query so that I return an array of group names which contains an array of users. I don’t want to return groups which have no users, but I would like to return users which have no group.

admin
    - dave
    - cher
- meg
- sarah

At the moment I am getting the groups which have items first.

SELECT name FROM groups
INNER JOIN user_groups ON groups.id = user_groups.group_id

Then iterating through the groups and adding users.

SELECT id, name FROM users
INNER JOIN user_groups ON users.id = user_groups.user_id
WHERE user_groups.group_id = $group_id

Is there a way to combine these queries?

Answer :

You should be able to easily combine your current queries to get the group and users by using the following:

SELECT g.name group_name, u.id, u.name
FROM groups g
INNER JOIN user_groups ug
  ON g.id = ug.group_id
INNER JOIN users u
  on ug.user_id  = u.id;

See SQL Fiddle with Demo. This will give you multiple rows for each group if there is more than one user per group, similar to this:

| GROUP_NAME | ID | NAME |
--------------------------
|      admin |  1 | dave |
|      admin |  2 | cher |

You could also use the GROUP_CONCAT() function to return a comma separated list of the users with each group:

SELECT g.name group_name, 
  GROUP_CONCAT(u.name SEPARATOR ', ') name
FROM groups g
INNER JOIN user_groups ug
  ON g.id = ug.group_id
INNER JOIN users u
  on ug.user_id  = u.id
GROUP BY g.name;

See SQL Fiddle with Demo. This returns a result:

| GROUP_NAME |       NAME |
---------------------------
|      admin | cher, dave |

Then if you want to also include the users that have no group associated with them, you could use a UNION ALL query:

SELECT g.name group_name, u.id, u.name
FROM groups g
INNER JOIN user_groups ug
  ON g.id = ug.group_id
INNER JOIN users u
  on ug.user_id  = u.id
UNION ALL
SELECT '' group_name, u.id, u.name
FROM users u
WHERE id not in (select user_id
                 from user_groups);

See SQL Fiddle with Demo. This returns:

| GROUP_NAME | ID |  NAME |
---------------------------
|      admin |  1 |  dave |
|      admin |  2 |  cher |
|            |  3 |   meg |
|            |  4 | sarah |

Perhaps something like this:

SET @given_group = 1;
SELECT
    IFNULL(`group`,'-') `group`,
    user
FROM
(SELECT A.name `group`,B.name user,0 groupwithusers
FROM user_groups AB
INNER JOIN groups A ON AB.group_id = A.id
INNER JOIN users B ON AB.user_id = B.id
WHERE AB.group_id = @given_group
UNION
SELECT NULL,A.name,1
FROM users A LEFT JOIN user_groups B
ON A.id = B.user_id
WHERE B.id IS NULL) M
ORDER BY groupwithusers;

When I run it, I get this:

mysql> SET @given_group = 1;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT
    ->     IFNULL(`group`,'-') `group`,
    ->     user
    -> FROM
    -> (SELECT A.name `group`,B.name user,0 groupwithusers
    -> FROM user_groups AB
    -> INNER JOIN groups A ON AB.group_id = A.id
    -> INNER JOIN users B ON AB.user_id = B.id
    -> WHERE AB.group_id = @given_group
    -> UNION
    -> SELECT NULL,A.name,1
    -> FROM users A LEFT JOIN user_groups B
    -> ON A.id = B.user_id
    -> WHERE B.id IS NULL) M
    -> ORDER BY groupwithusers;
+-------+-------+
| group | user  |
+-------+-------+
| admin | dave  |
| admin | cher  |
| -     | meg   |
| -     | sarah |
+-------+-------+
4 rows in set (0.00 sec)

mysql>

Give it a Try !!!

Leave a Reply

Your email address will not be published. Required fields are marked *