PostgreSQL 入門

PostgreSQL データベース・トランザクション

トランザクション(Transactions)はデータベース管理の基盤であり、データの整合性(Consistency)と信頼性を担保するための仕組みです。

本章では、トランザクションのコア・コンセプトを深く掘り下げ、PostgreSQLにおいて BEGINCOMMIT コマンドを使用してトランザクションを開始・終了する方法を重点的に学習します。

1. ACID特性の振り返り

前モジュールの簡潔な復習として、トランザクションはデータベース操作が信頼性と予測可能性を保ち、以下の ACID特性 に厳密に従うことを保証します。

  • 原子性 (Atomicity): トランザクションは分割不可能な作業単位です。その中のすべての操作がすべて成功するか、あるいはすべて失敗するかのどちらかです。
  • 一貫性 (Consistency): トランザクションは、データベースをある有効な状態から別の有効な状態へと遷移させます。事前定義されたルールとコンストレイント(制約)を遵守することでデータの整合性を維持します。
  • 分離性 (Isolation): トランザクション間は相互に分離されています。これにより相互干渉を防ぎ、コンカレント(同時並行)なトランザクションがデータを破壊しないことを保証します。
  • 永続性 (Durability): トランザクションがコミットされると、その変更は永久的なものとなり、システム障害が発生してもデータは保存されます。

2. BEGINコマンドによるトランザクションの開始

PostgreSQLでは、BEGIN コマンドを使用してトランザクションを初期化(開始)します。これは、単一の論理的作業単位として扱われるべき一連の操作の始まりを意味します。COMMIT(確定)または ROLLBACK(ロールバック)コマンドを発行するまで、データベースに対するすべての変更はこのトランザクションの一部となり、永久的には保存されません。

2.1 構文

基本構文は非常にシンプルです:

BEGIN;

オプションとして、トランザクション開始時に 分離レベル (Isolation Level)アクセスモード (Access Mode) を指定できます。分離レベルについては後の章で詳述しますが、現時点では「並行するトランザクション間がどの程度分離されるか」を決定するものと理解してください。アクセスモードは、そのトランザクションが読み取り専用(Read-only)か読み書き(Read-write)かを定義します。

BEGIN ISOLATION LEVEL level READ WRITE; -- あるいは READ ONLY (読み取り専用)

ここでの level には、SERIALIZABLEREPEATABLE READ、あるいは READ COMMITTED(デフォルト)を指定できます。ISOLATION LEVELREAD WRITE を指定しない場合、データベースのデフォルト設定が使用されます。

2.2 例

簡略化した銀行業務のシナリオを考えます。account_id(アカウントID)と balance(残高)を持つ accounts テーブルがあります。アカウント1からアカウント2へ100ドルを送金したい場合、次のように記述します。

-- 新しいトランザクションを開始
BEGIN;

-- アカウント1から100ドルを差し引く
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;

-- アカウント2へ100ドルを加算する
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;

-- 変更を保存するためにトランザクションを確定(コミット)
COMMIT;

この例では、BEGIN コマンドがトランザクションを開始し、続く UPDATE ステートメントがテーブルを操作します。しかし、これらの変更はこの時点ではまだ永久的なものではありません。COMMIT を実行した瞬間に初めて保存されます。例えばアカウント1の残高が不足していることが判明した場合、ROLLBACK コマンド(次章で詳述)を使用して変更を破棄します。

2.3 暗黙的なトランザクション

一部のPostgreSQLクライアントツールや設定では、トランザクションが暗黙的(自動的)に開始されることがあります。例えば、対話型ターミナルである psql は通常、デフォルトで 「オートコミット (Autocommit)」 モードで動作します。オートコミットモードでは、個々のSQLステートメントが独立したトランザクションとして扱われ、実行成功後に自動でコミットされます。

SET AUTOCOMMIT OFF; コマンドを使用すると、オートコミットを無効にできます。オートコミットがオフの場合、BEGINCOMMIT(または ROLLBACK)を明示的に使用してトランザクションを管理する必要があります。

3. COMMITコマンドによるトランザクションの確定

COMMIT コマンドは、トランザクションを最終的に完了させ、データベース内のすべての変更を永久的なものにするために使用されます。一度トランザクションがコミットされると、それらの変更には 永続性 (Durability) が備わり、システムクラッシュやその他の障害が発生してもデータが失われないことが保証されます。

3.1 構文

トランザクションをコミットする構文も非常にシンプルです:

COMMIT;

3.2 例

先ほどの銀行送金のシナリオを続けます。転送操作を実行した後、COMMIT を使用して変更を保存します:

-- トランザクションの開始
BEGIN;

-- 出金
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;

-- 入金
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;

-- 保存
COMMIT;

COMMIT コマンドが成功すると、これら2つの UPDATE 操作は永久的に accounts テーブルへ適用されます。

3.3 COMMITの重要性

COMMIT コマンドを実行しない限り、トランザクション内部で行われた変更は現在のセッション(接続)に対してのみ可視であり、永久的には保存されません。このメカニズムにより、トランザクションのどこかでエラーが発生した場合、全体をロールバックしてデータの一貫性を維持できるのです。

4. 実践ケーススタディ:ECサイトの注文処理

顧客が注文を行うECアプリケーションを想定します。以下の操作は、単一のトランザクション内で完了させる必要があります:

  1. products(商品)テーブルを更新し、注文された商品の在庫数を減らす。
  2. orders(注文)テーブルに注文詳細を含む新しいレコードを作成する。
  3. order_items(注文明細)テーブルに各商品のレコードを作成する。
  4. customer_accounts(顧客アカウント)テーブルを更新し、支払額を差し引く。
BEGIN;

-- 商品在庫の更新
UPDATE products SET quantity = quantity - 2 WHERE product_id = 101;
UPDATE products SET quantity = quantity - 1 WHERE product_id = 102;

-- 注文詳細のインサート
INSERT INTO orders (customer_id, order_date, total_amount) VALUES (123, NOW(), 199.98);

-- 生成されたばかりの注文IDを取得
-- 注意:本例はデモ用です。実戦では RETURNING 句などを使用してIDを取得します
SELECT currval(pg_get_serial_sequence('orders', 'order_id')) INTO order_id;

-- 注文明細のインサート
INSERT INTO order_items (order_id, product_id, quantity, price) VALUES (order_id, 101, 2, 79.99);
INSERT INTO order_items (order_id, product_id, quantity, price) VALUES (order_id, 102, 1, 39.99);

-- 顧客残高の更新
UPDATE customer_accounts SET balance = balance - 199.98 WHERE customer_id = 123;

COMMIT;

これらの操作のいずれかが失敗した場合(例:在庫不足、決済エラー)、ROLLBACK を実行してトランザクション全体を元に戻し、データの不整合を防ぐ必要があります。

5. 想定シナリオ:大学の履修登録システム

学生がコースを登録する大学の履修システムを想像してください。履修登録トランザクションには以下のステップが含まれます:

  1. 学生がそのコースの履修条件を満たしているかチェック。
  2. コースに空席があるか検証。
  3. 学生のレコードを更新し、コースを追加。
  4. コースのレコードを更新し、登録人数を反映。
BEGIN;

-- 履修条件のチェック (簡略版)
SELECT has_prerequisites FROM students WHERE student_id = 'S123';

-- 空席のチェック (簡略版)
SELECT seats_available FROM courses WHERE course_id = 'CS101';

-- 学生レコードの更新
INSERT INTO student_courses (student_id, course_id) VALUES ('S123', 'CS101');

-- コースレコードの更新
UPDATE courses SET seats_available = seats_available - 1 WHERE course_id = 'CS101';

COMMIT;

これらのチェックのいずれかが失敗した場合(例:条件未達、満席)、トランザクションをロールバックして履修データの完全性を維持します。