この内容は古いバージョンです。最新バージョンを表示するには、戻るボタンを押してください。
バージョン:3
ページ更新者:guest
更新日時:2026-06-11 07:07:02

タイトル: ストアド・プロシージャ
SEOタイトル: SQL ストアド・プロシージャ完全ガイド

この記事の要点
  • ストアド・プロシージャは SQL + 手続き言語で書かれた処理を DB サーバに保存する仕組み
  • 構文: CREATE PROCEDURE name(args) BEGIN ... END。Oracle PL/SQL / PostgreSQL PL/pgSQL / MySQL ストアド
  • パラメータは IN / OUT / INOUT。ローカル変数・制御構文 (IF/WHILE/LOOP)・例外処理を持つ
  • 性能利点: DB サーバ側で実行 = ネットワーク往復削減、コンパイル済
  • 欠点: 言語固有・バージョン管理が困難・テストしにくい → 現代では Application 側でロジックを持つ設計が主流

ストアド・プロシージャとは

ストアド・プロシージャ (Stored Procedure) は、SQL に加えて変数・制御構文・例外処理などの手続き的機能を持つ「DB に保存される関数」です。アプリケーションから CALL procedure_name(args) で呼び出すと、DB サーバ内で実行されます。

1980 年代に Sybase / SQL Server で導入され、Oracle の PL/SQL、PostgreSQL の PL/pgSQL、MySQL のストアドプロシージャとして標準的な機能になりました。

基本構文 (標準 SQL/PSM 風)

CREATE PROCEDURE procedure_name (
    IN  param1 INT,
    OUT param2 VARCHAR(100),
    INOUT param3 DECIMAL
)
BEGIN
    -- ローカル変数
    DECLARE local_var INT DEFAULT 0;

    -- 処理
    SELECT name INTO param2 FROM users WHERE id = param1;
    SET param3 = param3 * 1.1;

    -- 制御構文
    IF local_var > 0 THEN
        UPDATE stats SET count = count + 1;
    END IF;
END;

-- 呼び出し
CALL procedure_name(1, @name_out, @price_inout);
SELECT @name_out;

パラメータの種類

種類方向用途
IN入力のみ引数渡し(デフォルト)
OUT出力のみ結果を返す
INOUT入出力値を渡して書き換えて返す

Oracle PL/SQL

CREATE OR REPLACE PROCEDURE update_user_salary (
    p_user_id   IN  NUMBER,
    p_increment IN  NUMBER,
    p_new_total OUT NUMBER
) AS
    v_current_salary NUMBER;
    v_user_name      VARCHAR2(100);
BEGIN
    -- データ取得
    SELECT salary, name
    INTO v_current_salary, v_user_name
    FROM employees
    WHERE id = p_user_id;

    -- 計算
    p_new_total := v_current_salary + p_increment;

    -- 更新
    UPDATE employees SET salary = p_new_total WHERE id = p_user_id;

    -- ログ
    DBMS_OUTPUT.PUT_LINE('Updated ' || v_user_name || ' to ' || p_new_total);

EXCEPTION
    WHEN NO_DATA_FOUND THEN
        RAISE_APPLICATION_ERROR(-20001, 'User not found: ' || p_user_id);
    WHEN OTHERS THEN
        ROLLBACK;
        RAISE;
END;
/

-- 呼び出し
DECLARE
    v_total NUMBER;
BEGIN
    update_user_salary(101, 5000, v_total);
    DBMS_OUTPUT.PUT_LINE('New total: ' || v_total);
END;
/

PostgreSQL PL/pgSQL

CREATE OR REPLACE PROCEDURE transfer_money(
    sender_id    INT,
    receiver_id  INT,
    amount       NUMERIC
)
LANGUAGE plpgsql
AS $$
DECLARE
    sender_balance NUMERIC;
BEGIN
    SELECT balance INTO sender_balance
    FROM accounts WHERE id = sender_id FOR UPDATE;

    IF sender_balance < amount THEN
        RAISE EXCEPTION '残高不足: % < %', sender_balance, amount;
    END IF;

    UPDATE accounts SET balance = balance - amount WHERE id = sender_id;
    UPDATE accounts SET balance = balance + amount WHERE id = receiver_id;

    INSERT INTO transfer_log(from_id, to_id, amount, ts)
    VALUES (sender_id, receiver_id, amount, NOW());

    COMMIT;
EXCEPTION
    WHEN OTHERS THEN
        ROLLBACK;
        RAISE;
END;
$$;

CALL transfer_money(1, 2, 1000);

-- PostgreSQL は CREATE FUNCTION の方が一般的
-- (RETURN 句があり、SELECT で呼べる)

MySQL ストアドプロシージャ

DELIMITER //

CREATE PROCEDURE add_order(
    IN  p_user_id  INT,
    IN  p_amount   DECIMAL(10,2),
    OUT p_order_id INT
)
BEGIN
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        RESIGNAL;
    END;

    START TRANSACTION;

    INSERT INTO orders(user_id, amount, created_at)
    VALUES (p_user_id, p_amount, NOW());

    SET p_order_id = LAST_INSERT_ID();

    UPDATE users SET total_orders = total_orders + 1 WHERE id = p_user_id;

    COMMIT;
END //

DELIMITER ;

-- 呼び出し
CALL add_order(101, 5000.00, @order_id);
SELECT @order_id;

制御構文

-- IF / ELSEIF / ELSE
IF condition THEN
    statements;
