簡體   English   中英

在WHERE子句中多次使用相同的列

[英]Using same column multiple times in WHERE clause

我有一個下表結構。

USERS

USERS數據

PROPERTY_VALUE

PROPERTY_VALUE數據

PROPERTY_NAME

PROPERTY_NAME數據

USER_PROPERTY_MAP

USER_PROPERTY_MAP數據

我試圖從users表中檢索具有property_value表中匹配屬性的users

單個用戶可以擁有多個屬性。 這里的示例數據有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表的所​​有列的單個記錄。

澄清我的要求

我正在開發一個應用程序,在UI上有一個用戶列表頁面,列出了系統中的所有用戶。 此列表包含用戶ID,用戶名,城市等信息 - USERS表中的所有列。 用戶可以擁有上面數據庫模型中詳述的屬性。

用戶列表頁面還提供基於這些屬性搜索用戶的功能。 搜索具有2個屬性'property1'和'property2'的用戶時,該頁面應該只獲取並顯示匹配的行。 根據上面的測試數據,只有用戶'1'符合賬單。

具有4個屬性( 包括 'property1'和'property2')的用戶符合條件。 但是由於缺少“property2”,將僅排除只有一個屬性“property1”的用戶。

這是一個例子。 我添加了標簽。

索引

假設USER_PROPERTY_MAP(property_value_id, user_id)上的PK或UNIQUE約束 - 按此順序排列以快速查詢。 有關:

您還應該在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 ,因為您似乎已經根據示例查詢解析了ID的屬性名稱。 否則,您可以在每個子查詢中向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 所有行,而此查詢(以及第一個查詢)可以盡早消除不相關的用戶。

優化性能

使用當前表統計信息(合理設置, autovacuum運行),Postgres了解每列中的“最常見值” ,並將在第一個查詢中重新排序連接,以首先評估最具選擇性的屬性過濾器(或至少不是最不具有選擇性的屬性過濾器) 。 達到一定限度: join_collapse_limit 有關:

對於第三個查詢 (遞歸CTE),這種“deus-ex-machina”干預是不可能的。 為了幫助提高性能(可能很多),您必須先自己安排更多選擇性過濾器。 但即使是最壞情況的排序,它仍然會勝過計數查詢。

有關:

更多血腥細節:

手冊中有更多解釋:

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

如果propery_name_id是kown,則查詢中不需要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

你在兩個pn.id=1pn.id=2之間使用AND運算符。 那么你如何得到答案的是:

(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 * 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為SAME ROW,你指定每行的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] =而不是子查詢來重新獲取id ...這是簡化。

如果我理解你的問題,我會這樣做。

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM