データベースを使ったシステムを作っていて、こんな心配をしたことはありませんか?
- 「お金の移動処理で、片方の口座だけ変更されたらどうしよう」
- 「途中でエラーが起きて、データがぐちゃぐちゃになったら…」
- 「複数の処理を安全にまとめて実行したい」
そんな不安を解決してくれるのが SQLのトランザクション です。
トランザクションとは
- 複数のSQL操作をひとまとめにする仕組み
- 「全部成功」または「全部キャンセル」ができる
- データの安全性を守る重要な機能
この記事では、トランザクションの基本から実際の使い方まで、初心者にも分かりやすく説明します。
トランザクションの基本概念

なぜトランザクションが必要なのか?
まず、トランザクションがない場合の問題を見てみましょう。
問題のある例:銀行の振込処理
-- 田中さんの口座から1万円を引く
UPDATE accounts SET balance = balance - 10000 WHERE name = '田中';
-- ここでシステムエラーが発生!
-- 佐藤さんの口座に1万円を追加(実行されない)
UPDATE accounts SET balance = balance + 10000 WHERE name = '佐藤';
何が起こるか
- 田中さんの口座からお金が消える
- 佐藤さんの口座にはお金が入らない
- 1万円が消失してしまう!
トランザクションを使った安全な例
BEGIN; -- トランザクション開始
-- 田中さんの口座から1万円を引く
UPDATE accounts SET balance = balance - 10000 WHERE name = '田中';
-- 佐藤さんの口座に1万円を追加
UPDATE accounts SET balance = balance + 10000 WHERE name = '佐藤';
-- 両方成功したら確定
COMMIT;
トランザクションの効果
- 途中でエラーが起きても、自動的に全部キャンセルされる
- 「全部成功」か「全部失敗」のどちらかだけになる
- データの整合性が保たれる
ACID特性 – トランザクションの4つの約束
トランザクションは「ACID特性」という4つの重要な性質を持っています。
A – Atomicity(原子性)
- 「分割できない」という意味
- 複数の操作を1つのかたまりとして扱う
- 全部成功するか、全部失敗するかのどちらか
-- 例:商品購入処理
BEGIN;
UPDATE products SET stock = stock - 1 WHERE id = 123; -- 在庫を減らす
INSERT INTO orders (user_id, product_id) VALUES (1, 123); -- 注文を記録
COMMIT; -- 両方成功したら確定
C – Consistency(一貫性)
- データの整合性を保つ
- ルール違反のデータは作らない
- トランザクション前後でデータの辻褄が合う
-- 例:残高がマイナスにならないようにチェック
BEGIN;
UPDATE accounts SET balance = balance - 10000
WHERE name = '田中' AND balance >= 10000; -- 残高が足りる場合のみ実行
-- (残高不足なら0件更新されて処理失敗)
COMMIT;
I – Isolation(独立性)
- 他のトランザクションの影響を受けない
- 同時に複数のトランザクションが動いても安全
- お互いの処理が干渉しない
D – Durability(永続性)
- COMMITした変更は永続的に保存される
- システムが壊れても、確定した変更は残る
- 停電やクラッシュに対しても安全
トランザクションの基本的な使い方

