巨大テーブルを分割して既存DBを軽くする方法
「access_logs テーブルが3億行を超えて、集計バッチが朝までに終わらなくなった」「notifications テーブルへの DELETE がロックを取って本番が固まった」——サービス開始から数年経ったプロダクトで、こうした相談を受けることが増えています。原因の多くは1つのテーブルに履歴やイベントを無限に積み続けたことです。
厄介なのは、こうしたテーブルはサービスの中核機能ではないのに、放置すると全体を巻き込んで壊れることです。この記事では、巨大テーブルへの対処の選択肢と、特に難しい「すでに本番で使われている巨大テーブル」への後付け対応の進め方を整理します。
まず症状を具体的に特定する
「テーブルが大きくて重い」は原因ではなく症状です。着手前に、どの種類の重さかを切り分けます。
- クエリが遅い:
EXPLAIN (ANALYZE, BUFFERS)でインデックスが効いているか確認する。数億行でもインデックスが正しく使われていれば数十ms以内に返ることは珍しくない。インデックスなしのフルスキャンで遅いなら、まず疑うべきはパーティションではなくインデックス設計 - メンテナンスが終わらない:
VACUUM/ANALYZEが数時間かかる、pg_dumpのバックアップ時間が伸び続けている、インデックス再構築でロックが長引く - 書き込みが詰まる: 古いレコードの
DELETEが大量行を巻き込みロック時間が伸びる、テーブル肥大化でINSERT自体は速くてもトランザクションログの負荷が増える - ディスクが圧迫される: テーブル本体よりインデックスのほうが大きくなっている、不要データがディスク費用を押し上げている
パーティショニングは「メンテナンスが終わらない」「古いデータの削除・アーカイブがロックを取る」に効きます。単純な検索遅延はインデックス追加で解決することが多く、パーティション化は目的に合わなければ効果が出ないまま複雑さだけ増えます。
選択肢は4つ、優先順位がある
- インデックス見直し: 複合インデックス・部分インデックス(
WHERE deleted_at IS NULL等)で解決するなら最優先。コストが最も低い - 削除ポリシーの導入: そもそも要らないデータを残していないか。「念のため全部残す」は多くの場合コストに見合わない。法務・監査要件を確認した上で、保持期間を決めて定期削除するだけで解決するケースがある
- アーカイブ(別ストレージへの退避): 頻繁に参照しない古いデータをS3やBigQuery、別のアーカイブ用DBに移す。アプリからの参照頻度が低いログ・イベントに向く。本体テーブルは小さく保てるが、アーカイブ先へのアクセス経路(たまに参照したいとき用)を別途用意する必要がある
- パーティショニング: 削除・アーカイブしたいがアプリからは引き続き同じテーブルとして参照したい場合。日付やIDレンジで物理的に分割し、古いパーティションを
DETACHするだけで実質O(1)の削除ができる
優先順位を無視してパーティショニングから着手すると、「本当はインデックスで直る問題を複雑な仕組みで解決してしまう」ことになりがちです。まず1と2を検討し、それでも解決しない継続的な蓄積型テーブルにだけ3・4を検討します。
パーティション設計の勘所
PostgreSQLの宣言的パーティショニング(PARTITION BY RANGE / LIST / HASH)を前提に、実務で効く判断基準です。
- キーは「削除・アーカイブの単位」に合わせる: ログ・履歴系はほぼ確実に日付範囲(月次 or 週次)。「今月分だけ検索したい」「先月分は消したい」という運用と一致させるのが鉄則。IDやハッシュでの分割は書き込み分散には効くが、古いデータの一括削除ができないため、ログ・履歴用途にはほぼ向かない
- パーティション数は増えすぎない粒度にする: 月次で切ると10年で120パーティション。プランナのオーバーヘッドが無視できなくなるのは数百を超えたあたりから。日次パーティションは書き込み量が非常に多いテーブル以外では過剰
- クエリの
WHERE句にパーティションキーを必ず含める設計にする: パーティションキーを含まないクエリは全パーティションをスキャンする(パーティションプルーニングが効かない)。既存のクエリパターンを洗い出し、日付条件のないクエリが残っていないか確認する - インデックスは各パーティションに個別に作られる: 親テーブルにインデックスを定義すれば子パーティション作成時に自動で継承されるが、既存の巨大インデックスを一発で作り直すわけではないので、移行時の負荷は分散して考える
- 自動化を仕込む: 新しい月のパーティションを毎月自動作成し、古いパーティションを自動
DETACHする仕組み(pg_partman等の拡張、または定期バッチ)を用意しないと、結局「誰かが手で作る」運用に戻って忘れられる
既存の巨大テーブルへの後付けが本当に難しい理由
新規テーブルなら最初からパーティション化して作ればいいだけですが、すでに数億行入っている本番テーブルを無停止でパーティション化するのが実務上の核心的な難所です。PostgreSQLは既存の非パーティションテーブルを直接パーティション化するコマンドを持っていません。
代表的な進め方は次の通りです。
- 新しいパーティション親テーブルを別名で作成する
- トリガーまたはアプリ側のデュアルライトで、新規の書き込みを旧テーブルと新テーブルの両方に流す
- 旧テーブルのデータを新テーブルへバッチでコピーする(数億行を一度に流すとレプリケーション遅延やロックの原因になるため、数千〜数万行単位でループしてスリープを挟みながら移す)
- コピー完了後、件数・チェックサムで整合性を検証する
- テーブル名をアトミックに入れ替える(
ALTER TABLE ... RENAMEはトランザクション内で高速に完了するため、切り替え自体の停止時間は数秒〜十数秒に抑えられる) - 旧テーブルは一定期間残してロールバック可能にしてから削除する
このプロセスはアプリケーションコードの変更を必要としない設計にするのが理想です(テーブル名は変わらない)。一方で、外部キー制約・シーケンス・パーティション未対応の一意制約(パーティションキーを含まないUNIQUE制約はPostgreSQLでは張れない)など、既存スキーマの制約と衝突する箇所が必ず出てきます。ここの洗い出しに、想定より時間がかかることが多いです。
いきなりやらない、が正しい判断であることも多い
パーティショニングは強力ですが、次に当てはまる場合は「今はやらない」が正しい判断です。
- テーブルは大きいが増加ペースが鈍化していて、数年は今のインデックス構成で耐えられる見込みがある
- クエリパターンが不安定でパーティションキーが確定しない(将来の機能追加で検索軸が変わりそう)
- チームにPostgreSQLのパーティション運用の知見がなく、後付けの移行より先に単純なアーカイブ+削除ポリシーで大部分の効果が出せる
「巨大テーブルがあるから不安」という感覚だけで着手すると、費用対効果の低いプロジェクトになりがちです。まず現状のクエリ実行計画とディスク使用量を計測し、放置した場合に何ヶ月後にどの障害が起きるかを見積もってから判断するのが安全です。この見極め方は既存プロダクトの改善、外部チームが最初に見る観点でも触れている通り、着手前の現状把握が改善の質を左右します。DBの重さ全般の切り分け方はDBボトルネックで遅いWebサービスの改善手順も参考にしてください。
外注時の進め方
パーティショニングの後付けは失敗すると本番影響が大きいため、外部に依頼する場合は次の順で進めるのが安全です。
- 診断フェーズを独立させる: いきなり移行作業を発注せず、まず現状のテーブルサイズ・クエリパターン・増加ペースを計測し、パーティショニングが本当に必要かを判定するフェーズを分ける
- リハーサル環境で移行手順を検証する: 本番相当のデータ量を用意したステージングで、バッチコピーの所要時間とロック影響を事前に計測する。「本番でやってみたら想定より時間がかかった」を避ける
- ロールバック手順を先に確定する: 切り替え後に問題が出た場合、旧テーブルに戻せる猶予期間を明確にする
まとめ
- 「テーブルが大きくて重い」はまずインデックスとクエリの切り分けから。パーティショニングは削除・アーカイブが絡む蓄積型テーブルに効く打ち手
- パーティションキーは「削除・アーカイブの単位」に合わせ、既存の巨大テーブルへの後付けは新テーブル作成+デュアルライト+バッチコピー+アトミックな入れ替えが定石
- 増加ペースが鈍い、キーが確定しない場合は「今はやらない」判断も正しい。着手前に診断フェーズを独立させて費用対効果を見極める
torcheeesでは、既存プロダクトの診断からDB改善を含む改善支援まで、既存の巨大テーブル・パフォーマンス課題への対応を行っています。まずは現状のテーブルサイズとクエリパターンの棚卸しから、お問い合わせください。