ELSEIF other_condition THEN
    statements;
ELSE
    statements;
END IF;

-- CASE
CASE input
    WHEN 1 THEN ...
    WHEN 2 THEN ...
    ELSE ...
END CASE;

-- WHILE ループ
WHILE i < 10 DO
    SET i = i + 1;
END WHILE;

-- REPEAT ループ
REPEAT
    SET i = i + 1;
UNTIL i >= 10
END REPEAT;

-- LOOP + LEAVE
my_loop: LOOP
    SET i = i + 1;
    IF i >= 10 THEN
        LEAVE my_loop;
    END IF;
END LOOP my_loop;

-- カーソル
DECLARE done INT DEFAULT FALSE;
DECLARE cur CURSOR FOR SELECT id FROM users;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;

OPEN cur;
read_loop: LOOP
    FETCH cur INTO user_id;
    IF done THEN LEAVE read_loop; END IF;
    -- 処理
END LOOP;
CLOSE cur;

エラーハンドリング

-- MySQL: DECLARE HANDLER
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
    ROLLBACK;
    RESIGNAL;
END;

DECLARE CONTINUE HANDLER FOR NOT FOUND
SET done = TRUE;

-- Oracle: EXCEPTION
EXCEPTION
    WHEN NO_DATA_FOUND THEN ...
    WHEN TOO_MANY_ROWS THEN ...
    WHEN DUP_VAL_ON_INDEX THEN ...
    WHEN OTHERS THEN
        -- ログ出力 + 再 RAISE
        DBMS_OUTPUT.PUT_LINE(SQLERRM);
        RAISE;
END;

-- PostgreSQL: BEGIN ... EXCEPTION
EXCEPTION
    WHEN no_data_found THEN ...
    WHEN unique_violation THEN ...
    WHEN OTHERS THEN
        RAISE NOTICE '%', SQLERRM;
        RAISE;
END;

FUNCTION との違い

項目PROCEDUREFUNCTION
呼び出しCALL proc()SELECT func()
戻り値無し (OUT で返す)必須 (RETURN 句)
SQL 内利用不可可能(WHERE 句等で使える)
副作用 (DML)OK制限あり(DB によっては不可)
トランザクション制御COMMIT/ROLLBACK 可不可 (DB による)

TRIGGER との違い

  • PROCEDURE: 明示的に CALL で起動
  • TRIGGER: テーブルへの INSERT/UPDATE/DELETE で自動起動
  • Trigger 内から PROCEDURE を呼ぶことも多い

性能上のメリット

  1. ネットワーク往復削減: 複数 SQL を 1 回の CALL で実行
  2. コンパイル済: 実行計画がキャッシュされる
  3. パーミッション集約: テーブル直接権限なしでも PROCEDURE 経由でアクセス可
  4. DB サーバ近接: 中間データを返さずに集計可能

欠点(現代的視点)

欠点詳細
言語固有PL/SQL ↔ PL/pgSQL ↔ MySQL で書換必要
バージョン管理が困難DB 内に保存、Git との連動が難しい
テストしにくいDB 接続が必要、CI/CD で扱いにくい
デバッグ困難IDE / ステップ実行が限定的
スケールしにくいDB サーバが計算リソース消費 → スケール先が高価
マイクロサービスと相性悪サービス境界がぼやける

現代の使い分け

場面推奨
レガシー大量データ処理 (ETL)ストアドで OK
OLTP のビジネスロジックApplication 側 (Java / PHP / Python)
セキュリティ境界としての権限制御ストアド有効
バッチ集計ストアド or バッチアプリ
マイクロサービスApplication 側、ストアド避ける

デバッグとプロファイリング

-- MySQL: SHOW PROCEDURE STATUS
SHOW PROCEDURE STATUS WHERE Db = 'mydb';
SHOW CREATE PROCEDURE proc_name;

-- Oracle: DBA_PROCEDURES, USER_OBJECTS
SELECT * FROM user_procedures;
SELECT text FROM user_source WHERE name = 'PROC_NAME' ORDER BY line;

-- PostgreSQL: \df+ proc_name
SELECT proname, prosrc FROM pg_proc WHERE proname = 'proc_name';

-- 削除
DROP PROCEDURE IF EXISTS proc_name;

アンチパターン

  • 巨大な単一プロシージャ(1 万行超): 保守不能。分割せよ
  • 業務ロジックを全部 DB に: テスト不能、ベンダーロックイン
  • Trigger ループ: A の更新で B のトリガが、B が C を…と連鎖し制御不能
  • 動的 SQL の濫用: SQL インジェクションリスク、実行計画キャッシュ無効

FAQ

Q: ストアドプロシージャは時代遅れ?
A: 「禁止」というほどではないが、新規プロジェクトでは Application 側にロジックを置くのが主流。レガシーシステムでは依然現役。

Q: バージョン管理する方法は?
A: ストアドの定義を SQL ファイルとして Git 管理し、Flyway / Liquibase 等のマイグレーションツールで適用するのが一般的。

Q: パフォーマンスはアプリ実装より速い?
A: ネットワーク往復が多い処理(カーソルループ等)では速いが、現代の Connection Pool + バッチ API で多くは並べる。「無条件にストアドが速い」は誤解。

Q: Java から呼ぶには?
A: JDBC の CallableStatement を使います。{call proc(?, ?, ?)} 形式。Spring Data JPA でも @Procedure アノテーションで呼べます。