PostgreSQL
 sql >> Database >  >> RDS >> PostgreSQL

Query SQL per trovare una riga con un numero specifico di associazioni

Questo è un caso di - con l'aggiunta speciale requisito che la stessa conversazione non abbia aggiuntive utenti.

Supponendo è il PK della tabella "conversationUsers" che impone l'unicità delle combinazioni, NOT NULL e fornisce anche implicitamente l'indice essenziale per la performance. Colonne della PK multicolonna in questo ordine! Altrimenti devi fare di più.
Informazioni sull'ordine delle colonne dell'indice:

Per la query di base, c'è la "forza bruta" approccio per contare il numero di utenti corrispondenti per tutti conversazioni di tutti gli utenti dati e quindi filtrare quelle corrispondenti a tutti gli utenti indicati. OK per tabelle piccole e/o solo array di input brevi e/o poche conversazioni per utente, ma non scala bene :

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

Eliminazione delle conversazioni con utenti aggiuntivi con un NOT EXISTS antisemiunione. Altro:

Tecniche alternative:

Ci sono vari altri, (molto) più veloci tecniche di interrogazione. Ma i più veloci non sono adatti a una dinamica numero di ID utente.

Per una interrogazione rapida che può anche gestire un numero dinamico di ID utente, considera un CTE ricorsivo :

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

Per facilità d'uso, avvolgilo in una funzione o in una dichiarazione preparata . Come:

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

Chiama:

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

db<>violino qui (mostrando anche una funzione )

C'è ancora spazio per migliorare:ottenere il top prestazioni è necessario inserire gli utenti con il minor numero di conversazioni al primo posto nell'array di input per eliminare il maggior numero possibile di righe in anticipo. Per ottenere le massime prestazioni puoi generare una query non dinamica e non ricorsiva in modo dinamico (utilizzando uno dei metodi veloci tecniche dal primo collegamento) ed eseguirlo a sua volta. Potresti persino avvolgerlo in una singola funzione plpgsql con SQL dinamico ...

Ulteriori spiegazioni:

Alternativa:MV per tabella scritta in modo sparso

Se la tabella "conversationUsers" è per lo più di sola lettura (è improbabile che le vecchie conversazioni cambino) potresti usare un MATERIALIZED VIEW con utenti preaggregati in array ordinati e crea un semplice indice btree su quella colonna dell'array.

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

L'indice di copertura dimostrato richiede Postgres 11. Vedi:

Informazioni sull'ordinamento delle righe in una sottoquery:

Nelle versioni precedenti usa un semplice indice multicolonna su (users, "conversationId") . Con array molto lunghi, un indice hash potrebbe avere senso in Postgres 10 o versioni successive.

Quindi la query molto più veloce sarebbe semplicemente:

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

db<>violino qui

Devi soppesare i costi aggiuntivi per l'archiviazione, le scritture e la manutenzione rispetto ai vantaggi in termini di prestazioni di lettura.

A parte:considera gli identificatori legali senza virgolette. conversation_id invece di "conversationId" ecc.: