PostgreSQLのROLLBACKは、現在のトランザクションの未確定の変更を取り消す命令です。確定済みの変更を巻き戻す機能ではありません。また、SQLエラーの後は接続が失敗状態のトランザクションに残ることがあり、適切な終了処理が必要です。この記事では、通常のSQL、psql、PL/pgSQL、Pythonドライバーを区別して、全体・部分ロールバックと復旧の考え方を説明します。
仕様の参照先はPostgreSQL 18の公式ドキュメントです(2026年10月20日確認)。SQL例は本番ではなく検証用データベースの専用接続で試してください。エラーを起こす例は一文ずつ実行し、後述の復旧命令まで確認します。各例の結果は初期データからの期待値であり、環境ごとの実行結果を保証するものではありません。
トランザクションの基本概念
トランザクションは、複数のデータベース操作をひとまとまりに確定・取り消しする単位です。例えば注文の登録だけ成功して在庫更新が失敗する、といった中途半端な確定を防ぎます。ただし、業務上の条件を満たすかどうかは、制約やアプリケーションでも確認する必要があります。
ACID特性
- 原子性(Atomicity):関連する更新を一括して確定し、途中で失敗した更新をそのまま確定しない。
- 一貫性(Consistency):主キー・外部キー・CHECK制約などと、業務ルールを守る処理を組み合わせる。
- 分離性(Isolation):他のトランザクションとの見え方や競合の扱いを分離レベルで制御する。
- 耐久性(Durability):確定した変更を障害後も維持する。実運用では耐久性に関わる設定やストレージの前提も確認する。
ロールバックが戻すのはそのトランザクションの更新であり、他の接続が確定した更新まで取り消すわけではありません。基本的な考え方は公式チュートリアル「Transactions」を参照してください。
トランザクションの開始と終了
BEGIN・COMMIT・ROLLBACKの役割
BEGIN;:同じ接続でトランザクションを開始する。COMMIT;:成功したトランザクションの変更を確定する。ROLLBACK;:現在のトランザクションを中止し、未確定の更新を取り消す。
一連のSQLは同じ接続で実行します。接続プールを利用する場合も、途中で別の接続に切り替えないでください。BEGINを重ねても独立した入れ子のトランザクションにはなりません。部分的に戻す場合はSAVEPOINTを使います。詳細はBEGIN、COMMIT、ROLLBACKの公式リファレンスにあります。
検証用の準備SQL
以下はセッション内だけで使う一時テーブルです。以降のrb_accountsの例は、例ごとに新しい検証用接続を開き、この準備SQLを一度実行してから試す想定です。通常のpsqlの自動コミット有効・トランザクション未開始の状態で実行します。接続を閉じると一時テーブルは削除されます。CREATE TABLEの一時テーブルの説明も参照してください。
CREATE TEMP TABLE rb_accounts (
account_id integer PRIMARY KEY,
balance integer NOT NULL CHECK (balance >= 0)
);
INSERT INTO rb_accounts VALUES (1, 1000), (2, 2000);未確定の更新を取り消す例
BEGIN;
UPDATE rb_accounts SET balance = balance - 100 WHERE account_id = 1;
SELECT balance FROM rb_accounts WHERE account_id = 1; -- 900
ROLLBACK;
SELECT balance FROM rb_accounts WHERE account_id = 1; -- 1000同じ接続では未確定の更新も確認できます。最後のSELECTでは、取り消し後の残高1000が返る想定です。確定したい場合は、実行結果と業務条件を確認したうえでROLLBACKの代わりにCOMMITを選びます。両方を順番に実行しても「確定してから取り消す」ことはできません。
自動コミットと手動コミット
サーバーのトランザクションとクライアント設定を区別する
PostgreSQLではSQLはトランザクションの中で実行されます。明示的なトランザクションブロックがなければ、通常は一つのSQL文が成功した時点で確定します。ただし、ドライバーが暗黙にBEGINを発行する場合や、複数文を一つのリクエストとして送信する場合があるため、実際の境界はクライアントの仕様も確認してください。
BEGINはその接続でトランザクションブロックを開始する命令であり、サーバー全体やクライアントの自動コミット設定を恒久的に無効にするものではありません。psqlではAUTOCOMMITが既定で有効です。次のコマンドはSQLではなくpsql固有のメタコマンドです。
\echo :AUTOCOMMIT
\echo :ON_ERROR_ROLLBACK
\echo :ON_ERROR_STOP本記事の対話例はAUTOCOMMIT=on、ON_ERROR_ROLLBACK=offを前提とします。ON_ERROR_ROLLBACK=onではpsqlが内部でセーブポイントを使うため、エラー後の挙動が変わります。これはサーバーの標準動作とは区別します。詳しくはpsqlの変数とスクリプト実行を確認してください。
既にコミットされた変更はROLLBACKでは戻らない
-- 新しい接続で準備SQLを実行してから、一文ずつ実行
UPDATE rb_accounts SET balance = 900 WHERE account_id = 1;
-- 自動コミットが有効で、BEGINしていないため、上の変更は確定済み
ROLLBACK; -- トランザクションがない旨の警告。確定済みの変更は戻らない
SELECT balance FROM rb_accounts WHERE account_id = 1; -- 900誤更新を確定した場合は、元の値と影響範囲を確認して補正用トランザクションを設計するか、事前のバックアップとWALによる復旧を検討します。復旧については「トランザクションログの活用」で説明します。
トランザクションの分離レベル
PostgreSQLはSQL標準の4つの名称を受け付けますが、実際の動作は3種類です。他のデータベースの説明をそのまま当てはめないようにしてください。
- Read Uncommitted:PostgreSQLではRead Committedと同じ動作です。ダーティリードを許すモードではありません。
- Read Committed:既定値です。通常のSELECTは文の開始時点で確定済みのデータを参照し、同じトランザクション内でも次のSELECTで他の接続の更新が見える場合があります。
- Repeatable Read:最初のトランザクション制御以外の文が始まる時点のスナップショットを使います。PostgreSQLではファントムリードも防ぎますが、直列化異常は起こり得ます。競合による失敗への備えが必要です。
- Serializable:成功したトランザクション群が直列実行と同等になるように制御します。業務ロジック自体の正しさを保証するものではなく、競合時には処理が失敗するため再試行を設計します。
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT account_id, balance FROM rb_accounts ORDER BY account_id;
ROLLBACK;各レベルの保証はTransaction Isolationを参照してください。直列化失敗(SQLSTATE 40001)では、失敗した一文だけでなく、判断に使った読み取りを含むトランザクション全体をやり直します。デッドロック(40P01)も再試行を検討する対象ですが、無条件・無制限に繰り返さず、原因と再実行可能性を確認します。Serialization Failure Handlingに公式の指針があります。
トランザクション内のエラーハンドリング
エラー後は「失敗状態のトランザクション」を終了する
明示的なトランザクション内でSQLエラーが起きると、通常はそのトランザクションが失敗状態になり、以後の通常SQLを実行できません。「サーバーがエラーを検出したから、接続はもう次の仕事に使える」と考えないでください。全体を捨てるならROLLBACK、事前のセーブポイントから続けるならROLLBACK TO SAVEPOINTを使います。
BEGIN;
UPDATE rb_accounts SET balance = balance - 100 WHERE account_id = 1;
SELECT 1 / 0; -- 意図的なエラー: division_by_zero (22012)
SELECT balance FROM rb_accounts; -- 失敗状態のため実行できない (25P02)
ROLLBACK;
SELECT balance FROM rb_accounts WHERE account_id = 1; -- 1000この例は、各行をpsqlの対話モードで順番に実行します。一つのSQLリクエストにまとめると、最初のエラーで後続文の実行が打ち切られることがあります。アプリケーションでも、エラーを受けた後に別の実行要求で終了処理を行います。複数文を送信した場合の公式仕様を参照してください。
25P02(in_failed_sql_transaction)は、先に発生したエラーの後でSQLを続けたことを示します。原因調査では最初のエラーとそのSQLSTATEを確認してください。メッセージ本文の文字列比較より、公式エラーコードを使った判定が安定します。
SQLとPL/pgSQLのEXCEPTIONは別の構文
通常のBEGIN; ... COMMIT;の間にEXCEPTION WHEN ...を書いても、SQLの例外処理にはなりません。SQLのBEGINはトランザクション開始、PL/pgSQLのBEGIN ... ENDは処理ブロックです。
次はPL/pgSQLの匿名ブロックDOを使う例です。内側のブロックで起きたゼロ除算を捕捉すると、その内側の更新だけが取り消されます。外側の更新は残りますが、ここでは最後に全体をROLLBACKするため、検証後は初期値に戻ります。
BEGIN;
DO $$
BEGIN
UPDATE rb_accounts SET balance = balance - 100 WHERE account_id = 1;
BEGIN
UPDATE rb_accounts SET balance = balance - 50 WHERE account_id = 1;
PERFORM 1 / 0;
EXCEPTION WHEN division_by_zero THEN
RAISE NOTICE 'Inner block was rolled back';
END;
END;
$$ LANGUAGE plpgsql;
SELECT balance FROM rb_accounts WHERE account_id = 1; -- 900
ROLLBACK;EXCEPTIONで捕捉すると、そのブロック内のデータベース更新は戻りますが、PL/pgSQLのローカル変数の値はエラー時点のままです。また、例外ハンドラーを持つブロック内でCOMMITやROLLBACKを実行して終了させることはできません。全体を失敗させたい場合は、例外を不用意に握りつぶさず呼び出し元へ伝えます。PL/pgSQLのエラー捕捉を参照してください。
PL/pgSQLの関数ではトランザクションを終了できません。プロシージャのCALLやDOでも終了できる条件に制約があり、明示的なトランザクション内から呼び出したDOでは終了できません。またPL/pgSQLはSQLのSAVEPOINT系命令を直接サポートしないため、通常は例外処理付きブロックを使います。Transaction ManagementとDOで呼び出し条件を確認してください。
実際のロールバック手順
全体を戻すか、一部を戻すかを決める
- 全体を取り消す:同じ接続で
ROLLBACK;を実行する。 - 一部を取り消して続行する:エラーや取り消したい処理の前に設定したセーブポイントへ戻る。
- 確定済み・結果不明:ROLLBACKで戻せると決めつけず、処理結果と復旧方法を確認する。
SAVEPOINTを使用した部分ロールバック
BEGIN;
UPDATE rb_accounts SET balance = balance - 100 WHERE account_id = 1;
SAVEPOINT before_optional_step;
UPDATE rb_accounts SET balance = balance - 50 WHERE account_id = 1;
SELECT 1 / 0; -- 意図的なエラー。ここまで一文ずつ実行
ROLLBACK TO SAVEPOINT before_optional_step;
SELECT balance FROM rb_accounts WHERE account_id = 1; -- 900
RELEASE SAVEPOINT before_optional_step;
COMMIT;
SELECT balance FROM rb_accounts WHERE account_id = 1; -- 900戻る先は指定したセーブポイントを作成した時点です。上の例では50の減算を取り消し、作成前の100の減算は残します。ROLLBACK TOの後も対象セーブポイントは有効ですが、それより後に作られたセーブポイントは破棄されます。存在しないポイントや、エラー後に新しく作ろうとしたポイントには戻れません。ROLLBACK TO SAVEPOINTが仕様の参照先です。
RELEASE SAVEPOINTはセーブポイントを解放するだけで、トランザクション全体を確定しません。その後に全体をROLLBACKすれば、残した更新も取り消されます。失敗状態のままRELEASEするのではなく、先にROLLBACK TOで回復します。RELEASE SAVEPOINTを参照してください。
ROLLBACKの対象外になるもの
- シーケンスの採番:
nextval()で得た番号は、トランザクションが中止されても再利用のために巻き戻りません。serialやidentity列に欠番ができても、それだけで不整合ではありません。 - 外部への副作用:決済APIへの要求、メール送信、ファイル出力などは、データベースのROLLBACKだけでは取り消せません。外部処理側の取消・補償処理や重複防止を別途設計します。
- 既に確定したトランザクション:別のトランザクションでの補正や、バックアップからの復旧を検討する対象です。
採番の仕様はSequence Manipulation Functionsを参照してください。ECサイトの注文・在庫を同じトランザクションで扱う場合でも、「DBが戻れば外部の支払いも戻る」とは限りません。
Pythonドライバーではトランザクションの所有者を明確にする
次はPsycopg 3の例です。psycopg2とは別のAPIです。既存の検証用データベースに接続し、一時テーブルだけを使います。conninfoには許可された検証先の接続情報を渡し、本番の接続文字列や認証情報をコードに埋め込まないでください。
import psycopg
def demo_rollback(conninfo):
with psycopg.connect(conninfo, autocommit=True) as conn:
conn.execute("CREATE TEMP TABLE rb_driver (id integer PRIMARY KEY, n integer)")
conn.execute("INSERT INTO rb_driver VALUES (1, 10)")
try:
with conn.transaction():
cur = conn.execute(
"UPDATE rb_driver SET n = n + %s WHERE id = %s",
(5, 1),
)
if cur.rowcount != 1:
raise ValueError("Expected exactly one row")
conn.execute("SELECT 1 / 0") # 検証用に意図的なエラー
except psycopg.errors.DivisionByZero:
# transaction() を抜けてロールバックされた後に捕捉する
return conn.execute("SELECT n FROM rb_driver WHERE id = 1").fetchone()[0]
# 許可された検証先で呼び出した場合の戻り値の想定は10。conn.transaction()は正常終了で確定し、例外がブロック外へ伝わると取り消します。ここでは接続をautocommit=Trueで開いているため、準備SQLの後に意図しない外側のトランザクションが残りません。Psycopg 3の既定ではSELECTでも暗黙のトランザクションが始まり、既にトランザクションがある状態のtransaction()はセーブポイントだけを管理します。外側の確定・取消責任は残ります。詳細はPsycopg 3公式「Transactions management」を確認してください。
ORMや接続プールがトランザクションを管理する場合は、そのAPIに終了処理を任せます。フレームワーク管理下で勝手に別のBEGIN・COMMITを混在させたり、失敗状態の接続をそのまま再利用したりしないでください。
トランザクションログの活用
WALは確定済みSQLを取り消すUndoボタンではない
WAL(Write-Ahead Logging)は、データファイルの変更を永続化する前にログを永続化する仕組みで、障害復旧などに使われます。通常のROLLBACKとは役割が異なり、任意のSQLを逆向きに実行して消すための履歴ではありません。Write-Ahead Loggingを参照してください。
誤更新をコミットした場合の復旧
まず誤更新の時刻・対象・処理IDと利用可能なバックアップを確認し、証跡を保全します。元の値が分かるなら、他の正しい更新を上書きしない条件を検討したうえで補正します。元の値が分からない場合に、推測した逆UPDATEを本番で実行しないでください。
PITR(時点復旧)には、適切な物理ベースバックアップと、そこから目標時点まで連続したWALが必要です。pg_dumpの論理バックアップだけではWAL再生の起点になりません。物理復旧はクラスタ単位であり、一つの誤操作だけを選んで戻すものではありません。
- 既存の本番データを削除せず、バックアップと必要なWALがそろうか確認する。
- 独立した復旧先へ復元し、利用するメジャーバージョン・タイムライン・目標時点を確認する。
- 通常のアーカイブ復旧では
restore_command、recovery.signal、必要な復旧目標設定などを用いる。マネージドDBでは提供元の復旧手順に従う。 - 復旧先の内容を検証してから、必要なデータの取り出しや切り替え方法を計画する。復旧時点以降の正しい更新への影響も確認する。
具体的な復旧は環境ごとに検証が必要です。本番ディレクトリを消すコマンドや、WALを一括コピーするだけの手順では代用できません。公式のContinuous Archiving and Point-in-Time Recoveryを起点に、復旧試験を含む手順を用意してください。
pg_basebackupと監査ログを混同しない
pg_basebackupは物理ベースバックアップを取得するツールです。現在のWAL取得指定は-X streamなどで、取得中に必要なWALを含められます。ただし、それだけで取得後の任意の時点まで復旧できるわけではなく、継続的なWAL保管は別途必要です。接続権限・容量・保存先などをpg_basebackup公式リファレンスで確認します。
WALとサーバーログは用途が異なります。誰が何を行ったかの監査には、サーバーのログ設定やアプリケーションの操作記録を目的に合わせて設計します。ログ解析ツールを使うだけで全変更の監査履歴がそろうわけではありません。SQLやパラメーターに個人情報・秘密情報が含まれる場合があるため、取得範囲・閲覧権限・保管期間も検討してください。Error Reporting and Loggingを参照してください。
トランザクション管理のベストプラクティス
短くするために原子性を壊さない
トランザクションを開いたままユーザー入力や外部APIを長時間待たないようにします。ただし、注文登録と在庫減算のように一緒に成功すべき更新を、単に短くするため別々にコミットしてはいけません。分割できるのは、業務上も独立して確定できる処理です。
SQLが成功しても更新件数を確認する
UPDATEが0件で終了してもSQLエラーにはなりません。対象がない・在庫が足りないなどを業務エラーとして扱うなら、更新件数やRETURNINGの結果を確認して処理全体を中止します。負の値を許さない制約も併用します。UPDATEの出力と件数を参照してください。
再試行とコミット結果不明を分ける
直列化失敗など「取り消された」と判定できる失敗と、COMMITの応答前に通信が切れたケースは別です。後者は、クライアントが応答を受け取れなくても確定している可能性があります。再接続して業務キーや処理IDで状態を照合し、同じ注文・請求を二重登録しない設計にします。新しい接続でROLLBACKしても、前の接続の確定済み更新は取り消せません。接続終了時の注意点はMessage FlowのTerminationにも記載されています。
psqlのバッチ実行はエラー停止と終了処理を設計する
通常の更新スクリプトでは、エラーを見落として後続処理を進めないようにします。psqlのON_ERROR_STOPと単一トランザクション実行の組み合わせなどを検討できますが、スクリプト自身にBEGIN・COMMITが含まれる場合や、トランザクション内で実行できない命令がある場合は適用できません。本記事の意図的にエラーを発生させる対話例とは分けて扱ってください。
トランザクションの監視と調整
次は自分に参照権限のある接続状態を調べる読み取り用SQLです。SQL本文は選択せず、状態と開始時刻を確認します。
SELECT pid, usename, state, xact_start, state_change,
wait_event_type, wait_event
FROM pg_stat_activity
WHERE datname = current_database()
AND state IN ('idle in transaction', 'idle in transaction (aborted)')
ORDER BY xact_start NULLS LAST;idle in transactionはトランザクション内で待機している状態、idle in transaction (aborted)は文のエラー後に待機している状態です。他ユーザーの接続の詳細は権限によって制限されます。見つけた接続を一律に強制終了するのではなく、所有するアプリケーションの終了処理・接続プール・タイムアウトを調査します。pg_stat_activityの公式説明を参照してください。
応用例と演習問題
問題1:顧客と注文を一緒に登録する
新しい検証用接続で、次の一時テーブルを用意します。顧客のIDを取得して注文に引き継ぎ、全体を取り消した後に両方とも0件になることを確認してください。
CREATE TEMP TABLE rb_customers (
customer_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL
);
CREATE TEMP TABLE rb_orders (
order_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id bigint NOT NULL REFERENCES rb_customers(customer_id),
total integer NOT NULL CHECK (total > 0)
);
BEGIN;
WITH new_customer AS (
INSERT INTO rb_customers (name) VALUES ('Demo customer')
RETURNING customer_id
)
INSERT INTO rb_orders (customer_id, total)
SELECT customer_id, 200 FROM new_customer;
ROLLBACK;
SELECT count(*) FROM rb_customers; -- 0
SELECT count(*) FROM rb_orders; -- 0LASTVAL()でセッション内の直近の採番を推測するより、INSERT ... RETURNINGで必要な列を直接取得すると、どのIDを引き継ぐかが明確になります。Returning Data from Modified Rowsを参照してください。発展問題として、金額を負数に変えてCHECK制約違反を起こし、エラー後に別途ROLLBACKして0件を確認してみてください。
問題2:在庫不足なら価格変更も取り消す
以下も新しい検証用接続で実行する例です。在庫がないときは、更新0件を検知して例外にし、同じDO文で行った価格変更も失敗させます。意図的なエラーの後にROLLBACKを一文として実行してください。
CREATE TEMP TABLE rb_products (
product_id integer PRIMARY KEY,
price integer NOT NULL CHECK (price >= 0)
);
CREATE TEMP TABLE rb_inventory (
product_id integer PRIMARY KEY REFERENCES rb_products(product_id),
quantity integer NOT NULL CHECK (quantity >= 0)
);
INSERT INTO rb_products VALUES (1, 100);
INSERT INTO rb_inventory VALUES (1, 0);
BEGIN;
DO $$
BEGIN
UPDATE rb_products SET price = 150 WHERE product_id = 1;
IF NOT FOUND THEN
RAISE EXCEPTION 'Product not found';
END IF;
UPDATE rb_inventory SET quantity = quantity - 1
WHERE product_id = 1 AND quantity >= 1;
IF NOT FOUND THEN
RAISE EXCEPTION 'Insufficient stock or missing inventory';
END IF;
END;
$$ LANGUAGE plpgsql;
-- 上のDOがエラーになった後、次を別に実行する
ROLLBACK;
SELECT price FROM rb_products WHERE product_id = 1; -- 100
SELECT quantity FROM rb_inventory WHERE product_id = 1; -- 0この解答は例外を捕捉せず呼び出し元へ返すため、エラーを成功として扱いません。成功ケースでは初期在庫を1にし、処理結果を確認して最後をCOMMITに変更すると、価格150・在庫0となる想定です。本番用の注文処理には、この例にない認可・重複防止・注文との関連付けなども必要です。FOUNDによる件数判定の仕様はPL/pgSQLの実行結果ステータスを参照してください。
まとめ
ROLLBACKが取り消せるのは現在の未確定の更新であり、確定済みの変更や外部APIの副作用ではない。- SQLエラー後の接続は失敗状態に残ることがある。全体をROLLBACKするか、事前のSAVEPOINTへ戻ってから続ける。
- SAVEPOINTの解放はCOMMITではない。SQL、PL/pgSQL、ドライバーのトランザクション境界を混同しない。
- 分離レベル・更新件数・再試行条件を確認し、原子性が必要な処理は一緒に確定する。
- 確定済みの誤更新への備えは、通常のROLLBACKとは別に、バックアップと復旧試験を用意する。


コメント