これは私が理解できない非常に基本的なクエリです....
次のような 2 列のテーブルがあるとします。
userid | roleid
--------|--------
1 | 1
1 | 2
1 | 3
2 | 1
1、2、3 を持つすべての個別のユーザー ID を取得したいと考えていますroleids。上記の例を使用すると、返される結果はuserid1 だけです。これを行うにはどうすればよいですか?
これは私が理解できない非常に基本的なクエリです....
次のような 2 列のテーブルがあるとします。
userid | roleid
--------|--------
1 | 1
1 | 2
1 | 3
2 | 1
1、2、3 を持つすべての個別のユーザー ID を取得したいと考えていますroleids。上記の例を使用すると、返される結果はuserid1 だけです。これを行うにはどうすればよいですか?
わかりました、私はこれに反対票を投じたので、それをテストすることにしました:
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デスクトップマシンで実行)で同じ演習を繰り返しましたが、結果はほぼ同じです。
結合は眉をひそめているように見えますが、私が示したように、集約クエリは桁違いに遅くなる可能性があります。
更新:いくつかの広範なテストの後、全体像はより複雑になり、答えはデータ、データベース、およびその他の要因によって異なります。物語の教訓は、テスト、テスト、テストです。
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は、さらに高速になるはずです。
これを行う古典的な方法は、関係分割の問題として扱うことです。
英語: 必要なロール 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 で「関係分割」を検索すると、多くの情報が見つかります。
useridを想定すると、roleidは一意のインデックスに含まれます(つまり、userid =xとroleid=1のレコードが2つ存在することはできません)。
select count(*), userid from t
where roleid in (1,2,3)
group by userid
having count(*) = 3
select userid from userrole where userid = 1
intersect
select userid from userrole where userid = 2
intersect
select userid from userrole where userid = 3
これで問題は解決しませんか?これは、典型的なリレーショナル DB でどの程度優れたソリューションですか? クエリオプティマイザーはこれを自動最適化しますか?
ここで何らかの一般性が必要な場合(異なる3ロールの組み合わせまたは異なるnロールの組み合わせ)...ロールにビットマスキングシステムを使用し、ビット演算子を使用してクエリを実行することをお勧めします...