エラーの内容
ERROR: out of shared memory
HINT: You might need to increase max_locks_per_transaction.
大規模なマイグレーション中にこのエラーに遭遇しましたか?あなただけではありません。このエラーは、1つのトランザクションがPostgreSQLの追跡スロット数を超えるロックを取得しようとした際に発生します。数百のテーブルに触れるマイグレーション、大量の一時テーブルを作成するバルクETLジョブ、あるいは200以上のパーティションを持つパーティションテーブルが代表的な原因です。PostgreSQLが表示するヒントは的確で、max_locks_per_transactionがほぼ常に問題の根本です。
根本原因
起動時に、PostgreSQLは共有メモリ内に固定サイズのロックテーブルを事前に確保します。そのサイズは以下によって決まります:
max_locks_per_transaction × (max_connections + max_prepared_transactions)
デフォルトの max_locks_per_transaction は 64 です。つまり、1つのトランザクションがテーブル、インデックス、シーケンス(それぞれ個別にカウント)を合わせて64以上のオブジェクトをロックすると、ロックテーブルが枯渇してこのエラーが発生します。
主なトリガーとなる状況:
- DjangoやRailsのマイグレーションが1つのトランザクション内で多数のテーブルにインデックスや制約を追加する場合
- LiquibaseやFlywayのチェンジセットが大規模スキーマ(50以上のテーブル)に対して実行される場合
- 数百の一時テーブルを明示的に削除せずに作成するバルクETLジョブ
- パーティションテーブル — 各パーティションが独自のロックオブジェクトを持つため、200の月次パーティションを持つテーブルはクエリごとに200スロットを消費する
- 長いトランザクション内で積み重ねられた明示的な
LOCK TABLE呼び出し
修正方法1:max_locks_per_transactionを増やす(主な修正)
まず現在の値を確認します:
SHOW max_locks_per_transaction;
次に postgresql.conf を編集します。場所がわからない場合はPostgreSQLに直接確認できます:
SELECT current_setting('config_file');
-- 一般的なパス:
-- Ubuntu/Debian: /etc/postgresql/16/main/postgresql.conf
-- Docker: /var/lib/postgresql/data/postgresql.conf
値を増やします:
# postgresql.conf
max_locks_per_transaction = 256 # デフォルト64から増加
この設定には完全な再起動が必要です。リロードでは不十分です:
# systemd
sudo systemctl restart postgresql
# Docker
docker restart your_postgres_container
# pg_ctl
pg_ctl restart -D /var/lib/postgresql/data
変更が反映されたことを確認します:
SHOW max_locks_per_transaction;
-- 256が返されるはずです
修正方法2:コード内のロック使用量を減らす
RDS、Cloud SQL、Supabaseなどのマネージドデータベースでは、postgresql.conf を変更できないことが多いです。その場合は、コードが取得するロックの数を減らすことが優先されます。
大規模マイグレーションを小さなトランザクションに分割する:
-- 悪い例: 1つのトランザクションで200テーブルに触れる
-- 良い例: トランザクションごとに20〜30テーブルずつ処理
BEGIN;
ALTER TABLE orders ADD COLUMN processed_at TIMESTAMPTZ;
ALTER TABLE order_items ADD COLUMN unit_cost NUMERIC(10,2);
-- 1トランザクションあたり約50操作以内に抑える
COMMIT;
新しい一時テーブルを作成する前に明示的に削除する:
DROP TABLE IF EXISTS tmp_processing;
CREATE TEMP TABLE tmp_processing AS
SELECT id FROM orders WHERE status = 'pending';
ON COMMIT DROP を使用して一時テーブルを自動クリーンアップする:
BEGIN;
CREATE TEMP TABLE batch_data (
id BIGINT
) ON COMMIT DROP; -- COMMITで自動削除され、ロックが即座に解放される
INSERT INTO batch_data SELECT ...;
-- 処理を実行
COMMIT; -- 一時テーブルは削除される
修正方法3:パーティションテーブルの場合
パーティションテーブルはこのエラーの見落とされがちな原因です。各パーティションが独自のロックを保持するため、365パーティションのイベントログに触れるクエリは、実際の処理を始める前に365以上のオブジェクトをロックします。
-- テーブルのパーティション数を確認する
SELECT count(*)
FROM pg_inherits
WHERE inhparent = 'events'::regclass;
おおよその目安として、max_locks_per_transaction を少なくとも パーティション数 × 2 に設定してください。
# 365の日次パーティションを持つテーブルの場合:
max_locks_per_transaction = 1024
また、パーティションプルーニングが有効になっているか確認してください。PG 12以降はデフォルトで有効で、クエリのWHERE句に一致しないパーティションをプランナーがスキップするよう指示します:
SHOW enable_partition_pruning;
-- 'on' が返されるはずです
修正方法4:ALTER SYSTEM(ファイル編集不要)
スーパーユーザー権限があるがSSHで接続して設定ファイルを編集したくない場合は、ALTER SYSTEM を使用します:
ALTER SYSTEM SET max_locks_per_transaction = 256;
-- この設定にはpg_reload_conf()では不十分です
SELECT pg_reload_conf(); -- これはスキップ
-- 完全な再起動が必要です
確認方法
再起動後、設定を確認し実際のロック使用状況を観察します:
-- 新しい値を確認
SELECT name, setting, unit
FROM pg_settings
WHERE name = 'max_locks_per_transaction';
-- 現在のロックのスナップショット(問題の操作が進行中に実行)
SELECT relation::regclass, mode, granted, pid
FROM pg_locks
WHERE relation IS NOT NULL
ORDER BY pid, relation;
-- 各バックエンドが保持しているロック数
SELECT pid, count(*) AS lock_count
FROM pg_locks
WHERE relation IS NOT NULL
GROUP BY pid
ORDER BY lock_count DESC;
元のエラーを再現し、操作中に最後のクエリを実行すると、正確なロック数が確認できます。これにより、推測ではなく適切な設定値を正確に把握できます。
予防策
- ロックテーブルの使用状況を積極的に監視する:バックエンドごとの
lock_countが定期的に50を超えるようであれば警告サインです。上限に達する前に制限値を引き上げてください。 - パーティションは保守的に設計する:月次パーティションでほとんどのユースケースはカバーできます。3年分のデータセットを日次パーティションにすると1,000以上のロックオブジェクトになり、すべてのトランザクションに大きな負荷がかかります。
- 重いマイグレーションはトランザクション外で実行する:Flywayの
--no-transactionフラグやDjangoのatomic = Falseを使用すると、多数のテーブルに触れるマイグレーションでラッピングトランザクションをスキップできます。または小さなバッチに分割してください。1トランザクションあたり30テーブルが安全な上限です。 - max_connectionsを考慮して計算する:ロックテーブルは接続数に応じてスケールします。
max_connectionsを100から200に倍増させると、max_locks_per_transactionを比例して増やさない限り、各トランザクションのロックスロットが実質的に半分になります。
複数の環境にわたってPostgreSQLの設定を管理していますか?ToolCraftのYAML ↔ JSON Converterは、Docker Composeのセットアップでよく使われるYAML形式の設定ファイルを検証するのに便利です。ブラウザ上で完結し、何もアップロードされません。

