Este é um caso de divisão relacional. Eu adicionei a etiqueta.
Índices
Assumindo uma restrição PK ou UNIQUE em
USER_PROPERTY_MAP(property_value_id, user_id)
- colunas nesta ordem para agilizar minhas consultas. Relacionado:- Um índice composto também é bom para consultas no primeiro campo?
Você também deve ter um índice em
PROPERTY_VALUE(value, property_name_id, id)
. Novamente, colunas nesta ordem. Adicione a última coluna id
somente se você obtiver varreduras somente de índice dele. Para um determinado número de propriedades
Há muitas maneiras de resolvê-lo. Este deve ser um dos mais simples e rápidos para exatamente dois propriedades:
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'
Não visitando a tabela
PROPERTY_NAME
, pois parece que você já resolveu nomes de propriedades para IDs, de acordo com sua consulta de exemplo. Caso contrário, você pode adicionar uma associação a PROPERTY_NAME
em cada subconsulta. Reunimos um arsenal de técnicas sob esta questão relacionada:
- Como filtrar resultados de SQL em uma relação de muitos
Para um número desconhecido de propriedades
@Mike e @Valera têm consultas muito úteis em suas respectivas respostas. Para tornar isso ainda mais 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);
Apenas adicione/remova linhas de
VALUES
expressão. Ou remova o WITH
cláusula e a JOIN
para nenhum filtro de propriedade de forma alguma. O problema com essa classe de consultas (contando todas as correspondências parciais) é desempenho . Minha primeira consulta é menos dinâmica, mas normalmente consideravelmente mais rápida. (Basta testar com
EXPLAIN ANALYZE
.) Especialmente para mesas maiores e um número crescente de propriedades. O melhor dos dois mundos?
Esta solução com um CTE recursivo deve ser um bom compromisso:rápido e 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 aqui
O manual sobre CTEs recursivos.
A complexidade adicional não compensa tabelas pequenas onde a sobrecarga adicional supera qualquer benefício ou a diferença é insignificante para começar. Mas escala muito melhor e é cada vez mais superior às técnicas de "contagem" com tabelas crescentes e um número crescente de filtros de propriedade.
As técnicas de contagem devem visitar todos linhas em
user_property_map
para todos os filtros de propriedade fornecidos, enquanto essa consulta (assim como a 1ª consulta) pode eliminar usuários irrelevantes antecipadamente. Otimização do desempenho
Com as estatísticas atuais da tabela (configurações razoáveis,
autovacuum
executando), o Postgres tem conhecimento sobre "valores mais comuns" em cada coluna e reordenará as junções na 1ª consulta avaliar primeiro os filtros de propriedade mais seletivos (ou pelo menos não os menos seletivos). Até um certo limite:join_collapse_limit
. Relacionado:- Postgresql join_collapse_limit e tempo para planejamento de consulta
- Por que uma pequena alteração no termo de pesquisa torna a consulta tão lenta?
Esta intervenção "deus-ex-machina" não é possível com a 3ª consulta (CTE recursiva). Para ajudar no desempenho (possivelmente muito), você deve colocar filtros mais seletivos primeiro. Mas mesmo com a ordenação do pior caso, ele ainda superará as consultas de contagem.
Relacionado:
- Verificar alvos de estatísticas no PostgreSQL
Detalhes muito mais sangrentos:
- Índice parcial do PostgreSQL não utilizado quando criado em uma tabela com dados existentes
Mais explicações no manual:
- Estatísticas usadas pelo planejador