Search code examples
mysqlsqltags

SQL tag list and tag filtering


I have a SQL database in which I store users and tags associated to users (many to many relationship). I have the classic schema with users table, tags table and the "bridge" table usertag which links users with tags:

users table:
    +---------+---------+
    | Id      |  Name   |
    +---------+---------+
    | 1       | Alice   |
    | 2       | Bob     |
    | 3       | Carl    |
    | 4       | David   |
    | 5       | Eve     |
    +---------+---------+

tags table:
    +---------+---------+
    | Id      | Name    |
    +---------+---------+
    | 10      | Red     |
    | 20      | Green   |
    | 30      | Blue    |
    +---------+---------+

usertag table:
    +---------+---------+
    | UserId  |  TagId  |
    +---------+---------+
    | 2       | 10      |
    | 2       | 20      |
    | 1       | 30      |
    | 4       | 20      |
    | 4       | 10      |
    | 4       | 30      |
    | 5       | 10      |
    +---------+---------+

Now, I made a query to retrieve all the users and their tags as a comma separated field, using the GROUP_CONCAT() function:

SELECT u.*, GROUP_CONCAT(ut.tagid) as tags FROM users as u LEFT JOIN usertag as ut ON u.id = ut.userid GROUP BY u.id

which gives me the correct output:

output:
    +---------+---------+----------+
    | Id      |  Name   | Tags     |
    +---------+---------+----------+
    | 1       | Alice   | 30       |
    | 2       | Bob     | 10,20    |
    | 3       | Carl    | (null)   |
    | 4       | David   | 10,30,20 |
    | 5       | Eve     | 10       |
    +---------+---------+----------+

The problem is that now I want to implement tag filtering on top of that, i.e. being able to query the users by tag (or multiple tags). The filter should work using the AND operator.

For example: Get users with tag Red (10) AND Green (20):

output:
    +---------+---------+----------+
    | Id      |  Name   | Tags     |
    +---------+---------+----------+
    | 2       | Bob     | 10,20    |
    | 4       | David   | 10,30,20 |
    +---------+---------+----------+

Another example: Get users with tag Red (10):

output:
    +---------+---------+----------+
    | Id      |  Name   | Tags     |
    +---------+---------+----------+
    | 2       | Bob     | 10,20    |
    | 4       | David   | 10,30,20 |
    | 5       | Eve     | 10       |
    +---------+---------+----------+

Another example: Get users with tag Red (10), Green (20) and Blue (30):

output:
    +---------+---------+----------+
    | Id      |  Name   | Tags     |
    +---------+---------+----------+
    | 4       | David   | 10,30,20 |
    +---------+---------+----------+

How can I implement such query? This question on SO is very similar and it actually works but it doesn't deal with the GROUP_CONCAT() field which is something I'd like to keep as it is

Here the SQL fiddle http://sqlfiddle.com/#!9/291a5c/8

EDIT

One may imagine that this query works:

Retrieve all users with tag Red (10) and Blue (20):

 SELECT u.name, GROUP_CONCAT(ut.tagid)
    FROM users as u
    JOIN usertag as ut ON u.id = ut.userid
   WHERE ut.tagid IN (10,20)
GROUP BY u.id
  HAVING COUNT(DISTINCT ut.tagid) = 2

Which gives:

output:
    +---------+---------+----------+
    | Id      |  Name   | Tags     |
    +---------+---------+----------+
    | 2       | Bob     | 10,20    |
    | 4       | David   | 10,20    |
    +---------+---------+----------+

which username-wise is correct (Bob and David) but the Tags field is missing the tag 30 from David's list!


Solution

  • left join the tags table and include the id's being searched for in the join clause and check for counts in having.

    SELECT u.id,u.name,GROUP_CONCAT(ut.tagid) as tags
    FROM users u 
    LEFT JOIN usertag as ut ON u.id = ut.userid 
    LEFT JOIN tags t ON t.id=ut.tagid AND t.ID IN (10,20,30) --change this as needed
    GROUP BY u.id,u.name
    HAVING COUNT(ut.tagid) >= COUNT(t.id) AND COUNT(t.id) = 3 --change this number to the number of tags
    

    One more option is to use FIND_IN_SET if there are limited values. For example,

    SELECT * FROM (
    SELECT u.*, GROUP_CONCAT(ut.tagid) as tags 
    FROM users as u 
    LEFT JOIN usertag as ut ON u.id = ut.userid 
    GROUP BY u.id
    ) T
    WHERE FIND_IN_SET('10',tags) > 0 AND FIND_IN_SET('20',tags) > 0