Claude Media
Claude CodeでPostgreSQLをパーティション化 — 既存テーブルを止めずに移す手順

Claude CodeでPostgreSQLをパーティション化 — 既存テーブルを止めずに移す手順

PostgreSQLは通常のテーブルをパーティションテーブルに変換できません。旧テーブルを履歴パーティションとして付け替える手順と、Claude Codeに任せる範囲・止める仕組み、EXPLAINでのプルーニング確認をまとめます。

PostgreSQLでは、通常のテーブルをそのままパーティションテーブルに変えることはできません。既存の大きなテーブルを止めずに移すには、パーティションテーブルを新しく作り、旧テーブルを「履歴の1パーティション」として付け替える流れになります。この記事は、その手順の組み立てと、Claude Codeに任せてよい範囲、本番を守る設定、EXPLAINでの確認までを順に書きます。

パーティション化の前に決める3つのこと

最初に決めるのはパーティションキー、方式、パーティション数です。ここをClaude Codeに丸投げすると、もっともらしいが運用に合わない案が出ます。クエリの実態を渡して選ばせるのが筋です。

パーティションキーは、実行されるクエリのWHERE句に最も頻繁に出てくる列が第一候補です。WHERE句がパーティションの境界条件と噛み合えば、不要なパーティションを読み飛ばせるためです。古いデータをまとめて消す運用があるなら、消す単位が1つのパーティションに収まる切り方が向きます。パーティションは短時間で切り離せるからです。

方式は3つです。

方式切り方向く場面
範囲(RANGE)切り方列の値の範囲で分ける。下限は含み、上限は含まない向く場面日付で古いデータを落とす、時系列を月ごとに分ける
リスト(LIST)切り方値を明示して振り分ける向く場面地域など、取りうる値が少なく固定の列
ハッシュ(HASH)切り方法(modulus)と余り(remainder)で均等に振り分ける向く場面値が増え続ける顧客IDなど、偏りを避けたい列

顧客ごとにLISTで分ける設計は、顧客が数社から数千社に増えたときに破綻します。そういう列はHASHで適度な数に分けるほうが堅い、という注意がPostgreSQLの手引きにあります。

パーティション数は多ければよいわけではありません。少なすぎると索引が大きいままでキャッシュに載りにくく、多すぎるとクエリの計画時間とメモリ消費が増えます。1回のクエリで絞り込めずに多数のパーティションが残るほど、この負担は大きくなります。OLTPよりデータウェアハウスのほうが、多めの数を許容しやすいとも書かれています。

また、パーティション化の効果が出るのは、テーブルが物理メモリを超えるほど大きいときが目安です。そこまで大きくないテーブルでは、手間に見合わないことがあります。

既存テーブルは変換できない — 履歴パーティションとして付け替える

通常のテーブルをパーティションテーブルに変える構文はありません。一方で、既存のテーブルをパーティションとして付ける(ATTACH PARTITION)ことと、パーティションを単独のテーブルに戻す(DETACH PARTITION)ことはできます。

この性質を使うと、データを1行もコピーせずに移せます。

手順

データをコピーしない移行の流れ(例)

  1. 1

    パーティションテーブルを新規に作る

    旧テーブルと同じ列を持つ親テーブルを PARTITION BY RANGE で作り、これから先の月のパーティションを用意します。

  2. 2

    旧テーブルに境界のCHECK制約を付ける

    「この日付より前の行しかない」という制約を NOT VALID で付け、あとから検証します。これで付け替え時の全件スキャンを避けます。

  3. 3

    旧テーブルを履歴パーティションとして付ける

    ATTACH PARTITION で、過去すべてを受け持つ1つのパーティションにします。

  4. 4

    テーブル名を入れ替える

    旧テーブルを履歴用の名前に、親テーブルを本来の名前にします。アプリケーションの接続先は変わりません。

付け替えで気をつけるのは、ロックの種類です。ATTACH PARTITIONは親テーブルにはSHARE UPDATE EXCLUSIVEロックを取りますが、付けられる側のテーブルにはACCESS EXCLUSIVEロックを取ります。さらに、付けるテーブルは境界条件を満たすかどうか全件スキャンされ、そのあいだACCESS EXCLUSIVEロックが続きます。これを避けるには、付ける前に同じ条件のCHECK制約を作っておきます。制約が証明してくれるので、スキャンは省かれます。付け替えが終わったら、重複したCHECK制約は削除して構いません。

CHECK制約は NOT VALID で追加すれば、追加時の全件スキャンが省かれます。検証は別のコマンド(VALIDATE CONSTRAINT)で行い、こちらは更新を止めずに走ります。

親テーブルにDEFAULTパーティションを置く場合は、同様に「DEFAULTパーティションに該当行が無い」ことを示す制約も用意しないと、そちらが全件スキャンされます。この記事の手順では置かない前提です。

主キーと一意制約はパーティションキーを含める

見落としやすいのがここです。パーティションテーブルに主キーや一意制約を付けるとき、制約の列にはパーティションキーのすべての列を含める必要があります。パーティションキーに式や関数呼び出しを使うこともできません。各パーティションの索引は、自分のパーティション内でしか一意性を保証できないからです。

結果として、id だけだった主キーは (id, created_at) に変わります。「id だけで一意」という保証はDB側から消え、アプリケーション側の採番(シーケンスなど)に頼ることになります。外部キーで旧テーブルの id を参照している表があると、主キーの付け替えがそこで止まります。着手前に参照関係を洗い出してください。

付ける側のテーブルの索引も重要です。ATTACH PARTITIONは、親の各索引に対して、同等の有効な索引が付け先にあればそれを接続し、無ければ新しく作ります。新しく作るぶんが大きなテーブルへの索引作成になり、ロック時間が伸びます。そこで、付ける前に旧テーブル側へ (id, created_at) の一意索引を CREATE UNIQUE INDEX CONCURRENTLY で作っておき、主キー制約をその索引に差し替えておきます。

切り替えのSQL(例)

次のSQLは、orders テーブル(id、customer_id、created_at、amount)を created_at の月ごとの範囲で分ける想定の一例です。日付やテーブル名は自分の環境に置き換え、必ず本番のコピーで所要時間とロックを測ってから使います。境界は、今後の書き込みが旧テーブルの条件を破らないよう、まだ来ていない日付(ここでは2026-12-01)に置きます。切り替えはその日より前に終える必要があります。

-- 0. 準備(旧テーブル側。更新は止めない)
ALTER TABLE orders ADD CONSTRAINT orders_created_at_notnull
  CHECK (created_at IS NOT NULL) NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT orders_created_at_notnull;
ALTER TABLE orders ALTER COLUMN created_at SET NOT NULL;  -- 有効なCHECKがあれば全件スキャンを省く
 
ALTER TABLE orders ADD CONSTRAINT orders_created_at_bound
  CHECK (created_at >= '2000-01-01' AND created_at < '2026-12-01') NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT orders_created_at_bound;
 
CREATE UNIQUE INDEX CONCURRENTLY orders_id_created_at_key
  ON orders (id, created_at);
 
-- 1. 親テーブルと新しい月のパーティション
CREATE TABLE orders_new (LIKE orders INCLUDING DEFAULTS)
  PARTITION BY RANGE (created_at);
ALTER TABLE orders_new ADD PRIMARY KEY (id, created_at);
CREATE TABLE orders_2026_12 PARTITION OF orders_new
  FOR VALUES FROM ('2026-12-01') TO ('2027-01-01');
CREATE TABLE orders_2027_01 PARTITION OF orders_new
  FOR VALUES FROM ('2027-01-01') TO ('2027-02-01');
 
