简体   繁体   中英

SQL one-to-many LEFT OUTER JOIN: perform an AND query

This should be common enough and I'm looking for the "best" way to perform this in one SQL query (MySQL).

I have three tables, an items table, a linker table and a tags table. Items can be tagged multiple times, so the linker is a simple foreign key linker table:

items   | linker  | tags  
--------+---------+-------
item_id | item_id | tag_id
...     | tag_id  | name  
--------+---------+-------

I can search items for single tags easily, how would I go to search items that have 2 or more specific tags?

SELECT *, `tags`.`name`
FROM `items`
LEFT OUTER JOIN `linker` USING (`item_id`)
LEFT OUTER JOIN `tags` USING (`tag_id`)
WHERE `tags`.`name` = "tag-a"

How does a sane person perform search for 2 or more tags, an item must have ALL the tags, ie an AND query?


Edit: What I have so far is the following, which works and doesn't seem to be slow, but looks crazy:

SELECT `items`.* FROM `items`
LEFT OUTER JOIN `linker` USING (`item_id`)
LEFT OUTER JOIN `tags` USING (`tag_id`)
WHERE (
        `item_id` IN (SELECT item_id FROM linker LEFT JOIN tags USING (tag_id) WHERE name = "tag-a")
    AND `item_id` IN (SELECT item_id FROM linker LEFT JOIN tags USING (tag_id) WHERE name = "tag-b")
    AND `item_id` IN (SELECT item_id FROM linker LEFT JOIN tags USING (tag_id) WHERE name = "tag-c")
    AND `item_stuff` = "whatever"
)

Assuming the PK for the linker table is (item_id,tag_id), I would use the following:

select *
  from items
  where item_id in (
    select item_id
      from linker
      join tags using(tag_id)
     where name in ('tag1', 'tag2', 'tag3')
     group by item_id
     having count(tag_id)=3
  )
;

The above query should be easy to maintain. You can easily add or subtract required tag names. You just need to make sure the having count matches the number of names in the list.

If the linker table PK is not (item_id,tag_id), then the having clause would have to change to having count(distinct tag_id)=3 , though that query may not perform so well, depending on how many duplicate (item_id,tag_id) pairs exist.

Another nice feature about the above is you can easily answer questions like, which items are associated with at least 2 of the following list of tags ('tag1','tag2','tag3'). You just need to set the having count to the correct value.

If I understand correctly (which I'm not sure I do :) ... ), you want to find results that contain a certain string (like a regular expression search).

you could try the RLIKE function

SELECT *, `tags`.`name`
FROM `items`
LEFT OUTER JOIN `linker` USING (`item_id`)
LEFT OUTER JOIN `tags` USING (`tag_id`)
WHERE `tags`.`name` RLIKE("tag-a"|"tag-b")

I think this is what you mean, but maybe not:

http://dev.mysql.com/doc/refman/5.0/en/regexp.html


Or if each entry has only one tag per entry, what about using IN :

SELECT *, `tags`.`name`
FROM `items`
LEFT OUTER JOIN `linker` USING (`item_id`)
LEFT OUTER JOIN `tags` USING (`tag_id`)
WHERE `tags`.`name` IN ("tag-a","tag-b")

http://dev.mysql.com/doc/refman/5.0/en/comparison-operators.html#function_in


And why not just a basic OR

 WHERE `tags`.`name` = "tag-a" OR `tags`.`name` = "tag-b"

I hope I'm understanding your goal correctly, please let me know if I don't.

edit I mis-read a part of your question...I may not be sane, but hope this doesn't disqualify me :P

To restate your question, you want all columns from table items that have all the tags in some list, is that correct? If so, I think you need to join to your tags table for each and use an INNER JOIN instead of a LEFT OUTER JOIN . Something like this:

SELECT DISTINCT `items`.* 
FROM   `items` a
JOIN   `linker` b 
ON     b.item_id=a.item_id

JOIN   `tags` c1
ON     c1.tag_id=b.tag_id
   and c1.name = "tag-a"

JOIN   `tags` c2
ON     c2.tag_id=b.tag_id
   and c2.name = "tag-a"

JOIN   `tags` c3
ON     c3.tag_id=b.tag_id
   and c3.name = "tag-c"

Using an INNER JOIN will select only rows that have all three tags. I'm not sure how you would do this with a variable number of tags (which I think is what you really want).

Of course this has been asked already: How to filter SQL results in a has-many-through relation

Turns out my interim solution is one of the fastest (number 4 in the linked question), here it is:

SELECT *
FROM `items`
WHERE (
        `item_id` IN (SELECT item_id FROM linker INNER JOIN tags USING (tag_id) WHERE name = "tag-a")
    AND `item_id` IN (SELECT item_id FROM linker INNER JOIN tags USING (tag_id) WHERE name = "tag-b")
    AND `item_id` IN (SELECT item_id FROM linker INNER JOIN tags USING (tag_id) WHERE name = "tag-c")
    AND `item_stuff` = "whatever"
)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM