Questo è un caso di divisione relazionale. Ho aggiunto il tag.
Indici
Assumendo un vincolo PK o UNIQUE su USER_PROPERTY_MAP(property_value_id, user_id)
- colonne in questo ordine per velocizzare le mie query. Correlati:
- Un indice composito va bene anche per le query sul primo campo?
Dovresti anche avere un indice su PROPERTY_VALUE(value, property_name_id, id)
. Ancora una volta, colonne in questo ordine. Aggiungi l'ultima colonna id
solo se ottieni scansioni solo indice.
Per un determinato numero di proprietà
Ci sono molti modi per risolverlo. Questo dovrebbe essere uno dei più semplici e veloci per esattamente due proprietà:
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'
Non sto visitando la tabella PROPERTY_NAME
, poiché sembra che tu abbia già risolto i nomi delle proprietà in ID, secondo la tua query di esempio. Altrimenti potresti aggiungere un join a PROPERTY_NAME
in ogni sottoquery.
Abbiamo assemblato un arsenale di tecniche sotto questa domanda correlata:
- Come filtrare i risultati SQL in una relazione ha molti-attraverso
Per un numero imprecisato di proprietà
@Mike e @Valera hanno domande molto utili nelle rispettive risposte. Per renderlo ancora più dinamico :
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);
Aggiungi/rimuovi solo righe da VALUES
espressione. Oppure rimuovi il WITH
clausola e il JOIN
per nessun filtro proprietà affatto.
Il problema con questa classe di query (contando tutte le corrispondenze parziali) è performance . La mia prima query è meno dinamica, ma in genere notevolmente più veloce. (Basta testare con EXPLAIN ANALYZE
.) Soprattutto per tavoli più grandi e un numero crescente di proprietà.
Il meglio di entrambi i mondi?
Questa soluzione con un CTE ricorsivo dovrebbe essere un buon compromesso:veloce e dinamico:
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 qui
Il manuale sui CTE ricorsivi.
La complessità aggiunta non paga per tavoli piccoli in cui le spese generali aggiuntive superano qualsiasi vantaggio o la differenza è trascurabile per cominciare. Ma si adatta molto meglio ed è sempre più superiore alle tecniche di "conteggio" con tabelle in aumento e un numero crescente di filtri di proprietà.
Le tecniche di conteggio devono visitare tutte righe in user_property_map
per tutti i filtri di proprietà specificati, mentre questa query (così come la prima query) può eliminare in anticipo gli utenti irrilevanti.
Ottimizzazione delle prestazioni
Con le statistiche attuali della tabella (impostazioni ragionevoli, autovacuum
in esecuzione), Postgres conosce i "valori più comuni" in ogni colonna e riordinerà i join nella 1a query valutare prima i filtri di proprietà più selettivi (o almeno non quelli meno selettivi). Fino a un certo limite:join_collapse_limit
. Correlati:
- Postgresql join_collapse_limit e tempo per la pianificazione delle query
- Perché una leggera modifica nel termine di ricerca rallenta così tanto la query?
Questo intervento "deus-ex-machina" non è possibile con la 3a query (CTE ricorsiva). Per migliorare le prestazioni (forse molto) devi prima posizionare tu stesso filtri più selettivi. Ma anche con l'ordine nel peggiore dei casi, supererà comunque il conteggio delle query.
Correlati:
- Controlla i target delle statistiche in PostgreSQL
Dettagli molto più cruenti:
- Indice parziale PostgreSQL non utilizzato quando viene creato su una tabella con dati esistenti
Maggiori spiegazioni nel manuale:
- Statistiche utilizzate dal pianificatore