シナリオ
トランザクション内でSQL文を実行している最中に — データ移行、バッチ挿入、またはpsqlセッションなどで — 1つの文が失敗したとします。すると、それ以降のすべてのコマンドが次のエラーをスローします:
ERROR: current transaction is aborted, commands ignored until end of transaction block
何も動かなくなります。SELECT、INSERT、UPDATE — すべて拒否されます。Postgresは、失敗を明示的に処理するまでトランザクションをシャットダウンしてしまいます。
なぜこうなるのか
Postgresは推測しません。トランザクションブロック内の文が失敗すると、トランザクション全体をアボート済みとしてマークし、そこで処理を停止します。ROLLBACKを発行するまで、そのトランザクション内ではいかなるコマンドも実行されません — 読み取り専用のSELECTでさえも。
これは意図的な設計上の選択です。一部のコマンドが成功し他が失敗した半端な状態で処理を継続すると、データが不整合な状態になりかねません。最もよくある原因は次のとおりです:
- 制約違反(ユニークキー、外部キー、NOT NULL)
- 型キャストエラー(例:
integerカラムへ'abc'を挿入する) - ゼロ除算
BEGINブロック内でのあらゆる実行時エラー
アプリケーションコードでは、クエリがクラッシュしてもエラーをキャッチせずに処理が続行された場合に表面化します。次のクエリがアボートされたトランザクションの壁にぶつかり、有用なエラーメッセージの代わりにこのメッセージが表示されます。
簡単な修正方法:ROLLBACKして最初からやり直す
毎回同じ修正方法です — トランザクションをロールバックしてエラー状態をクリアします:
ROLLBACK;
その後、Postgresは再びコマンドを受け付けます。必要であれば新しいトランザクションを開始します:
BEGIN;
-- ここにSQL文を記述
COMMIT;
psqlでは、ROLLBACKは安全です。現在の(失敗した)トランザクション内で行われた処理だけを元に戻します — それ以外には影響しません。
SAVEPOINTを使って完全なロールバックなしに復旧する
複数ステップのトランザクションで、最初からやり直したくない場合はセーブポイントを使いましょう。危険な操作の前にセーブポイントを設置します:
BEGIN;
INSERT INTO orders (id, user_id, amount) VALUES (1, 42, 99.99);
SAVEPOINT before_discount;
-- これが失敗する可能性がある(例:制約違反)
INSERT INTO discounts (order_id, code) VALUES (1, 'INVALID_CODE');
-- トランザクション全体ではなく、セーブポイントまでロールバック
ROLLBACK TO SAVEPOINT before_discount;
-- 通常どおり処理を続行
UPDATE orders SET status = 'confirmed' WHERE id = 1;
COMMIT;
ordersへのINSERTはそのまま保持されます。セーブポイント以降の処理だけが元に戻ります。
アプリケーションコードでの修正
実際の問題のほとんどは、例外が適切にキャッチされていないアプリケーションコードで発生します。一般的な各スタックでの正しいパターンを紹介します。
Python (psycopg2)
import psycopg2
conn = psycopg2.connect("dbname=mydb user=postgres")
cur = conn.cursor()
try:
cur.execute("BEGIN")
cur.execute("INSERT INTO users (email) VALUES (%s)", ("test@example.com",))
cur.execute("INSERT INTO profiles (user_id) VALUES (%s)", (9999,)) # 失敗する可能性あり
conn.commit()
except psycopg2.Error as e:
conn.rollback() # <-- 重要:トランザクション状態をリセット
print(f"Transaction failed: {e}")
finally:
cur.close()
conn.close()
exceptブロックでconn.rollback()を省略すると、その接続を以降に使用するたびに同じアボートされたトランザクションエラーが発生します。
Python (psycopg2) コンテキストマネージャ使用
with psycopg2.connect("dbname=mydb user=postgres") as conn:
with conn.cursor() as cur:
try:
cur.execute("INSERT INTO users (email) VALUES (%s)", ("test@example.com",))
conn.commit()
except psycopg2.Error:
conn.rollback()
raise
Node.js (node-postgres / pg)
const { Pool } = require('pg');
const pool = new Pool();
async function runTransaction() {
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query('INSERT INTO users(email) VALUES($1)', ['test@example.com']);
await client.query('INSERT INTO profiles(user_id) VALUES($1)', [9999]); // 失敗する可能性あり
await client.query('COMMIT');
} catch (err) {
await client.query('ROLLBACK'); // <-- トランザクションをリセット
throw err;
} finally {
client.release();
}
}
Java (JDBC)
try {
conn.setAutoCommit(false);
stmt.executeUpdate("INSERT INTO users (email) VALUES ('test@example.com')");
stmt.executeUpdate("INSERT INTO profiles (user_id) VALUES (9999)");
conn.commit();
} catch (SQLException e) {
conn.rollback(); // <-- 必須
throw e;
} finally {
conn.setAutoCommit(true);
}
注意:コネクションプーリング
コネクションプールはサイレントな障害モードをもたらします。アボート状態のままプールに返却された接続は、次にその接続を取得したリクエストを汚染します — たとえそのリクエストが元のエラーとは無関係であっても、壊れた接続をそのまま引き継いでしまいます。
接続をプールに戻す前に必ずロールバックしてください。ほとんどのプールライブラリは正しく設定すれば自動的に処理しますが、以下を確認してください:
- HikariCP:デフォルトで
autoCommit=trueが設定されており、返却時に状態がリセットされる - psycopg2 pool:接続がエラー状態の場合は
pool.putconn(conn, close=True)を呼び出す - PgBouncer:トランザクションモードでは状態がリセットされない — 解放前に必ず明示的にロールバックすること
修正が成功したか確認する
ロールバック後、接続がクリーンな状態であることを確認します:
-- psqlでこれがエラーなく返れば正常:
SELECT 1;
-- 現在のトランザクション状態を確認
SELECT pg_current_xact_id_if_assigned();
-- アクティブなトランザクションがなければNULLを返す(クリーンな状態)
アプリケーションコードでは、ロールバック後に処理を続行する前に簡単な確認クエリを実行します:
cur.execute("SELECT 1")
# これが成功すれば、接続はクリーンな状態
再発を防ぐには
- すべてのトランザクションをtry/catchで囲み、明示的にROLLBACKする — ロールバックせずに例外を伝播させない
- セーブポイントを活用する — 部分的な失敗が許容される複数ステップのトランザクションに有効
- 読み取り専用ワークロードにはオートコミットを有効にする — データを読むだけならトランザクションブロックは不要
- 挿入前にデータを検証する — アプリケーションコードで不正な値を検出する方が、トランザクション途中でDB制約に引っかかるよりクリーン
- このエラーだけでなく元のエラーをログに記録する — アボートされたトランザクションのメッセージは症状であり、根本原因はその直前に発生したエラー

