Este é um caso de relational-division - com o requisito especial adicional de que a mesma conversa não deve ter adicionais usuários.
Supondo é o PK da tabela
"conversationUsers"
que impõe exclusividade de combinações, NOT NULL
e também fornece o índice essencial para o desempenho implicitamente. Colunas do PK de várias colunas neste ordem! Caso contrário, você precisa fazer mais.Sobre a ordem das colunas de índice:
Para a consulta básica, existe a "força bruta" abordagem para contar o número de usuários correspondentes para todos conversas de todos os usuários e, em seguida, filtre as que correspondem a todos os usuários. OK para tabelas pequenas e/ou apenas matrizes de entrada curtas e/ou poucas conversas por usuário, mas não dimensiona bem :
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[])
);
Eliminando conversas com usuários adicionais com um
NOT EXISTS
anti-semi-junção. Mais:Técnicas alternativas:
Existem várias outras relational-division (muito) mais rápidas técnicas de consulta. Mas os mais rápidos não são adequados para uma dinâmica número de IDs de usuário.
Para uma consulta rápida que também pode lidar com um número dinâmico de IDs de usuário, considere um CTE recursiva :
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[])
);
Para facilidade de uso, envolva isso em uma função ou instrução preparada . Curti:
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);
Ligar:
EXECUTE conversations('{1,4,6}');
db<>fiddle aqui (também demonstrando uma função )
Ainda há espaço para melhorias:ficar top desempenho, você precisa colocar os usuários com o menor número de conversas primeiro em sua matriz de entrada para eliminar o maior número possível de linhas mais cedo. Para obter o melhor desempenho, você pode gerar uma consulta não dinâmica e não recursiva dinamicamente (usando uma das opções rápidas técnicas do primeiro link) e executá-lo por sua vez. Você pode até envolvê-lo em uma única função plpgsql com SQL dinâmico ...
Mais explicação:
Alternativa:MV para tabela escassamente escrita
Se a tabela
"conversationUsers"
é principalmente somente leitura (é improvável que as conversas antigas mudem), você pode usar um MATERIALIZED VIEW
com usuários pré-agregados em matrizes classificadas e crie um índice btree simples nessa coluna da matriz. 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");
O índice de cobertura demonstrado requer o Postgres 11. Veja:
Sobre a classificação de linhas em uma subconsulta:
Nas versões mais antigas, use um índice simples de várias colunas em
(users, "conversationId")
. Com arrays muito longos, um índice de hash pode fazer sentido no Postgres 10 ou posterior. Então a consulta muito mais rápida seria simplesmente:
SELECT "conversationId"
FROM mv_conversation_users c
WHERE users = '{1,4,6}'::int[]; -- sorted array!
db<>fiddle aqui
Você precisa pesar os custos adicionais de armazenamento, gravações e manutenção em relação aos benefícios do desempenho de leitura.
À parte:considere identificadores legais sem aspas duplas.
conversation_id
em vez de "conversationId"
etc.: