【问题标题】:Selecting matching subset in many-to-many relation在多对多关系中选择匹配子集
【发布时间】:2013-01-27 22:44:25
【问题描述】:

假设我在用户和项目之间存在多对多关系:一个用户可能属于多个项目,一个项目可能有多个用户。这个关系编码在表user_projects中:

create table user_projects
(
proj_id int references projs(id) not null,
user_id int references users(id) not null,
primary key (proj_id, user_id)
);

这是我的问题:给定一组用户(user1、user2、...),我想选择给定用户集是其所有用户子集的所有项目。

例如,如果我在下面插入数据,然后询问用户 1 和 2 的所有项目,那么查询应该只返回项目 1。

insert into user_projects values (1, 1);
insert into user_projects values (1, 2);
insert into user_projects values (1, 3);
insert into user_projects values (2, 1);
insert into user_projects values (2, 3);

(我使用的是 PostgreSQL,如果最好的解决方案恰好是非标准的。)

编辑:为澄清起见,用户集应被解释为对要返回的项目列表的约束。集合 {u1, u2} 意味着项目列表应该只包括那些至少有用户 u1 和 u2 的项目;集合 {u1} 表示应返回至少具有用户 u1 的所有项目,作为限制情况,空集表示应返回 所有 个项目。

【问题讨论】:

    标签: sql postgresql


    【解决方案1】:
    Select project_ID 
    from user_projects
    where user_ID in (1,2)
    group by project_ID
    Having count(*) = 2
    

    您知道您有 2 个用户,您知道他们将是唯一的(主键) 所以你知道如果有 2 条记录,对于同一个项目,那么它就是你想要的。

    您的问题表明您已经发送了 GIVEN 用户,因此您知道哪些用户以及有多少用户。上述 SQL 可以更新以接受这些已知参数,因此保持动态,不仅限于 2 个用户。

    where user_ID in (userlist)
    having count(*) = (cntuserList)
    

    -----------处理用户集为空的情况-----

    Select P.project_ID 
    from Projects P
    LEFT JOIN user_projects UP
    where (UP.user_ID in (1,2) OR UP.USER_ID is null)
    group by project_ID
    Having count(*) = 2
    

    这就是它的作用。它返回所有项目,如果有用户附属于该项目,它会识别它们。 如果您设置包含用户,则返回的项目列表由该集合过滤,确保整个集合在项目中通过have子句。

    如果集合为空,则 LEFT join 以及 userID 为 null 语句将保留没有列出用户的项目,无论集合是否为空。 have 子句将进一步减少集合到您在集合中定义的用户数,或 0 表示返回所有未分配用户的项目。

    我们尚未讨论的另一个极端情况是,如果项目包含的用户数超过您在集合中定义的用户数,会发生什么情况。目前该项目将被退回;但我不肯定这就是你想要的。

    顺便说一句,感谢您让我思考。我不再深入研究代码了。这就是为什么我不时来这里看看我能不能帮忙的原因!

    【讨论】:

    • 我喜欢这种方法!但是有一个小问题:它不能优雅地处理用户集为空的限制情况。我意识到 SQL 标准是部分原因,因为集合表达式不能为空。尽管如此,在不诉诸额外逻辑的情况下处理这种极端情况也很好。
    • 在调用 SP/Function 之前检查以确保您没有空集。如果您的集合中的计数
    • 是的,我有点不清楚“用户集”的实际含义。实际上,它应该被解释为对项目列表的限制。因此,当我使用用户集 (user1, user2) 查询所有项目时,这意味着“给我所有满足让所有用户都在这个集合中的约束的项目”。因此,空集限制应翻译为“给我所有项目,期间”。是的,我知道这可以通过额外的逻辑来完成;我仍然认为一个非常好的解决方案的标志是自动处理极端情况而不需要额外的逻辑......
    • 我想我可以在上面处理它我只是不知道这是你想要的。
    • 澄清:@Jon:空集是否意味着你没有给用户(因此,上面的(1,2) 将正式为(),或者你的意思是没有用户分配给他们的项目?在第一种情况,我相信@xQbert的解决方案不会成功。此外,不太通用的解决方案的正确性仍然需要设置主键(或唯一约束) - 根据我的经验,依赖这样的边界条件是危险的,因为他们可能会在不记得调整特定查询的情况下更改。我相信我的解决方案也应该处理边界情况,如果我理解正确的话。
    【解决方案2】:

    这种关系划分往往可以用SELECT FROM a WHERE NOT EXISTS ( b WHERE NOT EXISTS (c))来表达

    WITH users AS (
            SELECT generate_series (1,2)::integer AS user_id
            )
    SELECT DISTINCT up.proj_id
    FROM user_projects up
       -- all the projects, but
       -- NOT the ones that miss (at least) one of the users
    WHERE NOT EXISTS (
            SELECT *
            FROM users us
              -- The projects that miss (at least) one of the users
            WHERE NOT EXISTS (
                    SELECT *
                    FROM user_projects nx
                    WHERE nx.user_id = us.user_id AND nx.proj_id = up.proj_id
                    )
            )
            ;
    

    【讨论】:

      【解决方案3】:

      这里还有一个解决方案,看起来更简单:

      select  proj_id
      from    user_projects
      group by proj_id
      having  array_agg ( user_id ) @> array [1, 2]
      

      正如@Thilo 所注意到的,有些项目可能没有分配给他们的用户。因此,如果用户的输入集为空,则查询应返回 projs 表中的所有项目。这是改进的解决方案:

      select      p.proj_id
      from        projs           p
      left join   user_projects   up
          on      p.proj_id = up.proj_id
      group by    p.proj_id
      having      array_agg ( up.user_id ) @> array (
          select  u
          from    generate_series ( 1, 2 )
          where   false   /* an empty set */
          )
      ;
      

      我已经测试了一段时间的额定解决方案的性能。至于查询小型数据集(user_projects 中的 1 670 行)时没有显着差异,另一种情况是表 user_projects 有 1 667 000 行
      (proj_id 和 user_id 列填充了从 1 到 1 000 000 的随机值;一个项目中平均有 2 个用户,最多 11 个用户):

      • array_agg 方法(从 projs 和 user_projects 读取)通常需要 24 秒(有时更短)才能给出结果。
      • Wildplasser 的方法:总是 31 秒。
      • Thilo 的查询耗时过长,我决定取消它。
      • xQbert 的“计数”方法强烈依赖于索引,速度要快很多倍 - 几乎总是只需要 0.5 秒。不过,它需要重写,以便处理空的用户输入集。

      [测试是在不是最新的 PC 上的 Postgresql 9.2.2 上运行的,尽管在较新的 PC 上的 Postgresql 8.4 上的比例相似]。

      【讨论】:

      • 这太棒了!实际上,您的初始解决方案就足够了,因为空用户集的限制情况应该导致返回所有项目。
      • 使用array_agg其实是个聪明的主意。 +1 !
      【解决方案4】:

      一个更通用的答案,允许您使用相同数量的代码拥有任意用户集。首先,我们用用户集创建一个表:

      CREATE TEMP TABLE user_set ( 
        u int
      );
      INSERT INTO user_set VALUES (1), (2);
      

      你可以用你可以放在下面FROM子句中的任何函数来替换这个表。

      现在选择实际项目:

      SELECT DISTINCT 
          proj_id 
      FROM 
          user_projects 
      WHERE 
          true = ALL (
              -- Select all required users and test if they are a member of the project
              SELECT u IN (
                  -- Select all user ids of this project
                  SELECT 
                      user_id 
                  FROM 
                      user_projects AS up 
                  WHERE 
                      up.proj_id = user_projects.proj_id
              )
              FROM 
                  user_set
         )
      

      还有Fiddle

      【讨论】:

      • 似乎适用于简单的示例,尽管它非常复杂。我对“true = ALL”有点困惑。不就够了吗?
      • @Jon 遗憾的是,仅使用 ALL (subselect) 并不是有效的 SQL。 ALL 的语法是 value <operator> ALL (subselect),这就是这个不直观的陈述的原因。见postgresql.org/docs/8.2/static/functions-subquery.html。 (请注意,@wildplasser 的回答或多或少是我的否定。)
      【解决方案5】:

      这样的事情应该可以工作:

      SELECT u.proj_id
      FROM user_projects u
         JOIN user_projects u2 on u.proj_id = u2.proj_id
      WHERE u.user_id = 1 and u2.user_id = 2
      

      这里是Fiddle

      祝你好运。

      【讨论】:

      • 我可能遗漏了一些东西,但是您将如何处理集合包含 N 个用户的情况?
      【解决方案6】:

      您可以使用多个 JOIN 块,例如:

       SELECT Up1.project_id
         FROM user_projects as up1
         JOIN user_projects as up2 on up1.project_id=up2.project_id
        WHERE up1.user_id=1
          AND up2.user_id=2;
      

      您应该为所需集合中的每个用户创建一个新的 JOIN 块。

      【讨论】:

      • 我可能遗漏了一些东西,但是您将如何处理集合包含 N 个用户的情况? (当然,N 事先是未知的,而且我不能对 SQL 语句进行元编程)
      猜你喜欢
      • 2016-03-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-04-12
      • 1970-01-01
      • 2015-11-21
      • 2017-09-26
      • 2018-06-25
      相关资源
      最近更新 更多