2009-01-25 78 views
29

這是一個非常基本的查詢我想不通....在不同行上選擇符合不同條件的值?

比方說,我有一個兩列的表是這樣的:

userid | roleid 
--------|-------- 
    1 | 1 
    1 | 2 
    1 | 3 
    2 | 1 

我想有roleids所有不同用戶標識1,2和3.使用上面的例子,我想返回的唯一結果是userid 1.我該怎麼做?

+1

廣泛的細節添加到我的答案。 – cletus 2009-01-26 00:32:16

+3

任何提示像@clettus @@@應答的問題值得+1 +1 – cori 2011-12-08 19:28:19

回答

22
SELECT userid 
FROM UserRole 
WHERE roleid IN (1, 2, 3) 
GROUP BY userid 
HAVING COUNT(DISTINCT roleid) = 3; 

爲了任何閱讀:我的答案是簡單明瞭的,並得到了「接受」的地位,但請不要再去讀通過@cletus給出的answer。它有更好的表現。


Justing想大聲,另一種方式來寫自參加由@cletus描述是:

SELECT t1.userid 
FROM userrole t1 
JOIN userrole t2 ON t1.userid = t2.userid 
JOIN userrole t3 ON t2.userid = t3.userid 
WHERE (t1.roleid, t2.roleid, t3.roleid) = (1, 2, 3); 

這可能是更容易閱讀的你,和MySQL支持類似的元組的比較。 MySQL也知道如何智能地利用這個查詢的覆蓋索引。只需運行EXPLAIN即可,並在所有三個表的註釋中查看「使用索引」,這意味着它正在讀取索引,甚至不必觸摸數據行。

我在我的Macbook上使用MySQL 5.1.48在210萬行(用於PostTag的堆棧溢出7月數據轉儲)上運行此查詢,並在1.08秒內返回結果。在分配足夠內存給innodb_buffer_pool_size的體面服務器上,它應該更快。

109

好吧,我得到downvoted這個,所以我決定測試一下:

CREATE TABLE userrole (
    userid INT, 
    roleid INT, 
    PRIMARY KEY (userid, roleid) 
); 

CREATE INDEX ON userrole (roleid); 

運行以下命令:

<?php 
ini_set('max_execution_time', 120); // takes over a minute to insert 500k+ records 

$start = microtime(true); 

echo "<pre>\n"; 
mysql_connect('localhost', 'scratch', 'scratch'); 
if (mysql_error()) { 
    echo "Connect error: " . mysql_error() . "\n"; 
} 
mysql_select_db('scratch'); 
if (mysql_error()) { 
    echo "Selct DB error: " . mysql_error() . "\n"; 
} 

$users = 200000; 
$count = 0; 
for ($i=1; $i<=$users; $i++) { 
    $roles = rand(1, 4); 
    $available = range(1, 5); 
    for ($j=0; $j<$roles; $j++) { 
     $extract = array_splice($available, rand(0, sizeof($available)-1), 1); 
     $id = $extract[0]; 
     query("INSERT INTO userrole (userid, roleid) VALUES ($i, $id)"); 
     $count++; 
    } 
} 

$stop = microtime(true); 
$duration = $stop - $start; 
$insert = $duration/$count; 

echo "$count users added.\n"; 
echo "Program ran for $duration seconds.\n"; 
echo "Insert time $insert seconds.\n"; 
echo "</pre>\n"; 

function query($str) { 
    mysql_query($str); 
    if (mysql_error()) { 
     echo "$str: " . mysql_error() . "\n"; 
    } 
} 
?> 

輸出:

499872 users added. 
Program ran for 56.5513510704 seconds. 
Insert time 0.000113131663847 seconds. 

這增加了50萬用戶隨意角色組合,大約有25,000個符合所選標準。

首先查詢:

SELECT userid 
FROM userrole 
WHERE roleid IN (1, 2, 3) 
GROUP by userid 
HAVING COUNT(1) = 3 

