Dies ist ein Fall von relational-division - 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 relational-division 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.: