I haven't used MySQL in awhile, but this is kind of a general problem.
The IN (...)
clause is an OR
based system. You've discovered that.
You can use EXISTS
subqueries, which are much faster than IN ()
subqueries (supposedly close to join speeds), like so:
SELECT p.*
FROM posts p
WHERE EXISTS (SELECT 1 FROM post_tag pt WHERE p.Post_Id = pt.Post_Id AND pt.tag_id = 1)
AND EXISTS (SELECT 1 FROM post_tag pt WHERE p.Post_Id = pt.Post_Id AND pt.tag_id = 4)
AND EXISTS (SELECT 1 FROM post_category pc WHERE p.Post_Id = pc.Post_Id AND pc.tag_id = 2)
AND EXISTS (SELECT 1 FROM post_category pc WHERE p.Post_Id = pc.Post_Id AND pc.tag_id = 3);
Notice that you don't need to join if you don't also need the category or tag information returned.
Another option is to use INTERSECT
, but that's more complicated for the query engine, typically, and probably ends up about as performant as an IN ()
clause with a subquery.