Day18 前半のゴール
「“なんとなく遅いSQL”を、筋道立てて改善ポイントまで分解できるようになる」
Day18 からはいよいよ「クエリチューニング」です。
テーマは「遅いSQLの改善」。
ここで目指すのは、
遅いSQLに出会ったときに、感覚ではなく手順で原因を追える
EXPLAIN とインデックス設計の知識を“実戦投入”できる
「どこがボトルネックか」を言葉で説明できる
この状態です。
前半では、まず「遅いSQLに出会ったときの基本の流れ」と「典型的な遅いパターン」を、シンプルな例で押さえていきます。
遅いSQLに出会ったときの基本ステップ
「いきなり書き換えない。まず“見える化”する」
いきなりクエリを書き換えるのは、正直ギャンブルです。
やるべき順番は決まっています。
まず、そのSQLを EXPLAIN する
どのテーブルがフルスキャンになっているかを見る
どのカラムにインデックスがなくて苦しんでいるかを見る
必要なら、対象テーブルの行数やデータ分布も確認する
この「見える化」をせずにチューニングを始めると、
たまたま速くなったり、逆に遅くなったりして、理由が分からないままになります。
Day16・Day17でやったことは、まさにこの「見える化」のための準備でした。
今日はそれを「遅いSQL」という具体的な相手にぶつけていきます。
例題:よくある“なんか遅い一覧画面”
「orders × users のJOIN+絞り込み+並び替え」
次のようなテーブルを想像してください。
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
status VARCHAR(20) NOT NULL,
created_at DATETIME NOT NULL
) ENGINE=InnoDB;
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
total INT NOT NULL,
created_at DATETIME NOT NULL,
CONSTRAINT fk_orders_user
FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB;
Pythonアプリ側で、こんな画面があるとします。
「アクティブユーザーの注文履歴を、新しい注文順に20件表示する」
それに対応するSQLは、だいたいこんな感じです。
SELECT
o.id,
u.name,
o.total,
o.created_at
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE u.status = 'active'
ORDER BY o.created_at DESC
LIMIT 20;
Pythonこれが「なんか遅い」とします。
ここから、チューニングの思考を始めます。
ステップ1:EXPLAINで“どこが重いか”を特定する
「テーブルごとに type / key / rows / Extra を見る」
まずは EXPLAIN します。
EXPLAIN
SELECT
o.id,
u.name,
o.total,
o.created_at
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE u.status = 'active'
ORDER BY o.created_at DESC
LIMIT 20;
Pythonインデックスがほとんど貼られていない前提だと、結果はだいたいこうなります(簡略版)。
| table | type | key | rows | Extra |
|---|---|---|---|---|
| u | ALL | NULL | 100000 | Using where |
| o | ALL | NULL | 500000 | Using where; Using filesort |
これを Day16 の視点で読むと、
users はフルスキャン(type = ALL, key = NULL, rows = 100000)
orders もフルスキャン(type = ALL, key = NULL, rows = 500000)
さらに orders 側で Using filesort(ORDER BY のために別途ソート)
という、かなり“重そうな顔”をしていることが分かります。
ここで大事なのは、
「遅い」という感覚を、「どのテーブルのどの部分が重いか」という言葉に変換すること
です。
ステップ2:クエリの意図を言葉に戻す
「DBに“何をしてほしいのか”を整理する」
いきなりインデックスを貼る前に、
このクエリの意図を日本語で整理します。
アクティブユーザー(status = ‘active’)の中から
そのユーザーたちの注文を取り出し
注文日時の新しい順に並べて
先頭20件だけ欲しい
この「意図」と EXPLAIN の結果を見比べると、
今の実行計画はこうなっています。
全ユーザー10万件をなめて、active だけに絞る
全注文50万件をなめて、activeユーザーに紐づくものだけに絞る
その結果を全部並べ替えて、先頭20件だけ返す
「やりたいこと」に対して、「やり方」がかなり雑なのが分かります。
ここから、
どこで絞り込みを効かせるべきか
どこにインデックスを貼ると効きそうか
を考えていきます。
ステップ3:インデックス候補を洗い出す
「WHERE・JOIN・ORDER BY に出てくるカラムをチェックする」
このクエリで重要なカラムは、明らかにここです。
JOIN の ON 句:o.user_id = u.id
WHERE:u.status = 'active'
ORDER BY:o.created_at DESC
Day17 でやった通り、
JOIN に使うカラム
WHERE で絞り込みに使うカラム
ORDER BY で並び替えに使うカラム
は、インデックス候補の最有力です。
なので、まずこう考えます。
users.status にインデックスを貼るべきか?
orders.user_id にインデックスを貼るべきか?
orders.created_at にインデックスを貼るべきか?
ここで、「status は値の種類が少ないから絞り込みが効きにくいかも」という前半の知識も効いてきます。
一方で、orders.user_id と orders.created_at は、かなり有力な候補です。
ステップ4:改善案を“1つずつ”試すイメージを持つ
「いきなり全部やらず、“打ち手”を分解する」
実務では、いきなりインデックスを3本まとめて貼るのはあまり良くありません。
「どれが効いたのか」が分からなくなるからです。
考え方としては、
まず orders.user_id にインデックスを貼って EXPLAIN を見直す
次に orders.created_at にもインデックスを貼ってみる
必要なら複合インデックス (user_id, created_at) を検討する
というように、「打ち手」を分解して考えます。
Day18 前半では、まだ実際のインデックス追加SQLまでは書きません。
大事なのは、
EXPLAIN を見て「どこが重いか」を特定し
クエリの意図から「どのカラムで絞りたいか・並べたいか」を整理し
インデックス候補を挙げて、「どの順で試すか」を考える
という“思考の流れ”を体に入れることです。
もう一つの典型:サブクエリで遅くなるパターン
「IN (SELECT …) をJOINに書き換える発想」
もう一つ、よくある「なんか遅い」パターンを見ておきます。
例えば、こんなSQLです。
SELECT *
FROM orders
WHERE user_id IN (
SELECT id
FROM users
WHERE status = 'active'
);
Python意味としては、さっきのJOINとほぼ同じです。
「アクティブユーザーの注文だけ欲しい」
これを EXPLAIN すると、環境によってはこうなります。
外側の orders がフルスキャン
内側の users のサブクエリが何度も評価される
結果として、JOIN より遅くなることがあります。
このときの改善の発想はシンプルです。
サブクエリを JOIN に書き換えられないか?
つまり、こうです。
SELECT o.*
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE u.status = 'active';
Pythonこの形にしてから EXPLAIN し直すと、
users に対して status で絞り込み
orders に対して user_id でインデックス検索
という「素直な実行計画」に乗りやすくなります。
Day18 前半では、
IN (SELECT …) や EXISTS を見たら、「JOIN で書けないか?」と一度立ち止まる
という癖を持っておくと、後半のチューニングがかなり楽になります。
Day18 前半のまとめ
遅いSQLに出会ったときはいきなり書き換えるのではなく、まず EXPLAIN して type / key / rows / Extra を見ながら「どのテーブルがフルスキャンになっているか」「どこで filesort が走っているか」を特定し、そのうえでクエリの意図を日本語に戻して「本当はどのカラムで絞り込みたいのか・どの順で並べたいのか」を整理するのが第一歩になる。
orders × users のJOINの例では、EXPLAIN から「users も orders もフルスキャン+filesort」という“雑な作戦”が見え、そのうえで JOIN の ON 句(user_id)、WHERE(status)、ORDER BY(created_at)に出てくるカラムをインデックス候補として洗い出し、「まず orders.user_id、その次に orders.created_at、必要なら複合インデックス」というように“打ち手”を分解して考えることで、理由を持ったチューニングの流れが作れる。
また、WHERE user_id IN (SELECT id FROM users WHERE status = 'active') のようなサブクエリベースの書き方は、環境によっては非効率な実行計画になりやすく、「JOIN に書き換えられないか?」と発想を切り替えることで、インデックスが効きやすい形に変えられることが多く、「遅いSQLの改善=EXPLAINで見える化→意図を言語化→インデックス候補と書き方の見直し」という3ステップで考えるのが、Day18 前半で身につけたい“思考の型”になる。