-- 2. 主キーを新しい索引に差し替える(短時間の排他ロックが入る)
BEGIN;
ALTER TABLE orders DROP CONSTRAINT orders_pkey;
ALTER TABLE orders ADD CONSTRAINT orders_pkey
  PRIMARY KEY USING INDEX orders_id_created_at_key;
COMMIT;
 
-- 3. 履歴パーティションとして付ける
ALTER TABLE orders_new ATTACH PARTITION orders
  FOR VALUES FROM ('2000-01-01') TO ('2026-12-01');
ALTER TABLE orders DROP CONSTRAINT orders_created_at_bound;
 
-- 4. 名前の入れ替え
BEGIN;
ALTER TABLE orders RENAME TO orders_history;
ALTER TABLE orders_new RENAME TO orders;
COMMIT;

流れの要点を3つ補足します。

  • orders_new の列の既定値(id のシーケンスなど)は INCLUDING DEFAULTS で引き継ぎます。シーケンスの持ち主(OWNED BY)は旧テーブルのままなので、旧テーブルを将来外すときに付け替えが必要です。
  • 付け替えたあとも、旧テーブルは1つの巨大なパーティションとして残ります。古いデータを落としたいなら、この履歴パーティションを切り離す(DETACH PARTITION)か、期間ごとの別テーブルに移し替える作業が別途必要です。
  • 切り離しにはCONCURRENTLYを付ける形があります。付けると親テーブルへのロックがSHARE UPDATE EXCLUSIVEに下がりますが、使える条件には制限があるので、実行前にALTER TABLEの手引きを確認してください。

Claude Codeに任せる範囲と、止める仕組み

ここまでのSQLは、Claude Codeが下書きを作り、人間が読んで本番のコピーで試す、という分担にすると事故が起きにくくなります。

計画だけを出させる

パーティション化のように後戻りしにくい変更は、最初にプランモードで始めます。プランモードのClaudeはファイルを読み、計画を提案しますが、承認するまで編集をしません。

claude --permission-mode plan

起動後に渡す指示は、決める材料を並べる形にします。例えば次のようにします。

orders テーブル(約2億行)を created_at の月単位でパーティション化したい。
db/queries/ にアプリのクエリがある。WHERE 句に出る列を集計して、
パーティションキー案と、主キー・一意制約・外部キーへの影響を表にして。
SQL はまだ書かず、旧テーブルを履歴パーティションにする手順の計画だけを出して。

承認後に、移行SQLと確認用のクエリをファイルに書かせます。実行は人間が行うのが基本です。

規約をCLAUDE.mdに書いておく

毎回のやり取りで伝えるのが面倒な制約は、プロジェクトの CLAUDE.md に書いておきます。

## DBマイグレーション
- 本番DBに直接接続しない。検証はリストア済みのコピー(rehearsal)だけ
- DDL は db/migrations/ にファイルで書く。psql -c での直接実行はしない
- ALTER TABLE は事前に lock_timeout を設定する
- パーティション化では、主キー・一意制約がパーティションキーを含むかを必ず確認する
- 変更後は EXPLAIN でプルーニングが効くことを、対象クエリ全件で確かめる

権限とフックで機械的に止める

文章の約束だけでは、守られない場面が残ります。設定ファイルで、psqlの実行に毎回確認を入れ、危険なコマンドを塞ぎます。

{
  "permissions": {
    "ask": ["Bash(psql *)"],
    "deny": ["Bash(dropdb *)"]
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-destructive-sql.sh"
          }
        ]
      }
    ]
  }
}

権限のルールは、deny、ask、allowの順に評価され、最初に当たったものが結果を決めます。細かいallowでdenyの例外は作れません。

フックの側は、標準入力にJSONで渡されるコマンド文字列を調べ、終了コード2で拒否する形です。

#!/bin/bash
cmd=$(jq -r .tool_input.command)
if echo "$cmd" | grep -qiE 'DROP TABLE|DETACH PARTITION|TRUNCATE'; then
  echo "破壊的なDDLは人間が実行します" >&2
  exit 2
