MySQL の JOIN が遅いときに確認すべきポイント

複数テーブルを JOIN したクエリが遅い場合、いくつかの原因が考えられます。ここでは、JOIN のパフォーマンス問題を診断し、改善するためのポイントを解説します。

まず EXPLAIN で確認する

遅い JOIN クエリに対して、最初にやるべきは EXPLAIN です。

EXPLAIN SELECT o.*, u.name 
FROM orders o 
JOIN users u ON o.user_id = u.id 
WHERE o.status = 'pending';

結果の各行が、テーブルごとのアクセス方法を示しています。特に注目すべきは typerows の列です。

確認ポイント1: 結合キーにインデックスがあるか

JOIN 条件で使われるカラムにインデックスがないと、テーブルフルスキャンが発生します。

-- user_id にインデックスがなければ追加
ALTER TABLE orders ADD INDEX idx_user_id (user_id);

外部キーを設定しているだけではインデックスは作られません。明示的にインデックスを作成する必要があります。

主キー側

通常、参照先(users.id など)は主キーなのでインデックスあり

外部キー側

参照元(orders.user_id など)は明示的なインデックス作成が必要なことが多い

確認ポイント2: 駆動表の選択

MySQL は結合するテーブルの順序を自動的に決めます。小さいテーブルを先に処理する(駆動表にする)ほうが効率的です。

EXPLAIN の結果で、最初に表示されるテーブルが駆動表です。想定と違う場合は STRAIGHT_JOIN で順序を固定できます。

SELECT STRAIGHT_JOIN o.*, u.name 
FROM orders o 
JOIN users u ON o.user_id = u.id;

ただし、通常はオプティマイザに任せるのがベストです。どうしても改善しない場合の最終手段として使ってください。

確認ポイント3: 結合アルゴリズム

MySQL 8.0.18 以降では Hash Join が導入されました。それ以前は Nested Loop Join のみでした。

Nested Loop Join

駆動表の各行に対して、内部表を繰り返し検索。インデックスがあれば高速。

Hash Join

インデックスがない場合に使われる。等価結合で大量データを扱う場合に有効。

EXPLAIN の Extra に Using join buffer (hash join) と表示されていれば Hash Join が使われています。

確認ポイント4: 不要な JOIN を排除する

本当にその JOIN は必要でしょうか?使っていないテーブルを JOIN していないか確認しましょう。

-- categories を JOIN しているが SELECT で使っていない
SELECT p.name, p.price 
FROM products p
JOIN categories c ON p.category_id = c.id  -- 不要?
WHERE p.stock > 0;

外部キー制約で整合性が保証されているなら、存在確認のためだけの JOIN は不要かもしれません。

確認ポイント5: JOIN する前に絞り込む

結合する前にデータを絞り込むことで、処理対象行を減らせます。

-- 先に絞り込んでから JOIN
SELECT o.*, u.name 
FROM (
    SELECT * FROM orders WHERE created_at >= '2024-01-01' LIMIT 100
) o
JOIN users u ON o.user_id = u.id;

ただし、MySQL のオプティマイザは通常これを自動で行います。サブクエリを使うとかえって遅くなることもあるため、EXPLAIN で比較してください。

join_buffer_size の調整

Nested Loop Join でインデックスが使えない場合、join buffer が使われます。デフォルトは 256KB ですが、大きなテーブル同士の JOIN では増やすと効果があることがあります。

-- セッションレベルで変更
SET SESSION join_buffer_size = 4194304;  -- 4MB

ただし、これは根本的な解決ではありません。インデックス追加を優先し、それでも改善しない場合の補助策として検討してください。

チェックリスト

JOIN が遅いときは、以下を順番に確認しましょう。

結合キーの両側にインデックスがあるか
EXPLAIN で type が ALL になっていないか
不要なテーブルを JOIN していないか
WHERE 句で十分に絞り込めているか
rows の値が想定より大きくないか

多くの場合、インデックスの追加で解決します。それでも改善しない場合は、クエリの分割やキャッシュの導入を検討してください。