[英]Using same column multiple times in WHERE clause
我有一個下表結構。
USERS
PROPERTY_VALUE
PROPERTY_NAME
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 在這里
增加的復雜性並不代表小額表,其中額外的開銷超過任何利益,或者差異可以忽略不計。 但它的擴展性能要好得多,而且越來越優於“計數”技術,增長表和越來越多的屬性過濾器。
對於所有給定的屬性過濾器,計數技術必須訪問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=1
和pn.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.