データベース性能の改善作業は、たいてい誰かが「もっと大きなインスタンスにしよう」と言い出すところから始まり、そしてたいていは、ページを開くたびにひとつのクエリが400万行を順次スキャンしていた、という発見で終わります。制約はもともとハードウェアではありませんでした。制約は実行計画のほうにあったのです。

この筋書きは十分に繰り返し起きるので、既定の仮説としてはっきり言葉にしておく価値があります。アプリケーションが遅く、そのうえデータベースが忙しいのなら、原因はほぼ必ず、ごく少数の特定のクエリであって、処理能力全体の不足ではありません。そしてサーバーを大きくする対処は、テーブルがふたたび育つのに必要な時間のぶんだけ、問題を覆い隠してくれるにすぎません。

何かを変える前に、まず測ってください。 当て推量で選んだクエリを最適化するのは、書き込みを遅くするだけで読み取りは少しも速くならないインデックスを、一週間かけて追加し続ける道です。どのデータベースも、どの文が合計時間をいちばん多く消費しているかを教えてくれます。そこから始めて、いちばん上にある文を直し、もう一度測る。この往復を二度か三度繰り返せば、たいていの障害はそれで収束します。


データベース性能は問題のクエリを見つけることから始まる

最悪値よりも合計時間のほうが重要です。2秒かかるクエリでも、一日に二度しか走らないのなら関係ありません。40ミリ秒しかかからないクエリが毎分8,000回走っているなら、そちらがあなたの問題であり、しかもしきい値を1秒に設定したスロークエリログには決して現れません。

Postgresでは、pg_stat_statements拡張がまさにこれを集計してくれます。正規化された文ごとに、呼び出し回数、合計時間、平均時間が並びます。合計時間で並べ替えれば、犯人はたいてい上位三行のなかにいます。MySQLでも、パフォーマンススキーマを通じて同じ性質の集計が得られます。

クエリそのものが悪いと結論づける前に、確かめておく価値のあることが二つあります。ひとつは、毎回遅いのか、それとも特定の時間帯だけ遅いのか。後者なら、実行計画よりも資源の奪い合いを示しています。もうひとつは、単独で実行しても遅いのか、それとも同時実行が増えたときだけ遅いのか。後者なら、ロックか接続数の上限を示しています。

推測ではなく実行計画を読む

対象の文が決まったら、その文をどう実行するつもりなのかを、データベース自身に尋ねます。PostgresはこれをEXPLAINで見せてくれます 。重要なのはEXPLAIN ANALYZEという変種で、こちらは推定値を並べるのではなく、クエリを実際に実行したうえで本物の所要時間を報告します。

その出力のうち、信号のほとんどを担っているのは三つです。

大きなテーブルに対する順次スキャン。 データベースがすべての行を読んでいます。小さなテーブルであれば、それは正しく、しかも速い。大きなテーブルでこれが出るのなら、あなたが書いた条件に使えるインデックスが存在しないか、プランナがそのインデックスは使う価値がないと判断したかの、どちらかです。

推定行数と実際の行数の大きな乖離。 プランナは統計情報をもとに戦略を選びます。ですから推定が桁違いに外れているときは、あなたのクエリとは何の関係もない理由で、まずい選択をしていることになります。統計情報が古いというのは、よくある原因であり、しかも直しやすい原因でもあります。

ひとつのノードに時間が集中している。 実行計画は木構造であり、直すべき場所は時間を消費したそのノードです。それ以外をどれだけ丁寧に最適化しても、結果は何も変わりません。

順次スキャンを見た瞬間にインデックスを足したくなる本能は、多くの場合それ自体は正しいのですが、それでも三十秒だけこらえる価値があります。インデックスが存在しないことよりも、インデックスが使われない理由のほうが重要な場合があるからです。

インデックスが効かない理由

存在しているインデックスは、必ずしも使われているインデックスではありません。

条件がsargableでない。 列を関数で包んだり、列に算術演算を施したりすると、その列に張ったインデックスはたいてい使えなくなります。インデックスが保持しているのは変換後の値ではなく、列そのものの値だからです。列を裸のまま残すように条件を書き直せば、たいていは使われる状態に戻ります。

複合インデックスの列順が誤っている。 複合インデックスが助けられるのは、その先頭の列を使うクエリです。ある列、次に別の列という順で作られたインデックスは、二番目の列だけで絞り込むクエリの役には立ちません。そしてこれは、いつまでも人を捕まえ続ける落とし穴です。

プランナがスキャンのほうが安いと判断している。 クエリがテーブルの大きな割合を返すのであれば、順に読んでいくほうが、インデックスを介して飛び回るよりも実際に速くなります。これは正しい振る舞いであり、直すべきなのはインデックスではなく、返す量を減らすことのほうです。

統計情報が古い。 大量投入や大規模な削除のあとは、プランナが持っているデータの姿は、統計情報を取り直すまでひどく間違ったままになりえます。

そして、どのインデックスにも費用があります。書き込みはそれを維持しなければならず、本来ならデータをキャッシュできたはずのメモリを占有します。インデックスが十五本もついたテーブルには、たいてい誰も必要としていないものが何本か混じっていて、その一本ごとに挿入が遅くなっていきます。

N+1問題はいまも単独では最大の原因

アプリケーションの遅さは、どんな実行計画の問題よりも、ここから多く生まれます。しかも個々のクエリはどれも速いので、スロークエリとしては決して姿を現しません。

