跨多对多关系查询“全部”

Eld*_*mir 5 postgresql relational-division many-to-many

想象一下三个表的设置,User、Group 和 UserGroup,其中 UserGroup 由指向每个 User 和 Group 表的简单外键组成。

User
----
id
name

Group
-----
id
name

UserGroup
---------
user_id
group_id
Run Code Online (Sandbox Code Playgroud)

现在,我想编写一个查询,选择所有指定组中的所有用户。例如,从用户是“group1”、“group2”和“group3”中的每一个的一部分的用户中选择*。

使用 Django ORM 查询,我会做类似的事情

users = (
    User.objects
    .filter(user_group__group_id=group1.id)
    .filter(user_group__group_id=group2.id)
    .filter(user_group__group_id=group2.id)
)
Run Code Online (Sandbox Code Playgroud)

这将为每次调用产生一个连接.filter,例如

SELECT * FROM users
INNER JOIN user_group g1 ON g1.user_id = id
INNER JOIN user_group g2 ON g2.user_id = id
INNER JOIN user_group g3 ON g3.user_id = id
WHERE g1.group_id = %s
  AND g2.group_id = %s
  AND g3.group_id = %s
Run Code Online (Sandbox Code Playgroud)

如果我要查询一个更大的集合来匹配,这会变得有点麻烦。

那么有什么更好的方法来做到这一点呢?如果我要问“任何”而不是“所有”,如果是一个简单的问题

SELECT * FROM users
INNER JOIN user_group g1 ON g1.user_id = id
WHERE g1.group_id in %s
Run Code Online (Sandbox Code Playgroud)

但这不是我需要的。

一个小提示:我的特定环境是在 Postgres 上,所以这里没有花哨的 MSSql 东西可以帮助我。最好,答案应该足够通用,可以在任何 SQL 风格中使用。

小智 8

您可以使用“花哨的 Postgres”功能来做到这一点 - 比“花哨的 MS SQL 功能”容易得多;)

您可以将所有组 ID 聚合到一个数组中,然后进行比较。

如果“全部”指的是那些恰好分配到这些组的用户,则可以使用如下内容:

SELECT u.id
FROM users u
  JOIN user_group ug on ug.user_id = u.id
group by u.id
having array_agg(ug.group_id order by ug.group_id) = array[1,2,3];
Run Code Online (Sandbox Code Playgroud)

请注意,=数组的运算符取决于顺序,[1,2,3]这是一个不同的数组,[3,1,2]这就是为什么array_agg()使用 anorder by并且数组中的值也会排序。

如果“全部”意味着那些至少分配给这些组的用户(但可以分配给更多组),那么您可以使用简单的“包含”运算符:

SELECT u.id
FROM users u
  JOIN user_group ug on ug.user_id = u.id
group by u.id
having array_agg(ug.group_id order by ug.group_id) @> array[1,2,3];
Run Code Online (Sandbox Code Playgroud)

“包含”运算符@不依赖于元素的顺序。


如果您需要从表中返回完整的行users,您可以在派生表中进行聚合并连接到该表:

SELECT u.id
FROM users u
  JOIN (
    SELECT user_id
    FROM user_group 
    GROUP BY user_id    
    HAVING array_agg(group_id) @> array[1,2,3]
  ) ug on ug.user_id = u.id
Run Code Online (Sandbox Code Playgroud)

第二个查询也可以使用标准 SQL 完成:

SELECT u.id
FROM users u
  JOIN (
    SELECT user_id
    FROM user_group 
    WHERE group_id in (1,2,3)
    GROUP BY user_id    
    HAVING count(distinct group_id) = 3
  ) ug on ug.user_id = u.id;
Run Code Online (Sandbox Code Playgroud)

此解决方案的缺点是,在更改 ID 列表时需要同步列表IN和表达式的值count(..) = 3