Il s'agit d'un cas de relational-division - 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 relational-division 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. :