【问题标题】:How to exclude rows when using a LEFT JOIN (MySQL)使用 LEFT JOIN (MySQL) 时如何排除行
【发布时间】:2016-09-20 07:24:16
【问题描述】:

我的用户有很多帖子。我想构建一个 SQL 查询,该查询将在 1 个查询(无子查询)中执行以下操作,如果可能的话,希望没有联合。我知道我可以通过联合来做到这一点,但我想知道这是否可以只使用连接来完成。

我想获得一个不同的活跃用户列表:

  1. 没有帖子
  2. 没有已批准的帖子

这是我目前所拥有的:

SELECT DISTINCT u.*
FROM users u
  LEFT JOIN posts p
    ON p.user_id = u.id
  LEFT JOIN posts p2
    ON p2.user_id = u.id
WHERE u.status = 'active'
  AND (p.status IS NULL
  OR p2.status != 'approved');

问题是当用户有多个帖子并且其中一个处于活动状态时。这仍然会返回我不想要的用户。如果用户有一个活跃的帖子,他应该从结果集中删除。有什么想法吗?

数据如下所示:

mysql> select * from users;
+----+---------+
| id | status  |
+----+---------+
|  1 | active  |
|  2 | pending |
|  3 | pending |
|  4 | active  |
|  5 | active  |
+----+---------+
5 rows in set (0.00 sec)

mysql> select * from posts;
+----+---------+----------+
| id | user_id | status   |
+----+---------+----------+
|  1 |       1 | approved |
|  2 |       1 | pending  |
|  3 |       4 | pending  |
+----+---------+----------+
3 rows in set (0.00 sec)

这里的答案应该只有用户 4 和 5。4 没有批准的帖子,5 没有帖子。它不应包含 1,它有一个已批准的帖子。

【问题讨论】:

  • 你为什么要加入users 表和posts 表的两个实例?
  • 第一次加入时,我试图让所有没有帖子的用户,在第二次加入时,我得到那些没有被批准的用户。这是错误的,因为这仍将包括拥有多个帖子且其中一个已获批准的用户。
  • “没有活跃的帖子” -> 你的意思是“没有批准的帖子”吗?
  • 每张表有多少行? (巨大的表需要考虑缓存问题。)你有什么索引? (如果没有适当的索引,一些提供的答案会很慢。)
  • @ChrisSalzberg 谢谢,已修复。

标签: mysql sql database activerecord


【解决方案1】:

不存在:

SELECT u.*
FROM users u
WHERE NOT EXISTS (
   SELECT 1 
   FROM posts p
   WHERE p.user_id = u.id AND p.status = 'approved');

或等效的左连接

SELECT u.*
FROM users u
LEFT JOIN posts p
   ON p.user_id = u.id AND p.status = 'approved'
WHERE p.user_id IS NULL;

【讨论】:

  • 我希望尽可能避免子查询。
  • 为什么要避免子查询?这就是它的情况。好的,将其转换为左连接,请参阅更新的答案。
  • 我将用不同的范围链接这个查询,并希望它尽可能快。我也有兴趣了解这是否可以仅使用连接来完成。
  • 啊!链接时使用EXISTS 实际上可能更快!您不一定要逐个优化查询。
【解决方案2】:

根据您的要求并将它们按字面意思翻译成 SQL,我明白了:

SELECT users.id,
       COUNT(posts.id) as posts_count,
       COUNT(approved_posts.id) as approved_posts_count
FROM users
LEFT JOIN posts ON posts.user_id = users.id
LEFT JOIN posts approved_posts
  ON approved_posts.status = 'approved'
  AND approved_posts.user_id = users.id
WHERE users.status = "active"
GROUP BY users.id
HAVING (posts_count = 0 OR approved_posts_count = 0);

对于上面的测试数据,返回:

4|1|0
5|0|0

即id为45的用户,第一个有1个帖子但没有批准的帖子,第二个没有帖子。

但是,在我看来,这可以简化,因为任何没有批准帖子的用户也将没有帖子,所以条件的联合是不必要的。

在这种情况下,SQL 就是:

SELECT users.id,
       COUNT(approved_posts.id) as approved_posts_count
FROM users
LEFT JOIN posts approved_posts
  ON approved_posts.status = 'approved'
  AND approved_posts.user_id = users.id
WHERE users.status = "active"
GROUP BY users.id
HAVING approved_posts_count = 0;

这也返回相同的两个用户。我错过了什么吗?

【讨论】:

  • 不错的配方。您“缺少”必要的索引。
  • 我相信这是最好的答案。它非常系统且解释清楚。谢谢!
  • 谢谢!很高兴您发现这个答案很有用。
【解决方案3】:

请解释您为什么不想要 JOIN 或 UNION。如果是因为性能,那么考虑以下几点:

CREATE TABLE t ( PRIMARY KEY(user_id) )
    SELECT user_id, MIN(status) AS z
    FROM Posts
    GROUP BY user_id;

SELECT  u.id AS user,
        IFNULL(z, 'no_posts') AS status
    FROM users u
    WHERE u.status = 'active'
    LEFT JOIN t ON t.user_id = u.id
    HAVING status != 'approved';

它将只对每个表进行一次传递,因此相当有效(考虑到查询的复杂性)。

