PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

SQL-Abfrage, um eine Zeile mit einer bestimmten Anzahl von Zuordnungen zu finden

Dies ist ein Fall von - mit der zusätzlichen besonderen Anforderung, dass dieselbe Konversation keine zusätzlichen haben darf Benutzer.

Angenommen ist der PK der Tabelle "conversationUsers" was die Eindeutigkeit von Kombinationen erzwingt, NOT NULL und liefert implizit auch den für die Performance wesentlichen Index. Spalten des mehrspaltigen PK in diesem bestellen! Sonst müssen Sie mehr tun.
Zur Reihenfolge der Indexspalten:

Für die Grundabfrage gibt es die "brute force" Ansatz, um die Anzahl der übereinstimmenden Benutzer für alle zu zählen Konversationen aller angegebenen Benutzer und filtern Sie dann diejenigen heraus, die mit allen angegebenen Benutzern übereinstimmen. OK für kleine Tabellen und/oder nur kurze Eingabearrays und/oder wenige Konversationen pro Benutzer, aber skaliert nicht gut :

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

Beseitigen von Konversationen mit zusätzlichen Benutzern mit einem NOT EXISTS Anti-Semi-Join. Mehr:

Alternative Techniken:

Es gibt verschiedene andere, (viel) schnellere Abfragetechniken. Aber die Schnellsten sind für eine Dynamik nicht gut geeignet Anzahl der Benutzer-IDs.

Für eine schnelle Abfrage der auch mit einer dynamischen Anzahl von Benutzer-IDs umgehen kann, ziehen Sie einen in Betracht rekursiver 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[])
   );

Um die Verwendung zu vereinfachen, schließen Sie dies in eine Funktion oder eine vorbereitete Anweisung ein . Wie:

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

Aufruf:

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

db<>fiddle hier (zeigt auch eine Funktion )

Es gibt noch Raum für Verbesserungen:um top zu werden Leistung müssen Sie Benutzer mit den wenigsten Konversationen an erster Stelle in Ihrem Eingabearray platzieren, um so viele Zeilen wie möglich frühzeitig zu eliminieren. Um die beste Leistung zu erzielen, können Sie eine nicht dynamische, nicht rekursive Abfrage dynamisch generieren (mithilfe einer der schnellen Techniken aus dem ersten Link) und führen diese wiederum aus. Sie könnten es sogar in eine einzige plpgsql-Funktion mit dynamischem SQL packen ...

Weitere Erklärung:

Alternative:MV für spärlich beschriebene Tabelle

Wenn die Tabelle "conversationUsers" größtenteils schreibgeschützt ist (alte Konversationen werden sich wahrscheinlich nicht ändern), könnten Sie einen verwenden MATERIALIZED VIEW mit voraggregierten Benutzern in sortierten Arrays und erstellen Sie einen einfachen btree-Index für diese Array-Spalte.

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

Der gezeigte abdeckende Index erfordert Postgres 11. Siehe:

Informationen zum Sortieren von Zeilen in einer Unterabfrage:

Verwenden Sie in älteren Versionen einen einfachen mehrspaltigen Index für (users, "conversationId") . Bei sehr langen Arrays kann ein Hash-Index in Postgres 10 oder höher sinnvoll sein.

Dann wäre die viel schnellere Abfrage einfach:

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

db<>fiddle hier

Sie müssen zusätzliche Kosten für Speicher, Schreibvorgänge und Wartung gegen Vorteile für die Leseleistung abwägen.

Beiseite:Betrachten Sie legale Bezeichner ohne doppelte Anführungszeichen. conversation_id statt "conversationId" usw.: