-- 1. 創建無參過程
DELIMITER //
CREATE PROCEDURE sp_hello()
BEGIN
SELECT 'Hello MySQL Stored Procedure!' AS message;
END //
DELIMITER ;
-- 2. 帶參數 (IN/OUT/INOUT)
CREATE PROCEDURE sp_register(
IN p_username VARCHAR(50),
IN p_password VARCHAR(100),
IN p_email VARCHAR(100),
OUT p_user_id BIGINT,
OUT p_result_code INT,
OUT p_result_msg VARCHAR(100)
)
BEGIN
DECLARE v_count INT DEFAULT 0;
-- 檢查用戶名是否存在
SELECT COUNT(*) INTO v_count FROM users WHERE username = p_username;
IF v_count > 0 THEN
SET p_result_code = -1;
SET p_result_msg = '用戶名已存在';
SET p_user_id = 0;
ELSE
INSERT INTO users (username, password_hash, email, created_at)
VALUES (p_username, SHA2(p_password, 256), p_email, NOW());
SET p_user_id = LAST_INSERT_ID();
SET p_result_code = 0;
SET p_result_msg = '註冊成功';
END IF;
END //
-- 3. 調用
CALL sp_register('test_user', 'pass123', 'test@example.com', @uid, @code, @msg);
SELECT @uid, @code, @msg;
-- IF-ELSEIF-ELSE
IF condition1 THEN
-- statements
ELSEIF condition2 THEN
-- statements
ELSE
-- statements
END IF;
-- CASE 語句
CASE var
WHEN 1 THEN -- statements
WHEN 2 THEN -- statements
ELSE -- statements
END CASE;
-- WHILE 循環
WHILE i <= 10 DO
SET i = i + 1;
END WHILE;
-- REPEAT UNTIL
REPEAT
SET i = i + 1;
UNTIL i > 10 END REPEAT;
-- LOOP + LEAVE (類似 break)
loop_label: LOOP
SET i = i + 1;
IF i > 10 THEN LEAVE loop_label; END IF;
END LOOP;
-- ITERATE (類似 continue)
loop_label: LOOP
SET i = i + 1;
IF i % 2 = 0 THEN ITERATE loop_label; END IF;
-- 只處理奇數
END LOOP;
-- 局部變量
DECLARE v_total INT DEFAULT 0;
DECLARE v_name VARCHAR(100);
DECLARE v_cursor CURSOR FOR SELECT id, name FROM users WHERE status = 1;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = TRUE;
OPEN v_cursor;
cursor_loop: LOOP
FETCH v_cursor INTO v_id, v_name;
IF v_done THEN LEAVE cursor_loop; END IF;
-- 處理邏輯
END LOOP;
CLOSE v_cursor;
CREATE PROCEDURE sp_dynamic_query(IN p_table VARCHAR(64), IN p_where VARCHAR(200))
BEGIN
SET @sql = CONCAT('SELECT * FROM `', p_table, '` WHERE ', p_where);
-- 防注入:嚴格驗證 p_table 白名單,p_where 參數化
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END //
-- 調用
CALL sp_dynamic_query('users', 'status = 1 AND age > 18');
絕不直接拼接用戶輸入!必須:
1. 表名/列名白名單校驗
2. 值通過 PREPARE ... USING 參數綁定
3. 或使用 sys.quote_identifier() (8.0+) 轉義標識符
CREATE PROCEDURE sp_transfer(
IN p_from_id BIGINT, IN p_to_id BIGINT, IN p_amount DECIMAL(15,2),
OUT p_code INT, OUT p_msg VARCHAR(100)
)
BEGIN
DECLARE v_balance DECIMAL(15,2);
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SET p_code = -1;
SET p_msg = '轉賬失敗:系統異常';
END;
START TRANSACTION;
-- 鎖定源賬戶 (SELECT ... FOR UPDATE)
SELECT balance INTO v_balance FROM accounts WHERE id = p_from_id FOR UPDATE;
IF v_balance < p_amount THEN
ROLLBACK;
SET p_code = -2;
SET p_msg = '餘額不足';
ELSE
UPDATE accounts SET balance = balance - p_amount WHERE id = p_from_id;
UPDATE accounts SET balance = balance + p_amount WHERE id = p_to_id;
INSERT INTO transfer_log (from_id, to_id, amount, created_at)
VALUES (p_from_id, p_to_id, p_amount, NOW());
COMMIT;
SET p_code = 0;
SET p_msg = '轉賬成功';
END IF;
END //
-- 1. 捕獲特定錯誤
DECLARE CONTINUE HANDLER FOR 1062 -- Duplicate key
BEGIN
SET p_code = -3; SET p_msg = '主鍵衝突';
END;
-- 2. 捕獲 SQLSTATE
DECLARE CONTINUE HANDLER FOR SQLSTATE '23000' -- 完整性約束違反
BEGIN
ROLLBACK; SET p_code = -4; SET p_msg = '數據完整性錯誤';
END;
-- 3. 通用異常處理
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
GET DIAGNOSTICS CONDITION 1
@sqlstate = RETURNED_SQLSTATE,
@errno = MYSQL_ERRNO,
@text = MESSAGE_TEXT;
INSERT INTO error_log (sqlstate, errno, msg, proc_name, created_at)
VALUES (@sqlstate, @errno, @text, 'sp_transfer', NOW());
ROLLBACK;
SET p_code = -99; SET p_msg = '系統錯誤';
END;
set_charset('utf8mb4');
$stmt = $mysqli->prepare("CALL sp_register(?, ?, ?, @uid, @code, @msg)");
$stmt->bind_param('sss', $username, $password_hash, $email);
$username = 'php_user';
$password_hash = hash('sha256', 'secret123');
$email = 'php@example.com';
$stmt->execute();
$stmt->close();
// 獲取 OUT 參數
$result = $mysqli->query("SELECT @uid AS user_id, @code AS code, @msg AS message");
$row = $result->fetch_assoc();
echo "User ID: {$row['user_id']}, Code: {$row['code']}, Msg: {$row['message']}";
// PDO 調用
$pdo = new PDO("mysql:host=localhost;dbname=db;charset=utf8mb4", 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false
]);
$stmt = $pdo->prepare("CALL sp_register(?, ?, ?, @uid, @code, @msg)");
$stmt->execute(['php_user2', hash('sha256', 'secret'), 'php2@example.com']);
$out = $pdo->query("SELECT @uid, @code, @msg")->fetch(PDO::FETCH_ASSOC);
print_r($out);
?>
CREATE PROCEDURE sp_batch_process_orders(
IN p_batch_size INT DEFAULT 1000,
IN p_status VARCHAR(20),
OUT p_processed INT,
OUT p_failed INT
)
BEGIN
DECLARE v_done BOOLEAN DEFAULT FALSE;
DECLARE v_order_id BIGINT;
DECLARE v_count INT DEFAULT 0;
DECLARE v_fail INT DEFAULT 0;
DECLARE cur CURSOR FOR
SELECT id FROM orders
WHERE status = p_status AND created_at < DATE_SUB(NOW(), INTERVAL 30 DAY)
ORDER BY id LIMIT p_batch_size;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = TRUE;
SET p_processed = 0; SET p_failed = 0;
OPEN cur;
read_loop: LOOP
FETCH cur INTO v_order_id;
IF v_done THEN LEAVE read_loop; END IF;
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
SET v_fail = v_fail + 1;
INSERT INTO process_log (order_id, status, error_msg, created_at)
VALUES (v_order_id, 'FAILED', 'Processing error', NOW());
END;
-- 業務處理邏輯
UPDATE orders SET status = 'PROCESSED', processed_at = NOW() WHERE id = v_order_id;
INSERT INTO archive_orders SELECT * FROM orders WHERE id = v_order_id;
DELETE FROM orders WHERE id = v_order_id;
SET v_count = v_count + 1;
END;
END LOOP;
CLOSE cur;
SET p_processed = v_count;
SET p_failed = v_fail;
END //
-- 查看過程定義
SHOW CREATE PROCEDURE sp_register;
-- 查看過程狀態
SHOW PROCEDURE STATUS WHERE Db = 'mydb';
-- 執行計劃分析 (8.0+)
EXPLAIN ANALYZE CALL sp_batch_process_orders(1000, 'PENDING', @p, @f);
-- 性能 Schema 監控
SELECT * FROM performance_schema.events_statements_summary_by_digest
WHERE DIGEST_TEXT LIKE '%sp_batch_process_orders%' ORDER BY AVG_TIMER_WAIT DESC LIMIT 5;
| 坑點 | 後果 | 解決 |
|---|---|---|
| 忘記 DELIMITER | 創建失敗,語法報錯 | 創建前 DELIMITER //,結束 DELIMITER ; |
| 游標未關閉 | 連接洩漏,報 "Commands out of sync" | 必須在 EXIT HANDLER 和正常流程都 CLOSE |
| OUT 參數未初始化 | 返回 NULL 導致應用邏輯錯誤 | BEGIN 即初始化所有 OUT 參數 |
| 動態 SQL 注入 | 安全漏洞 | 白名單 + 參數綁定 + quote_identifier |
| 大事務長鎖表 | 阻塞所有寫入 | 拆分批次、提交頻率、索引優化 |
| 遞歸調用深度過大 | 棧溢出 | 改迭代、或增大 max_sp_recursion_depth (默認 0 禁用) |
CTE (WITH) 在過程中可用,簡化複雜查詢窗口函數 可用於過程內統計INVISIBLE INDEX 測試索引影響SET PERSIST 運行時持久化配置Data Dictionary 替代 frm 文件,DDL 更快