【问题标题】:In postgres, what is the best way to do an AND with a join table?在 postgres 中,使用连接表执行 AND 的最佳方法是什么?
【发布时间】:2017-04-18 12:22:10
【问题描述】:

我有不同类别的用户,以及一个允许用户属于多个类别的连接表。我的联接表称为 categories_users,它由 user_id 和 category_id 组成。

我想过滤同时属于 category1 和 category2 的用户。例如,我想找到所有对棒球和足球都感兴趣的人。

在 PostgreSQL 中执行此操作的最佳方法是什么?我有以下工作:

select * from users 
  where users.id IN 
    ( Select categories_users.user_id from categories_users 
      JOIN categories ON categories.id = categories_users.category_id 
      where categories.id = 1 OR categories.parent_id = 1) 
  AND users.id IN 
    (Select categories_users.user_id from categories_users 
    JOIN categories ON categories.id = categories_users.category_id 
    where categories.id = 2 OR categories.parent_id = 2)

但是这感觉很笨拙,我想知道是否有更好的方法来做到这一点。我尝试了各种连接,但最终总是在 category_users 表中搜索 category_id 为 1 和 2 的行,这是不可能的。

编辑,我实际上还需要搜索父类别,所以我将上面的查询更改为包含 parent_id

【问题讨论】:

  • 我正在使用@jcaron 的答案。这似乎是最有效的
  • 不要忘记将 jcaron 的答案标记为首选答案,或者如果您认为它们匹配,则标记为多个答案;)

标签: sql postgresql relational-division


【解决方案1】:

只需加入同一张表两次(使用别名):

SELECT u.*
    FROM users u
    JOIN categories_users cu1 ON cu1.user_id = u.id
    JOIN categories_users cu2 ON cu2.user_id = u.id
    WHERE cu1.category_id = 1 AND cu2.category_id = 2

【讨论】:

  • 是的,谢谢,那行得通。这是 Postgres 中更有效的查询吗?
  • 您可以检查EXPLAINEXPLAIN ANALYZE 的两个查询的输出,看看哪个最有效。没有检查,但如果查询计划使用子查询或连接完全相同,我不会感到惊讶。当然,我跳过了不必要的categories 查找,因为您已经有了 ID,但您可能需要根据查找方式来更改它。一旦你的表增长一点,适当的索引肯定是必要的。
  • 再次谢谢你,我会尝试解释。由于其他原因,我确实需要类别查找,这实际上是我尝试做的简化版本。您认为什么是适当的索引? user_id 和 category_id 现在都是索引 - 使用 btree 创建。
  • 对于 categories_users,您确实可能需要两个索引,因为常见的计划是根据类别搜索 categories_users,找到用户,然后根据这些用户 ID 再次搜索 categories_users。但这可能很大程度上取决于数据的基数。
  • 实际上我无法弄清楚如何使用 category.parent_id 来做到这一点。我已更改上述查询以显示我真正想要的内容。
【解决方案2】:
select u.*
from
    users u
    inner join (
        select user_id
        from categories_users
        group by user_id
        having
            bool_or(1 in (category_id, parent_id)) and
            bool_or(2 in (category_id, parent_id))
    ) s on s.user_id = u.id

【讨论】:

  • 我还想在 category.parent_id 上进行搜索,并且我已经更改了上面的原始查询以显示这一点。您将如何将 parent_id 添加到这种解决问题的方法中?
【解决方案3】:

您还可以在分区上使用 COUNT(*) 来查看用户在搜索的类别集中有多少类别。

我创建了以下示例,以了解如何对其进行定义和参数化。 我创建了一个函数test.find_users_in_categories(BIGINT[]),它接受我们需要用户列表的类别数组。 因此该函数将返回所有给定类别中的所有用户。

解决方案 - 获取所有给定类别中的用户

CREATE SCHEMA test;

CREATE TABLE test.categories_users (
  category_id BIGINT NOT NULL,
  user_id BIGINT NOT NULL
);

INSERT INTO test.categories_users
  (user_id, category_id)
  VALUES
  (33, 103),
  (34, 104),
  (35, 105),
  (37, 105),
  (35, 106),
  (37, 106);

CREATE OR REPLACE FUNCTION test.find_users_in_categories(BIGINT[])
  RETURNS TABLE (
    user_id BIGINT
  )
AS
$$
DECLARE
  categories ALIAS FOR $1;
BEGIN
  RETURN QUERY
  SELECT t.user_id
    FROM
      (
        SELECT
          cu.user_id,
          cu.category_id,
          COUNT(*) OVER (PARTITION BY cu.user_id ) AS cnt
        FROM test.categories_users AS cu
        WHERE cu.category_id = ANY(categories)
      ) AS t
      WHERE t.cnt = array_length(categories, 1)
      GROUP BY t.user_id;
END;
$$
LANGUAGE plpgsql;

SELECT * FROM test.find_users_in_categories(ARRAY[105, 106]);

DROP SCHEMA test CASCADE;

编辑 - [递归解决方案]

解决方案 - 获取在所有给定类别和子类别中找到的用户

请参阅以下代码,了解如何使用 JOIN + 递归 CTE 实现解决方案。我使用了 JOIN 而不是 COUNT(),因为它看起来更适合这种情况。

CREATE SCHEMA test;

CREATE TABLE test.categories (
  category_id BIGINT PRIMARY KEY,
  parent_id BIGINT REFERENCES test.categories(category_id)
);

CREATE TABLE test.categories_users (
  category_id BIGINT NOT NULL REFERENCES test.categories(category_id),
  user_id BIGINT NOT NULL
);

INSERT INTO test.categories
  (category_id, parent_id)
  VALUES
  (100, NULL),
  (101, 100),
  (102, 100),
  (103, 101),
  (104, 101),
  (105, 101),
  (106, NULL);


INSERT INTO test.categories_users
  (user_id, category_id)
  VALUES
  (33, 103),
  (34, 104),
  (35, 105),
  (37, 105),
  (35, 106),
  (37, 106);


CREATE OR REPLACE FUNCTION test.find_users_in_categories(BIGINT[])
  RETURNS TABLE (
    user_id BIGINT
  )
AS
$$
DECLARE
  main_categories ALIAS FOR $1;
BEGIN
  RETURN QUERY
  WITH
    -- get all main categories and subcategories
    RECURSIVE cte_categories (category_id, main_category_id) AS
    (
      SELECT cat.category_id, cat.category_id AS main_category_id
        FROM test.categories AS cat
        WHERE cat.category_id = ANY(main_categories)
      UNION ALL
      SELECT cat.category_id, cte.main_category_id
        FROM cte_categories AS cte
        INNER JOIN test.categories AS cat
          ON cte.category_id = cat.parent_id
    ),
    -- filter main categories that are found as children of other categories
    cte_categories_unique AS
    (
      SELECT cte.*
        FROM cte_categories AS cte
        LEFT JOIN
        (
          SELECT category_id
            FROM cte_categories
            WHERE category_id <> main_category_id
            GROUP BY category_id
        ) AS to_exclude
          ON cte.main_category_id = to_exclude.category_id
        WHERE to_exclude.category_id IS NULL
    ),
    -- compute the count of main categories
    cte_main_categories_count AS
    (
      SELECT COUNT(DISTINCT main_category_id) AS cnt
        FROM cte_categories_unique
    )
  SELECT t.user_id
    FROM
      (
        -- get the users which are found in each category/sub-category then group them under the main category
        SELECT
          cu.user_id,
          cte.main_category_id
        FROM test.categories_users AS cu
        INNER JOIN cte_categories_unique AS cte
          ON cu.category_id = cte.category_id
        GROUP BY cu.user_id, cte.main_category_id
      ) AS t
      GROUP BY t.user_id
      -- filter users that do not have a match on all main categories or their sub-categories
      HAVING COUNT(*) = (SELECT cnt FROM cte_main_categories_count);
END;
$$
LANGUAGE plpgsql;


SELECT * FROM test.find_users_in_categories(ARRAY[101, 106]);

DROP SCHEMA test CASCADE;

【讨论】:

  • 我已经编辑了我的原始查询以在搜索中包含 category.parent_id,这是我真正想要的。如果我以这种方式接近它,我将如何包含 parent_id?
  • @wildrhombus 这取决于您的行为方式。那么,您有一个类别树,哪些用户应该符合您的标准?假设您将两个类别作为参数传递。这两个类别是它们自己的子树的父级。什么样的用户应该符合这个标准?是每个子树中至少一个类别的一部分的用户,还是拥有每个子树中所有类别的用户?作为所有类别的一部分,我的意思是,如果 A 是 B 和 C 的父级,C 是 D 和 E 的父级。那么以下可能是等价的 - 用户有 A,或者用户有 B 和 C,或者用户有 B、D , E.
  • @wildrhombus 应该首先问这个问题:您的类别树有多深? 2-3 级或尽可能多的级别?
  • 用户可以属于父类别 A 和/或子类别之一。用户连接到类别/子类别的方式没有限制。
  • @andrea-macarie 现在我的树只有 2 层深 - 顶级类别有孩子,孩子没有孩子。但是它很可能会改变,所以我的答案可能是 - 尽可能多的级别。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-11-15
  • 2020-01-18
  • 1970-01-01
  • 2012-04-17
  • 1970-01-01
  • 1970-01-01
  • 2023-03-17
相关资源
最近更新 更多