「リリース当時は一瞬だった検索画面、最近やたら重い」── 1年前は 100万行だったテーブルが 1億行になっている、というのが本当の原因。「機能はそのままなのに、データ量が増えた」だけで本番が壊れるのがDBの怖いところ。
あの時こうすれば良かった、と思う症状
・「最近この画面遅くないですか?」と現場から散発的に報告 / ・p99 だけがじわじわ悪化(p50 は変わらない) / ・毎日同じ時刻にスロークエリ警告 / ・「夏休み前後で急に遅くなった」(データ量の節目)
起きる仕組み
- フルスキャン化:データ量増でインデックスを使わない実行計画にオプティマイザが切り替えた
- 統計情報の古さ:PostgreSQL `ANALYZE`、MySQL の analyze_table が走ってない
- インデックスの劣化(断片化):B-tree が更新で歪んでパフォーマンスが落ちる
- 結合順序の悪化:テーブル肥大で hash → nested loop が変わって遅延
- N+1 化:アプリ側のループで件数が増え、1回呼びが万回呼びに(次回予告)
調査手順
PostgreSQL:実行計画と統計情報
SQL
-- スロークエリの実行計画EXPLAIN (ANALYZE, BUFFERS) SELECT ... FROM big_table WHERE ...;
-- インデックスが本当に使われているかSELECT schemaname, relname, idx_scan, seq_scan, n_live_tupFROM pg_stat_user_tablesORDER BY seq_scan DESC LIMIT 20;
-- 統計情報が古くないか(last_analyze)SELECT schemaname, relname, last_analyze, last_autoanalyze, n_mod_since_analyzeFROM pg_stat_user_tablesORDER BY n_mod_since_analyze DESC NULLS LAST LIMIT 20;MySQL:スロークエリ・インデックス使用状況
SQL
-- スロークエリログから上位SELECT digest_text, count_star, sum_timer_wait/1e9 AS total_sec_waitFROM performance_schema.events_statements_summary_by_digestORDER BY sum_timer_wait DESC LIMIT 20;
-- 使われてない(or 使われすぎている)インデックスSELECT object_schema, object_name, index_name, count_fetchFROM performance_schema.table_io_waits_summary_by_index_usageWHERE index_name IS NOT NULLORDER BY count_fetch DESC LIMIT 20;短期対処
- 強制 ANALYZE:統計情報を最新化。多くのケースでこれだけで実行計画が戻る
- REINDEX / OPTIMIZE TABLE:インデックスの断片化を解消
- 実行計画ヒント:`/*+ INDEX(...) */` などでオプティマイザを誘導(最後の手段)
- 部分結果のキャッシュ:マテビューや Redis に逃がす
中長期対策
- パーティショニング:日次・月次で物理分割(古いデータは別領域へ)
- インデックス再設計:複合インデックス・部分インデックス・カバリングインデックス
- 読み込みレプリカ:分析・ 用は別レプリカへオフロード
- OLTP と OLAP の分離: → ( / )に集計を移送
- 継続的計測:pg_stat_statements / Datadog DBM で `query_id` ごとの p99 を時系列追跡
- スキーマ進化のレビュー:「テーブルに列を1個足す」変更でも実行計画が変わる前提でレビュー
ふくふくの進め方
「重い画面の上位10」を pg_stat_statements / DBM ログから抽出 → 各々に最低1つの 再生PR(インデックス追加 / クエリ書き換え / マテビュー化)を提示するのが最初の一手。3週間で「重い順 TOP10 → 全部 1秒以下」が現実的なゴールです。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。