SQLのトランザクションとは?初心者でもわかる仕組みと使い方|完全ガイド

データベース・SQL

データベースを使ったシステムを作っていて、こんな心配をしたことはありませんか?

  • 「お金の移動処理で、片方の口座だけ変更されたらどうしよう」
  • 「途中でエラーが起きて、データがぐちゃぐちゃになったら…」
  • 「複数の処理を安全にまとめて実行したい」

そんな不安を解決してくれるのが 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;

データベース別の実装の違い

主要データベースの対応状況

データベーストランザクション特徴
PostgreSQLDDL文もトランザクション内で実行可能
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サイトなど

避けるべきこと

  • 長時間のトランザクション
  • 不必要な大きなトランザクション
  • トランザクションの入れ子

コメント

タイトルとURLをコピーしました