Web アプリを運用していると、「最近ちょっとレスポンスが遅い」「ピーク時にタイムアウトが出る」「インフラ費が地味に膨らんでいる」といった声が必ず出てきます。
こうしたとき、つい 「スケールアップして様子を見る」 という対症療法に走りがちですが、多くの場合は アプリ側 or データ側にもっと根本的な原因 が隠れています。
この記事では、コードもインフラもまとめて見直すときの王道アプローチを、SQL 最適化・キャッシュ戦略・不要リソース整理 の3つを軸に整理します。
1. まず「計測」から始める
パフォーマンス改善でいちばんやってはいけないのが、計測せずにいきなり手を入れる ことです。「たぶんここが遅いはず」で改修してもほぼ外します。
最低限、以下は揃えておきたいところ。
- APM ツール(Datadog APM / New Relic / Sentry Performance など)でリクエストごとのレイテンシ内訳を可視化
- スロークエリログを必ず有効化(MySQL なら
slow_query_log、PostgreSQL ならlog_min_duration_statement) - p50 / p95 / p99 のレイテンシを見る(平均だけ見ると重要な悪化を見逃す)
計測した結果、「リクエスト全体の8割の時間がDB待ちだった」 ということもザラにあります。そのケースで CPU を増やしても効果はほぼゼロです。
2. SQL クエリ最適化
多くの Web アプリで、レスポンス時間の支配項は DB クエリ です。ここを叩くと最も費用対効果が高いです。
2-1. N+1 クエリを潰す
ORM を使っているとほぼ確実に発生するのが N+1 問題。リストを取得したあとに、各要素ごとに関連テーブルへ追加 SELECT が走るパターンです。
# 悪い例:ユーザー数 N に対して N+1 回のクエリ
users = User.objects.all()
for u in users:
print(u.profile.bio) # ← ここで毎回 SELECT が走る
# 良い例:JOIN で1回にまとめる
users = User.objects.select_related("profile").all()
for u in users:
print(u.profile.bio)ORM の eager loading / preload / JOIN 系のメソッドを使って、「リクエスト1回 = クエリ数定数」 を目指します。
2-2. 必要なカラムだけ取る
SELECT * は楽ですが、TEXT/JSON カラムや BLOB が混ざるとあっという間にデータ転送量が増えます。使うカラムだけ列挙 するだけで、レスポンスがガクンと速くなることがあります。
2-3. インデックスを EXPLAIN で確認する
遅いクエリは必ず EXPLAIN(PostgreSQL なら EXPLAIN ANALYZE)で実行計画を確認します。
- Seq Scan / Full Table Scan が見えたら、インデックスが効いていないサイン
- WHERE 句の左辺を関数で包んでいる(例:
WHERE LOWER(email) = ?)とインデックスが使われない - 複合インデックスは カラムの並び順 が重要(先頭カラムでフィルタしないと効かない)
2-4. ページネーションは OFFSET より cursor で
大量データで LIMIT 20 OFFSET 100000 のような書き方は、内部で 100,020 行を読んで先頭 100,000 行を捨てる動きをします。
無限スクロールのような UX なら、カーソル方式(WHERE id < ? ORDER BY id DESC LIMIT 20)に切り替えると、ページが深くなっても速度が落ちません。
3. キャッシュ戦略
クエリを速くするのが王道ですが、それでも足りない・そもそも計算結果を毎回作る必要がないものは キャッシュ で解決します。
- CDN キャッシュ(CloudFront / Cloudflare)— 画像・CSS・JS、それからキャッシュ可能な GET API は CDN で逃がす
- アプリケーションキャッシュ(Redis / Memcached)— 集計結果やマスタデータなど、計算コストが高く更新頻度が低いものを置く
- DB レイヤのキャッシュ(クエリキャッシュ・マテリアライズドビュー)— 重い集計を事前計算しておく
キャッシュを入れるときは TTL とキャッシュキー設計 をセットで考えるのがポイント。「気付いたら古いデータを返し続けていた」を防ぐため、無効化の仕組みも最初から設計しておきます。
4. 不要なリソースを削除する
パフォーマンス改善というと「速くする」イメージが強いですが、「使っていないものを止める / 消す」 ことで実質的に高速化&コスト削減ができるケースも多いです。
4-1. 起動しっぱなしのインスタンスを止める
検証用に立てたまま忘れている EC2 / RDS / ElastiCache、止め忘れた踏み台、テスト用 ECS タスク…。タグでオーナーと用途を必須化 し、定期棚卸しでオーナー不明のリソースは止める運用にすると、コストもパフォーマンスも自然に整います。
4-2. 使われていないテーブル・カラム・インデックス
使われていないインデックスは、書き込みコストを増やすだけのお荷物になります。PostgreSQL なら pg_stat_user_indexes、MySQL なら sys.schema_unused_indexes から 「一度も使われていないインデックス」 を抽出して定期的に整理します。
4-3. ログ・古いデータの保持期間を見直す
CloudWatch Logs、アプリログ、監査ログ、無限に伸びるテーブル…。保持期間を明確に決めて自動削除 するだけで、ストレージ I/O とコストの両方が軽くなります。
5. 改善を継続的に回す仕組み
パフォーマンスは「一度直して終わり」にはなりません。コード追加・データ増加でいつでも悪化します。
- 主要 API の p95 レイテンシをダッシュボード化 し、閾値超えでアラート
- リリース前後の 負荷テスト / ベンチマークを CI に組み込む
- スロークエリは Slack 通知するなど、変化に気付ける導線を作る
「重くなったら直す」ではなく 「重くなりかけたら気付ける」 仕組みを先に整えておくと、本番でユーザーから指摘される前に対処できます。
まとめ
パフォーマンス改善は、いきなり「スケールアップで解決」に走ると無駄なコストと労力を払いがちです。以下の順で見ていくと、費用対効果が高いポイントから着手できます。
- まず計測して、ボトルネックを事実ベースで特定する
- SQL クエリを最適化する(N+1、インデックス、必要カラムだけ)
- 足りない分はキャッシュで逃がす
- 不要なリソースは削る(インスタンス・インデックス・古いデータ)
- 継続的に監視できる仕組みを作る
「速いアプリ」は良いコードと良いインフラの両方から生まれます。次回以降は、それぞれをさらに深掘りした記事も書いていく予定です。
