MySQL 存儲過程支援動態 SQL (`PREPARE`/`EXECUTE`/`DEALLOCATE PREPARE`),配合輸入參數可實現靈活的條件查詢、動態插入、分表路由等進階場景。本文詳解語法細節、注入防範、效能考量與最佳實踐。
PREPARE stmt_name FROM @sql_string; -- 預處理 (解析、優化、生成執行計劃)
EXECUTE stmt_name [USING @var1, @var2, ...]; -- 執行 (可傳參數)
DEALLOCATE PREPARE stmt_name; -- 釋放 (必須釋放,否則會話結束前佔用 prepared_statement 配額)
CREATE PROCEDURE GetUsersDynamic(
IN p_where_condition VARCHAR(500), -- 如 'age <= 30 AND status = 1'
IN p_order_by VARCHAR(100) -- 如 'age DESC' 或 '' (不排序)
)
BEGIN
DECLARE v_sql TEXT DEFAULT '';
DECLARE v_order_clause VARCHAR(120) DEFAULT '';
-- 安全處理 ORDER BY:白名單驗證,防注入
IF p_order_by != '' THEN
-- 簡單白名單:僅允許指定欄位 + ASC/DESC
IF p_order_by REGEXP '^(age|created_at|username)( ASC| DESC)?$' THEN
SET v_order_clause = CONCAT(' ORDER BY ', p_order_by);
ELSE
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Invalid ORDER BY clause';
END IF;
END IF;
-- 建構動態 SQL (WHERE 條件同樣需白名單或參數化,此處示範拼接風險)
SET v_sql = CONCAT(
'SELECT id, username, age, created_at ',
'FROM users ',
'WHERE ', p_where_condition,
v_order_clause
);
PREPARE stmt FROM v_sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END;
CREATE PROCEDURE InsertUserDynamic(
IN p_username VARCHAR(50),
IN p_password_hash CHAR(60),
IN p_age INT,
IN p_created_at DATETIME(6)
)
BEGIN
DECLARE v_sql TEXT;
-- 關鍵:使用 QUOTE() 函數自動處理字串轉義與引號包裹
-- QUOTE('abc') -> 'abc' ; QUOTE("it's") -> 'it\'s' ; QUOTE(NULL) -> NULL
SET v_sql = CONCAT(
'INSERT INTO users (username, password_hash, age, created_at) VALUES (',
QUOTE(p_username), ',',
QUOTE(p_password_hash), ',',
p_age, ',',
QUOTE(p_created_at), ')'
);
PREPARE stmt FROM v_sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END;
CREATE PROCEDURE GetUsersByAgeRange(
IN p_min_age INT,
IN p_max_age INT,
IN p_limit INT
)
BEGIN
-- 使用 ? 佔位符,EXECUTE 時 USING 綁定變數
SET @sql = 'SELECT id, username, age FROM users WHERE age BETWEEN ? AND ? LIMIT ?';
PREPARE stmt FROM @sql;
EXECUTE stmt USING @p_min_age, @p_max_age, @p_limit;
DEALLOCATE PREPARE stmt;
END;
**優勢**:
CREATE FUNCTION FormatDate(p_dt DATETIME(6))
RETURNS VARCHAR(20) DETERMINISTIC
RETURN DATE_FORMAT(p_dt, '%Y-%m-%d %H:%i:%s');
| 限制 | 說明 | 對策 |
|---|---|---|
| 動態 SQL 不能直接使用存儲過程參數 | `PREPARE` 只能引用**用戶變數** (`@var`),不能直接用 `p_param` | 先 `SET @var = p_param;` 再在 SQL 中用 `?` 或 `QUOTE(@var)` |
| 預處理語句作用域 = 會話 | 同一會話內 `PREPARE` 同名會報錯,不同會話隔離 | 用唯一名稱 (如 `CONCAT('stmt_', CONNECTION_ID(), '_', UUID_SHORT())`) 或用畢 `DEALLOCATE` |
| 最大預處理數限制 | `max_prepared_stmt_count` (預設 16382) | 及時 `DEALLOCATE`,監控 `Prepared_stmt_count` 狀態 |
| 動態 SQL 內不可用 `DECLARE` 變數 | 只能用用戶變數 `@var` | 同上 |
| DDL 操作不能參數化 | `PREPARE` 不支援 `CREATE`/`ALTER`/`DROP` 等 DDL 的參數綁定 | 只能拼接字串 (極度謹慎,白名單驗證) |
| 執行計劃緩存不跨會話 | 每個連線獨立準備 | 長連線池 (HikariCP) 可受益於會話級計劃復用 |
MySQL 動態存儲過程是把雙刃劍:**用對是靈活復用的利器,用錯是注入漏洞與效能黑洞的溫床**。堅守「白名單驗證 + 參數化綁定 + 及時釋放」三原則,將動態能力釋放在真正需要的場景裡。