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

ben*_*nto 1 sql postgresql sequelize.js relational-division

使用Postgres我有一个有conversations和的模式conversationUsers.每个conversation都有很多conversationUsers.我希望能够找到具有确切指定数量的对话conversationUsers.换句话说,提供了一个userIds(例如[1, 4, 6])我希望能够找到仅包含那些用户的对话的数组,而不是更多.

到目前为止,我试过这个:

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

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

Erw*_*ter 5

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

假设是表的PK,"conversationUsers"它强制组合的唯一性,NOT NULL并且还隐含地提供了对性能至关重要的索引.多列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[])
   );
Run Code Online (Sandbox Code Playgroud)

通过NOT EXISTS反半连接消除与其他用户的对话.更多:

替代技术:

还有各种其他(更快) 查询技术.但最快的不适合动态数量的用户ID.

对于也可以处理动态数量的用户ID 的快速查询,请考虑递归CTE:

WITH RECURSIVE rcte AS (
   SELECT "conversationId", 1 AS idx
   FROM   "conversationUsers"
   WHERE  "userId" = ('{1,4,6}'::int[])[1]

   UNION ALL
   SELECT c."conversationId", r.idx + 1
   FROM   rcte                r
   JOIN   "conversationUsers" c USING ("conversationId")
   WHERE  c."userId" = ('{1,4,6}'::int[])[idx + 1]
   )
SELECT "conversationId"
FROM   rcte r
WHERE  idx = array_length(('{1,4,6}'::int[]), 1)
AND    NOT EXISTS (
   SELECT FROM "conversationUsers"
   WHERE  "conversationId" = r."conversationId"
   AND    "userId" <> ALL('{1,4,6}'::int[])
   );
Run Code Online (Sandbox Code Playgroud)

为了便于使用,请将其包装在函数或预处理语句中.喜欢:

PREPARE conversations(int[]) AS
WITH RECURSIVE rcte AS (
   SELECT "conversationId", 1 AS idx
   FROM   "conversationUsers"
   WHERE  "userId" = $1[1]

   UNION ALL
   SELECT c."conversationId", r.idx + 1
   FROM   rcte                r
   JOIN   "conversationUsers" c USING ("conversationId")
   WHERE  c."userId" = $1[idx + 1]
   )
SELECT "conversationId"
FROM   rcte r
WHERE  idx = array_length($1, 1)
AND    NOT EXISTS (
   SELECT FROM "conversationUsers"
   WHERE  "conversationId" = r."conversationId"
   AND    "userId" <> ALL($1);
Run Code Online (Sandbox Code Playgroud)

呼叫:

EXECUTE conversations('{1,4,6}');
Run Code Online (Sandbox Code Playgroud)

db <> 在这里小提琴(也展示了一个功能)

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

更多解释:

替代方案:用于稀疏编写表的MV

如果表"conversationUsers"大部分是只读的(旧的会话不太可能改变),您可以MATERIALIZED VIEW在排序的数组中使用预先聚合的用户,并在该数组列上创建普通的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");
Run Code Online (Sandbox Code Playgroud)

证明覆盖指数需要Postgres 11.参见:

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

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

那么更快的查询就是:

SELECT "conversationId"
FROM   mv_conversation_users c
WHERE  users = '{1,4,6}'::int[];  -- sorted array!
Run Code Online (Sandbox Code Playgroud)

db <> 在这里小提琴

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

除此之外:考虑没有双引号的合法标识符.conversation_id而不是"conversationId"等: