PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Requête SQL pour trouver une ligne avec un nombre spécifique d'associations

Il s'agit d'un cas de - avec l'exigence spéciale supplémentaire que la même conversation n'ait pas d'élément supplémentaire utilisateurs.

En supposant est le PK de la table "conversationUsers" qui applique l'unicité des combinaisons, NOT NULL et fournit également implicitement l'indice essentiel à la performance. Colonnes du PK multicolonne dans this ordre! Sinon, vous devez en faire plus.
À propos de l'ordre des colonnes d'index :

Pour la requête de base, il y a la "force brute" approche pour compter le nombre d'utilisateurs correspondants pour tous conversations de tous les utilisateurs donnés, puis filtrez celles qui correspondent à tous les utilisateurs donnés. OK pour les petites tables et/ou uniquement les tableaux d'entrée courts et/ou quelques conversations par utilisateur, mais ne s'adapte pas bien :

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

Élimination des conversations avec des utilisateurs supplémentaires avec un NOT EXISTS anti-semi-jointure. Plus :

Techniques alternatives :

Il y en a plusieurs autres, (beaucoup) plus rapides techniques d'interrogation. Mais les plus rapides ne sont pas bien adaptés pour un dynamique nombre d'ID utilisateur.

Pour une requête rapide qui peut également gérer un nombre dynamique d'ID utilisateur, envisagez un CTE récursif :

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

Pour faciliter l'utilisation, enveloppez ceci dans une fonction ou instruction préparée . Comme :

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

Appel :

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

db<>violon ici (illustrant également une fonction )

Il y a encore place à l'amélioration :pour obtenir top performances, vous devez placer les utilisateurs avec le moins de conversations en premier dans votre tableau d'entrée pour éliminer autant de lignes que possible tôt. Pour obtenir des performances optimales, vous pouvez générer dynamiquement une requête non dynamique et non récursive (en utilisant l'une des méthodes rapide techniques du premier lien) et exécutez-le à son tour. Vous pouvez même l'envelopper dans une seule fonction plpgsql avec du SQL dynamique...

Plus d'explication :

Alternative :MV pour un tableau peu écrit

Si la table "conversationUsers" est principalement en lecture seule (il est peu probable que les anciennes conversations changent), vous pouvez utiliser un MATERIALIZED VIEW avec des utilisateurs pré-agrégés dans des tableaux triés et créez un index btree simple sur cette colonne de tableau.

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

L'index de couverture démontré nécessite Postgres 11. Voir :

À propos du tri des lignes dans une sous-requête :

Dans les anciennes versions, utilisez un index multicolonne simple sur (users, "conversationId") . Avec de très longs tableaux, un index de hachage peut avoir un sens dans Postgres 10 ou version ultérieure.

Alors la requête beaucoup plus rapide serait simplement :

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

db<>violon ici

Vous devez peser les coûts supplémentaires liés au stockage, aux écritures et à la maintenance par rapport aux avantages des performances de lecture.

À part :considérez les identificateurs légaux sans guillemets doubles. conversation_id au lieu de "conversationId" etc. :