32

这是一个非常基本的查询,我无法弄清楚....

假设我有一个像这样的两列表:

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

我想获取所有具有roleids1、2 和 3 的不同用户 ID。使用上面的示例,我想要返回的唯一结果是userid1。我该怎么做?

4

6 回答 6

120

好的,我对此表示反对,所以我决定对其进行测试:

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.

这增加了 500,000 个随机用户角色组合,并且大约有 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

这是正确的。我提出的加入版本比聚合版本快二十倍。

抱歉,我这样做是为了在现实世界中谋生和工作,在现实世界中,我们测试 SQL,结果不言而喻。

原因应该很清楚。聚合查询的成本会随着表的大小而增加。每一行都通过该HAVING子句进行处理、聚合和过滤(或不过滤)。加入版本将(使用索引)基于给定角色选择用户子集,然后将该子集与第二个角色进行检查,最后将该子集与第三个角色进行检查。每个选择(在关系代数术语中)都在越来越小的子集上起作用。由此你可以得出结论:

加入版本的性能变得更好,匹配的发生率更低。

如果只有 500 个用户(在上面的 500k 样本中)具有上述三个角色,则加入版本将显着加快。聚合版本不会(任何性能改进都是传输 500 个用户而不是 25k 用户的结果,加入版本显然也得到了)。

我也很想知道真正的数据库(即 Oracle)如何处理这个问题。所以我基本上在 Oracle XE 上重复了相同的练习(在与上一个示例中的 MySQL 相同的 Windows XP 桌面计算机上运行),结果几乎相同。

连接似乎不受欢迎,但正如我所展示的,聚合查询可能会慢一个数量级。

更新:经过一些广泛的测试,情况更加复杂,答案将取决于您的数据、您的数据库和其他因素。故事的寓意是测试,测试,测试。

于 2009-01-25T01:05:43.493 回答
30
SELECT userid
FROM UserRole
WHERE roleid IN (1, 2, 3)
GROUP BY userid
HAVING COUNT(DISTINCT roleid) = 3;

对于阅读本文的任何人:我的回答简单明了,并且获得了“已接受”状态,但请务必阅读@cletus 给出的答案。它的性能要好得多。


只是大声思考,编写@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 万行的查询(PostTags 的 Stack Overflow 7 月数据转储),它在 1.08 秒内返回了结果。在分配了足够内存的体面服务器上innodb_buffer_pool_size,它应该更快。

于 2009-01-25T01:33:24.950 回答
5

执行此操作的经典方法是将其视为关系划分问题。

英语:选择没有缺少任何所需角色标识值的用户。

我假设您有一个 UserRole 表所引用的 Users 表,并且我假设所需的 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);

我还将假设所有相关列都不可为空,因此 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
  )
);

这可能会或可能不会最终有效,具体取决于索引、平台、数据等。在网上搜索“关系划分”,你会发现很多。

于 2009-08-07T20:08:12.143 回答
4

假设 userid,roleid 包含在唯一索引中(意味着不能有 2 个记录,其中 userid = x 和 roleid = 1

select count(*), userid from t
where roleid in (1,2,3)
group by userid
having count(*) = 3
于 2009-01-25T01:34:34.947 回答
2
select userid from userrole where userid = 1
intersect
select userid from userrole where userid = 2
intersect
select userid from userrole where userid = 3

这不能解决问题吗?这在典型的关系数据库上的解决方案有多好?查询优化器会自动优化吗?

于 2010-11-26T07:08:44.387 回答
-6

如果您需要任何类型的通用性(不同的 3 角色组合或不同的 n 角色组合)...我建议您为您的角色使用位掩码系统并使用位运算符来执行您的查询...

于 2009-01-25T01:27:26.117 回答