这是relational-division 的情况 - 增加了一个特殊要求,即同一对话不应有其他用户。
假设是表 "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 消除与其他用户的对话。更多:
替代技术:
还有其他各种(更快)relational-division 查询技术。但最快的那些并不适合 动态 数量的用户 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" 等: