2017-04-18 75 views
3

我有不同的用戶類別和一個允許用戶在多個類別中的連接表。我的連接表稱爲categories_users,它由user_id和category_id組成。在postgres中,使用連接表執行AND的最佳方法是什麼?

我想篩選category1和category2中的用戶。例如,我想找到所有對棒球和足球都感興趣的人。

在PostgreSQL中這樣做的最好方法是什麼?我有以下工作:

select * from users 
    where users.id IN 
    (Select categories_users.user_id from categories_users 
     JOIN categories ON categories.id = categories_users.category_id 
     where categories.id = 1 OR categories.parent_id = 1) 
    AND users.id IN 
    (Select categories_users.user_id from categories_users 
    JOIN categories ON categories.id = categories_users.category_id 
    where categories.id = 2 OR categories.parent_id = 2) 

但是這種感覺笨重,我想知道是否有更好的方法來做到這一點。我嘗試了各種連接,但最終總是在categories_users表中搜索category_id爲1和2的行,這是不可能的。

編輯,我其實也需要對父類搜索,所以我已經改變了上面的查詢到包括PARENT_ID

+0

我正在使用@ jcaron的回答。這似乎是最高效的 – wildrhombus

+0

不要忘記將jcaron的答案標記爲首選答案,或者如果您認爲他們匹配,則可以提供多個答案;) – andreim

回答

2

與同桌剛剛加入兩次(使用化名):

SELECT u.* 
    FROM users u 
    JOIN categories_users cu1 ON cu1.user_id = u.id 
    JOIN categories_users cu2 ON cu2.user_id = u.id 
    WHERE cu1.category_id = 1 AND cu2.category_id = 2 
+0

是的,謝謝,那是行得通的。這是Postgres中更高效的查詢嗎? – wildrhombus

+0

您可以檢查'EXPLAIN'或'EXPLAIN ANALYSE'的輸出以查看哪一個查詢效率最高。沒有檢查,但如果查詢計劃使用子查詢或連接完全相同,我不會感到驚訝。當然,我已經跳過了不必要的'categories'查找,因爲您已經擁有了ID,但是您可能需要根據您如何找到這些ID來進行更改。只要你的表增長一點,一定要有適當的索引。 – jcaron

+0

再次感謝您,我會嘗試解釋。我確實需要其他原因的類別查找,這實際上是我正在嘗試做的簡化版本。你認爲什麼是適當的索引? user_id和category_id都是現在使用btree創建的索引。 – wildrhombus

1
select u.* 
from 
    users u 
    inner join (
     select user_id 
     from categories_users 
     group by user_id 
     having 
      bool_or(1 in (category_id, parent_id)) and 
      bool_or(2 in (category_id, parent_id)) 
    ) s on s.user_id = u.id 
+0

我也想在category.parent_id上搜索,並且我已經更改了我上面的原始查詢以顯示此內容。你如何將parent_id添加到解決問題的方式中? – wildrhombus

1

您也可以在分區上使用COUNT(*)以查看用戶在搜索的類別集中有多少類別。

我創建了以下示例,以瞭解如何定義和參數化。 我創建了一個函數test.find_users_in_categories(BIGINT[]),它接受我們需要用戶列表的類別數組。 因此,該函數將返回所有給定類別中的所有用戶。

解決方案 - 讓所有給定的類別中找到的用戶

CREATE SCHEMA test; 

CREATE TABLE test.categories_users (
    category_id BIGINT NOT NULL, 
    user_id BIGINT NOT NULL 
); 

INSERT INTO test.categories_users 
    (user_id, category_id) 
    VALUES 
    (33, 103), 
    (34, 104), 
    (35, 105), 
    (37, 105), 
    (35, 106), 
    (37, 106); 

CREATE OR REPLACE FUNCTION test.find_users_in_categories(BIGINT[]) 
    RETURNS TABLE (
    user_id BIGINT 
) 
AS 
$$ 
DECLARE 
    categories ALIAS FOR $1; 
BEGIN 
    RETURN QUERY 
    SELECT t.user_id 
    FROM 
     (
     SELECT 
      cu.user_id, 
      cu.category_id, 
      COUNT(*) OVER (PARTITION BY cu.user_id) AS cnt 
     FROM test.categories_users AS cu 
     WHERE cu.category_id = ANY(categories) 
    ) AS t 
     WHERE t.cnt = array_length(categories, 1) 
     GROUP BY t.user_id; 
END; 
$$ 
LANGUAGE plpgsql; 

SELECT * FROM test.find_users_in_categories(ARRAY[105, 106]); 

DROP SCHEMA test CASCADE; 

編輯 - [遞歸解決方案]

解決方案 - 讓所有給定的類別和子發現用戶 - 類別

請參閱以下關於使用JOIN +遞歸CTE。我使用JOIN而不是COUNT(),因爲它對於這種情況看起來更好。

CREATE SCHEMA test; 

CREATE TABLE test.categories (
    category_id BIGINT PRIMARY KEY, 
    parent_id BIGINT REFERENCES test.categories(category_id) 
); 

CREATE TABLE test.categories_users (
    category_id BIGINT NOT NULL REFERENCES test.categories(category_id), 
    user_id BIGINT NOT NULL 
); 

INSERT INTO test.categories 
    (category_id, parent_id) 
    VALUES 
    (100, NULL), 
    (101, 100), 
    (102, 100), 
    (103, 101), 
    (104, 101), 
    (105, 101), 
    (106, NULL); 


INSERT INTO test.categories_users 
    (user_id, category_id) 
    VALUES 
    (33, 103), 
    (34, 104), 
    (35, 105), 
    (37, 105), 
    (35, 106), 
    (37, 106); 


CREATE OR REPLACE FUNCTION test.find_users_in_categories(BIGINT[]) 
    RETURNS TABLE (
    user_id BIGINT 
) 
AS 
$$ 
DECLARE 
    main_categories ALIAS FOR $1; 
BEGIN 
    RETURN QUERY 
    WITH 
    -- get all main categories and subcategories 
    RECURSIVE cte_categories (category_id, main_category_id) AS 
    (
     SELECT cat.category_id, cat.category_id AS main_category_id 
     FROM test.categories AS cat 
     WHERE cat.category_id = ANY(main_categories) 
     UNION ALL 
     SELECT cat.category_id, cte.main_category_id 
     FROM cte_categories AS cte 
     INNER JOIN test.categories AS cat 
      ON cte.category_id = cat.parent_id 
    ), 
    -- filter main categories that are found as children of other categories 
    cte_categories_unique AS 
    (
     SELECT cte.* 
     FROM cte_categories AS cte 
     LEFT JOIN 
     (
      SELECT category_id 
      FROM cte_categories 
      WHERE category_id <> main_category_id 
      GROUP BY category_id 
     ) AS to_exclude 
      ON cte.main_category_id = to_exclude.category_id 
     WHERE to_exclude.category_id IS NULL 
    ), 
    -- compute the count of main categories 
    cte_main_categories_count AS 
    (
     SELECT COUNT(DISTINCT main_category_id) AS cnt 
     FROM cte_categories_unique 
    ) 
    SELECT t.user_id 
    FROM 
     (
     -- get the users which are found in each category/sub-category then group them under the main category 
     SELECT 
      cu.user_id, 
      cte.main_category_id 
     FROM test.categories_users AS cu 
     INNER JOIN cte_categories_unique AS cte 
      ON cu.category_id = cte.category_id 
     GROUP BY cu.user_id, cte.main_category_id 
    ) AS t 
     GROUP BY t.user_id 
     -- filter users that do not have a match on all main categories or their sub-categories 
     HAVING COUNT(*) = (SELECT cnt FROM cte_main_categories_count); 
END; 
$$ 
LANGUAGE plpgsql; 


SELECT * FROM test.find_users_in_categories(ARRAY[101, 106]); 

DROP SCHEMA test CASCADE; 
+0

我編輯了我的原始查詢,在搜索中包含category.parent_id,這是我真正想要的。如果我以這種方式接近它,我將如何包含parent_id? – wildrhombus

+0

@wildrhombus這取決於你想如何表現。那麼,你有一個類別樹,哪個用戶應該符合你的標準?讓我們假設你傳遞了兩個類別作爲參數。這兩個類別是他們自己的子樹的父母。什麼樣的用戶應該符合這個標準?作爲每個子樹中至少一個類別的一部分的用戶,還是每個子樹中具有所有類別的用戶?作爲所有類別的一部分,我的意思是,如果A是B和C的父親,C是D和E的父親。那麼以下內容可能是等價的 - 用戶有A,或者用戶有B和C或用戶有B,D ,E. – andreim

+0

@wildrhombus應該首先提出這個問題:你的分類樹有多深? 2-3級或可以支持多少級別? – andreim

相關問題