【讨论】:

  • 出于性能考虑,我试图避免联合,我也想知道这是否可以仅使用 JOINS 来完成。这似乎是一个常见且简单的问题,所以我想我可能会遗漏一些东西。不过,您的解决方案使用另一个表(持久而不是临时表)。您认为仅加入的解决方案可行吗?
  • 如果性能是真正的目标,我建议您测试多个答案中的每一个,修正错别字,计时(运行两次,并使用SQL_NO_CACHE)。然后选择最快的。更好的是,FLUSH STATUS; SELECT ...; SHOW SESSION STATUS LIKE 'Handler%' 可以更准确地了解所涉及的努力。 (这甚至适用于小桌子。)
  • “相关”子查询可能相当昂贵; “派生”表也可以(JOIN ( SELECT ... ))。我的临时表也很昂贵。
  • 我查看了数千个论坛问题;我不记得见过一个很像你的。答案表明有许多可能的方法。你的某些方面很常见,但这种组合很少见。
  • "How to do 'join-only'" 是一个学术问题。我认为真正的问题是“如何做到最快”。
【解决方案4】:

这个可能有帮助:

SELECT DISTINCT u.*
FROM users u
LEFT JOIN posts p ON 1=1
  -- matches only if user has any post
  AND p.user_id = u.id 
  -- matches only if user has any active post
  AND p.status = 'approved'
WHERE 1=1
  -- matches only active users
  AND u.status = 'active'
  -- matches only users with no matches on the LEFT JOIN
  AND p.status IS NULL
;

【讨论】:

    【解决方案5】:

    我认为这应该很容易。

    SELECT u.`id`, u.`status` FROM `users` u
    LEFT OUTER JOIN `post` p ON p.`user_id` = u.`id` AND p.`status` = 'approved'
    WHERE u.`status` = 'active' AND p.`id` IS NULL
    

    给出 4 和 5 的结果。

    [编辑] 只是想补充一下为什么会这样:

    u.status = '活动'

    这会排除所有不活跃的用户。

    p.status = '批准'

    这不包括所有已批准的帖子。

    因此,通过使用这两行,我们排除了所有符合您的标准的用户。

    [编辑 2]

    如果您还需要知道有多少待处理和多少已批准,这里有一个更新版本:

    SELECT u.`id`, u.`status`, SUM(IF(p.`status` = 'approved', 1, 0)) AS `Approved_Posts`, SUM(IF(p.`status` = 'pending', 1, 0)) AS `Pending_Posts`
    FROM `test_users` u
    LEFT OUTER JOIN `test_post` p ON p.`user_id` = u.`id`
    WHERE u.`status` = 'active' 
    GROUP BY u.`id`
    HAVING SUM(IF(p.`id` IS NOT NULL, 1, 0))
    

    【讨论】:

    • 这似乎是最简单的答案。但我选择了 Chris 的答案,因为我相信它对更广泛的问题更有帮助。
    • 我不知道您还需要等待和批准的帖子数量。我进一步修改了查询。另外,请注意 2 个左连接的效率低于 1 个左连接。
    【解决方案6】:

    试试这个

    SELECT DISTINCT u.*
    FROM users u LEFT JOIN posts p
        ON p.user_id = u.id
    WHERE p.status IS NULL 
      OR p.status != 'approved';
    

    【讨论】:

    • 这不会让用户没有帖子,因为这是一个内部连接
    • 这将不支持用户有 2 个帖子,一个待处理,一个已批准的用例。
    【解决方案7】:

    你能试试下面的查询吗:

     SELECT DISTINCT u.*
        FROM users u
          LEFT JOIN posts p
            ON p.user_id = u.id
        WHERE 
            u.status = 'active' AND (
            p.user_id IS NULL
            OR p.status != 'approved');
    

    编辑

    根据更新的问题,上面的查询将包括用户 1。如果我们想防止这种情况,并且不想使用内部查询,我们可以使用 MySQL 的group_concat 函数来获取所有(不同的)状态并查看它是否包含“活动”状态,下面的查询应该给出所需的输出:

    SELECT u.id, group_concat(distinct p.status) as statuses
        FROM users u
          LEFT JOIN posts p
            ON u.id = p.user_id
        WHERE 
            u.status = 'active'
    group by u.id
    having (statuses is null or statuses not like '%approved%');
    

    【讨论】:

    • 这是错字吗?其中 p.user_id 为空
    • 不,如果post表中没有对应的记录,p.user_id将为空。它是检查用户是否有任何帖子。
    • hmm..这实际上是我所拥有的简化版本。实际上,我在这里为用户省略了另一个过滤条件,所以对我来说问题是 p.user_id IS NULL 将返回所有没有帖子的用户。我已经用用例更新了问题。
    • @gerky 我还添加了另一个条件来检查用户的状态。现在可以试试吗?
    • hm.. 仍然不起作用,这仍然会从我的示例中返回用户 1,该用户有 2 个帖子,1 个待处理,1 个已批准。
    猜你喜欢
    • 2011-02-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-12
    • 2018-02-28
    • 2013-03-09
    • 1970-01-01
    • 2011-12-08
    相关资源
    最近更新 更多