SQL查询以查找具有特定关联数的行

SQL查询以查找具有特定关联数的行,sql,postgresql,sequelize.js,relational-division,Sql,Postgresql,Sequelize.js,Relational Division,使用Postgres,我有一个模式,其中包含对话和对话用户。每个对话都有许多对话用户。我希望能够找到具有确切指定数量的conversationUsers的对话。换句话说,如果提供了一组用户ID(比如说,[1,4,6]),我希望能够找到只包含这些用户的对话,而不是更多 到目前为止,我已经尝试过: SELECT c."conversationId" FROM "conversationUsers" c WHERE c."userId" IN (1, 4) GROUP BY c."conversati

使用Postgres,我有一个模式,其中包含
对话
对话用户
。每个
对话
都有许多
对话用户
。我希望能够找到具有确切指定数量的
conversationUsers
的对话。换句话说,如果提供了一组
用户ID
(比如说,
[1,4,6]
),我希望能够找到只包含这些用户的对话,而不是更多

到目前为止,我已经尝试过:

SELECT c."conversationId"
FROM "conversationUsers" c
WHERE c."userId" IN (1, 4)
GROUP BY c."conversationId"
HAVING COUNT(c."userId") = 2;

不幸的是,这似乎也返回了包括这两个用户在内的对话。(例如,如果对话还包括
“userId”
5,则返回结果)

您可以像这样修改查询,它应该可以工作:

SELECT c."conversationId"
FROM "conversationUsers" c
WHERE c."conversationId" IN (
    SELECT DISTINCT c1."conversationId"
    FROM "conversationUsers" c1
    WHERE c1."userId" IN (1, 4)
    )
GROUP BY c."conversationId"
HAVING COUNT(DISTINCT c."userId") = 2;

您可以这样修改查询,它应该可以工作:

SELECT c."conversationId"
FROM "conversationUsers" c
WHERE c."conversationId" IN (
    SELECT DISTINCT c1."conversationId"
    FROM "conversationUsers" c1
    WHERE c1."userId" IN (1, 4)
    )
GROUP BY c."conversationId"
HAVING COUNT(DISTINCT c."userId") = 2;

这可能更容易理解。您需要对话ID,请按它分组。添加HAVING子句,该子句基于组内所有可能的匹配用户ID计数之和。这将起作用,但由于没有预限定符,处理时间将更长

select
      cu.ConversationId
   from
      conversationUsers cu
   group by
      cu.ConversationID
   having 
      sum( case when cu.userId IN (1, 4) then 1 else 0 end ) = count( distinct cu.UserID )
要进一步简化列表,请对至少有一个人参与的对话进行预查询。。。如果他们一开始不在,为什么还要考虑其他的对话呢

select
      cu.ConversationId
   from
      ( select cu2.ConversationID
           from conversationUsers cu2
           where cu2.userID = 4 ) preQual
      JOIN conversationUsers cu
         preQual.ConversationId = cu.ConversationId
   group by
      cu.ConversationID
   having 
      sum( case when cu.userId IN (1, 4) then 1 else 0 end ) = count( distinct cu.UserID )

这可能更容易理解。您需要对话ID,请按它分组。添加HAVING子句,该子句基于组内所有可能的匹配用户ID计数之和。这将起作用,但由于没有预限定符,处理时间将更长

select
      cu.ConversationId
   from
      conversationUsers cu
   group by
      cu.ConversationID
   having 
      sum( case when cu.userId IN (1, 4) then 1 else 0 end ) = count( distinct cu.UserID )
要进一步简化列表,请对至少有一个人参与的对话进行预查询。。。如果他们一开始不在,为什么还要考虑其他的对话呢

select
      cu.ConversationId
   from
      ( select cu2.ConversationID
           from conversationUsers cu2
           where cu2.userID = 4 ) preQual
      JOIN conversationUsers cu
         preQual.ConversationId = cu.ConversationId
   group by
      cu.ConversationID
   having 
      sum( case when cu.userId IN (1, 4) then 1 else 0 end ) = count( distinct cu.UserID )
这是一个例子-增加了一个特殊要求,即同一对话不应有额外的用户

假设是表
“conversationUsers”
的主键,它强制组合的唯一性,
不为空
,并隐式提供性能所必需的索引。按此顺序排列的多列PK!否则你必须做得更多。
关于索引列的顺序:

对于基本查询,有一种“蛮力”方法来计算所有给定用户的所有对话的匹配用户数,然后过滤与所有给定用户匹配的用户数。对于小表格和/或每个用户只有简短的输入数组和/或很少的对话是可以的,但是不能很好地扩展

SELECT "conversationId"
FROM   "conversationUsers" c
WHERE  "userId" = ANY ('{1,4,6}'::int[])
GROUP  BY 1
HAVING count(*) = array_length('{1,4,6}'::int[], 1)
AND    NOT EXISTS (
   SELECT FROM "conversationUsers"
   WHERE  "conversationId" = c."conversationId"
   AND    "userId" <> ALL('{1,4,6}'::int[])
   );
为便于使用,请将其包装在函数或中。比如:

dbfiddle(同时演示函数)

还有改进的余地:要获得最佳性能,您必须将对话最少的用户放在输入数组的第一位,以便尽早消除尽可能多的行。为了获得最佳性能,您可以动态生成一个非动态、非递归的查询(使用第一个链接中的一种快速技术)并依次执行。您甚至可以使用动态SQL将其封装在单个plpgsql函数中

更多说明:

备选方案:MV用于稀疏写入的表格 如果表
“conversationUsers”
大部分是只读的(旧的对话不太可能更改),您可以对排序数组中的预聚合用户使用a,并在该数组列上创建一个普通的btree索引

CREATE MATERIALIZED VIEW mv_conversation_users AS
SELECT "conversationId", array_agg("userId") AS users  -- sorted array
FROM (
   SELECT "conversationId", "userId"
   FROM   "conversationUsers"
   ORDER  BY 1, 2
   ) sub
GROUP  BY 1
ORDER  BY 1;

CREATE INDEX ON mv_conversation_users (users) INCLUDE ("conversationId");
已证明的覆盖指数要求Postgres 11。见:

关于对子查询中的行进行排序:

在旧版本中,在
(用户,“conversationId”)
上使用普通的多列索引。对于非常长的数组,哈希索引在Postgres 10或更高版本中可能有意义

那么更快的查询就是:

SELECT "conversationId"
FROM   mv_conversation_users c
WHERE  users = '{1,4,6}'::int[];  -- sorted array!
小提琴

您必须权衡存储、写入和维护的额外成本与读取性能的好处

撇开:没有双引号的法律标识符。代码>对话id而不是
“对话id”
等:

这是一种情况,增加了一项特殊要求,即同一对话不得有其他用户

假设是表
“conversationUsers”
的主键,它强制组合的唯一性,
不为空
,并隐式提供性能所必需的索引。按此顺序排列的多列PK!否则你必须做得更多。
关于索引列的顺序:

对于基本查询,有一种“蛮力”方法来计算所有给定用户的所有对话的匹配用户数,然后过滤与所有给定用户匹配的用户数。对于小表格和/或每个用户只有简短的输入数组和/或很少的对话是可以的,但是不能很好地扩展

SELECT "conversationId"
FROM   "conversationUsers" c
WHERE  "userId" = ANY ('{1,4,6}'::int[])
GROUP  BY 1
HAVING count(*) = array_length('{1,4,6}'::int[], 1)
AND    NOT EXISTS (
   SELECT FROM "conversationUsers"
   WHERE  "conversationId" = c."conversationId"
   AND    "userId" <> ALL('{1,4,6}'::int[])
   );
为便于使用,请将其包装在函数或中。比如:

dbfiddle(同时演示函数)

还有改进的余地:要获得最佳性能,您必须将对话最少的用户放在输入数组的第一位,以便尽早消除尽可能多的行。为了获得最佳性能,您可以动态生成一个非动态、非递归的查询(使用第一个链接中的一种快速技术)并依次执行。您甚至可以使用动态SQL将其封装在单个plpgsql函数中

更多说明:

备选方案:MV用于稀疏写入的表格 如果表
“conversationUsers”
大部分是只读的(旧的对话不太可能更改),您可以对排序数组中的预聚合用户使用a,并在该数组列上创建一个普通的btree索引

CREATE MATERIALIZED VIEW mv_conversation_users AS
SELECT "conversationId", array_agg("userId") AS users  -- sorted array
FROM (
   SELECT "conversationId", "userId"
   FROM   "conversationUsers"
   ORDER  BY 1, 2
   ) sub
GROUP  BY 1
ORDER  BY 1;

CREATE INDEX ON mv_conversation_users (users) INCLUDE ("conversationId");
已证明的覆盖指数要求Postgres 11。见:

关于对子查询中的行进行排序:

在旧版本中,使用普通的多列索引