はじめに
SQLを書くとき、INNER JOIN
とEXISTS
のどちらを使うべきか迷うことがあります。特に、DISTINCTが不要なケースでは、出力結果が同じでもパフォーマンスに差が出ることがあります。
この記事では、重複排除が不要な1対1のテーブル構造において、INNER JOIN と EXISTS のパフォーマンス差を検証し、実行結果・実行時間・実行計画の違いを詳しく比較します。
MySQLは8.4.5を利用しています。
テストテーブルの作成とデータ挿入
テスト用に以下の2テーブルを用意しました。
users
: ユーザー情報(10万件)orders
: 各ユーザーの注文情報(1対1、インデックスなし)
データは以下のように生成しました(高速連番生成)。
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(100)
);
CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT,
product VARCHAR(100)
);
-- users データ挿入
INSERT INTO users (id, name)
SELECT
@row := @row + 1 AS id,
CONCAT('user_', @row)
FROM
(SELECT 0 FROM information_schema.columns LIMIT 1000) AS t1,
(SELECT 0 FROM information_schema.columns LIMIT 100) AS t2,
(SELECT @row := 0) AS init;
-- orders データ挿入
INSERT INTO orders (id, user_id, product)
SELECT
@row2 := @row2 + 1 AS id,
@row2 AS user_id,
CONCAT('product_', @row2)
FROM
(SELECT 0 FROM information_schema.columns LIMIT 1000) AS t3,
(SELECT 0 FROM information_schema.columns LIMIT 100) AS t4,
(SELECT @row2 := 0) AS init;
INNER JOIN と EXISTS のクエリ比較
INNER JOIN クエリ
SELECT
U.id,
U.name
FROM
users AS U
INNER JOIN
orders AS O
ON U.id = O.user_id;
実行結果(上位10件):
id | name |
---|---|
1 | user_1 |
2 | user_2 |
3 | user_3 |
4 | user_4 |
5 | user_5 |
6 | user_6 |
7 | user_7 |
8 | user_8 |
9 | user_9 |
10 | user_10 |
EXISTS クエリ
SELECT
U.id,
U.name
FROM
users AS U
WHERE
EXISTS (
SELECT
1
FROM
orders AS O
WHERE
O.user_id = U.id
);
実行結果(上位10件):
id | name |
---|---|
1 | user_1 |
2 | user_2 |
3 | user_3 |
4 | user_4 |
5 | user_5 |
6 | user_6 |
7 | user_7 |
8 | user_8 |
9 | user_9 |
10 | user_10 |
パフォーマンス比較
実行時間(10回の平均)
クエリ種別 | COUNT_STAR | total_time_sec | avg_time_sec |
---|---|---|---|
INNER JOIN | 10 | 1.9675 | 0.1967 |
EXISTS | 10 | 1.3704 | 0.1370 |
EXPLAIN結果(INNER JOIN)
id | select_type | table | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
---|---|---|---|---|---|---|---|---|---|---|
1 | SIMPLE | O | ALL | NULL | NULL | NULL | NULL | 100040 | 100.00 | Using where |
1 | SIMPLE | U | eq_ref | PRIMARY | PRIMARY | 4 | testdb.O.user_id | 1 | 100.00 | NULL |
EXPLAIN結果(EXISTS)
id | select_type | table | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
---|---|---|---|---|---|---|---|---|---|---|
1 | SIMPLE | U | ALL | PRIMARY | NULL | NULL | NULL | 97689 | 100.00 | NULL |
1 | SIMPLE | <subquery2> | eq_ref | <auto_distinct_key> | <auto_distinct_key> | 5 | testdb.U.id | 1 | 100.00 | NULL |
2 | MATERIALIZED | O | ALL | NULL | NULL | NULL | NULL | 100040 | 100.00 | NULL |
考察:INNER JOINとEXISTSの使い分け
今回の検証では、出力結果は同一ながら、EXISTSの方が平均30%ほど高速でした。これはサブクエリが「マッチする1行が見つかれば終了」する特性を活かせたためです。
一方、INNER JOINは結合先を全スキャンしてからフィルタするため、特にインデックスがない場合はコストが高くなりがちです。
結論として、次のような基準で使い分けると良いでしょう:
- 結合先テーブルのデータを取得する必要がある場合 → INNER JOIN
- 存在チェックだけで十分な場合 → EXISTS
MySQL まとめ
INNER JOIN と EXISTS のパフォーマンス差は、構文上は同じ意味でも、テーブル構造・インデックス有無・スキャン行数によって変わります。今回のようなDISTINCT不要なケースでは、特にEXISTSが有利になるシーンも多いことがわかりました。
実行計画を確認し、適切に使い分けることが高速化への第一歩です。
次回のテーマ
EXPLAINの使い方をやってみたいと思います。