🚀 パフォーマンス改善のためのSQL戦略
業務システムやポータルサイトのバックエンドでは、検索画面で大量のデータを結合し、条件で絞り込み、並べ替え・ページングする構成がよくあります。
SELECT *
FROM staff
JOIN store ON ...
JOIN area ON ...
LEFT JOIN video ON ...
WHERE 条件
GROUP BY staff.id
ORDER BY staff.updated_at DESC
LIMIT 0, 30;
このような「複数のテーブルをJOINして、ソートして、LIMITする」SQLは、一見シンプルに見えても、パフォーマンスがボトルネックになりやすい構成です。
この記事では、こういったクエリがなぜ遅くなるのか、どう改善すれば良いのかを、実例をもとに分かりやすく解説します。
💥 問題となる典型パターン
🛑 よくある構造
- 10以上のテーブルをJOIN(INNER / LEFT混在)
- 複雑なWHERE条件(サブクエリ、OR、CASE文)
- GROUP BYとORDER BYが混在
- LIMIT / OFFSET でページング
🔧 実際の EXPLAIN
の一部(例)
table | type | rows | Extra |
---|---|---|---|
staff | ref | 1200 | Using index condition |
video | ref | 5000 | Using where |
area | const | 1 | |
store | ref | 3000 | Using where; Using temporary; filesort |
❗ 問題ポイント
-
Using temporary
+Using filesort
→ ソートのためのテンポラリテーブルとファイルソートが発生 - LIMITが適用される前に、全ての行に対してソートが走る
- LEFT JOIN先の条件が複雑でインデックスが効きづらい
⚠️ なぜ遅くなるのか?
- JOINで絞り込み前の行数が爆発的に増える
- 全行をORDER BYでソート
- LIMITは最後に適用されるため、途中の行もすべて評価対象
-
MySQL/MariaDBはこのパターンに非常に弱い
→ 特にOFFSETが大きくなるとパフォーマンスが劣化
✅ ベストプラクティス:2段階取得戦略
🥇 ステップ1:IDだけ先に取得(軽量)
SELECT staff.id
FROM staff
JOIN store ON ...
WHERE 条件
ORDER BY staff.updated_at DESC
LIMIT 0, 30;
- この段階では、必要なIDだけを取得
- 重いJOINやLEFT JOINは後回し!
🥈 ステップ2:詳細情報を取得(JOIN)
SELECT s.*, v.*, a.*
FROM staff s
LEFT JOIN video v ON ...
LEFT JOIN area a ON ...
WHERE s.id IN (1, 2, 3, ..., 30);
- 件数が少ないため、JOINしても高速
- ORDER BYは不要(前段で完了しているため)
🧪 パフォーマンス比較
パターン | 実行時間 |
---|---|
1段階フルJOIN | 1.4秒 |
2段階戦略 | 0.12秒 |
- 約10倍以上の高速化を実現!
- 特に大きなOFFSETになると差は指数的に拡大
✨ 補足:さらに高みを目指すなら
- WITH句で一時的なサブクエリ(MariaDB 10.2+)
-
IDソート用インデックスを作る(例:
(updated_at, id)
) - OFFSETを使わず、「カーソル方式(seek)」でページング
WHERE (updated_at, id) < (?, ?)
🎯 まとめ
問題 | 解決策 |
---|---|
JOINが多くソートが重い | IDだけ先に取得し、詳細は後段で |
ソートに時間がかかる | ソート対象を最小限に絞る |
LIMIT OFFSETが遅い | IDベースのカーソル方式でページング |
📘 最後に
JOIN・ソート・ページングはWebシステムの根幹ですが、正しい戦略で設計しないとスケールしないクエリになります。
「まずはIDだけ取る」「JOINを遅延する」
この2段階戦略は、データ量が増えたときにも強い構造です。