形はおなじみのものです。100件のレコードの一覧を取得し、それを回しながら、一件ごとに関連データを取りに行く。結果として、一度か二度で済んだはずのところに101回の往復が生じます。各クエリは3ミリ秒で返ってくるのに、ページは相変わらず半秒かかる。費用は仕事の量そのものではなく、往復の回数のほうにあるからです。

オブジェクト関係マッパーは、これをうっかり書きやすくします。関連データの取得が、データベース呼び出しではなく、属性へのアクセスのように見えてしまうからです。直し方は、関連データを親の集合と一緒に一回のクエリで読み込むことで、成熟したORMならどれも対応していますが、既定の設定では大半がそうしてくれません。

検出そのものは難しくありません。リクエストあたりのクエリ数を数えるだけです。表示している項目数に比例した本数のクエリをページが発行しているなら、それが探していた相手です。これは、アプリケーションがエッジで遅いときにもっとも費用対効果の高い確認でもあります。Cloudflare Hyperdrive の解説でも触れているとおり、距離が伸びるほど往復の代償は大きくなるからです。

接続と競合

遅さのように見えて、実は遅さではない問題が二つあります。

接続の枯渇。 どのデータベースにも同時接続数の上限があり、接続はひとつごとにメモリを消費します。プールが許す数を超えてアプリケーションが接続を開こうとすると、リクエストは空きを待って行列を作り、データベースは暇そうにしているのにアプリケーションだけが遅く見えます。症状は、データベースのCPU使用率が低いままアプリケーションの応答時間だけが高いこと。そして直し方は、大きなマシンではなく接続プーリングです。

ロック競合。 ロックを握ったまま長く続くトランザクションは、その後ろに並ぶすべてを止めてしまいます。よくある原因は、データベースを必要としない処理、たとえば別のサービスへのHTTP呼び出しをまたいで、トランザクションを開いたままにしてしまうことです。トランザクションは短く保ち、データベースに対する作業そのものだけに限ってください。

どちらの場合も早めに切り分けておく価値があります。どちらもクエリの問題と読み違えやすく、そしてどちらもインデックスでは直らないからです。

何をどの順で行うか

まず、合計時間をもっとも多く消費している文を探します。そのなかで最悪のものにEXPLAIN ANALYZEをかけ、時間が実際にどこへ消えているのかを読みます。何かを最適化する前に、リクエストあたりのクエリ数を確かめてN+1を除外します。インデックスを足す前に統計情報を取り直します。それだけで解決してしまうことがあるからです。そのうえで、条件を満たす最も狭いインデックスを足し、もう一度測ります。

Mecanikはこれをソフトウェア開発 の仕事の一部として行っていますが、結末はほとんど毎回同じです。原因は二つか三つのクエリで、直しはごく小さく、誰も買っていなかった大きなインスタンスは、最後まで必要ありませんでした。


関連記事: APIのバージョニング:いつ壊してよいか、どう壊さずに済ませるか2026年にウェブアプリを構築する方法 - 英国開発者ガイドパスワードの保存:2026年に何を使うべきか英国におけるカスタムソフトウェア開発


よくあるご質問

どのクエリがアプリケーションを遅くしているのかを、どうやって見つけますか。 最悪値ではなく合計時間で並べ替えてください。40ミリ秒のクエリが毎分8,000回走るほうが、2秒のクエリが一日に二度走るよりもはるかに高くつきますし、しきい値を1秒に設定したスロークエリログには決して現れません。Postgresではpg_stat_statementsが文ごとの呼び出し回数と合計時間を集計し、MySQLでもパフォーマンススキーマから同じものが得られます。

EXPLAIN ANALYZEの出力では何を見ればよいですか。 信号のほとんどを担うのは三つです。大きなテーブルへの順次スキャンは、使えるインデックスがないという意味です。推定行数と実際の行数の大きな乖離は、プランナが悪い統計情報のもとで働いているという意味です。そして実行計画の木のひとつのノードに時間が集中しているなら、直すべき場所はまさにそこです。ほかのノードを最適化しても何も変わりません。

作ったインデックスがなぜ使われないのですか。 たいていは四つの理由のどれかです。条件が列を関数や算術で包んでいて、インデックスが噛み合わなくなっている。複合インデックスの列順がそのクエリを支えていない。クエリがテーブルの十分に大きな割合を返すため、スキャンのほうが本当に安い。あるいは大量投入や削除のあとで統計情報が古くなっている。

N+1クエリ問題とは何ですか。 レコードの一覧を取得したうえで、一件ごとに関連データ用の別のクエリを発行し、一度か二度で済んだはずのところに101回の往復を作ってしまうことです。各クエリが速いためスロークエリログには現れず、費用は往復のほうにあります。リクエストあたりのクエリ数を数え、表示している項目数に比例していないかを見れば検出できます。

データベースサーバーを大きくすれば遅いクエリは直りますか。 めったに直りませんし、直っても一時的です。アプリケーションが遅く、データベースが忙しいとき、原因はほぼ必ず処理能力の不足ではなく、ごく少数の特定のクエリです。ですから増強はテーブルがふたたび育つまで問題を覆い隠すだけです。データベースのCPU使用率が低いままアプリケーションの応答時間だけが高い場合は、たいてい接続プールの枯渇を示しており、そちらはハードウェアを足しても解決しません。