To jest przypadek relational-division - z dodanym specjalnym wymaganiem, aby ta sama rozmowa nie miała dodatkowych użytkowników.
Zakładając jest PK tabeli "conversationUsers"
co wymusza unikalność kombinacji, NOT NULL
a także dostarcza niejawnie indeks niezbędny do wydajności. Kolumny wielokolumnowego PK w tym zamówienie! W przeciwnym razie musisz zrobić więcej.
O kolejności kolumn indeksu:
W przypadku podstawowego zapytania istnieje „brute force” podejście do liczenia pasujących użytkowników dla wszystkich konwersacje wszystkich podanych użytkowników, a następnie filtruj te, które pasują do wszystkich danych użytkowników. OK dla małych tabel i/lub tylko krótkich tablic wejściowych i/lub kilku rozmów na użytkownika, ale nie skaluje się dobrze :
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[])
);
Eliminowanie rozmów z dodatkowymi użytkownikami z NOT EXISTS
anty-semi-sprzężenie. Więcej:
Alternatywne techniki:
Istnieje wiele innych (znacznie) szybszych relational-division techniki zapytań. Jednak najszybsze nie nadają się do dynamicznego liczba identyfikatorów użytkowników.
Dla szybkiego zapytania które mogą również radzić sobie z dynamiczną liczbą identyfikatorów użytkowników, rozważ rekurencyjne 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[])
);
Aby ułatwić korzystanie, zapakuj to w funkcję lub przygotowane oświadczenie . Na przykład:
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);
Zadzwoń:
EXECUTE conversations('{1,4,6}');
db<>fiddle tutaj (również demonstrując funkcję )
Wciąż jest miejsce na ulepszenia:zdobyć top wydajność, musisz umieścić użytkowników z najmniejszą liczbą konwersacji na pierwszym miejscu w tablicy wejściowej, aby wyeliminować jak najwięcej wierszy na wczesnym etapie. Aby uzyskać najwyższą wydajność, możesz dynamicznie generować niedynamiczne, nierekurencyjne zapytanie (za pomocą jednego z szybkich techniki z pierwszego łącza) i wykonaj je po kolei. Możesz nawet zapakować go w pojedynczą funkcję plpgsql z dynamicznym SQL ...
Więcej wyjaśnień:
Alternatywnie:MV dla rzadko zapisanej tabeli
Jeśli tabela "conversationUsers"
jest w większości tylko do odczytu (stare rozmowy raczej się nie zmienią) możesz użyć MATERIALIZED VIEW
ze wstępnie zagregowanymi użytkownikami w posortowanych tablicach i utwórz zwykły indeks btree dla tej kolumny tablicy.
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");
Zademonstrowany indeks pokrycia wymaga Postgres 11. Zobacz:
Informacje o sortowaniu wierszy w podzapytaniu:
W starszych wersjach użyj zwykłego indeksu wielokolumnowego w (users, "conversationId")
. W przypadku bardzo długich tablic indeks mieszający może mieć sens w Postgresie 10 lub nowszym.
Wtedy znacznie szybsze zapytanie byłoby po prostu:
SELECT "conversationId"
FROM mv_conversation_users c
WHERE users = '{1,4,6}'::int[]; -- sorted array!
db<>fiddle tutaj
Musisz porównać dodatkowe koszty przechowywania, zapisu i konserwacji z korzyściami wynikającymi z wydajności odczytu.
Na bok:rozważ legalne identyfikatory bez podwójnych cudzysłowów. conversation_id
zamiast "conversationId"
itp.: