【问题标题】:SQL query to find a row with a specific number of associations用于查找具有特定数量关联的行的 SQL 查询
【发布时间】:2019-04-13 14:11:48
【问题描述】:

使用 Postgres,我有一个包含 conversationsconversationUsers 的架构。每个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;

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

【问题讨论】:

  • 提供您的 Postgres 版本、最小表定义和一些示例行进行测试会很有帮助。
  • 嗨。这是一个常见问题解答。请始终在谷歌上搜索您的问题/问题/目标的许多清晰、简洁和特定的版本/措辞,带和不带您的特定字符串/名称,并阅读许多答案。将您发现的相关关键字添加到搜索中。如果您没有找到答案,请发布,使用 1 个变体搜索作为标签的标题和关键字。请参阅向下投票箭头鼠标悬停文本。如果您确实有要发布的非重复代码问题,请阅读并在minimal reproducible example 上采取行动。

标签: sql postgresql sequelize.js relational-division


【解决方案1】:

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

假设是表 "conversationUsers" 的 PK,它强制组合 NOT NULL 的唯一性,并且还隐式提供了对性能至关重要的索引。多列PK的列按this的顺序!否则你必须做更多。
关于索引列的顺序:

对于基本查询,有 "brute force" 方法来计算所有给定用户的 all 对话的匹配用户数量,然后过滤匹配的用户所有给定的用户。对于小型表格和/或只有较短的输入数组和/或每个用户的对话很少,但不能很好地扩展

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[])
   );

使用NOT EXISTS anti-semi-join 消除与其他用户的对话。更多:

替代技术:

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

对于还可以处理动态用户 ID 数量的快速查询,请考虑使用recursive 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[])
   );

为了便于使用,请将其包装在一个函数或prepared statement 中。喜欢:

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);

呼叫:

EXECUTE conversations('{1,4,6}');

dbfiddle here(也演示了一个函数

仍有改进的余地:要获得最佳性能,您必须在输入数组中将会话最少的用户放在首位,以便尽早消除尽可能多的行。要获得最佳性能,您可以动态生成非动态、非递归查询(使用第一个链接中的一种 fast 技术)并依次执行。您甚至可以使用动态 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");

演示的覆盖索引需要 Postgres 11。请参阅:

关于子查询中的行排序:

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

那么更快的查询就是:

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

db小提琴here

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

另外:考虑不带双引号的合法标识符。 conversation_id 而不是 "conversationId" 等:

【讨论】:

  • 非常令人印象深刻。
【解决方案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;

【讨论】:

    【解决方案3】:

    这可能更容易理解。您想要对话 ID,按它分组。添加基于匹配用户 ID 计数的总和等于组内所有可能的 HAVING 子句。这会起作用,但由于没有预选赛,处理时间会更长。

    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 )
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-09-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-04-06
      相关资源
      最近更新 更多