sql >> Database >  >> RDS >> PostgreSQL

SQL-query om een ​​rij te vinden met een specifiek aantal associaties

Dit is een geval van - 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 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.:



  1. pdo_oci_handle_factory:Fout bij het ophalen van tekst voor fout ORA-01804

  2. Bepaling en configuratie van de R12.2-editie

  3. SQL Bereken het gemiddelde tijdsverschil tussen de totale rijen

  4. Hoofdlettergevoeligheid van ID forceren in orakel