PostgreSQL
 sql >> Baza danych >  >> RDS >> PostgreSQL

Zapytanie SQL w celu znalezienia wiersza z określoną liczbą skojarzeń

To jest przypadek - 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 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.:



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Ograniczenie wykluczenia kolumny ciągu bitowego z bitowym operatorem AND

  2. Czy istnieje bezpieczny sposób na zmodyfikowanie tabeli pg_constraint tak, aby nie wykonywać więcej sprawdzania (tymczasowo)?

  3. Przechowuj datę z opcjonalnym miesiącem / dniem

  4. kod powrotu psql, jeśli znaleziono zero wierszy

  5. Kodowanie Postgresql base64