サイトが遅いなと思ったら読む、実行計画×AIで始めるDB高速化
「ページ表示が遅い」「検索に時間がかかる」といったシステムのパフォーマンス問題。その多くは、データベースのクエリ処理が原因です。中でも、MySQLの 実行計画(Explain)を確認することは、ボトルネックを特定し、最適化するための第一歩です。
本記事では、実行計画の基礎から、インデックス設計のコツ、そしてAIを使って実行計画の解釈と改善案を得る方法まで、初心者でも取り組める内容で解説します。
実行計画ってなに?
MySQLにSQLを発行すると、データベースは最適と思われる方法でデータを取りにいきます。その内部処理を「どう処理しようとしているか」を示すのが実行計画です。
EXPLAIN SELECT ...
と記述すれば、そのクエリがどのように実行されるかが表形式で返ってきます。
こんな感じ。
+----+-------------+-------------+------------+------+---------------+------+---------+------+--------+----------+----------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+------+---------------+------+---------+------+--------+----------+----------------------------------------------+
| 1 | SIMPLE | product_fku | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using where; Using temporary; Using filesort |
この表の中身を理解することで、「テーブルをどう結合しているのか」「全件スキャンしているのか」などが分かります。でもこれって結構読み解くの難しいんですよね。まずは簡単な読み方をおさらい。
実行計画の読み方:どこを見るべき?
初めて見ると複雑に感じますが、まず注目すべきは以下の項目です:
type:アクセス方法。ALL(全件スキャン)は遅い処理の代表例。
key:使われたインデックス名。NULLならインデックス未使用。
rows:MySQLが想定するスキャン行数。
Extra:補足情報。Using temporaryやUsing filesortがあると注意。
これらを見て、クエリのどこが重いのか、おおよその見当がつけられます。
これが出たら危険信号!
特に注意すべき実行計画のキーワードを以下に挙げます:
type = ALL:インデックスが使われておらず、全件走査。
Extra に Using temporary:一時テーブルの使用はコストが高くなる。
Using filesort:ソート処理がメモリ上でなくディスクに行われる可能性があり遅くなりがち。
このような表示が出た場合、インデックスの追加やクエリの見直しが必要です。
実践
では実際にどうやるか見ていきましょう。
以下はFKUと商品を1対多で管理しているテーブルから一覧用データを取得するSQLです。よくある形ですね。これをチューニングしてみましょう。
select
`product_fku`.`id`,
`product_fku`.`fku`,
`product_fku`.`fku_name`,
`pfi`.`s3_object_key`,
COUNT(p.prod_code) as prod_count,
GROUP_CONCAT(CONCAT(p.prod_code, ":", p.prod_name, ":", IFNULL(pis.s3_object_key, "")) ORDER BY p.prod_code ASC) as product_list
from
`product_fku`
left join
`product_fku_images` as `pfi`
on `pfi`.`fku` = `product_fku`.`fku`
and `pfi`.`image_type` = '0'
left join
`products` as `p`
on `p`.`fku` = `product_fku`.`fku`
and `p`.`deleted_at` is null
left join
`product_images` as `pis`
on `pis`.`prod_code` = `p`.`prod_code`
and `pis`.`deleted_at` is null
where
`product_fku`.`deleted_at` is null
group by
`product_fku`.`id`,
`product_fku`.`fku`,
`product_fku`.`fku_name`,
`pfi`.`s3_object_key`
order by
`product_fku`.`fku` desc
limit 50 offset 0;
実行計画を取る
ではまず以下のSQLを実行してみます。
explain select
`product_fku`.`id`,
・・・(以下略)
すると以下のような分析結果が返却されます。
+----+-------------+-------------+------------+------+---------------+------+---------+------+--------+----------+----------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+------+---------------+------+---------+------+--------+----------+----------------------------------------------+
| 1 | SIMPLE | product_fku | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using where; Using temporary; Using filesort |
| 1 | SIMPLE | pfi | NULL | ALL | NULL | NULL | NULL | NULL | 4 | 100.00 | Using where; Using join buffer (hash join) |
| 1 | SIMPLE | p | NULL | ALL | NULL | NULL | NULL | NULL | 155806 | 100.00 | Using where; Using join buffer (hash join) |
| 1 | SIMPLE | pis | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using where; Using join buffer (hash join) |
+----+-------------+-------------+------------+------+---------------+------+---------+------+--------+----------+----------------------------------------------+
4 rows in set, 1 warning (0.06 sec)
この実行計画を見てみると、すべてのテーブルで type = ALL になっていることがわかります。これはつまり、フルスキャンが行われているということです。
では実際に上記のSQLと実行計画をChatGPTに送信して解決策を見出してもらいましょう。
プロンプト
MySQLで以下のSQLと実行計画を改善する方法を考えてください
explain select ・・・
+----+-------------+-------------+------------+------+---------------+------+---------+------+--------+----------+----------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+------+---------------+------+---------+------+--------+----------+----------------------------------------------+
| 1 | SIMPLE | product_fku | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using where; Using temporary; Using filesort |
・・・
すると以下のような内容を返してくれました。
レスポンス
ありがとうございます。いただいたSQLと実行計画から、以下のような改善提案が考えられます:
🔍 問題点の分析:
-
すべてのテーブルでtype=ALLとなっており、インデックスが使用されていない
→ 全件スキャン(フルテーブルスキャン)が発生しており、特にproducts
テーブルでは15万件超の走査が無駄に発生しています。 -
Extraに「Using temporary」「Using filesort」が含まれる
→GROUP BY
とORDER BY
の組み合わせにより、一時テーブルとファイルソート処理が発生。これも処理コストを増大させています。 -
JOIN条件に使われているカラムにインデックスが存在しない
→ ハッシュJOIN(join buffer)になっており、インデックスJOINが使われていません。
💡 改善案:
-
インデックスの追加
-
product_fku
.deleted_at
に単一インデックスを追加 -
products
テーブルに(fku, deleted_at)
の複合インデックス -
product_images
テーブルに(prod_code, deleted_at)
の複合インデックス -
product_fku_images
テーブルに(fku, image_type)
のインデックス
-
-
GROUP BY・ORDER BYの最適化
-
GROUP BY
列とORDER BY
列が同じなら、並び順を揃えるとUsing filesort
を回避できる可能性あり -
GROUP_CONCAT(... ORDER BY)
を使っているが、ORDER BYの内部処理が全体のORDER BY
と重複してコストになっている可能性あり
-
-
件数の多いテーブルにLIMITを使う
- 可能であれば、サブクエリなどで先に
product_fku
の対象IDのみ絞り込むことでJOIN対象を減らす
- 可能であれば、サブクエリなどで先に
🛠 インデックス例(DDL):
CREATE INDEX idx_fku_deleted ON products (fku, deleted_at);
CREATE INDEX idx_prod_code_deleted ON product_images (prod_code, deleted_at);
CREATE INDEX idx_fku_image_type ON product_fku_images (fku, image_type);
CREATE INDEX idx_deleted ON product_fku (deleted_at);
📌 注意点:
インデックスを追加した後は、必ず再度 EXPLAIN を実行して効果を確認してください。また、データ量やアクセス頻度に応じて、インデックスが逆効果になることもあるため、慎重な検証が必要です。
以上が改善提案となります。必要であれば、インデックス追加後の再分析もお手伝いできます。
といった内容が返却されます。
しかしながら、これを鵜呑みにするとちょっとした罠に引っかかったりするので注意しましょう。
ここからが人間のお仕事です。
良いインデックス・悪いインデックスとは?
インデックスはクエリ高速化の強力な武器ですが、万能ではありません。適切に設計されたインデックスはパフォーマンスを劇的に改善しますが、用途に合っていないインデックスは逆に処理を遅くしたり、更新処理のコストを上げたりしてしまいます。
では、良いインデックスと悪いインデックスの違いは何でしょうか?
低選択性カラムへのインデックスは要注意
たとえば、今回の実行計画に登場した deleted_at カラム。このカラムは論理削除フラグとして、NULL(未削除)か 日時(削除済)という値を取ります。運用的には「WHERE deleted_at IS NULL」で未削除のレコードだけを扱う、というケースが多く見られます。
しかしこのような選択肢が2つか3つしかないカラム:低選択性カラムは、インデックスを張ってもあまり意味がありません。なぜなら、インデックスを使っても結局は大量の行を読む必要があるため、MySQLが「それならテーブルを全件読んだ方が早い」と判断してしまうことが多いからです。
つまり、deleted_at のようなカラムへのインデックスは、必ずしも EXPLAIN 上で使われるとは限らず、インデックスがあるのに使用されない=無駄なメンテナンスコストだけ発生する状態になります。
選択性が高く、JOINやWHEREに頻出するカラムは良い候補
逆に、一意性が高い(ユニークに近い)、またはWHERE句やJOIN句で頻繁に使われるカラムには、インデックスが非常に効果的です。
たとえば今回のSQLでは:
products.fku:多対1のJOINで結合キーとして頻出
product_images.prod_code:こちらもJOIN条件として登場
product_fku_images.fku, image_type:複数条件で絞り込みに使われている
これらは絞り込み効果が高く、インデックスによってJOIN効率が格段に向上する可能性があるため、優先してインデックスを検討するべきです。
汎用性が低すぎるインデックスの弊害
また、「なんとなく必要そうだから」と付けたインデックスが肥大化の原因になっているケースもよく見かけます。たとえば、ほとんどのクエリで使われない単体カラムに対するインデックス、あるいは順番を考えずに作成した複合インデックスなどです。
これらは以下のような問題を引き起こします:
インデックスのメモリ使用量が増える
INSERT, UPDATE, DELETE時のオーバーヘッドが大きくなる
オプティマイザが間違ったインデックスを選び、逆に遅くなる
実際、product_fkuのような静的に見えるマスタテーブルであっても、インデックスの設計は慎重に行う必要があります。
インデックス設計のコツ
効果的なインデックス設計には、以下の観点が重要です:
使用頻度と絞り込み効果を意識する
→ クエリのWHERE句・JOIN句・ORDER BYに使われているか?
複合インデックスの順序に注意する
→ 複数カラムで検索・並び替えされる場合は、左側から順に絞り込める構造になっているか?
冗長なインデックスは整理する
→ 似た構造のインデックスが複数あると、無駄なスペースとコストが発生
EXPLAINで実際に使われているかを確認する
→ インデックスを付けたら、それが実行計画で選ばれているか確認するのが鉄則
結局のところ、インデックスは「付ければ速くなる」魔法のツールではなく、「どこに・なぜ・どういう目的で付けるか」を明確にすることで初めて力を発揮します。逆に、誤ったインデックス設計は、DB全体のパフォーマンスを悪化させる危険性すらあるのです。
まとめ:実行計画×AIで高速化は誰でもできる
MySQLの高速化は、難しいようで実は「実行計画を見る」ことから始めれば誰でも取り組めます。そして、AIを活用すれば、知識が浅くても実行計画を正しく読み解けるようになります。
あなたのクエリが遅いと感じたら、まずはEXPLAINを実行して、AIに見せてみましょう。最適化の第一歩は、そこから始まります。
最後に:パフォーマンスチューニングも、まるごとお任せください
本記事では、MySQLの実行計画を読み解き、ボトルネックをAIと一緒に見つけ出す方法をご紹介しました。私たちが日頃から大切にしているのは、技術的な課題を“根っこ”から理解し、丁寧に向き合うことです。
私たち oneframe inc. では、上流の要件定義から設計・開発、運用保守に至るまで一貫した受託開発を行っています。単なる「作業の請負」ではなく、お客様のビジョンや情熱を共有し、ともに形にしていく——そんなスタイルを大切にしています。
Giving shape to your passion
あなたの情熱を、確かなカタチに。
誰かの「こうしたい!」という熱量を、私たちの技術とチームワークで最大限に引き出す。そして、その人が心躍る「クリエイティブ」に専念できる環境を提供することが、私たちの使命です。
もし「Webアプリのパフォーマンスが気になる」「開発を相談したい」「アイデアはあるけど、どう形にしたらいいかわからない」といったお悩みがあれば、ぜひお気軽にご相談ください。
一緒に、あなたの情熱を“確かなカタチ”にしていきましょう。
Discussion