SQLでよく使われるJOINは、複数テーブルを結合して、共通の情報を一つの結果にまとめるための機能です。
この節では、JOINのパフォーマンスについて検証してみます。
検証用のテーブル構築
本記事ではパフォーマンス検証のため、100万件のユーザーと60万件の注文データを持つ実テーブルを使用します。
-- 既存のテーブル削除
DROP TABLE IF EXISTS orders_large;
DROP TABLE IF EXISTS users_large;
-- ユーザーテーブル(100万件)
CREATE TABLE users_large (
id INT PRIMARY KEY,
name VARCHAR(50)
);
-- 注文テーブル(ランダムに60万件)
CREATE TABLE orders_large (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
product VARCHAR(100),
INDEX(user_id)
);
-- ユーザー 100万件挿入
INSERT INTO users_large (id, name)
SELECT seq, CONCAT('User_', seq)
FROM (
SELECT @row := @row + 1 AS seq
FROM (SELECT 0 FROM information_schema.columns LIMIT 100) t1,
(SELECT 0 FROM information_schema.columns LIMIT 100) t2,
(SELECT 0 FROM information_schema.columns LIMIT 100) t3,
(SELECT 0 FROM information_schema.columns LIMIT 100) t4,
(SELECT 0 FROM information_schema.columns LIMIT 100) t5,
(SELECT @row := 0) t0
) AS numbers
LIMIT 1000000;
-- 注文 60万件挿入(存在しないuser_idも混在)
INSERT INTO orders_large (user_id, product)
SELECT FLOOR(1 + RAND() * 1200000),
CONCAT('Product_', FLOOR(1 + RAND() * 100))
FROM (
SELECT @row := @row + 1 AS seq
FROM (SELECT 0 FROM information_schema.columns LIMIT 100) t1,
(SELECT 0 FROM information_schema.columns LIMIT 100) t2,
(SELECT 0 FROM information_schema.columns LIMIT 100) t3,
(SELECT 0 FROM information_schema.columns LIMIT 100) t4,
(SELECT 0 FROM information_schema.columns LIMIT 100) t5,
(SELECT @row := 0) t0
) AS numbers
LIMIT 600000;
このようにして構築された2つのテーブル(users_large
と orders_large
)を使って、JOINの実行計画やパフォーマンスを比較していきます。メモリが足りずINSERTできない場合はt5の行を削除して一度の挿入量を減らしてみてください。
INNER JOIN と LEFT JOIN + WHERE の実行計画比較
まずはそれぞれのEXPLAIN出力を確認します。
INNER JOIN の実行計画
EXPLAIN SELECT *
FROM users_large u
INNER JOIN orders_large o ON u.id = o.user_id;
実行結果
1 SIMPLE o NULL ALL user_id NULL NULL NULL 598774 100.00 Using where
1 SIMPLESIMPLE\u NULL eq_ref PRIMARY PRIMARY 4 testdb.o.user_id 1 100.00 NULL
LEFT JOIN + WHERE の実行計画
EXPLAIN SELECT *
FROM users_large u
LEFT JOIN orders_large o ON u.id = o.user_id
WHERE o.user_id IS NOT NULL;
実行結果
1 SIMPLE o NULL ALL user_id NULL NULL NULL 598774 50.00 Using where
1 SIMPLESIMPLE\u NULL eq_ref PRIMARY PRIMARY 4 testdb.o.user_id 1 100.00 NULL
上記2つの実行計画を比較すると、テーブルの走査順やインデックス利用の有無が異なります。特に注目すべき点として:
ref
: インデックスを使った参照結合eq_ref
: 1件の一致行に対するインデックス結合(最も効率的)ALL
: フルスキャン(インデックス未使用)
実行時間の比較
パフォーマンスの検証には主に2つの方法があります:
- SHOW PROFILE: 最大15件までのクエリ実行詳細を確認できる(ただし非推奨)
- performance_schema: 実行時間や頻度など統計的に分析できる(推奨)
今回は MySQL 8.4.5 を使用しているため、performance_schema
を使って計測を行いました。
10回実行後の平均実行時間は以下のとおりです:
クエリ | 実行回数 | 合計時間(秒) | 平均時間(秒) |
---|---|---|---|
INNER JOIN |
10 | 19.8914 | 1.9891 |
LEFT JOIN + WHERE |
10 | 19.7331 | 1.9733 |
今回はほぼ同じ結果となりましたが、一般的には INNER JOIN
の方が高速になるケースが多いです。(多いはずなんだけど…。)特にインデックスの設計やフィルタ条件、カラム数の違いが結果に影響します。
INNER JOIN の実行計画(10回実行後)
EXPLAIN SELECT *
FROM users_large u
INNER JOIN orders_large o ON u.id = o.user_id;
実行結果
1 SIMPLESIMPLE\u NULL ALL PRIMARY NULL NULL NULL 999453 100.00 NULL
1 SIMPLE o NULL ref user_id user_id 5 testdb.u.id 1 100.00 NULL
クエリを10回実行したあと、INNER JOIN
の実行計画に変化がありました。初回は orders_large
テーブルを先にフルスキャンしていましたが、その後は users_large
側のフルスキャンに切り替わり、orders_large
にインデックス参照(ref)を適用するように変化しました。
このような最適化は、MySQLのクエリオプティマイザによって統計情報や実行履歴が蓄積された結果と考えられます。
ここまでのまとめ
本記事では、JOINの基本から始まり、LEFT JOINとINNER JOINを使った場合の実行計画(EXPLAIN)の違いと、パフォーマンスへの影響を検証しました。
- 同じ結果を得られるクエリでも、MySQLは内部的に異なる実行計画を選択することがある
- 結合順序やインデックスの使い方は、構文や統計情報によって変化する
- LEFT JOIN + WHERE形式とINNER JOIN形式ではパフォーマンスがわずかに異なる場合がある
- performance_schemaを使えば、長期的なクエリ実行傾向を可視化できる
- INNER JOINではクエリを10回繰り返すことで、実行計画が変化した=MySQLのクエリ最適化が働いた可能性がある
- (結局クエリの最適化があるから何度か実行してみないと実行時間ってわからないな。2時間動かして終わらなかったクエリが2回目は20分で終わったとかもあるし)
このように、MySQLのJOINは単なる構文比較にとどまらず、実行環境・データ量・統計情報にも深く関係しています。次回以降では、JOINの組み合わせ周りをいじってみます。
補足: 同じ結果を返すように見えるクエリでも、MySQLはクエリ構文や統計情報に基づいて異なる実行計画(EXPLAIN)を選択することがあります。たとえば、LEFT JOIN + WHERE句の形式とINNER JOINの形式では、結合順序や使われるインデックスが変わる場合があります。これはMySQLのクエリオプティマイザが最も効率的と判断した戦略に基づいており、書き方の違いがパフォーマンスに影響を与える可能性があることを意味します。詳細は MySQL公式ドキュメント(EXPLAIN) を参照してください。