查詢時間:0.312s

SELECT t1.userid 
FROM userrole t1 
JOIN userrole t2 ON t1.userid = t2.userid AND t2.roleid = 2 
JOIN userrole t3 ON t2.userid = t3.userid AND t3.roleid = 3 
AND t1.roleid = 1 

查詢時間:0.016s

這是正確的。我提出的加入版本是,比彙總版本快20倍。

對不起,我這樣做是爲了生活,在現實世界和現實世界中工作,我們測試SQL,結果可以說明一切。

原因應該很清楚。聚合查詢將按照表的大小進行成本縮放。每行都通過HAVING條款進行處理,彙總和過濾(或不)。連接版本將(使用索引)根據給定角色選擇用戶的子集,然後根據第二個角色檢查該子集,最後針對第三個角色檢查該子集。每個selection(在relational algebra條款)工作在一個越來越小的子集。從這裏你可以得出結論:

連接版本的性能變得更好,匹配率更低。

如果只有500個用戶(500k以上的示例中)有三個角色,那麼連接版本將顯着加快。彙總版本不會(並且任何性能改進都是因爲傳輸500個用戶而不是25k,連接版本顯然也會得到)。

我也很好奇,看看真正的數據庫(如Oracle)如何處理這個問題。所以我基本上重複了在Oracle XE上的相同練習(與上一個示例中的MySQL相同的Windows XP桌面機器上運行),結果幾乎完全相同。

連接似乎不被接受,但正如我已經證明的那樣,聚合查詢可能會慢一個數量級。

更新:一些extensive testing後,畫面更加複雜,答案將取決於你的數據,你的數據庫和其他因素。故事的寓意是測試,測試和測試。

+5

對downvote沒有評論?這實際上起作用。 – cletus 2009-01-25 01:18:00

+0

這個dv不是從我這裏來的......但是認真的......你會把它放在你的系統中嗎? – 2009-01-25 01:19:47

+1

我也沒有dv它,但我會用這個,如果我絕對必須......我應該重新設計我的數據庫,所以我不必這樣的查詢? – John 2009-01-25 01:26:23

-5

如果您在這裏需要任何一種通用性(不同的3角色組合或不同的n角色組合)......我建議您爲您的角色使用位掩碼系統,並使用位運算符來執行您的查詢...

3

假設用戶ID,角色ID被包含在唯一索引(意味着不可能有2條記錄,其中用戶ID = x和角色ID = 1

select count(*), userid from t 
where roleid in (1,2,3) 
group by userid 
having count(*) = 3 
2

經典方式做到這一點是把它當作一個關係除法問題

英文:選擇那些對他們來說沒有任何希望的角色ID值丟失用戶

我假設你有其中的UserRole表是指一個用戶表,我會承擔所需roleid值位於表中:

create table RoleGroup(
    roleid int not null, 
    primary key(roleid) 
) 
insert into RoleGroup values (1); 
insert into RoleGroup values (2); 
insert into RoleGroup values (3); 

我也會假設所有相關的列都不是NULLable,所以IN和NOT EXISTS都沒有意外。這裏有一個SQL查詢表達了英語上面:

select userid from Users as U 
where not exists (
    select * from RoleGroup as G 
    where not exists (
    select R.roleid from UserRole as R 
    where R.roleid = G.roleid 
    and R.userid = U.userid 
) 
); 

另一種方式來寫它是這個

select userid from Users as U 
where not exists (
    select * from RoleGroup as G 
    where G.roleid not in (
    select R.roleid from UserRole as R 
    where R.userid = U.userid 
) 
); 

這可能會或可能不會最終被高效,視指標,平臺,數據等。在網上搜索「關係部門」,你會發現很多。

1
select userid from userrole where userid = 1 
intersect 
select userid from userrole where userid = 2 
intersect 
select userid from userrole where userid = 3 

這不會解決問題嗎?在典型的關係數據庫上,這個解決方案有多好?查詢優化器會自動優化這個嗎?