預存程序將業務邏輯下沉到資料庫層,減少網路往返、提升封裝性。本文基於 MySQL 5.1/5.5 實戰案例,深度解析動態 SQL 分頁、字元集 Collation 衝突修復、ROW_COUNT 獲取影響行數、控制流語句、參數檢查等核心技術點。
| 技術點 | 問題背景 | 解決方案 | 適用版本 |
|---|---|---|---|
| 動態 SQL 分頁 | MySQL 5.1 不支援 LIMIT ?,? 變數 | PREPARE + EXECUTE 構建動態 SQL | 5.1 (5.5+ 原生支援) |
| ROW_COUNT() | UPDATE/DELETE 後需返回影響行數 | 緊接執行 SELECT ROW_COUNT(); | 所有版本 |
| Collation 衝突 | Illegal mix of collations 錯誤 | CONVERT(col USING utf8) COLLATE utf8_unicode_ci | 所有版本 |
| 控制流 (IF/CASE/LOOP) | 業務邏輯分支、迴圈 | IF ... THEN ... END IF; 等標準語法 | 所有版本 |
| 參數計數檢查 | Column count doesn't match value count | 嚴格核對 VALUES 與欄位列表 | 所有版本 |
反引號 \` | 識別符與關鍵字衝突 | 包裹表名/欄位名:\`order\` | 所有版本 |
DROP PROCEDURE IF EXISTS sp_select_reply_article;
DELIMITER ;;
CREATE PROCEDURE sp_select_reply_article(
IN ra_id INT,
IN pagefrom INT,
IN pagesize INT
)
BEGIN
SET @ra_id = ra_id;
SET @pagefrom = pagefrom;
SET @pagesize = pagesize;
-- 動態構建含 LIMIT 變數的 SQL
SET @sql = CONCAT('SELECT * FROM gk_article WHERE id = ? LIMIT ?, ?');
PREPARE stmt FROM @sql;
EXECUTE stmt USING @ra_id, @pagefrom, @pagesize;
DEALLOCATE PREPARE stmt;
END;;
DELIMITER ;
關鍵:CONCAT 構建字串 → PREPARE 編譯 → EXECUTE ... USING 綁定參數 → DEALLOCATE 釋放。MySQL 5.5+ 可直接用 LIMIT pagefrom, pagesize 無需動態 SQL。
DROP PROCEDURE IF EXISTS sp_update_permission;
DELIMITER ;;
CREATE PROCEDURE sp_update_permission(
IN puser_uid VARCHAR(20),
IN plevel INT,
IN ppower INT
)
BEGIN
SET @puser_uid = puser_uid;
SET @plevel = plevel;
SET @ppower = ppower;
UPDATE gk_permission
SET `level` = @plevel, power = @ppower
WHERE user_uid = CONVERT(@puser_uid USING utf8) COLLATE utf8_unicode_ci;
-- 緊接返回影響行數
SELECT ROW_COUNT() AS affected_rows;
END;;
DELIMITER ;
-- 錯誤:Illegal mix of collations (utf8_unicode_ci,IMPLICIT) and (utf8_general_ci,IMPLICIT) for operation '='
-- 原因:表欄位 collation 與變數/常數 collation 不一致
-- 修復:統一轉換比較雙方
WHERE user_uid = CONVERT(@puser_uid USING utf8) COLLATE utf8_unicode_ci
-- 或在建表時統一
CREATE TABLE t (c VARCHAR(20) CHARACTER SET utf8 COLLATE utf8_unicode_ci);
DROP PROCEDURE IF EXISTS sp_insert_user;
DELIMITER ;;
CREATE PROCEDURE sp_insert_user(
IN uid VARCHAR(20), IN upw VARCHAR(32), IN uname VARCHAR(20),
IN sex INT, IN phone VARCHAR(20),
IN u_id INT, IN s_id INT, IN j_id INT
)
BEGIN
SET @uid = uid; SET @upw = upw; SET @uname = uname;
SET @sex = sex; SET @phone = phone;
SET @u_id = u_id; SET @s_id = s_id; SET @j_id = j_id;
SET @verifytime = DATE('0000-00-00');
INSERT INTO gk_user(uid, upw, uname, sex, phone, u_id, s_id, j_id, verifytime)
VALUES (@uid, @upw, @uname, @sex, @phone, @u_id, @s_id, @j_id, @verifytime);
-- INSERT 會自動返回 affected_rows (客戶端可直接讀取)
END;;
DELIMITER ;
DROP PROCEDURE IF EXISTS sp_delete_exchange_by_id;
DELIMITER ;;
CREATE PROCEDURE sp_delete_exchange_by_id(IN eid INT)
BEGIN
SET @eid = eid;
DELETE FROM gk_exchange WHERE id = @eid;
END;;
DELIMITER ;
DROP PROCEDURE IF EXISTS sp_select_user_by_uid;
DELIMITER ;;
CREATE PROCEDURE sp_select_user_by_uid(
IN uid VARCHAR(20),
IN getAdmin INT
)
BEGIN
SET @uid = uid;
IF getAdmin = 1 THEN
-- 查詢管理員 (含權限表 JOIN)
SELECT us.*, un.`name`, se.`name`, jo.`name`, pe.`level`, pe.power
FROM gk_user AS us
JOIN gk_unit AS un ON us.u_id = un.id
JOIN gk_section AS se ON us.s_id = se.id
JOIN gk_jobtitle AS jo ON us.j_id = jo.id
JOIN gk_permission AS pe ON us.uid = pe.user_uid
WHERE us.uid = CONVERT(@uid USING utf8) COLLATE utf8_unicode_ci;
END IF;
IF getAdmin = 0 THEN
-- 查詢普通用戶
SELECT us.*, un.`name`, se.`name`, jo.`name`
FROM gk_user AS us
JOIN gk_unit AS un ON us.u_id = un.id
JOIN gk_section AS se ON us.s_id = se.id
JOIN gk_jobtitle AS jo ON us.j_id = jo.id
WHERE us.uid = CONVERT(@uid USING utf8) COLLATE utf8_unicode_ci;
END IF;
END;;
DELIMITER ;
IF condition1 THEN
statements;
ELSEIF condition2 THEN
statements;
ELSE
statements;
END IF;
CASE case_value
WHEN when_value THEN statements;
WHEN when_value THEN statements;
ELSE statements;
END CASE;
-- 或條件式 CASE
CASE
WHEN condition1 THEN statements;
WHEN condition2 THEN statements;
ELSE statements;
END CASE;
-- LOOP (需 LEAVE 跳出)
label: LOOP
statements;
IF condition THEN LEAVE label; END IF;
END LOOP;
-- REPEAT (後測試)
REPEAT
statements;
UNTIL condition
END REPEAT;
-- WHILE (前測試)
WHILE condition DO
statements;
END WHILE;
-- ITERATE: 類似 continue,跳到迴圈開頭
-- LEAVE: 類似 break,跳出迴圈/標籤區塊
| 場景 | 建議 | 反例 |
|---|---|---|
| 動態 SQL | 僅在必要時使用 (如 5.1 LIMIT 變數);優先靜態 SQL | 所有查詢都用 PREPARE,增加解析開銷、難除錯 |
| 字元集 | 庫/表/欄位/連接 全鏈路 utf8mb4 + utf8mb4_unicode_ci | 混用 utf8/general_ci/unicode_ci 導致隱式轉換 |
| 參數驗證 | 入口驗證非空、型別、範圍;IF param IS NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '...'; END IF; | 直接執行 SQL,報錯訊息不可控 |
| 事務控制 | 預存程序內可管理事務:START TRANSACTION; ... COMMIT/ROLLBACK; | 自動提交模式下部分失敗導致資料不一致 |
| 除錯 | SELECT ... INTO @var; 記錄中間值、GET DIAGNOSTICS 獲取錯誤碼 | 盲目猜測、無日誌 |
| 命名規範 | sp_,如 sp_order_create | 隨意命名、無前綴 |
WITH cte AS (...) SELECT ...ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)ALTER TABLE ... ALTER INDEX ... INVISIBLE 測試索引影響SET RESOURCE GROUP 控制 CPU 佔用PREPARE/EXECUTE/DEALLOCATE 三部曲CONVERT(... USING charset) COLLATE collation 強制統一\`table\`、\`column\`