fi

このフックが見るのはコマンドラインの文字列だけです。SQLファイルの中身までは検査しないため、psql -f で流すファイルの確認は、人間のレビューが受け持ちます。

EXPLAINでプルーニングが効いているか確かめる

パーティション化の効果は、不要なパーティションを読み飛ばす「プルーニング」で出ます。切り替えたあと、アプリの主要なクエリごとにEXPLAINを取り、読むべき行を決めておきます。

SHOW enable_partition_pruning;  -- on が既定値
 
EXPLAIN
SELECT count(*) FROM orders
WHERE created_at >= DATE '2026-12-01';

見るのは、実行計画に登場するパーティションの数です。プルーニングが効いていれば、条件に合う orders_2026_12 などだけが出ます。効いていなければ、すべてのパーティションが Append の下に並びます。PostgreSQLの手引きは、enable_partition_pruning を off にした場合と on の場合の計画を並べて、この差を示しています。

読むときの注意が3つあります。

  • プルーニングは、インデックスの有無ではなく、パーティションキーが作る暗黙の制約で決まります。キー列に索引が無くても効きます。
  • 実行時に値が決まる条件(パラメータやサブクエリの結果)では、計画時ではなく実行の初期化時や実行中に絞り込まれます。初期化時に除外されたパーティションは、EXPLAINにもEXPLAIN ANALYZEにも出てきません。
  • WHERE句がパーティションキーを関数で包んだり、型を変換したりしていると、境界と比較できず全パーティションが残ることがあります。キー列を素のまま比較する書き方に直します。

Claude Codeには、確認の反復を任せられます。クエリを1本ずつ流してEXPLAINの出力を読ませ、パーティションが全部残ったクエリを一覧にさせます。遅いクエリをEXPLAIN ANALYZEで絞り込む手順はClaudeでスロークエリをEXPLAIN ANALYZEで絞る手順にまとめています。

つまずきやすい点

  • パーティションに存在しない範囲の行を挿入するとエラーになる。月ごとの分割では、翌月分のパーティションを前もって作る運用が要ります。作成を忘れるとINSERTが失敗するので、パーティション作成を定期ジョブにしておきます。
  • 親テーブルにない列をパーティション側に足せない。付ける側のテーブルは、列が親と完全に一致している必要があります。旧テーブルに余分な列があれば、先に取り除きます。
  • 親に付ける索引はCONCURRENTLYを指定できない。ロックを避けるには、CREATE INDEX ON ONLY で親に無効な索引を作り、各パーティションにCONCURRENTLYで索引を作って、親の索引に接続する手順を踏みます。
  • ロックの待ち行列に巻き込まれる。長く走るトランザクションの後ろにDDLが並ぶと、後続のクエリまで止まります。DDLの前に lock_timeout を設定すると、取れなければ諦めて戻れます。ロックの待ち合いを調べる手順はClaude Codeでデッドロックの原因を調べる手順が参考になります。

まとめ

PostgreSQLでの既存テーブルのパーティション化は、変換ではなく「新しい親を作り、旧テーブルを履歴パーティションとして付ける」作業です。難所はATTACH時のスキャンを避けるCHECK制約の準備と、主キーへのパーティションキーの追加の2点に集まります。Claude Codeは、クエリの集計、計画、SQLの下書き、EXPLAINの反復確認に向きます。一方、本番での実行と、破壊的DDLの判断は人間の側に残すと決めておくほうが安全です。切り替え後に最初にやるのは、主要クエリのEXPLAINでプルーニングを確かめることです。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. postgresql.org/docs/current/ddl-partitioning.html
  2. postgresql.org/docs/current/sql-altertable.html
  3. code.claude.com/docs/en/common-workflows
  4. code.claude.com/docs/en/permissions
  5. code.claude.com/docs/en/hooks-guide
この記事を共有:XはてブLinkedIn