用于将条件应用于连接中的多个行的SQL

Bin*_*gic 24 sql

我想我找到了问题的答案,我只是不确定语法,我不断收到SQL错误.

基本上,我想做与IN相反的事情.举个例子:

SELECT * 
  FROM users INNER JOIN 
       tags ON tags.user_id = users.id 
 WHERE tags.name IN ('tag1', 'tag2');
Run Code Online (Sandbox Code Playgroud)

以上将返回任何具有'tag1'OR'tag2'的用户.我希望用户同时拥有.他们必须要返回两个标签.我假设应该使用关键字ALL,但无法使其工作.

谢谢你的帮助.

Lar*_*tig 29

让我们首先谈谈这个问题,然后再详细说明.

在这个问题中,您要做的是从表A中选择行,具体取决于表B中的两个(或一般情况下,两个以上)行中的条件.为了实现这一点,您需要执行以下两项操作之一:

  1. 对表B中的不同行执行测试

  2. 将表B中感兴趣的行聚合成一行,它以某种方式包含测试表B中原始行所需的信息

我认为,这种问题是人们在VARCHAR字段中创建逗号分隔列表而不是正确规范其数据库的重要原因.

在您的示例中,您希望user根据匹配两个特定条件的行的存在来选择行tags.

(1)测试不同的行.

有三种方法可以使用技术(1)(测试不同的行).他们使用EXISTS,使用子查询和使用JOIN:

1A.使用EXIST是(在我看来,无论如何)清楚,因为它匹配你想要做的 - 检查行的存在.如果您正在生成动态SQL,那么在编写SQL创建方面可以适度扩展到更多标记,您可以为每个标记添加一个额外的AND EXISTS子句(当然,性能会受到影响):

SELECT * FROM users WHERE 
  EXISTS (SELECT * FROM tags WHERE user_id = users.id AND name ='tag1') AND
  EXISTS (SELECT * FROM tags WHERE user_id = users.id AND name ='tag2')
Run Code Online (Sandbox Code Playgroud)

我认为这清楚地表达了查询的意图.

1B使用子查询也很清楚.由于此技术不涉及相关子查询,因此某些引擎可以更好地优化它(这部分取决于具有任何给定标记的用户数):

SELECT * FROM users WHERE 
  id IN (SELECT user_id FROM tags WHERE name ='tag1') AND
  id IN (SELECT user_id FROM tags WHERE name ='tag2') 
Run Code Online (Sandbox Code Playgroud)

这与选项1A的工作方式相同.它(对我来说,无论如何)也很清楚.

1C使用JOIN涉及INNER将tags表连接到users表,每个标签一次.它也不能扩展,因为生成动态SQL更难(但仍然可能):

SELECT u.* FROM users u 
     INNER JOIN tags t1 ON u.id = t1.user_id
     INNER JOIN tags t2 ON u.id = t2.user_id
  WHERE t1.name = 'tag1' AND t2.name = 'tag2'
Run Code Online (Sandbox Code Playgroud)

就个人而言,我觉得这比其他两个选项要清晰得多,因为看起来目标是创建一个JOINed记录集而不是过滤用户表.此外,可伸缩性受到影响,因为您需要添加INNER JOIN 更改WHERE子句.请注意,这种技术跨越了技术1和2,因为它使用JOIN来聚合标记中的两行.

(2)聚合行.

使用COUNT并使用字符串处理有两种主要方法:

2A如果您的标签表被"保护"以防止将相同的标签应用于同一用户两次,则使用COUNTs会容易得多.您可以通过在标记中创建(user_id,name)PRIMARY KEY,或者在这两列上创建UNIQUE INDEX来完成此操作. 如果以这种方式保护行,您可以执行以下操作:

 SELECT users.id, users.user_name 
   FROM users INNER JOIN tags ON users.id = tags.user_id
   WHERE tags.name IN ('tag1', 'tag2')
   GROUP BY users.id, users.user_name
   HAVING COUNT(*) = 2
Run Code Online (Sandbox Code Playgroud)

在这种情况下,您将HAVING COUNT(*)=测试值与IN子句中的标记名称数相匹配.如果每个标记可以多次应用于用户,则这不起作用,因为2的计数可以由两个'tag1'实例生成而没有'tag2'(并且该行符合它不应该的地方)或者'tag1'的两个实例加上 'tag2'的一个实例将创建一个3的计数(并且用户即使它们应该也不符合条件).

请注意,这是性能最具扩展性的技术,因为您可以添加其他标记,而不需要其他查询或JOIN.

如果允许多个标记,则可以执行内部聚合以删除重复项.您可以在我上面显示的相同查询中执行此操作,但为了简单起见,我将打破逻辑到单独的视图中:

 CREATE VIEW tags_dedup (user_id, name) AS
 SELECT DISTINCT user_id, name FROM tags
Run Code Online (Sandbox Code Playgroud)

然后你回到上面的查询并用tags_dedup替换标签.

2B使用字符串处理是特定于数据库的,因为没有标准的SQL聚合函数可以从多行生成字符串列表.但是,有些数据库提供了扩展功能.在MySQL中,您可以使用GROUP_CONCAT和FIND_IN_SET来执行此操作:

SELECT user.id, users.user_name, GROUP_CONCAT(tags.name) as all_tags
  FROM users INNER JOIN tags ON users.id = tags.user_id
  GROUP BY users.id, users.user_name
  HAVING FIND_IN_SET('tag1', all_tags) > 0 AND
         FIND_IN_SET('tag2', all_tags) > 0 
Run Code Online (Sandbox Code Playgroud)

注意,这是非常低效的并使用MySQL独特的扩展.


squ*_*man 17

您将再次加入标签表.

SELECT * FROM users
INNER JOIN tags as t1 on t1.user_id = users.id and t1.name='tag1'
INNER JOIN tags as t2 on t2.user_id = users.id and t2.name='tag2'
Run Code Online (Sandbox Code Playgroud)


Jak*_*kob 5

我会先做你正在做的事情,因为这会得到一个包含'tag1'的所有用户的列表和一个包含'tag2'的所有用户的列表,但显然是在相同的响应中.所以,我们还要补充一些:

做一个group by users(或users.id)然后having count(*) == 2.这将对重复的用户进行分组(这意味着同时包含tag1和tag2的用户),然后有部分将删除只有两个标签之一的用户.

这个解决方案避免添加另一个join语句,但说实话,我不确定哪个更快.人们,随意评论性能部分:)

编辑:只是为了让它更容易尝试,这里是整个事情:

SELECT * 
FROM users INNER JOIN 
     tags ON tags.user_id = users.id 
WHERE tags.name = 'tag1' OR tags.name = 'tag2'
GROUP BY users.id
HAVING COUNT(*) = 2
Run Code Online (Sandbox Code Playgroud)

  • +1因为这也适用于更多数量的标签名称. (2认同)