MySQL - ストアドプロシージャ
概要
MySQLのストアドプロシージャ (Stored Procedure) は、一連のSQL文をデータベース内に保存し、名前を付けて呼び出せるようにするプログラム単位である。
複雑な処理をカプセル化し、再利用性とメンテナンス性を向上させることができる。
ストアドプロシージャは、パラメータを受け取り、複雑な制御フロー構文 (IF、CASE、LOOP、WHILE、REPEAT) を使用して処理を実行できる。
また、カーソルを使用して結果セットを行単位で処理したり、ハンドラーを使用してエラーや例外を処理することもできる。
ストアドプロシージャは、CALL 文で呼び出され、IN (入力)、OUT (出力)、INOUT (入出力) の3種類のパラメータをサポートする。
OUT および INOUT パラメータを使用することで、呼び出し側に値を返すことができる。
ストアドプロシージャの主なメリットには、ネットワークトラフィックの削減、セキュリティの向上、コードの一元管理がある。
アプリケーション側で複数のSQL文を個別に実行する代わりに、サーバ側で一括処理できるため、パフォーマンスが向上する。
ストアドプロシージャは、データベース内に永続化されるため、SHOW CREATE PROCEDURE 文や INFORMATION_SCHEMA.ROUTINES テーブルから定義を確認できる。
プロシージャ本体を変更する場合は、DROP + CREATE による再作成が必要である。
ストアドプロシージャと類似した機能として、ストアドファンクション (Stored Function) がある。
ファンクションはRETURN文で単一の値を返し、SELECT文内で使用できるが、プロシージャは CALL 文で呼び出し、OUT / INOUTパラメータで複数の値を返す点で異なる。
ストアドプロシージャの作成
基本構文
ストアドプロシージャを作成するには、CREATE PROCEDURE 文を使用する。
完全な構文を以下に示す。
CREATE
[DEFINER = user]
PROCEDURE [IF NOT EXISTS] <プロシージャ名> ([<パラメータ>[,...]])
[characteristic ...]
routine_body
proc_parameter:
[ IN | OUT | INOUT ] param_name type
characteristic: {
COMMENT 'string'
| LANGUAGE SQL
| [NOT] DETERMINISTIC
| { CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA }
| SQL SECURITY { DEFINER | INVOKER }
}
routine_body:
Valid SQL routine statement
基本的な例を以下に示す。
DELIMITER //
CREATE PROCEDURE simple_proc()
BEGIN
SELECT 'Hello, World!';
END//
DELIMITER ;
DELIMITER 文は、ストアドプロシージャ内でセミコロン (;) を使用するため、一時的にデリミタを変更する必要がある。
DELIMITER // で変更し、プロシージャ定義の最後に DELIMITER ; で元に戻す。
IF NOT EXISTS句を指定すると、同名のプロシージャが既に存在する場合にエラーにならず、警告のみが出力される。
DELIMITER //
CREATE PROCEDURE IF NOT EXISTS get_user_count()
BEGIN
SELECT COUNT(*) FROM users;
END//
DELIMITER ;
パラメータの種類
ストアドプロシージャのパラメータには、IN、OUT、INOUTの3種類がある。
| パラメータ | 説明 |
|---|---|
| IN |
|
| OUT |
|
| INOUT |
|
各パラメータの使用例を以下に示す。
DELIMITER //
CREATE PROCEDURE calc_total(
IN price DECIMAL(10,2),
IN quantity INT,
OUT total DECIMAL(10,2)
)
BEGIN
SET total = price * quantity;
END//
DELIMITER ;
-- 呼び出し例
CALL calc_total(100.50, 3, @result);
SELECT @result; -- 結果: 301.50
INOUTパラメータの使用例を以下に示す。
DELIMITER //
CREATE PROCEDURE double_value(INOUT num INT)
BEGIN
SET num = num * 2;
END//
DELIMITER ;
-- 呼び出し例
SET @value = 10;
CALL double_value(@value);
SELECT @value; -- 結果: 20
変数の宣言と代入
ストアドプロシージャ内では、DECLARE 文で変数を宣言し、SET 文や SELECT INTO 文で値を代入できる。
変数宣言の構文を以下に示す。
DECLARE <変数名> [, <変数名>] ... <データ型> [DEFAULT <デフォルト値>]
変数への代入には、以下の2つの方法がある。
SET <変数名> = <式> [, <変数名> = <式>] ...
SELECT <列名> [, ...] INTO <変数名> [, ...] FROM <テーブル名> WHERE ...
使用例を以下に示す。
DELIMITER //
CREATE PROCEDURE variable_example()
BEGIN
DECLARE user_count INT DEFAULT 0;
DECLARE max_price DECIMAL(10,2);
DECLARE product_name VARCHAR(100);
-- SET文による代入
SET user_count = 100;
-- SELECT INTO文による代入
SELECT MAX(price), name
INTO max_price, product_name
FROM products
WHERE price = (SELECT MAX(price) FROM products);
SELECT user_count, max_price, product_name;
END//
DELIMITER ;
変数のスコープは、宣言されたBEGIN...ENDブロック内に限定される。
変数は、宣言された順序で初期化される。
ストアドプロシージャの実行
ストアドプロシージャを実行するには、CALL 文を使用する。
CALL <プロシージャ名>([<パラメータ>[, ...]]);
- パラメータなしのプロシージャを呼び出す例
CALL simple_proc();
- INパラメータを持つプロシージャを呼び出す例
CALL get_user_info(123);
- OUT / INOUTパラメータを持つプロシージャを呼び出す場合は、ユーザ定義変数 (
@変数名) を使用する。CALL calc_total(100.50, 3, @result); SELECT @result;
- 複数のOUTパラメータを持つプロシージャの例
DELIMITER // CREATE PROCEDURE get_user_stats( OUT total_users INT, OUT active_users INT, OUT inactive_users INT ) BEGIN SELECT COUNT(*) INTO total_users FROM users; SELECT COUNT(*) INTO active_users FROM users WHERE status = 'active'; SELECT COUNT(*) INTO inactive_users FROM users WHERE status = 'inactive'; END// DELIMITER ; -- 呼び出し例 CALL get_user_stats(@total, @active, @inactive); SELECT @total, @active, @inactive;
制御フロー構文
IF文
IF文は、条件に応じて処理を分岐させる。
IF <条件式> THEN
<処理>
[ELSEIF <条件式> THEN
<処理>] ...
[ELSE
<処理>]
END IF
使用例を以下に示す。
DELIMITER //
CREATE PROCEDURE check_price(IN price DECIMAL(10,2), OUT category VARCHAR(20))
BEGIN
IF price < 100 THEN
SET category = '安価';
ELSEIF price < 1000 THEN
SET category = '中価格';
ELSE
SET category = '高価';
END IF;
END//
DELIMITER ;
CASE文
CASE文には、単純CASEと検索CASEの2種類がある。
単純CASE文の構文を以下に示す。
CASE <判定値>
WHEN <値> THEN <処理>
[WHEN <値> THEN <処理>] ...
[ELSE <処理>]
END CASE
検索CASE文の構文を以下に示す。
CASE
WHEN <条件式> THEN <処理>
[WHEN <条件式> THEN <処理>] ...
[ELSE <処理>]
END CASE
単純CASE文の使用例を以下に示す。
DELIMITER //
CREATE PROCEDURE get_day_type(IN day_num INT, OUT day_type VARCHAR(20))
BEGIN
CASE day_num
WHEN 1 THEN SET day_type = '月曜日';
WHEN 2 THEN SET day_type = '火曜日';
WHEN 3 THEN SET day_type = '水曜日';
WHEN 4 THEN SET day_type = '木曜日';
WHEN 5 THEN SET day_type = '金曜日';
WHEN 6 THEN SET day_type = '土曜日';
WHEN 7 THEN SET day_type = '日曜日';
ELSE SET day_type = '不明';
END CASE;
END//
DELIMITER ;
検索CASE文の使用例を以下に示す。
DELIMITER //
CREATE PROCEDURE grade_score(IN score INT, OUT grade CHAR(1))
BEGIN
CASE
WHEN score >= 90 THEN SET grade = 'A';
WHEN score >= 80 THEN SET grade = 'B';
WHEN score >= 70 THEN SET grade = 'C';
WHEN score >= 60 THEN SET grade = 'D';
ELSE SET grade = 'F';
END CASE;
END//
DELIMITER ;
LOOP文
LOOP文は、無限ループを実装する。
LEAVE文でループを抜け、ITERATE文でループの先頭に戻る。
[<ラベル>:] LOOP
<処理>
END LOOP [<ラベル>]
使用例を以下に示す。
DELIMITER //
CREATE PROCEDURE loop_example()
BEGIN
DECLARE counter INT DEFAULT 0;
DECLARE sum INT DEFAULT 0;
my_loop: LOOP
SET counter = counter + 1;
SET sum = sum + counter;
IF counter >= 10 THEN
LEAVE my_loop;
END IF;
END LOOP my_loop;
SELECT sum; -- 結果: 55
END//
DELIMITER ;
ITERATE文を使用した例を以下に示す。
DELIMITER //
CREATE PROCEDURE iterate_example()
BEGIN
DECLARE counter INT DEFAULT 0;
DECLARE even_sum INT DEFAULT 0;
my_loop: LOOP
SET counter = counter + 1;
IF counter > 10 THEN
LEAVE my_loop;
END IF;
IF counter MOD 2 = 1 THEN
ITERATE my_loop; -- 奇数はスキップ
END IF;
SET even_sum = even_sum + counter;
END LOOP my_loop;
SELECT even_sum; -- 結果: 30 (2+4+6+8+10)
END//
DELIMITER ;
WHILE文
WHILE文は、条件がTRUEの間、処理を繰り返す。
[<ラベル>:] WHILE <条件式> DO
<処理>
END WHILE [<ラベル>]
使用例を以下に示す。
DELIMITER //
CREATE PROCEDURE while_example()
BEGIN
DECLARE counter INT DEFAULT 1;
DECLARE factorial INT DEFAULT 1;
WHILE counter <= 5 DO
SET factorial = factorial * counter;
SET counter = counter + 1;
END WHILE;
SELECT factorial; -- 結果: 120 (5の階乗)
END//
DELIMITER ;
REPEAT文
REPEAT文は、条件がTRUEになるまで処理を繰り返す。
WHILE文とは異なり、最低1回は処理が実行される。
[<ラベル>:] REPEAT
<処理>
UNTIL <条件式>
END REPEAT [<ラベル>]
使用例を以下に示す。
DELIMITER //
CREATE PROCEDURE repeat_example()
BEGIN
DECLARE counter INT DEFAULT 1;
DECLARE sum INT DEFAULT 0;
REPEAT
SET sum = sum + counter;
SET counter = counter + 1;
UNTIL counter > 10
END REPEAT;
SELECT sum; -- 結果: 55
END//
DELIMITER ;
カーソル
カーソルの基本
カーソルは、SELECT文の結果セットを行単位で処理するための機能である。
カーソルの基本的な構文を以下に示す。
DECLARE <カーソル名> CURSOR FOR <SELECT文>;
OPEN <カーソル名>;
FETCH <カーソル名> INTO <変数名> [, <変数名>] ...;
CLOSE <カーソル名>;
カーソルの宣言は、変数宣言の後、ハンドラー宣言の前に行う必要がある。
SELECT文にはINTO句を使用できない。
基本的な使用例を以下に示す。
DELIMITER //
CREATE PROCEDURE cursor_example()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE user_name VARCHAR(50);
DECLARE user_email VARCHAR(100);
DECLARE cur CURSOR FOR SELECT username, email FROM users;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN cur;
read_loop: LOOP
FETCH cur INTO user_name, user_email;
IF done THEN
LEAVE read_loop;
END IF;
-- 各行に対する処理
SELECT user_name, user_email;
END LOOP;
CLOSE cur;
END//
DELIMITER ;
カーソルと繰り返し処理
カーソルは、NOT FOUND ハンドラーと組み合わせて使用する。
カーソルの終端に達すると、NOT FOUND条件が発生する。
カーソルは、使用後に必ず CLOSE 文で閉じる必要がある。
実用的な例を以下に示す。
DELIMITER //
CREATE PROCEDURE curdemo()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE a CHAR(16);
DECLARE b, c INT;
DECLARE cur1 CURSOR FOR SELECT id, data FROM test.t1;
DECLARE cur2 CURSOR FOR SELECT i FROM test.t2;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN cur1;
OPEN cur2;
read_loop: LOOP
FETCH cur1 INTO a, b;
FETCH cur2 INTO c;
IF done THEN
LEAVE read_loop;
END IF;
IF b < c THEN
INSERT INTO test.t3 VALUES (a, b);
ELSE
INSERT INTO test.t3 VALUES (a, c);
END IF;
END LOOP;
CLOSE cur1;
CLOSE cur2;
END//
DELIMITER ;
複数のカーソルを同時に使用する場合は、上記の例のように個別に開閉する。
ハンドラー
ハンドラーは、エラーや例外が発生した際の処理を定義する。
DECLARE handler_action HANDLER
FOR <条件値> [, <条件値>] ...
statement;
handler_action: { CONTINUE | EXIT | UNDO }
condition_value: {
mysql_error_code
| SQLSTATE [VALUE] sqlstate_value
| condition_name
| SQLWARNING
| NOT FOUND
| SQLEXCEPTION
}
| アクション | 説明 |
|---|---|
| CONTINUE | ハンドラー実行後、処理を続行する |
| EXIT | ハンドラー実行後、現在のBEGIN...ENDブロックを終了する |
| UNDO | 未サポート |
| 条件値 | 説明 |
|---|---|
| SQLWARNING | '01'で始まるSQLSTATEの短縮形 |
| NOT FOUND | '02'で始まるSQLSTATEの短縮形 (カーソルの終端等) |
| SQLEXCEPTION | '00', '01', '02'以外のSQLSTATEの短縮形 |
使用例を以下に示す。
DELIMITER //
CREATE PROCEDURE handler_example()
BEGIN
DECLARE duplicate_key INT DEFAULT 0;
DECLARE CONTINUE HANDLER FOR 1062
SET duplicate_key = 1;
INSERT INTO users (user_id, username) VALUES (1, 'test');
IF duplicate_key = 1 THEN
SELECT 'Duplicate key error occurred';
ELSE
SELECT 'Insert successful';
END IF;
END//
DELIMITER ;
- SQLSTATE値を使用した例
DELIMITER // CREATE PROCEDURE sqlstate_handler() BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE '23000' SELECT 'Integrity constraint violation'; INSERT INTO orders (order_id, customer_id) VALUES (1, 999); END// DELIMITER ;
- 複数の条件を処理する例
DELIMITER // CREATE PROCEDURE multi_handler() BEGIN DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; SELECT 'An error occurred, transaction rolled back'; END; DECLARE CONTINUE HANDLER FOR SQLWARNING SELECT 'Warning occurred'; START TRANSACTION; -- SQL文 COMMIT; END// DELIMITER ;
ストアドプロシージャの変更と削除
ストアドプロシージャの本体 (routine_body) を変更する場合は、DROP PROCEDURE と CREATE PROCEDURE を組み合わせて再作成する必要がある。
ALTER PROCEDURE 文は、プロシージャの特性 (characteristics) のみを変更できる。
変更可能な特性は、COMMENT、LANGUAGE SQL、SQL SECURITY、その他のcharacteristicsである。
ALTER PROCEDURE <プロシージャ名>
[characteristic ...]
characteristic: {
COMMENT 'string'
| LANGUAGE SQL
| SQL SECURITY { DEFINER | INVOKER }
}
使用例を以下に示す。
DROP PROCEDURE IF EXISTS get_user_count;
IF EXISTS句を指定すると、プロシージャが存在しない場合でもエラーにならず、警告のみが出力される。
- コメントを変更する例
ALTER PROCEDURE get_user_count COMMENT '全ユーザ数を取得するプロシージャ';
- ストアドプロシージャを削除するには、
DROP PROCEDURE文を使用する。DROP PROCEDURE [IF EXISTS] <プロシージャ名>
ストアドプロシージャの確認
ストアドプロシージャの定義を確認するには、SHOW CREATE PROCEDURE 文を使用する。
SHOW CREATE PROCEDURE <プロシージャ名>;
データベース内の全てのプロシージャ一覧を確認するには、SHOW PROCEDURE STATUS 文を使用する。
SHOW PROCEDURE STATUS;
-- 特定のデータベースのプロシージャのみを表示
SHOW PROCEDURE STATUS WHERE Db = 'database_name';
-- パターンマッチング
SHOW PROCEDURE STATUS LIKE 'get_%';
INFORMATION_SCHEMA.ROUTINES テーブルから詳細情報を取得することもできる。
SELECT ROUTINE_NAME, ROUTINE_TYPE, ROUTINE_DEFINITION
FROM INFORMATION_SCHEMA.ROUTINES
WHERE ROUTINE_SCHEMA = '<データベース名>'
AND ROUTINE_TYPE = 'PROCEDURE';
プロシージャのパラメータ情報を確認する場合は、INFORMATION_SCHEMA.PARAMETERS テーブルを使用する。
SELECT PARAMETER_NAME, PARAMETER_MODE, DATA_TYPE
FROM INFORMATION_SCHEMA.PARAMETERS
WHERE SPECIFIC_SCHEMA = '<データベース名>'
AND SPECIFIC_NAME = '<プロシージャ名>'
ORDER BY ORDINAL_POSITION;
トランザクション制御
ストアドプロシージャ内で、トランザクション制御を行うことができる。
START TRANSACTION;
-- SQL文
COMMIT;
-- または
ROLLBACK;
トランザクションを使用した例を以下に示す。
DELIMITER //
CREATE PROCEDURE transfer_money(
IN from_account INT,
IN to_account INT,
IN amount DECIMAL(10,2)
)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SELECT 'Transaction failed, rolled back';
END;
START TRANSACTION;
UPDATE accounts SET balance = balance - amount WHERE account_id = from_account;
UPDATE accounts SET balance = balance + amount WHERE account_id = to_account;
COMMIT;
SELECT 'Transaction successful';
END//
DELIMITER ;
SAVEPOINTを使用した例を以下に示す。
DELIMITER //
CREATE PROCEDURE savepoint_example()
BEGIN
START TRANSACTION;
INSERT INTO log_table (message) VALUES ('Start');
SAVEPOINT sp1;
INSERT INTO log_table (message) VALUES ('Step 1');
SAVEPOINT sp2;
INSERT INTO log_table (message) VALUES ('Step 2');
-- sp2までロールバック
ROLLBACK TO SAVEPOINT sp2;
COMMIT;
END//
DELIMITER ;
ストアドプロシージャ内でトランザクションを使用する場合、エラーハンドラーと組み合わせることで、エラー時の自動ロールバックを実装できる。
ストアドファンクションとの違い
ストアドプロシージャとストアドファンクションの主な違いを以下に示す。
| 項目 | ストアドプロシージャ | ストアドファンクション |
|---|---|---|
| 呼び出し方法 | CALL文 |
SELECT文内で使用 |
| 戻り値 | OUT / INOUTパラメータで複数の値を返す | RETURN文で単一の値を返す |
| RETURNS句 | 不要 | 必須 |
| パラメータ | IN / OUT / INOUT | INのみ |
| トランザクション制御 | 可能 | 制限あり (COMMIT / ROLLBACK不可) |
| DML文 | 使用可能 | 使用可能 |
| 結果セットを返すSELECT文 | 使用可能 | 使用不可 |
| DETERMINISTIC / NO SQL / READS SQL DATA | オプション | 必須 (いずれか1つ) |
ストアドファンクションの例を以下に示す。
DELIMITER //
CREATE FUNCTION calc_tax(price DECIMAL(10,2))
RETURNS DECIMAL(10,2)
DETERMINISTIC
BEGIN
RETURN price * 0.10;
END//
DELIMITER ;
-- 使用例
SELECT product_name, price, calc_tax(price) AS tax
FROM products;
ストアドファンクションは、DETERMINISTIC または NO SQL / READS SQL DATAの指定が必須である。
これにより、MySQLはファンクションが決定的 (同じ入力に対して常に同じ出力を返す) かどうかを判断できる。
ストアドプロシージャは、複雑な処理や複数の値を返す場合に適している。
ストアドファンクションは、単一の計算結果を返し、SELECT文内で使用する場合に適している。
サンプルクエリ
ページネーション処理
DELIMITER //
CREATE PROCEDURE get_users_paginated(
IN page_num INT,
IN page_size INT,
OUT total_count INT
)
BEGIN
DECLARE offset_val INT;
SET offset_val = (page_num - 1) * page_size;
SELECT COUNT(*) INTO total_count FROM users;
SELECT user_id, username, email, created_at
FROM users
ORDER BY user_id
LIMIT page_size OFFSET offset_val;
END//
DELIMITER ;
-- 呼び出し例: 2ページ目を取得 (1ページあたり10件)
CALL get_users_paginated(2, 10, @total);
SELECT @total AS total_users;
条件付き集計処理
DELIMITER //
CREATE PROCEDURE get_sales_summary(
IN start_date DATE,
IN end_date DATE
)
BEGIN
SELECT
COUNT(*) AS total_orders,
SUM(total_amount) AS total_sales,
AVG(total_amount) AS avg_order_value,
MAX(total_amount) AS max_order_value,
MIN(total_amount) AS min_order_value
FROM orders
WHERE order_date BETWEEN start_date AND end_date;
SELECT
product_id,
SUM(quantity) AS total_quantity,
SUM(quantity * unit_price) AS total_revenue
FROM order_items oi
JOIN orders o ON oi.order_id = o.order_id
WHERE o.order_date BETWEEN start_date AND end_date
GROUP BY product_id
ORDER BY total_revenue DESC
LIMIT 10;
END//
DELIMITER ;
データ検証と挿入
DELIMITER //
CREATE PROCEDURE insert_order(
IN p_customer_id INT,
IN p_product_id INT,
IN p_quantity INT,
OUT p_order_id INT,
OUT p_status VARCHAR(50)
)
BEGIN
DECLARE v_stock INT;
DECLARE v_price DECIMAL(10,2);
DECLARE v_total DECIMAL(10,2);
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SET p_status = 'エラーが発生しました';
SET p_order_id = NULL;
END;
-- 在庫確認
SELECT stock, price INTO v_stock, v_price
FROM products
WHERE product_id = p_product_id;
IF v_stock < p_quantity THEN
SET p_status = '在庫不足';
SET p_order_id = NULL;
ELSE
START TRANSACTION;
SET v_total = v_price * p_quantity;
INSERT INTO orders (customer_id, order_date, total_amount)
VALUES (p_customer_id, NOW(), v_total);
SET p_order_id = LAST_INSERT_ID();
INSERT INTO order_items (order_id, product_id, quantity, unit_price)
VALUES (p_order_id, p_product_id, p_quantity, v_price);
UPDATE products
SET stock = stock - p_quantity
WHERE product_id = p_product_id;
COMMIT;
SET p_status = '注文が完了しました';
END IF;
END//
DELIMITER ;
-- 呼び出し例
CALL insert_order(123, 456, 5, @order_id, @status);
SELECT @order_id, @status;
関連ページ