繁体   English   中英

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

[英]SQL for applying conditions to multiple rows in a join

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

基本上,我想做与IN相反的事情。 拿这个例子:

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

以上将返回具有“tag1”或“tag2”的任何用户。 我想要同时拥有两者的用户。 他们必须同时返回两个标签。 我假设应该使用关键字 ALL,但不能让它工作。

谢谢你的帮助。

我们先笼统地谈谈这个问题,然后再具体谈谈。

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

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

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

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

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

(1) 测试不同的行。

您可以通过三种方式使用技术 (1)(测试不同的行)。 他们使用 EXISTS、使用子查询和使用 JOIN:

1A。 使用 EXISTs是(在我看来,无论如何)很清楚,因为它匹配你想要做的 - 检查行的存在。 如果您正在生成动态 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')

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

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') 

这与选项 1A 的缩放方式相同。 这也(对我来说,无论如何)很清楚。

1C 使用 JOIN涉及对每个标签将标签表与用户表进行内部联接。 它不能很好地扩展,因为它更难(但仍然可能)生成动态 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'

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

(2) 聚合行。

有两种主要方法可以做到这一点,使用 COUNTs 和使用字符串处理:

2A如果您的标签表受到“保护”,不会将同一标签应用于同一用户两次,则使用 COUNT会容易得多。 您可以通过在标签中创建 (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

在这种情况下,您将 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

然后你回到上面的查询并用 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 

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

您将要再次加入标签表。

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'

我会先做你正在做的事情,因为这会得到一个包含“tag1”的所有用户的列表和一个包含“tag2”的所有用户的列表,但显然是在相同的响应中。 所以,我们必须添加更多:

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

此解决方案避免添加另一个连接语句,但老实说,我不确定哪个更快。 人们,请随时对性能部分发表评论:)

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

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

好的,再次说明问题。

“查找在 tag1 和 tag2 的标签表中都有条目的用户”。 这意味着每个用户表条目的子标签表中至少有 2 行

方案一:“users with tag1”和“users with tag2”的交集

SELECT u.*
FROM 
    users u INNER JOIN 
    (
    SELECT user_id FROM tags WHERE name = 'tag1'
    INTERSECT
    SELECT user_id FROM tags WHERE name = 'tag2'
    ) t ON u.id = t.user_id

解决方案 2:存在

SELECT u.*
FROM 
    users u
WHERE 
    EXISTS (SELECT * FROM tags t1 WHERE t1.name = 'tag1'
                     AND u.id = t1.user_id)
    AND
    EXISTS (SELECT * FROM tags t2 WHERE t2.name = 'tag2'
                     AND u.id = t2.user_id)

解决方案3:加入

SELECT u.* FROM
   users u
   INNER JOIN
   tags as t1 on t1.user_id = u.id
   INNER JOIN
   tags as t2 on t2.user_id = u.id 
WHERE
   t1.name='tag1' AND t2.name='tag2'

解决方案 4:输入

SELECT u.*
FROM 
    users u
WHERE 
    u.id (SELECT t1.user_id FROM tags t1 WHERE t1.name = 'tag1')
    AND
    u.id (SELECT t2.user_id FROM tags t2 WHERE t2.name = 'tag2')

所有的 EXISTS、INTERSECT 和 IN 应该在 SQL Server 中给出相同的执行计划

现在,这些都是针对您正在寻找 2 个标签的情况。 当您需要更多标签时,它们会变得很麻烦,因此请使用 shahkalpesh 的解决方案。

但是,我会修改它,以便标记在表中并且不需要额外的 OR 子句

SELECT u.*
FROM
    Users u
    Inner join
    tags t ON t.user_id = u.id
    JOIN
    @MyTags mt ON t.name = mt.name
GROUP BY u.*
HAVING count(tags.*) = COUNT(DISTINCT mt.name)
SELECT Users.id, count(tags.*) as tagCount
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(tags.*) = 2

请尝试以下操作:

SELECT * 
FROM users u, tags t1, tags t2
WHERE t1.user_id = t2.user_id
AND t1.name = 'tag1'
AND t2.name = 'tag2'
AND t1.user_id = u.id

显然,对于大量的标签,这个查询的性能会严重下降。

select * from users u
where 2 = (select count(*) from tags t where t.user_id = u.id and name in ('tag1','tag2'))

假设任何给定的标签每个用户只能出现一次。

试试这个

SELECT *
FROM users
INNER JOIN tags ON tags.user_id = users.id
WHERE users.id in
    (
    SELECT user_id
    FROM tags
    WHERE name IN ('tag1', 'tag2')
    GROUP BY user_id
    HAVING COUNT(*) = 2
    )

你需要检查行的存在,而不是能够做一个简单的IN (这只会每个加盟记录检查值)。 也许是这样的:

SELECT * 
from users
WHERE EXISTS (SELECT NULL FROM tags WHERE tags.user_id = users.id AND tags.name = 'tag1')
  AND EXISTS (SELECT NULL FROM tags WHERE tags.user_id = users.id AND tags.name = 'tag2');

关于什么

SELECT * FROM users, tags WHERE tags.user_id = users.user_id AND tags.name = 'tag1'
INTERSECT
SELECT * FROM users, tags WHERE tags.user_id = users.user_id AND tags.name = 'tag2'

试试WHERE tags.name IN ('tag1') and tags.name IN ('tag2');

效率不高,但可能是多种方式之一。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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