基本の構文
BEGIN; -- トランザクション開始
-- (または START TRANSACTION;)
-- 何らかのSQL操作を実行
UPDATE ...;
INSERT ...;
DELETE ...;
COMMIT; -- 変更を確定して保存
-- (または ROLLBACK;) -- 変更をキャンセルして元に戻す
成功パターン
-- ユーザー登録の例
BEGIN;
-- ユーザーテーブルに追加
INSERT INTO users (name, email) VALUES ('田中太郎', 'tanaka@example.com');
-- プロフィールテーブルに追加
INSERT INTO profiles (user_id, age, city) VALUES (LAST_INSERT_ID(), 25, '東京');
-- 両方成功したので確定
COMMIT;
-- 結果:ユーザーとプロフィールの両方が正常に作成される
失敗パターン(手動でのロールバック)
-- 商品注文の例
BEGIN;
-- 在庫をチェック
SELECT stock FROM products WHERE id = 123; -- 結果:在庫0個
-- 在庫がないので注文をキャンセル
ROLLBACK;
-- 結果:何も変更されずに処理終了
エラー時の自動ロールバック
-- エラーが発生する例
BEGIN;
-- 正常な操作
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
-- エラーが発生する操作(存在しないカラム)
UPDATE accounts SET invalid_column = 100 WHERE id = 2; -- エラー!
-- この時点で自動的にROLLBACKされる
-- 最初のUPDATEも取り消される
実践的な使用例
例1:ECサイトの注文処理
-- 商品注文の完全な処理
BEGIN;
-- 1. 在庫を確認・減少
UPDATE products
SET stock = stock - 2
WHERE id = 101 AND stock >= 2; -- 在庫が足りる場合のみ
-- 更新された行数をチェック(通常はアプリケーション側で行う)
-- 0行なら在庫不足でエラー
-- 2. 注文レコードを作成
INSERT INTO orders (user_id, total_amount, order_date)
VALUES (1, 5000, NOW());
-- 3. 注文詳細を記録
INSERT INTO order_details (order_id, product_id, quantity, price)
VALUES (LAST_INSERT_ID(), 101, 2, 2500);
-- 4. ユーザーのポイントを使用
UPDATE users
SET points = points - 100
WHERE id = 1 AND points >= 100;
-- 5. 全て成功したら確定
COMMIT;
例2:社員の部署異動処理
-- 社員の部署移動と履歴記録
BEGIN;
-- 1. 社員の部署を変更
UPDATE employees
SET department_id = 2, updated_at = NOW()
WHERE employee_id = 123;
-- 2. 異動履歴を記録
INSERT INTO transfer_history (employee_id, from_dept, to_dept, transfer_date)
VALUES (123, 1, 2, NOW());
-- 3. 新しい部署の人数を更新
UPDATE departments
SET employee_count = employee_count + 1
WHERE id = 2;
-- 4. 元の部署の人数を更新
UPDATE departments
SET employee_count = employee_count - 1
WHERE id = 1;
COMMIT;
例3:エラーハンドリング付きの処理
多くのデータベースシステムでは、プログラムでエラーハンドリングを行います:
-- PostgreSQLでのエラーハンドリング例
BEGIN;
-- 安全な操作を試行
DO $$
BEGIN
-- 残高移動を試行
UPDATE accounts SET balance = balance - 10000
WHERE id = 1 AND balance >= 10000;
-- 更新行数をチェック
IF NOT FOUND THEN
RAISE EXCEPTION '残高不足です';
END IF;
-- 受取側を更新
UPDATE accounts SET balance = balance + 10000 WHERE id = 2;
EXCEPTION
WHEN OTHERS THEN
-- エラー時は自動的にROLLBACK
RAISE;
END $$;
COMMIT;
データベース別の実装の違い

主要データベースの対応状況
データベース | トランザクション | 特徴 |
---|---|---|
PostgreSQL | ◎ | DDL文もトランザクション内で実行可能 |
MySQL | ◎ (InnoDB) | MyISAMエンジンは非対応 |
SQLite | ◎ | 軽量だが基本的な機能は完備 |
Oracle | ◎ | 高度な制御機能が豊富 |
SQL Server | ◎ | エラーハンドリングが充実 |
PostgreSQL
-- DDL(テーブル作成など)もトランザクション内で可能
BEGIN;
CREATE TABLE temp_table (id INTEGER, name VARCHAR(50));
INSERT INTO temp_table VALUES (1, 'テスト');
-- エラーがあればテーブル作成も取り消される
ROLLBACK; -- temp_tableは作られない
MySQL
-- InnoDBエンジンを使用(トランザクション対応)
CREATE TABLE accounts (
id INT PRIMARY KEY,
balance DECIMAL(10,2)
) ENGINE=InnoDB;
-- MyISAMエンジン(トランザクション非対応)
CREATE TABLE log_table (
id INT PRIMARY KEY,
message TEXT
) ENGINE=MyISAM; -- BEGINやCOMMITが効かない
MySQL固有の注意点
-- 自動コミットの制御
SET autocommit = 0; -- 自動コミットを無効化
BEGIN;
-- 操作...
COMMIT;
SET autocommit = 1; -- 自動コミットを有効化
SQLite
-- SQLiteでのトランザクション
BEGIN TRANSACTION; -- BEGINだけでもOK
UPDATE users SET last_login = datetime('now') WHERE id = 1;
INSERT INTO login_history (user_id, login_time) VALUES (1, datetime('now'));
COMMIT TRANSACTION; -- COMMITだけでもOK
高度なトランザクション制御
セーブポイント(部分ロールバック)
長いトランザクションの一部だけを取り消したい場合:
-- セーブポイントの使用例
BEGIN;
-- 最初の操作
INSERT INTO orders (user_id, amount) VALUES (1, 1000);
-- セーブポイントを作成
SAVEPOINT order_items;
-- 商品追加を試行
INSERT INTO order_items (order_id, product_id, quantity)
VALUES (LAST_INSERT_ID(), 999, 1); -- 存在しない商品でエラー
-- エラーが発生したら、セーブポイントまで戻る
ROLLBACK TO order_items;
-- 正しい商品で再試行
INSERT INTO order_items (order_id, product_id, quantity)
VALUES (LAST_INSERT_ID(), 123, 1);
-- 全体をコミット(orderは残る)
COMMIT;
分離レベル(Isolation Level)
トランザクション同士がどの程度影響し合うかを制御:
-- 分離レベルの設定
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- または
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
-- 操作...
COMMIT;
分離レベルの種類
レベル | 説明 | 利用場面 |
---|---|---|
READ UNCOMMITTED | 他の未確定変更も読める | 高速だが危険 |
READ COMMITTED | 確定した変更のみ読める | 一般的な設定 |
REPEATABLE READ | 同じ読み取り結果を保証 | データ分析など |
SERIALIZABLE | 最も厳密な制御 | 重要な処理 |
プログラミング言語での実装

