MySQL INNER JOIN と EXISTS のパフォーマンス差2

はじめに

SQLを書くとき、INNER JOINEXISTSのどちらを使うべきか迷うことがあります。特に、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の使い方をやってみたいと思います。