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 ,则返回结果).
这是关系划分的一种情况- 增加了特殊要求,即同一对话不应有其他用户.
假设是表的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函数中...
更多解释:
如果表"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"等:
| 归档时间: |
|
| 查看次数: |
138 次 |
| 最近记录: |