C'est un cas de division relationnelle. J'ai ajouté la balise.
Index
En supposant une contrainte PK ou UNIQUE sur USER_PROPERTY_MAP(property_value_id, user_id)
- colonnes dans cet ordre pour accélérer mes requêtes. Connexe :
- Un index composite est-il également adapté aux requêtes sur le premier champ ?
Vous devriez également avoir un index sur PROPERTY_VALUE(value, property_name_id, id)
. Encore une fois, les colonnes dans cet ordre. Ajoutez la dernière colonne id
uniquement si vous obtenez des analyses d'index uniquement.
Pour un nombre donné de propriétés
Il existe de nombreuses façons de le résoudre. Cela devrait être l'un des plus simples et des plus rapides pour exactement deux propriétés :
SELECT u.*
FROM users u
JOIN user_property_map up1 ON up1.user_id = u.id
JOIN user_property_map up2 USING (user_id)
WHERE up1.property_value_id =
(SELECT id FROM property_value WHERE property_name_id = 1 AND value = '101')
AND up2.property_value_id =
(SELECT id FROM property_value WHERE property_name_id = 2 AND value = '102')
-- AND u.user_name = 'user1' -- more filters?
-- AND u.city = 'city1'
Ne visite pas la table PROPERTY_NAME
, puisque vous semblez avoir déjà résolu les noms de propriété en ID, selon votre exemple de requête. Sinon, vous pouvez ajouter une jointure à PROPERTY_NAME
dans chaque sous-requête.
Nous avons rassemblé un arsenal de techniques sous cette question connexe :
- Comment filtrer les résultats SQL dans une relation has-many-through
Pour un nombre inconnu de propriétés
@Mike et @Valera ont des questions très utiles dans leurs réponses respectives. Pour rendre cela encore plus dynamique :
WITH input(property_name_id, value) AS (
VALUES -- provide n rows with input parameters here
(1, '101')
, (2, '102')
-- more?
)
SELECT *
FROM users u
JOIN (
SELECT up.user_id AS id
FROM input
JOIN property_value pv USING (property_name_id, value)
JOIN user_property_map up ON up.property_value_id = pv.id
GROUP BY 1
HAVING count(*) = (SELECT count(*) FROM input)
) sub USING (id);
Ajouter/supprimer uniquement des lignes des VALUES
expression. Ou supprimer le WITH
clause et le JOIN
pour aucun filtre de propriété du tout.
Le problème avec cette classe de requêtes (en comptant toutes les correspondances partielles) est la performance . Ma première requête est moins dynamique, mais généralement beaucoup plus rapide. (Il suffit de tester avec EXPLAIN ANALYZE
.) Surtout pour les grandes tables et un nombre croissant de propriétés.
Le meilleur des deux mondes ?
Cette solution avec un CTE récursif devrait être un bon compromis :rapide et dynamique :
WITH RECURSIVE input AS (
SELECT count(*) OVER () AS ct
, row_number() OVER () AS rn
, *
FROM (
VALUES -- provide n rows with input parameters here
(1, '101')
, (2, '102')
-- more?
) i (property_name_id, value)
)
, rcte AS (
SELECT i.ct, i.rn, up.user_id AS id
FROM input i
JOIN property_value pv USING (property_name_id, value)
JOIN user_property_map up ON up.property_value_id = pv.id
WHERE i.rn = 1
UNION ALL
SELECT i.ct, i.rn, up.user_id
FROM rcte r
JOIN input i ON i.rn = r.rn + 1
JOIN property_value pv USING (property_name_id, value)
JOIN user_property_map up ON up.property_value_id = pv.id
AND up.user_id = r.id
)
SELECT u.*
FROM rcte r
JOIN users u USING (id)
WHERE r.ct = r.rn; -- has all matches
dbfiddle ici
Le manuel sur les CTE récursifs.
La complexité supplémentaire ne paie pas pour les petites tables où les frais généraux supplémentaires l'emportent sur tout avantage ou la différence est négligeable pour commencer. Mais il évolue beaucoup mieux et est de plus en plus supérieur aux techniques de "comptage" avec des tables croissantes et un nombre croissant de filtres de propriétés.
Les techniques de comptage doivent visiter tous lignes dans user_property_map
pour tous les filtres de propriété donnés, alors que cette requête (ainsi que la 1ère requête) peut éliminer les utilisateurs non pertinents plus tôt.
Optimisation des performances
Avec les statistiques actuelles de la table (paramètres raisonnables, autovacuum
en cours d'exécution), Postgres connaît les "valeurs les plus courantes" dans chaque colonne et réorganisera les jointures dans la 1ère requête d'évaluer d'abord les filtres de propriété les plus sélectifs (ou du moins pas les moins sélectifs). Jusqu'à une certaine limite :join_collapse_limit
. Connexe :
- Postgresql join_collapse_limit et temps de planification des requêtes
- Pourquoi une légère modification du terme de recherche ralentit-elle autant la requête ?
Cette intervention "deus-ex-machina" n'est pas possible avec la 3ème requête (CTE récursif). Pour améliorer les performances (peut-être beaucoup), vous devez d'abord placer vous-même des filtres plus sélectifs. Mais même avec le pire des cas, il surpassera toujours les requêtes de comptage.
Connexe :
- Vérifier les cibles de statistiques dans PostgreSQL
Des détails bien plus sanglants :
- Index partiel PostgreSQL inutilisé lorsqu'il est créé sur une table avec des données existantes
Plus d'explications dans le manuel :
- Statistiques utilisées par le planificateur