業務システムの遅さは、その相当部分がSQLに起因します。前回の切り分けで DB が候補に挙がったら、この回の内容が直接使えます。
遅いSQLの原因はパターン化できます。まず の読み方を押さえ、そのうえで頻出する10種類を並べます。
1. 実行計画に答えが書いてある
実行計画 は、データベースがそのSQLをどう処理するつもりかを示した手順書です。`EXPLAIN` で確認できます。SQLが遅い理由は、ほぼここに書いてあります。
-- PostgreSQL: ANALYZE を付けると実際に実行して実測値も出るEXPLAIN (ANALYZE, BUFFERS)SELECT o.id, o.total, c.nameFROM orders oJOIN customers c ON c.id = o.customer_idWHERE o.created_at >= '2026-09-01' AND o.status = 'paid';
-- MySQLEXPLAIN ANALYZESELECT ...;見るべき点は3つに絞れます。何行読んだか、見積もりと実際がずれていないか、索引を使ったかです。
| 見る箇所 | 危険な状態 | 意味 |
|---|---|---|
| 読んだ行数 | 返す行数に対して桁違いに多い | 無駄に読んでいる |
| 見積もり行数と実際 | 大きく乖離している | 統計情報が古い。判断を誤っている |
| 走査方法 | 少数しか返さないのに | 索引が使えていない |
| 結合方法 | 件数が多いのに1件ずつ突き合わせ | 結合方式の選択を誤っている |
データベースは統計情報をもとに処理方法を決めます。統計が古いと、100万行あるのに100行だと思い込み、その前提で最適でない方法を選びます。見積もりと実際が桁で違うなら、まず統計の更新を疑ってください。
2. 頻出10パターン
以下は現場で繰り返し出会うものです。上から順に頻度が高いという並びにしてあります。
① 索引が使える形になっていない
列に関数や演算を適用すると、 が使えなくなります。条件に合う行を索引から引けず、全件を読んで1行ずつ計算することになります。
-- 悪い例: created_at に関数を適用しているため索引が使えないSELECT * FROM ordersWHERE DATE(created_at) = '2026-09-01';
-- 良い例: 列はそのまま、範囲で指定するSELECT * FROM ordersWHERE created_at >= '2026-09-01' AND created_at < '2026-09-02';② 複合索引の列順が条件と合っていない
複数列の索引は先頭の列から順に使われます。`(status, created_at)` の索引は `status` だけの条件では使えますが、`created_at` だけの条件では使えません。条件の指定順ではなく、索引の定義順で決まります。
③ 少数しか返さないのに全件走査している
フルスキャン 自体は悪ではありません。表の大半が該当するなら全部読むほうが速いからです。問題なのは、数件しか返さないのに数百万行を読んでいる場合です。
④ 統計情報が古い
前述のとおり。大量の投入や削除の直後は、統計が実態とずれます。バッチ処理の後に統計を更新する運用にしておくと、この種の事故は減ります。
⑤ 結合の順序・方式が不適切
複数の表を結合するとき、どれを先に絞るかで読む量が大きく変わります。先に絞れば後段が小さくなり、後から絞ると大量の中間結果を作ることになります。
⑥ 結合で行が増えている
結合条件が不十分だと、意図せず行数が掛け算で増えます。返ってくる件数が想定より多いなら、集計する前に行数がどうなっているかを確認してください。この事故は データ基盤トラブル事件簿 EP.07「JOIN の多重度」 に詳しくまとめてあります。
⑦ 必要のない列を取っている
`SELECT *` で全列を取ると、転送量とメモリ使用量が増えます。特に大きなテキスト列やバイナリ列が含まれていると影響が大きい。加えて、必要な列だけなら索引だけで完結できる場合があり、表本体を読まずに済むことがあります。
⑧ 並べ替えが重い
`ORDER BY` で大量の行を並べ替えると、メモリに収まらず一時領域を使います。索引の並び順を利用できれば並べ替え自体が不要になるため、頻繁に使う並び順は索引の設計に反映する価値があります。
⑨ サブクエリが行ごとに実行されている
相関サブクエリは、外側の行ごとに実行されることがあります。1000行なら1000回です。これは SQL の中で起きる と言えます。結合や集計に書き換えると1回で済みます。
-- 悪い例: customers の行ごとにサブクエリが走るSELECT c.id, c.name, (SELECT COUNT(*) FROM orders o WHERE o.customer_id = c.id) AS order_countFROM customers c;
-- 良い例: 先に集計してから結合するSELECT c.id, c.name, COALESCE(o.order_count, 0) AS order_countFROM customers cLEFT JOIN ( SELECT customer_id, COUNT(*) AS order_count FROM orders GROUP BY customer_id) o ON o.customer_id = c.id;⑩ ページングが深いところで遅い
`OFFSET` で読み飛ばす方式は、飛ばす分も読んでいます。1000ページ目を出すには、その手前を全部読んでから捨てることになります。前ページの最後の値を基準に絞る方式へ変えると、深さに関係なく一定の速さになります。
3. 直す前に、そのクエリが本当に必要か
10パターンを挙げましたが、最も効く改善は「そのクエリを実行しないこと」です。チューニングに入る前に一度確認する価値があります。
- 画面に表示されていない項目のために取っていないか — 使われない集計は珍しくない
- 毎回計算する必要があるか — 日次で十分なら事前に集計しておける
- 取得件数の上限があるか — 上限なしの一覧は、いずれ必ず破綻する
- 同じ問い合わせを繰り返していないか — EP.6 の の対象になる
3番目は設計上の落とし穴です。開発時は数十件でも、本番で数万件になる一覧は多く、そのときには一覧を出すこと自体が無理になります。件数の上限は最初から入れておくべき制約です。
4. 直したことを確認する
SQL の改善は、本番相当のデータ量で確認しないと意味がありません。開発用の少量データでは、索引の有無で差が出ないためです。
加えて、改善前後で結果が一致することを必ず確認してください。書き換えの過程で結合条件や絞り込みが変わり、速くなったが結果が違うというのは実際に起きます。件数と代表的な値を突き合わせるだけでも、大半の事故は防げます。
性能改善の文脈では、結果が変わっていないことの確認が省略されがちです。特に集計クエリの書き換えは、行の重複や NULL の扱いで結果が変わりやすい。改善前後の結果を比較する手順を、作業の一部として組み込んでください。
5. どこまでやるか
SQL のチューニングは、やろうと思えばいくらでも深掘れます。ヒント句を与える、書き換えを重ねる、専用の索引を増やす ── しかし深く作り込むほど、データの分布が変わったときに崩れます。
EP.1 で決めた目標値に届いたら止める、というのが実務的な線です。目標がないと、際限のない作業になります。そして深い作り込みは、後任者にとって理解しにくい負債にもなります。
答えは実行計画に書いてある。見るのは読んだ行数・見積もりのずれ・索引の使用・結合方式の4点。頻出パターンの上位は索引が使える形になっていないこと。直す前にそのクエリが必要かを問い、直したあとは結果が変わっていないことを確認する。次回は索引の設計そのものを扱います。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。