はじめに
SQLで「関連するデータを持つ行」を取得する際、INNER JOIN
とEXISTS
のどちらを使うべきか迷ったことはありませんか?見た目の違いはあっても、結果は同じに見える場面が多いため、なんとなく使い分けている人も多いかもしれません。
本記事では、INNER JOINとEXISTSの構文と挙動、そしてパフォーマンス差について、10万件のユーザーと15万件の関連データを用いて実験・検証してみます。
MySQL テーブル構成とデータ準備
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50)
);
CREATE TABLE purchases (
id INT PRIMARY KEY,
user_id INT,
product VARCHAR(100)
);
それぞれに以下のような大量データを挿入します:
-- ユーザー10万件
INSERT INTO users (id, name)
SELECT
seq,
CONCAT('user_', seq)
FROM (
SELECT
@row := @row + 1 AS seq
FROM
information_schema.columns,
information_schema.columns,
(SELECT @row := 0) AS init
LIMIT 100000
) AS seed;
-- 購入履歴15万件(ユーザーIDに偏りあり)
INSERT INTO purchases (id, user_id, product)
SELECT
seq,
FLOOR(1 + RAND() * 100000),
CONCAT('item_', FLOOR(RAND() * 100))
FROM (
SELECT
@row := @row + 1 AS seq
FROM
information_schema.columns,
information_schema.columns,
(SELECT @row := 0) AS init
LIMIT 150000
) AS seed;
これにより、特定のユーザーには複数の購入履歴が紐付きます。
SELECT
*
FROM
purchases
ORDER BY
user_id desc
limit 10;
などでuser_idがmax100000であること、1つのIDに複数のproductが紐づいていることを確認してください。
※ランダム生成なのでmaxが99999とかになることもあります。
INNER JOINを使った「購入履歴のあるユーザー」抽出
SELECT DISTINCT
u.id
FROM
users AS u
INNER JOIN
purchases AS p
ON
u.id = p.user_id;
INNER JOINは、両テーブルで一致する行を返します。この場合、購入履歴のあるユーザーのみが取得されます。ただし、同じユーザーが複数購入している場合は重複するため、DISTINCT
での除外が必要です。
EXISTSを使った同様の取得方法
SELECT
u.id
FROM
users AS u
WHERE
EXISTS (
SELECT
1
FROM
purchases AS p
WHERE
p.user_id = u.id
);
この構文では、ユーザーごとにサブクエリで購入履歴が存在するかを調べています。重複行が存在しないため、DISTINCT
も不要です。
パフォーマンス比較(10万×15万)
インデックスは張らずに、MySQLでクエリ時間を計測してみました。
INNER JOIN と EXISTS のパフォーマンス比較
クエリ | 実行回数 (COUNT_STAR) |
合計実行時間 (total_time_sec) |
平均実行時間 (avg_time_sec) |
---|---|---|---|
SELECT DISTINCTROW `u`.`id` FROM `users` AS `u` INNER JOIN `purcha…` |
10 | 3.8297 | 0.3830 |
SELECT `u`.`id` FROM `users` AS `u` WHERE EXISTS (SELECT ? FROM …) |
10 | 1.6099 | 0.1610 |
- INNER JOIN + DISTINCT:約3.8秒
- EXISTS:約1.6秒
INNER JOIN
は中間結果が膨大になり、DISTINCT
での重複排除にも時間がかかります。一方、EXISTS
は条件を満たす1件さえ見つかればサブクエリを終了するため、効率が良いです。
EXPLAINによる実行計画の違い
INNER JOINの実行計画では:
id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | SIMPLE | p | NULL | ALL | NULL | NULL | NULL | NULL | 150080 | 100.00 | Using where |
1 | SIMPLE | u | NULL | eq_ref | PRIMARY | PRIMARY | 4 | testdb.p.u_id | 1 | 100.00 | Using index |
- users: equal reference
- purchases: full scan
- 重複行をJOINで大量に生成 → DISTINCTでフィルタリング
EXISTSの場合:
id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | SIMPLE | u | NULL | index | PRIMARY | PRIMARY | 4 | NULL | 97689 | 100.00 | Using index |
1 | SIMPLE | <subquery2> | NULL | eq_ref | <auto_distinct_key> | <auto_distinct_key> | 5 | testdb.u.id | 1 | 100.00 | NULL |
2 | MATERIALIZED | p | NULL | ALL | NULL | NULL | NULL | NULL | 150080 | 100.00 | NULL |
- users: index full scan
- purchases: full scan
- サブクエリ: equal reference
- 不要なデータの生成が少ない
”結合結果が大きくなるほど、EXISTSの効率が際立ちます。”
↑ってチャッピー(ChatGPT)のコメントなんだけど、DISTINCTが効率が悪いって考えの方が近いんじゃないかな。
MySQL 使い分けの指針
- INNER JOINは結果を結合して「複数の列を同時に取得」したいときに有効。つまりDISTINCTしないなら早い
- EXISTSは「存在するかどうか」だけを調べる用途に最適
- EXISTSは重複を持たない構造になっており、パフォーマンスに優れることが多い
- データ量が多くなるほど、JOINよりEXISTSの利点が顕著になる
次回のテーマ
LEFT JOINとNOT EXISTSの違いとパフォーマンス比較 をやってみたいと思います。