如何使用JOIN搜索MySQL?

时间:2011-11-12 18:22:35

标签: mysql

有人能告诉我在数据库中进行此类搜索的方法吗?

我得到了这些表格:

posts (id, tags_cache)
tags (id, name)
posts_tags (post_id, tag_id)

用户输入搜索查询(例如“ water blue ”),我想显示包含这两个标签的帖子。 我能想到搜索的唯一方法是使用 FIND_IN_SET ,这样:

SELECT p.*, GROUP_CONCAT(t.name) AS tags_search
FROM posts p
LEFT JOIN posts_tags pt ON p.id = pt.post_id
LEFT JOIN tags t ON pt.tag_id = t.id
GROUP BY p.id
HAVING FIND_IN_SET('water', tags_search) > 0
AND FIND_IN_SET('blue', tags_search) > 0

posts.tags_cache 文字列存储其所属标记的名称和ID(这样:water:15 blue:20)。

要使用此列进行搜索以避免加入,我尝试了 LIKE INSTR ,但由于您可以搜索“ ter”,因此会产生不准确的结果“并且您将获得标记为' water '和' termal '的帖子。我也尝试了 REGEXP ,它可以提供准确的结果,但这是一个缓慢的过程。

我不能使用 MATCH ,因为表格使用InnoDB。

那么......是否有其他方法可以实现这一目标?

[编辑]

我忘了提到用户可以搜索许多标签(不仅仅是2个),甚至可以排除标签:搜索帖子标记为'water'而不是'blue'。使用FIND_IN_SET这对我有用:

HAVING FIND_IN_SET('water', tags_search) > 0
AND NOT FIND_IN_SET('blue', tags_search) > 0

[EDIT2]

我做了一些性能测试(即只检查了查询花了多长时间,缓存了),这是ypercube建议的,结果如下:

muists | Bill K | ypercu | includes:excludes
--------------------------
0.0137 | 0.0009 | 0.0029 | 2:0
0.0096 | 0.0081 | 0.0033 | 2:1
0.0111 | 0.0174 | 0.0033 | 2:2
0.0281 | 0.0081 | 0.0025 | 5:1
0.0014 | 0.0013 | 0.0015 | 0:2

我不知道这个信息是否是有效的资源......但是它表明ypercube的方法每个标签的JOIN是最快的。

3 个答案:

答案 0 :(得分:4)

我不明白为什么你不想使用JOIN,也不知道你为什么要尝试使用LEFT JOIN。你正在寻找那里的东西(而不是那里),所以摆脱LEFT JOIN并加入。摆脱tags_cache专栏,你只是在问这类事情。

这就是你要找的东西:

select p.id
from posts p
join posts_tags pt on p.id = pt.post_id
join tags t on pt.tag_id = t.id
where t.name in ('water', 'blue')
group by p.id
having count(t.id) = 2

HAVING子句中的2是您要查找的标签数。

如果要排除某些标记,可以将其添加到WHERE子句中,如下所示:

select p.id
from posts p
join posts_tags pt on p.id = pt.post_id
join tags t on pt.tag_id = t.id
where t.name in ('water', 'blue')
  and p.id not in (
    select pt.post_id
    from posts_tags pt
    join tags t on pt.tag_id = t.id
    where t.name in ('pancakes', 'eggs') -- Exclude these
)
group by p.id
having count(t.id) = 2

答案 1 :(得分:3)

查找与不同行中几个条件的所有匹配的帖子是一个常见问题。

以下是两种方法:

SELECT p.*
FROM posts p
INNER JOIN posts_tags pt ON p.id = pt.post_id
INNER JOIN tags t ON pt.tag_id = t.id
WHERE t.name IN ('water', 'blue')
GROUP BY p.id
HAVING COUNT(DISTINCT t.name) = 2;

或者:

SELECT p.*
FROM posts p
INNER JOIN posts_tags pt1 ON p.id = pt1.post_id
INNER JOIN tags t1 ON pt1.tag_id = t1.id
INNER JOIN posts_tags pt2 ON p.id = pt2.post_id
INNER JOIN tags t2 ON pt2.tag_id = t2.id
WHERE (t1.name, t2.name) = ('water', 'blue');

重新评论和编辑:

HAVING解决方案的问题在于它必须执行表扫描,搜索表中的每一行。这通常比JOIN慢得多(当你有适当的索引时)。

为了支持标签排除条件,以下是我的编写方式:

SELECT p.*
FROM posts p
INNER JOIN posts_tags pt1 ON p.id = pt1.post_id
INNER JOIN tags t1 ON pt1.tag_id = t1.id AND t1.name = 'water'
LEFT OUTER JOIN (posts_tags pt2 
INNER JOIN tags t2 ON pt2.tag_id = t2.id AND t2.name = 'blue')
  ON p.id = pt2.post_id
WHERE t2.id IS NULL;

避免使用JOIN,因为你在某个地方看到它们是坏的是毫无意义的。您必须了解JOIN是关系数据库中的基本操作,您应该在作业调用它的地方使用它。

答案 2 :(得分:1)

对于您的其他请求,不包括某些标记,您可以使用下一种方法。它将为您提供所有包含水和蓝色标签但没有黑色,白色或红色的帖子:

SELECT p.*
FROM posts p
  INNER JOIN posts_tags pt1 ON p.id = pt1.post_id 
  INNER JOIN tags t1 ON pt1.tag_id = t1.id
  INNER JOIN posts_tags pt2 ON p.id = pt2.post_id
  INNER JOIN tags t2 ON pt2.tag_id = t2.id
WHERE (t1.name, t2.name) = ('water', 'blue')          --- include
  AND NOT EXISTS
      ( SELECT *
        FROM posts_tags pt
          INNER JOIN tags t ON pt.tag_id = t.id
        WHERE p.id = pt.post_id 
          AND t.name IN ('black', 'white', 'red')     --- exclude
      )