Python(psycopg2)での例
import psycopg2
def transfer_money(from_account, to_account, amount):
conn = psycopg2.connect(
host="localhost",
database="mydb",
user="user",
password="password"
)
try:
with conn: # 自動的にトランザクション管理
with conn.cursor() as cur:
# 送金元から引き出し
cur.execute(
"UPDATE accounts SET balance = balance - %s WHERE id = %s AND balance >= %s",
(amount, from_account, amount)
)
if cur.rowcount == 0:
raise Exception("残高不足です")
# 送金先に入金
cur.execute(
"UPDATE accounts SET balance = balance + %s WHERE id = %s",
(amount, to_account)
)
# withブロックを抜ける時に自動COMMIT
print("送金完了")
except Exception as e:
# エラー時は自動ROLLBACK
print(f"送金失敗: {e}")
finally:
conn.close()
JavaScript(Node.js)での例
const { Pool } = require('pg');
const pool = new Pool({/* 接続設定 */});
async function transferMoney(fromAccount, toAccount, amount) {
const client = await pool.connect();
try {
await client.query('BEGIN');
// 送金元から引き出し
const result1 = await client.query(
'UPDATE accounts SET balance = balance - $1 WHERE id = $2 AND balance >= $1',
[amount, fromAccount]
);
if (result1.rowCount === 0) {
throw new Error('残高不足です');
}
// 送金先に入金
await client.query(
'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
[amount, toAccount]
);
await client.query('COMMIT');
console.log('送金完了');
} catch (error) {
await client.query('ROLLBACK');
console.log('送金失敗:', error.message);
} finally {
client.release();
}
}
よくある間違いと注意点
間違い1:長すぎるトランザクション
-- 悪い例:長時間のトランザクション
BEGIN;
-- 大量のデータを処理(数分かかる)
UPDATE huge_table SET processed = true WHERE status = 'pending';
-- 他の処理(さらに時間がかかる)
INSERT INTO log_table SELECT * FROM another_huge_table;
COMMIT; -- 他のユーザーが長時間待たされる
改善案
-- 良い例:バッチ処理で分割
DO $$
DECLARE
batch_size INTEGER := 1000;
processed INTEGER := 0;
BEGIN
LOOP
BEGIN;
UPDATE huge_table
SET processed = true
WHERE id IN (
SELECT id FROM huge_table
WHERE status = 'pending' AND NOT processed
LIMIT batch_size
);
processed := ROW_COUNT;
COMMIT;
EXIT WHEN processed = 0;
END LOOP;
END $$;
間違い2:トランザクションの入れ子
-- 悪い例:トランザクションの中でトランザクション
BEGIN;
-- 何らかの処理
BEGIN; -- エラー!既にトランザクション中
-- 処理
COMMIT;
COMMIT;
間違い3:DDL文の扱い
-- データベースによって動作が異なる
BEGIN;
INSERT INTO users (name) VALUES ('田中');
-- MySQL: この時点で自動的にCOMMITされる
CREATE TABLE temp (id INT);
ROLLBACK; -- INSERTは取り消されるが、CREATE TABLEは残る
パフォーマンスの考慮事項
トランザクションの最適化
短時間で完了させる
-- 良い例:必要最小限の処理
BEGIN;
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE id = 2;
COMMIT; -- 短時間で完了
ロック競合を避ける
-- 悪い例:デッドロックの可能性
-- トランザクション1
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- accountsテーブルをロック
UPDATE orders SET status = 'paid' WHERE user_id = 1; -- ordersテーブルをロック
COMMIT;
-- トランザクション2(同時実行)
BEGIN;
UPDATE orders SET status = 'shipped' WHERE id = 123; -- ordersテーブルをロック
UPDATE accounts SET balance = balance + 50 WHERE id = 2; -- accountsテーブルをロック(待機)
COMMIT;
改善案:テーブルアクセス順序を統一
-- 良い例:常に同じ順序でテーブルにアクセス
-- どちらのトランザクションも accounts → orders の順序
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE orders SET status = 'paid' WHERE user_id = 1;
COMMIT;
まとめ
SQLのトランザクションは、データベースを安全に操作するための重要な機能です。
重要なポイント
- ACID特性で安全性を保証
- BEGIN/COMMIT/ROLLBACKの基本構文
- エラー時の自動ロールバック
- 短時間での完了を心がける
基本的な使い方
BEGIN; -- 開始
-- SQL文を実行
COMMIT; -- 成功時:確定
-- ROLLBACK; -- 失敗時:取り消し
使うべき場面
- 複数のテーブルを同時に更新するとき
- データの整合性が重要なとき
- エラー時に安全に処理を中断したいとき
- 金融システムやECサイトなど
避けるべきこと
- 長時間のトランザクション
- 不必要な大きなトランザクション
- トランザクションの入れ子
コメント