Использование одного и того же столбца несколько раз в предложении WHERE

У меня есть следующая структура таблицы.

USERS

Данные ПОЛЬЗОВАТЕЛЕЙ

PROPERTY_VALUE

PROPERTY_VALUE данные

PROPERTY_NAME

Данные PROPERTY_NAME

USER_PROPERTY_MAP

Данные USER_PROPERTY_MAP

Я пытаюсь получить пользователя / ей из users таблица с подходящими свойствами в property_value Таблица.

Один пользователь может иметь несколько свойств. Пример данных здесь имеет 2 свойства для пользователя '1', но их может быть больше 2. Я хочу использовать все эти пользовательские свойства в WHERE пункт.

Этот запрос работает, если у пользователя есть одно свойство, но он не работает для более чем 1 свойства:

SELECT * FROM users u
INNER JOIN user_property_map upm ON u.id = upm.user_id
INNER JOIN property_value pv ON upm.property_value_id = pv.id
INNER JOIN property_name pn ON pv.property_name_id = pn.id
WHERE (pn.id = 1 AND pv.id IN (SELECT id FROM property_value WHERE value like '101')
AND pn.id = 2 AND pv.id IN (SELECT id FROM property_value WHERE value like '102')) and u.user_name = 'user1' and u.city = 'city1'

Я понимаю, так как запрос имеет pn.id = 1 AND pn.id = 2 это всегда потерпит неудачу, потому что pn.id может быть 1 или 2, но не оба одновременно. Так, как я могу переписать это, чтобы заставить это работать для n числа свойств?

В приведенном выше примере данных есть только один пользователь с id = 1 который имеет оба соответствующих свойства, используемые в WHERE пункт. Запрос должен возвращать одну запись со всеми столбцами USERS Таблица.

Чтобы уточнить мои требования

Я работаю над приложением, в котором на пользовательском интерфейсе есть страница со списком пользователей, в которой перечислены все пользователи системы. Этот список содержит информацию, такую ​​как идентификатор пользователя, имя пользователя, город и т. Д. - все столбцы в USERS Таблица. Пользователи могут иметь свойства, подробно описанные в модели базы данных выше.

Страница списка пользователей также предоставляет функции для поиска пользователей на основе этих свойств. При поиске пользователей с 2 ​​свойствами, "property1" и "property2", страница должна извлекаться и отображать только совпадающие строки. На основании приведенных выше тестовых данных, только пользователь "1" отвечает требованиям.

Квалифицируется пользователь с 4 свойствами, включая "property1" и "property2". Но пользователь, имеющий только одно свойство 'property1', будет исключен из-за отсутствия свойства 'property2'.

8 ответов

Это случай реляционного деления. Я добавил тег к вопросу.

Индексы

Предполагая PK или УНИКАЛЬНОЕ ограничение на USER_PROPERTY_MAP(property_value_id, user_id) - столбцы в этом порядке, чтобы сделать мои запросы быстрыми. Связанные с:

Вы также должны иметь индекс на PROPERTY_VALUE(value, property_name_id, id), Опять столбцы в таком порядке. Добавьте последний столбец id только если вы получаете только индексные сканы.

Для данного количества свойств

Есть много способов ее решить. Это должно быть одним из самых простых и быстрых точно для двух свойств:

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'

Не посещая стол PROPERTY_NAME, поскольку вы, кажется, уже разрешили имена свойств в идентификаторы, в соответствии с вашим примером запроса. В противном случае вы можете добавить присоединение к PROPERTY_NAME в каждом подзапросе.

Мы собрали арсенал методов по этому смежному вопросу:

Для неизвестного количества свойств

У @Mike и @Valera есть очень полезные запросы в соответствующих ответах. Чтобы сделать это еще более динамичным:

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);

Добавлять / удалять только строки из VALUES выражение. Или удалите WITH пункт и JOIN без фильтров свойств вообще.

Проблема с этим классом запросов (считая все частичные совпадения) заключается в производительности. Мой первый запрос менее динамичен, но обычно значительно быстрее. (Просто проверить с EXPLAIN ANALYZE.) Особенно для больших столов и растущего числа свойств.

Лучшее из обоих миров?

Это решение с рекурсивным CTE должно быть хорошим компромиссом: быстрым и динамичным:

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 здесь

Руководство по рекурсивным CTE.

Дополнительная сложность не окупается для небольших столов, где дополнительные накладные расходы перевешивают любую выгоду или разница незначительна для начала. Но он масштабируется гораздо лучше и все больше превосходит методы "подсчета" с растущими таблицами и растущим числом фильтров свойств.

Методы подсчета должны посещать все ряды в user_property_map для всех заданных фильтров свойств, в то время как этот запрос (а также 1-й запрос) может устранить ненужных пользователей на раннем этапе.

Оптимизация производительности

С текущей статистикой таблицы (разумные настройки, autovacuum работает), Postgres обладает знаниями о "наиболее распространенных значениях" в каждом столбце и будет переупорядочивать объединения в 1-м запросе, чтобы сначала оценить наиболее селективные фильтры свойств (или, по крайней мере, не наименее селективные). До определенного предела: join_collapse_limit, Связанные с:

Это вмешательство "deus-ex-machina" невозможно с 3-м запросом (рекурсивный CTE). Чтобы повысить производительность (возможно, многое), вы должны сначала установить более селективные фильтры. Но даже при наихудшем порядке это все равно превзойдет количество запросов.

Связанные с:

Гораздо больше кровавых подробностей:

Больше объяснений в руководстве:

SELECT *
  FROM users u
 WHERE u.id IN(
         select m.user_id
           from property_value v
           join USER_PROPERTY_MAP m
             on v.id=m.property_value_id 
          where (v.property_name_id, v.value) in( (1, '101'), (2, '102') )
          group by m.user_id
         having count(*)=2
      )

ИЛИ ЖЕ

SELECT u.id
  FROM users u
 INNER JOIN user_property_map upm ON u.id = upm.user_id
 INNER JOIN property_value pv ON upm.property_value_id = pv.id
 WHERE (pv.property_name_id=1 and pv.value='101')
    OR (pv.property_name_id=2 and pv.value='102')
 GROUP BY u.id
HAVING count(*)=2

нет property_name таблица, необходимая в запросе, если имя_свойства установлено.

Если вы хотите просто отфильтровать:

SELECT users.*
FROM users
where (
    select count(*)
    from user_property_map
    left join property_value on user_property_map.property_value_id = property_value.id
    left join property_name on property_value.property_name_id = property_name.id
    where user_property_map.user_id = users.id -- join with users table
    and (property_name.name, property_value.value) in (
        values ('property1', '101'), ('property2', '102') -- filter properties by name and value
    )
) = 2 -- number of properties you filter by

Или, если вам нужны пользователи, упорядоченные по убыванию числа совпадений, вы можете сделать:

select * from (
    SELECT users.*, (
        select count(*) as property_matches
        from user_property_map
        left join property_value on user_property_map.property_value_id = property_value.id
        left join property_name on property_value.property_name_id = property_name.id
        where user_property_map.user_id = users.id -- join with users table
        and (property_name.name, property_value.value) in (
            values ('property1', '101'), ('property2', '102') -- filter properties by name and value
        )
    )
    FROM users
) t
order by property_matches desc
SELECT * FROM users u
INNER JOIN user_property_map upm ON u.id = upm.user_id
INNER JOIN property_value pv ON upm.property_value_id = pv.id
INNER JOIN property_name pn ON pv.property_name_id = pn.id
WHERE (pn.id = 1 AND pv.id IN (SELECT id FROM property_value WHERE value 
like '101') )
OR ( pn.id = 2 AND pv.id IN (SELECT id FROM property_value WHERE value like 
'102'))

OR (...)
OR (...)

Вы не можете сделать AND, потому что нет такого случая, когда id равен 1 и 2 для ОДНОГО ЖЕ, а вы задаете условие where для каждой строки!

Если вы запустите простой тест, как

SELECT * FROM users where id=1 and id=2 

Вы получите 0 результатов. Для достижения этого использования

 id in (1,2) 

или же

 id=1 or id=2

Этот запрос можно оптимизировать больше, но я надеюсь, что это хорошее начало.

Если вам просто нужны отдельные столбцы в U, это:

SELECT DISTINCT u.* 
  FROM Users u INNER JOIN USER_PROPERTY_MAP upm ON u.id = upm.[user_id]
                INNER JOIN PROPERTY_VALUE pv ON upm.property_value_id = pv.id
                INNER JOIN PROPERTY_NAME pn ON pv.property_name_id = pn.id

  WHERE (pn.id = 1 AND pv.[value] = '101')
     OR (pn.id = 2 AND pv.[value] = '102')

Обратите внимание, что я использовал pv.[value] = вместо подзапроса для получения идентификатора... это упрощение.

Ты используешь AND оператор между двумя pn.id=1 а также pn.id=2, тогда как вы получите ответ между этим:

(SELECT id FROM property_value WHERE value like '101') and
(SELECT id FROM property_value WHERE value like '102') 

Так как выше комментарии, используйте or оператор.

Обновление 1:

SELECT * FROM users u
INNER JOIN user_property_map upm ON u.id = upm.user_id
INNER JOIN property_value pv ON upm.property_value_id = pv.id
INNER JOIN property_name pn ON pv.property_name_id = pn.id
WHERE pn.id in (1,2) AND pv.id IN (SELECT id FROM property_value WHERE value like '101' or value like '102');

Если я правильно понимаю ваш вопрос, я бы сделал это так.

SELECT u.id, u.user_name, u.city FROM users u 
WHERE (SELECT count(*) FROM property_value v, user_property_map m 
WHERE m.user_id = u.id AND m.property_value_id = v.id AND v.value IN ('101', '102')) = 2

Это должно вернуть список пользователей, которые имеют все свойства, перечисленные в предложении IN. 2 представляет количество искомых свойств.

Предполагая, что вы хотите выбрать все поля в таблице USERS

SELECT u.* 
FROM USERS u
INNER JOIN 
(
    SELECT USERS.id as user_id, COUNT(*) as matching_property_count
    FROM USERS
    INNER JOIN (
        SELECT m.user_id, n.name as property_name, v.value
        FROM PROPERTY_NAME n
        INNER JOIN PROPERTY_VALUE v ON n.id = v.property_name_id
        INNER JOIN USER_PROPERTY_MAP m ON m.property_value_id = v.property_value_id
        WHERE  (n.id = @property_id_1 AND v.value = @property_value_1) -- Property Condition 1
            OR (n.id = @property_id_2 AND v.value = @property_value_2) -- Property Condition 2
            OR (n.id = @property_id_3 AND v.value = @property_value_3) -- Property Condition 3
            OR (n.id = @property_id_N AND v.value = @property_value_N) -- Property Condition N
    ) USER_PROPERTIES ON USER_PROPERTIES.user_id = USERS.id
    GROUP BY USERS.id
    HAVING COUNT(*) = N     --N = the number of Property Condition in the WHERE clause
    -- Note : 
    -- Use HAVING COUNT(*) = N if property matches will be "MUST MATCH ALL"
    -- Use HAVING COUNT(*) > 0 if property matches will be "MUST MATCH AT LEAST ONE"
) USER_MATCHING_PROPERTY_COUNT ON u.id = USER_MATCHING_PROPERTY_COUNT.user_id
Другие вопросы по тегам