PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Usando a mesma coluna várias vezes na cláusula WHERE


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