Toto je případ relational-division - s přidaným zvláštním požadavkem, že stejná konverzace nesmí obsahovat žádné další uživatelů.
Za předpokladu je PK tabulky "conversationUsers"
který vynucuje jedinečnost kombinací, NOT NULL
a také implicitně poskytuje index nezbytný pro výkon. Sloupce vícesloupcového PK v tomto objednat! Jinak musíte udělat více.
O pořadí sloupců indexu:
Pro základní dotaz existuje "hrubá síla" přístup k počítání počtu odpovídajících uživatelů pro všechny konverzace všech daných uživatelů a poté filtrovat ty, které odpovídají všem daným uživatelům. OK pro malé tabulky a/nebo pouze krátká vstupní pole a/nebo málo konverzací na uživatele, ale neškáluje se dobře :
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[])
);
Eliminace konverzací s dalšími uživateli s NOT EXISTS
anti-polo-spojení. Více:
Alternativní techniky:
Existují různé další, (mnohem) rychlejší relational-division dotazovací techniky. Ty nejrychlejší se ale pro dynamické moc nehodí počet uživatelských ID.
Pro rychlý dotaz který si také poradí s dynamickým počtem uživatelských ID, zvažte rekurzivní 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[])
);
Pro snadné použití to zabalte do funkce nebo připraveného prohlášení . Jako:
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);
Volejte:
EXECUTE conversations('{1,4,6}');
db<>fiddle zde (také demonstruje funkci )
Stále je co zlepšovat:dostat se nahoru výkon musíte do vstupního pole umístit uživatele s nejmenším počtem konverzací jako první, abyste co nejdříve odstranili co nejvíce řádků. Chcete-li dosáhnout špičkového výkonu, můžete dynamicky generovat nedynamický, nerekurzivní dotaz (pomocí jednoho z rychlých techniky z prvního odkazu) a proveďte je postupně. Můžete to dokonce zabalit do jediné funkce plpgsql s dynamickým SQL ...
Další vysvětlení:
Alternativa:MV pro řídce psanou tabulku
Pokud je tabulka "conversationUsers"
je většinou jen pro čtení (staré konverzace se pravděpodobně nezmění), můžete použít MATERIALIZED VIEW
s předem agregovanými uživateli v seřazených polích a v tomto sloupci pole vytvořte prostý index btree.
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");
Demonstrovaný index pokrytí vyžaduje Postgres 11. Viz:
O řazení řádků v poddotazu:
Ve starších verzích používejte prostý vícesloupcový index na (users, "conversationId")
. S velmi dlouhými poli může mít hash index smysl v Postgresu 10 nebo novějším.
Pak by mnohem rychlejší dotaz byl jednoduše:
SELECT "conversationId"
FROM mv_conversation_users c
WHERE users = '{1,4,6}'::int[]; -- sorted array!
db<>fiddle zde
Musíte zvážit dodatečné náklady na úložiště, zápisy a údržbu s přínosy pro výkon čtení.
Stranou:zvažte legální identifikátory bez uvozovek. conversation_id
místo "conversationId"
atd.: