sql >> Databasteknik >  >> RDS >> PostgreSQL

SQL-fråga för att hitta en rad med ett specifikt antal associationer

Det här är ett fall av - med det extra speciella kravet att samma konversation inte ska ha några ytterligare användare.

Förutsatt är PK för tabellen "conversationUsers" som tvingar fram unika kombinationer, NOT NULL och tillhandahåller även det index som är väsentligt för prestanda implicit. Kolumner i flerkolumnen PK i denna ordning! Annars måste du göra mer.
Om ordningen på indexkolumnerna:

För den grundläggande frågan finns "brute force" metod för att räkna antalet matchande användare för alla konversationer med alla givna användare och filtrera sedan de som matchar alla givna användare. OK för små tabeller och/eller bara korta inmatningsmatriser och/eller få konversationer per användare, men skalar inte bra :

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[])
   );

Eliminera konversationer med ytterligare användare med en NOT EXISTS anti-semi-join. Mer:

Alternativa tekniker:

Det finns flera andra, (mycket) snabbare frågetekniker. Men de snabbaste är inte väl lämpade för en dynamik antal användar-ID.

För en snabb fråga som också kan hantera ett dynamiskt antal användar-ID:n, överväg en rekursiv 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[])
   );

För enkel användning linda in detta i en funktion eller förberedd uttalande . Gilla:

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);

Ring:

EXECUTE conversations('{1,4,6}');

db<>fiol här (som också demonstrerar en funktion )

Det finns fortfarande utrymme för förbättringar:att nå topp prestanda måste du sätta användare med minsta möjliga konversationer först i din inmatningsuppsättning för att eliminera så många rader som möjligt tidigt. För att få högsta prestanda kan du generera en icke-dynamisk, icke-rekursiv fråga dynamiskt (med en av de snabba tekniker från den första länken) och exekvera det i sin tur. Du kan till och med slå in den i en enda plpgsql-funktion med dynamisk SQL ...

Mer förklaring:

Alternativ:MV för sparsamt skriven tabell

Om tabellen "conversationUsers" är oftast skrivskyddad (gamla konversationer kommer sannolikt inte att ändras) kan du använda en MATERIALIZED VIEW med föraggregerade användare i sorterade arrayer och skapa ett vanligt btree-index på den arraykolumnen.

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");

Det visade täckningsindexet kräver Postgres 11. Se:

Om sortering av rader i en underfråga:

I äldre versioner använd ett vanligt index med flera kolumner på (users, "conversationId") . Med mycket långa arrayer kan ett hashindex vara vettigt i Postgres 10 eller senare.

Då skulle den mycket snabbare frågan helt enkelt vara:

SELECT "conversationId"
FROM   mv_conversation_users c
WHERE  users = '{1,4,6}'::int[];  -- sorted array!

db<>fiol här

Du måste väga extra kostnader för lagring, skrivning och underhåll mot fördelarna för att läsa prestanda.

Bortsett från:överväg juridiska identifierare utan dubbla citattecken. conversation_id istället för "conversationId" etc.:



  1. hur kan vi gruppera 5 pm igår till 5 pm idag poster till dagens datum

  2. kommaavgränsad lista som en enda sträng, T-SQL

  3. spara ansi specialtecken i Oracle-databasen med php

  4. Hur skriver man en hql-fråga för mellan-sats för datumintervall?