32

これは私が理解できない非常に基本的なクエリです....

次のような 2 列のテーブルがあるとします。

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

1、2、3 を持つすべての個別のユーザー ID を取得したいと考えていますroleids。上記の例を使用すると、返される結果は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.312秒

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.016秒

それは正しい。私が提案した結合バージョンは、集約バージョンよりも20倍高速です。

申し訳ありませんが、私はこれを現実の世界での生活と仕事のために行っています。現実の世界ではSQLをテストし、結果がそれを物語っています。

この理由はかなり明確なはずです。集計クエリは、テーブルのサイズに応じてコストがスケーリングされます。すべての行は、HAVING句を介して処理、集約、およびフィルタリングされます(またはフィルタリングされません)。参加バージョンは、(インデックスを使用して)特定の役割に基づいてユーザーのサブセットを選択し、そのサブセットを2番目の役割と照合し、最後にそのサブセットを3番目の役割と照合します。各選択関係代数の用語で)は、ますます小さなサブセットで機能します。これから、次のように結論付けることができます。

結合バージョンのパフォーマンスは、一致の発生率が低くなるとさらに向上します。

上記の3つの役割を持つユーザーが500人(上記の500kサンプルのうち)しかない場合、参加バージョンは大幅に高速化されます。集約バージョンはそうではありません(そして、パフォーマンスの向上は、25kではなく500ユーザーを転送した結果であり、結合バージョンも明らかに得られます)。

また、実際のデータベース(つまり、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、3つのテーブルすべてのメモにある「インデックスの使用」を参照してください。これは、インデックスを読み取っており、データ行に触れる必要がないことを意味します。

MacbookでMySQL5.1.48を使用してこのクエリを210万行(PostTagsのスタックオーバーフロー7月のデータダンプ)で実行したところ、1.08秒で結果が返されました。に十分なメモリが割り当てられているまともなサーバーでinnodb_buffer_pool_sizeは、さらに高速になるはずです。

于 2009-01-25T01:33:24.950 に答える
5

これを行う古典的な方法は、関係分割の問題として扱うことです。

英語: 必要なロール ID 値が欠落していないユーザーを選択します。

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);

また、関連するすべての列が NULL 可能ではないと仮定するので、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
  )
);

これは、インデックス、プラットフォーム、データなどに応じて、効率的である場合とそうでない場合があります。Web で「関係分割」を検索すると、多くの情報が見つかります。

于 2009-08-07T20:08:12.143 に答える
4

useridを想定すると、roleidは一意のインデックスに含まれます(つまり、userid =xとroleid=1のレコードが2つ存在することはできません)。

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

これで問題は解決しませんか?これは、典型的なリレーショナル DB でどの程度優れたソリューションですか? クエリオプティマイザーはこれを自動最適化しますか?

于 2010-11-26T07:08:44.387 に答える
-6

ここで何らかの一般性が必要な場合(異なる3ロールの組み合わせまたは異なるnロールの組み合わせ)...ロールにビットマスキングシステムを使用し、ビット演算子を使用してクエリを実行することをお勧めします...

于 2009-01-25T01:27:26.117 に答える