Este es un caso de división relacional. Agregué la etiqueta.
Índices
Asumiendo una restricción PK o ÚNICA en USER_PROPERTY_MAP(property_value_id, user_id)
- columnas en este orden para agilizar mis consultas. Relacionado:
- ¿Un índice compuesto también es bueno para consultas en el primer campo?
También debe tener un índice en PROPERTY_VALUE(value, property_name_id, id)
. De nuevo, columnas en este orden. Agregue la última columna id
solo si obtiene escaneos de solo índice.
Para un número dado de propiedades
Hay muchas formas de solucionarlo. Este debería ser uno de los más simples y rápidos para exactamente dos propiedades:
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'
No visitar la tabla PROPERTY_NAME
, ya que parece que ya ha resuelto los nombres de propiedad en ID, de acuerdo con su consulta de ejemplo. De lo contrario, podría agregar una unión a PROPERTY_NAME
en cada subconsulta.
Hemos reunido un arsenal de técnicas bajo esta pregunta relacionada:
- Cómo filtrar los resultados de SQL en una relación de varios procesos
Para un número desconocido de propiedades
@Mike y @Valera tienen consultas muy útiles en sus respectivas respuestas. Para hacer esto aún más dinámico :
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);
Solo agregue/elimine filas de los VALUES
expresión. O elimina el WITH
cláusula y JOIN
para sin filtros de propiedad en absoluto.
El problema con esta clase de consultas (contando todas las coincidencias parciales) es rendimiento . Mi primera consulta es menos dinámica, pero generalmente considerablemente más rápida. (Solo prueba con EXPLAIN ANALYZE
.) Especialmente para mesas más grandes y un número creciente de propiedades.
¿Lo mejor de ambos mundos?
Esta solución con un CTE recursivo debería ser un buen compromiso:rápido y dinámico:
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 aquí
El manual sobre CTE recursivos.
La complejidad añadida no compensa las mesas pequeñas en las que los gastos generales adicionales superan cualquier beneficio o la diferencia es insignificante para empezar. Pero escala mucho mejor y es cada vez más superior a las técnicas de "conteo" con tablas crecientes y un número creciente de filtros de propiedades.
Las técnicas de conteo tienen que visitar todas filas en user_property_map
para todos los filtros de propiedad dados, mientras que esta consulta (así como la primera consulta) puede eliminar usuarios irrelevantes antes.
Optimización del rendimiento
Con las estadísticas de la tabla actual (configuraciones razonables, autovacuum
ejecutándose), Postgres tiene conocimiento sobre "valores más comunes" en cada columna y reordenará las uniones en la primera consulta para evaluar primero los filtros de propiedad más selectivos (o al menos no los menos selectivos). Hasta cierto límite:join_collapse_limit
. Relacionado:
- Postgresql join_collapse_limit y tiempo para la planificación de consultas
- ¿Por qué un ligero cambio en el término de búsqueda ralentiza tanto la consulta?
Esta intervención "deus-ex-machina" no es posible con la 3ra consulta (CTE recursivo). Para ayudar al rendimiento (posiblemente mucho), primero debe colocar filtros más selectivos. Pero incluso con el pedido en el peor de los casos, seguirá superando las consultas de conteo.
Relacionado:
- Comprobar objetivos de estadísticas en PostgreSQL
Mucho más detalles sangrientos:
- El índice parcial de PostgreSQL no se usa cuando se crea en una tabla con datos existentes
Más explicación en el manual:
- Estadísticas utilizadas por el planificador