みなさん、こんにちは。
先日、PostgreSQLを使っているシステムのバージョンアップ作業中に、ちょっとした不具合修正を行いました。内容はこんな感じです。
「データ件数の増加に伴い、メイン画面や管理画面のクエリ実行時間が増大。タイムアウトが発生する問題を修正」
調べてみると、原因はいたって当然のことでした。sensor_log という膨大な時系列テーブルに対して、インデックスがない状態で複雑なJOINをしていたこと、そしてアプリケーション側で「N+1問題」が発生していたことです。
開発初期の少量のデータではサクサク動いていたので、問題が表面化しなかったんですね。実際の運用でデータが積み上がってから悲鳴を上げる……という、典型的なケースでした。
この経験から、「スピードを落とさずに、どうやって将来の爆弾を防ぐか」について、私なりの教訓をまとめてみたいと思います。
なぜ開発時に気づけなかったのか
このシステムのプロトタイプは「とにかく早く動くものを!」という方針で開発されました。
これ自体は、ビジネスとして非常に正しい判断です。仮説検証のためのプロトタイプに、最初から本番級のガチガチなパフォーマンス設計を求めるのは、正直「過剰投資」になってしまいます。
ただ、今回の問題点は「プロトタイプで妥協した箇所」がどこにも記録されていなかったことにありました。
開発した本人の頭の中には「ここは一旦逃げたな」という自覚があったはずです。でも、それがコードにもドキュメントにも残っていなかった。結果として、プロトタイプがそのまま「正式なプロダクト」として育ってしまったのが盲点でした。
開発時からパフォーマンスを意識するための4つの習慣
もし「初期段階からもう少しだけ意識しておきたい」という場合は、以下の手法が有効です。
1. 本番相当のデータ量を用意してみる
これが一番確実です。少量のデータだと、データベースはインデックスを使わずに全件走査(Seq Scan)した方が速いと判断してしまいます。特に時系列データのように増え続けるテーブルは、早い段階でダミーデータをドバッと入れてテストするのがおすすめです。
2. 「EXPLAIN ANALYZE」をクセにする
クエリを書いたら、一度実行計画を覗いてみましょう。
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT ... -- 開発中のクエリ
見るべきポイントは3つだけです。
- 大きなテーブルで Seq Scan(全件スキャン)が出ていないか?
- 推定行数と実際の行数が大きくズレていないか?
- 実行時間(actual time)が許容範囲内か?
3. スロークエリログを味方につける
postgresql.conf で設定を変えておくと、遅いクエリを勝手に教えてくれます。
log_min_duration_statement = 1000 -- 1秒以上かかったらログに出す
開発中からこれを出しておくと、無意識に書いた重いクエリにすぐ気づけます。
4. N+1問題をコードレビューで捕まえる
「ループの中でデータベースに問い合わせていないか?」
「集計処理を無理やりアプリ側のメモリでやっていないか?」
これを確認するだけで、リリース後のトラブルは激減します。
スピードを殺さない「現実的な」アプローチ
とはいえ、毎回EXPLAINをかけるのは大変ですよね。そこで、もっと低コストで将来に備える方法を提案します。
「後から直せるもの」を見極める
まず、全てを完璧にする必要はありません。以下の表のように切り分けて考えましょう。
| 後から比較的ラクに直せる | 後から直すのがめちゃくちゃ大変 |
| インデックスの追加 | テーブル設計・カラム定義の変更 |
| クエリの書き換え | データの正規化・構造の見直し |
| DBサーバの設定変更 | 外部キー制約の有無 |
| N+1問題の解消 | データ型の選択ミス |
インデックスやクエリは、後からでも「力技」でなんとかなります。でも、スキーマ設計(テーブルの形)だけは、プロトタイプ段階から丁寧に行うのが、結局一番コスパが良いんです。
「負債メモ」を残す勇気
コードの中に、一言 // TODO を残すだけで世界が変わります。
// TODO: sensor_logは月100万件ペースで増える想定。将来的にインデックス追加が必要
List<DashboardItem> list = dashboardRepository.fetchFilteredItems(condition);
完璧に設計する時間はなくても、「どこを妥協したか」を可視化しておくだけで、後から対処するコストは劇的に下がります。
ドキュメントに「データ量の想定」を1行書く
設計の片隅に、これだけ書いておきましょう。
sensor_log: 1施設あたり月100万件増加を想定
この1行があるだけで、次にそのコードを見る人の意識がガラッと変わります。
経験の浅いメンバーと一緒に開発するときは
プロトタイプ開発を若手や経験の浅いメンバーに任せる場合は、さらに注意が必要です。彼らは「逃げた」という自覚すらないまま、爆弾を埋め込んでしまうことがあるからです。
特に以下の4点は、仕組みとしてチェックするようにしましょう。
- SELECT * を使わない
通信量やメモリを無駄に食いつぶします。 - ループ内のDBアクセス(N+1)
デバイスが100台あれば100回クエリが飛ぶ……という恐ろしいことが起きます。 - インデックスの概念を伝える
外部キーにはとりあえず貼っておく、くらいのルールでも効果があります。 - ページングを実装する
「今はデータが少ないから」と全件取得(findAll)にしてしまうと、将来確実にタイムアウトします。
これらを個人のスキルに頼るのではなく、「レビューのチェックリスト」や「SQLを書く前に相談する文化」といった仕組みでカバーするのが、チームとしての正解だと私は思います。
妥協の記録が大事
今回の教訓を一言でいうなら、これに尽きます。
完璧な設計を目指すより、「どこを妥協したか」を記録する文化を作ろう
プロトタイプのスピードを維持しつつ、将来の破綻を防ぐ「最安」の対策は、コードではなく「メモ」で負債を可視化しておくことです。
これは人間同士のコミュニケーションはもちろん、生成AIを活用した開発においても、指示(コンテキスト)として非常に有用です。AIに「ここは将来的にこうしたい箇所だ」と伝える手がかりになりますからね。
「スピード重視」と「品質への誠実さ」を両立させるために、ぜひ皆で心がけていきましょう!
本日も最後までお読みいただき、ありがとうございました。
それでは、よいシステム開発を!



