シナリオ
アプリケーションは順調に動作していましたが、突然、新規ユーザー登録や注文処理がすべて失敗するようになります。本番環境のログには、次のような不可解なメッセージが記録されています。
ERROR: integer out of range
これは通常、最もアクティブなテーブルへの標準的なINSERT操作中に発生します。これは、列(多くの場合、自動インクリメントの主キー)が上限値に達したことを意味します。データベースは新しいデータの受け入れを拒否し、アプリケーションを実質的に停止させてしまいます。
発生理由
PostgreSQLの標準的なINTEGER(またはINT4)データ型には、符号付き32ビット整数が使用されます。この型には次のような厳格な制限があります。
- 最小値: -2,147,483,648
- 最大値: 2,147,483,647
シーケンスや手動入力で2,147,483,648を保存しようとすると、システムは失敗します。これは典型的な「成功ゆえの問題」です。データベースが当初のスキーマ設計の想定よりも早く成長し、正の数に使用できる31ビットを使い果たしてしまったのです。
迅速な修正:直接的な型変更
500万行未満の小規模なテーブルであれば、通常は直接変更が可能です。数分間のダウンタイムや一時的なテーブルロックが許容できる場合は、BIGINT(INT8)を使用してください。これにより、上限は922京という、ほとんどのユースケースで実質的に無限と言える値まで拡張されます。
-- 列の型を直接変更する
ALTER TABLE orders ALTER COLUMN id TYPE bigint;
警告: このコマンドには注意が必要です。テーブル全体の書き換えが発生し、ACCESS EXCLUSIVEロックが保持されます。2億行あるテーブルの場合、この操作には1時間かかる可能性があり、その間は誰もテーブルの読み書きができなくなります。
安全な修正:ダウンタイムなしの移行
高トラフィックな環境では、より慎転なアプローチが必要です。メインのテーブルを数時間もロックすることはできません。代わりに、バックグラウンドでデータを移動するマルチステップ戦略を採用します。
1. 一時的なBIGINT列を追加する
まず、最終的に主キーとなる新しい列を作成します。
ALTER TABLE orders ADD COLUMN id_new bigint;
2. 管理可能なバッチ単位でデータを同期する
5億行を一度に更新しようとしてはいけません。先行書き込みログ(WAL)が肥大化し、サーバーがクラッシュする可能性があります。代わりに、5万行から10万行ずつのチャンクで更新してください。
-- バッチ更新ロジックの例
UPDATE orders SET id_new = id
WHERE id_new IS NULL
AND id BETWEEN 1 AND 100000;
3. トリガーでデータの同期を維持する
古いデータの移行中も、新しい行は追加され続けます。INSERTやUPDATEが発生するたびに、idの値をid_newに自動的にコピーするトリガーを作成します。
4. 最終的な入れ替え
列の同期が完了したら、単一のトランザクション内で入れ替えを実行します。これにより、重いロックがかかる時間をわずか数ミリ秒に短縮できます。
BEGIN;
LOCK TABLE orders IN ACCESS EXCLUSIVE MODE;
-- 名前を入れ替える
ALTER TABLE orders RENAME COLUMN id TO id_old;
ALTER TABLE orders RENAME COLUMN id_new TO id;
-- シーケンスが新しい列を指すように更新する
ALTER SEQUENCE orders_id_seq OWNED BY orders.id;
COMMIT;
シーケンスの管理
SERIAL列を使用している場合、エラーの原因はシーケンス自体ではなく、列の容量にあることが多いです。ただし、シーケンスがリセットされたり遅延したりしないように確認する必要があります。現在の位置を確認するには、次のコマンドを使用します。
SELECT nextval('orders_id_seq');
移行によってシーケンスの位置がずれた場合は、テーブル内の最大のIDに手動で同期させます。
SELECT setval('orders_id_seq', (SELECT MAX(id) FROM orders));
検証
psqlコンソールでテーブルの定義を表示して、作業内容を確認します。
\d orders
IDの「Type」列がbigintになっていれば成功です。修正を確認するために、以前なら失敗していたはずの値を挿入してみます。
INSERT INTO orders (id, status) VALUES (3000000000, '検証済み');
その行が保存されれば、オーバーフローの問題は解決です。
重要なポイント
- デフォルトでBIGINTを使用する: 新規プロジェクトでは、すべての主キーに
BIGINTを使用してください。ストレージコストは4バイトではなく8バイトになりますが、将来の本番環境での停止を避けるためのコストとしては非常に安価です。 - 外部キーを確認する: 主キーを
BIGINTに変更する場合、それを参照しているすべての外部キーも変更する必要があります。そうしないと、JOIN操作が失敗するか、極端に遅くなります。 - プロアクティブな監視: クラッシュするまで待たないでください。定期的に
information_schema.sequencesをクエリして、21億の制限の80%に達している列がないか確認しましょう。

