PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Consulta SQL para encontrar uma linha com um número específico de associações


Este é um caso de - 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 (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.: