Dit is een geval van relational-division - met de toegevoegde speciale eis dat hetzelfde gesprek geen extra . mag hebben gebruikers.
Aangenomen is de PK van tabel "conversationUsers"
die uniciteit van combinaties afdwingt, NOT NULL
en biedt impliciet ook de index die essentieel is voor prestaties. Kolommen van de PK met meerdere kolommen in dit bestellen! Anders moet u meer doen.
Over de volgorde van indexkolommen:
Voor de basisquery is er de "brute force" benadering om het aantal overeenkomende gebruikers te tellen voor alle conversaties van alle gegeven gebruikers en filter vervolgens degenen die overeenkomen met alle gegeven gebruikers. OK voor kleine tabellen en/of alleen korte input-arrays en/of weinig gesprekken per gebruiker, maar schaalt niet goed :
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[])
);
Het elimineren van gesprekken met extra gebruikers met een NOT EXISTS
anti-semi-join. Meer:
Alternatieve technieken:
Er zijn verschillende andere, (veel) snellere relational-division query technieken. Maar de snelste zijn niet goed geschikt voor een dynamische aantal gebruikers-ID's.
Voor een snelle zoekopdracht die ook een dynamisch aantal gebruikers-ID's aankan, overweeg dan een recursieve 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[])
);
Verpak dit voor gebruiksgemak in een functie of voorbereide verklaring . Vind ik leuk:
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);
Bel:
EXECUTE conversations('{1,4,6}');
db<>fiddle hier (ook demonstreren van een functie )
Er is nog ruimte voor verbetering:om top . te krijgen prestaties moet u gebruikers met de minste conversaties als eerste in uw invoerarray plaatsen om zo veel mogelijk rijen vroegtijdig te elimineren. Om topprestaties te krijgen, kunt u dynamisch een niet-dynamische, niet-recursieve zoekopdracht genereren (met behulp van een van de snelle technieken uit de eerste link) en voer die op zijn beurt uit. Je zou het zelfs in een enkele plpgsql-functie kunnen verpakken met dynamische SQL ...
Meer uitleg:
Alternatief:MV voor schaars geschreven tabel
Als de tabel "conversationUsers"
is meestal alleen-lezen (oude gesprekken zullen waarschijnlijk niet veranderen) u kunt een gebruiken MATERIALIZED VIEW
met vooraf geaggregeerde gebruikers in gesorteerde arrays en maak een gewone btree-index op die arraykolom.
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");
De gedemonstreerde dekkingsindex vereist Postgres 11. Zie:
Over het sorteren van rijen in een subquery:
Gebruik in oudere versies een gewone index met meerdere kolommen op (users, "conversationId")
. Met zeer lange arrays kan een hash-index zinvol zijn in Postgres 10 of later.
Dan zou de veel snellere vraag eenvoudig zijn:
SELECT "conversationId"
FROM mv_conversation_users c
WHERE users = '{1,4,6}'::int[]; -- sorted array!
db<>fiddle hier
U moet de extra kosten voor opslag, schrijven en onderhoud afwegen tegen de voordelen om de prestaties te lezen.
Terzijde:overweeg juridische identifiers zonder dubbele aanhalingstekens. conversation_id
in plaats van "conversationId"
enz.: