MySQL 存儲過程、游標、存儲函數(shù)與觸發(fā)器最佳實踐
本文將全面介紹 MySQL 存儲過程、游標、存儲函數(shù)、觸發(fā)器的概念、語法、應用場景以及在現(xiàn)代開發(fā)中的定位。這四者是 MySQL 可編程對象的核心,無論你是面試準備還是項目實戰(zhàn),讀完這篇文章你將對它們建立起清晰、完整的認知。
一、什么是存儲過程
1.1 概念定義
存儲過程(Stored Procedure) 是一組為了完成特定功能而預先編譯好的 SQL 語句集合,存儲在數(shù)據庫服務器中,客戶端通過指定存儲過程的名字并給出參數(shù)(如果有)來調用執(zhí)行。
用一個通俗的類比來理解:
- 沒有存儲過程:你每次去餐廳都要一步一步告訴廚師——先放油、再放蒜、然后放肉、翻炒三分鐘……
- 有存儲過程:你只需要說"來一份宮保雞丁",廚師已經知道所有步驟了
本質上,存儲過程就是數(shù)據庫端的"函數(shù)",把一段可重復使用的業(yè)務邏輯封裝起來,對外暴露一個調用接口。
1.2 核心特征
| 特征 | 說明 |
|---|---|
| 預編譯 | 創(chuàng)建時編譯一次,后續(xù)調用直接執(zhí)行編譯后的代碼 |
| 持久化存儲 | 存儲在數(shù)據庫的系統(tǒng)表中,數(shù)據庫重啟后依然存在 |
| 參數(shù)化 | 支持輸入(IN)、輸出(OUT)、輸入輸出(INOUT)三種參數(shù)模式 |
| 流程控制 | 支持變量聲明、條件判斷、循環(huán)等編程語言的基本結構 |
| 事務支持 | 內部可以使用事務控制(BEGIN / COMMIT / ROLLBACK) |
1.3 存儲過程 vs 普通 SQL
┌─────────────────────────────────────────────────────────────┐ │ 普通 SQL 執(zhí)行流程 │ ├─────────────────────────────────────────────────────────────┤ │ 客戶端 → 發(fā)送SQL語句 → 數(shù)據庫解析 → 編譯 → 優(yōu)化 → 執(zhí)行 → 返回 │ │ (每次都要重復上述全部步驟) │ └─────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────┐ │ 存儲過程執(zhí)行流程 │ ├─────────────────────────────────────────────────────────────┤ │ 第一次:客戶端 → CALL → 解析 → 編譯 → 優(yōu)化 → 執(zhí)行 → 返回 │ │ 后續(xù)次:客戶端 → CALL → 直接執(zhí)行已編譯代碼 → 返回 │ │ (省去了解析、編譯、優(yōu)化步驟) │ └─────────────────────────────────────────────────────────────┘
二、存儲過程有什么用
2.1 五大核心作用
① 封裝復雜業(yè)務邏輯
將多條 SQL 語句和邏輯判斷封裝為一個整體,對外只暴露一個名字和參數(shù)列表,隱藏內部實現(xiàn)細節(jié)。
-- 不用存儲過程:客戶端需要發(fā)送多條SQL SELECT balance FROM account WHERE id = 1; -- 客戶端判斷余額是否足夠 UPDATE account SET balance = balance - 100 WHERE id = 1; UPDATE account SET balance = balance + 100 WHERE id = 2; INSERT INTO transfer_log VALUES (...); -- 用存儲過程:一條命令搞定 CALL TransferMoney(1, 2, 100.00, @status);
② 提升執(zhí)行效率
- 存儲過程在首次執(zhí)行時被編譯,后續(xù)調用跳過解析和編譯階段
- 減少了客戶端與數(shù)據庫之間的網絡往返(只傳一個 CALL 命令,而不是一大段 SQL)
③ 增強安全性
- 可以只授予用戶執(zhí)行存儲過程的權限,而不給予直接操作表的權限
- 防止 SQL 注入:參數(shù)傳入存儲過程時,不會被當作 SQL 代碼執(zhí)行
④ 減少網絡傳輸
假設一個業(yè)務需要執(zhí)行 10 條 SQL,如果不用存儲過程需要 10 次網絡往返;用存儲過程只需要 1 次網絡調用。
⑤ 保證數(shù)據一致性
存儲過程內部可以包含事務控制,保證一組操作要么全部成功,要么全部回滾。
2.2 典型應用場景
| 場景 | 說明 |
|---|---|
| 銀行轉賬 | 扣款 + 加款 + 記流水,必須在事務中完成 |
| 批量數(shù)據處理 | 定時結算、對賬、數(shù)據遷移 |
| 復雜報表統(tǒng)計 | 多表關聯(lián) + 聚合計算 + 分組排序 |
| 權限控制 | DBA 封裝好存儲過程,開發(fā)人員只能調用 |
| ETL 數(shù)據清洗 | 數(shù)據倉庫中從源表加工到目標表 |
三、快速入門
3.1 基礎語法
-- 修改結束符(因為存儲過程內部有分號)
DELIMITER //
CREATE PROCEDURE 過程名([參數(shù)列表])
BEGIN
-- SQL 語句和邏輯
END //
-- 恢復結束符
DELIMITER ;為什么需要 DELIMITER?
MySQL 默認用;作為語句結束符。存儲過程體內包含多個;,如果不改結束符,MySQL 會在第一個;處就認為語句結束了。所以我們臨時將結束符改為//,定義完后再改回;。
3.2 參數(shù)類型
存儲過程支持三種參數(shù)模式:
CREATE PROCEDURE 過程名(
IN 輸入參數(shù)名 數(shù)據類型, -- 調用方傳入,過程內只讀
OUT 輸出參數(shù)名 數(shù)據類型, -- 過程內寫入,調用方獲取
INOUT 雙向參數(shù)名 數(shù)據類型 -- 調用方傳入,過程內修改后調用方可獲取
)
| 模式 | 方向 | 類比 |
|---|---|---|
| IN | 調用方 → 存儲過程 | 函數(shù)的入參 |
| OUT | 存儲過程 → 調用方 | 函數(shù)的返回值 |
| INOUT | 雙向傳遞 | 引用傳遞 |
3.3 快速實戰(zhàn) 6 例
示例 1:最簡單的存儲過程(無參數(shù))
DELIMITER //
CREATE PROCEDURE HelloWorld()
BEGIN
SELECT 'Hello, Stored Procedure!' AS message;
END //
DELIMITER ;
-- 調用
CALL HelloWorld();輸出:
+---------------------------+ | message | +---------------------------+ | Hello, Stored Procedure! | +---------------------------+
示例 2:帶輸入參數(shù)(IN)—— 根據 ID 查用戶
DELIMITER //
CREATE PROCEDURE GetUserById(IN p_id INT)
BEGIN
SELECT id, username, email, create_time
FROM users
WHERE id = p_id;
END //
DELIMITER ;
-- 調用
CALL GetUserById(1);
CALL GetUserById(5);示例 3:帶輸出參數(shù)(OUT)—— 統(tǒng)計用戶數(shù)量
DELIMITER //
CREATE PROCEDURE GetUserCount(OUT p_count INT)
BEGIN
SELECT COUNT(*) INTO p_count FROM users;
END //
DELIMITER ;
-- 調用
CALL GetUserCount(@total);
SELECT @total AS user_count;注意:
@total是 MySQL 的用戶變量(Session 級別),用@前綴聲明,可以在同一個會話中跨語句使用。
示例 4:輸入輸出參數(shù)(INOUT)—— 累加計數(shù)器
DELIMITER //
CREATE PROCEDURE Accumulate(INOUT p_counter INT, IN p_increment INT)
BEGIN
SET p_counter = p_counter + p_increment;
END //
DELIMITER ;
-- 調用
SET @counter = 100;
CALL Accumulate(@counter, 50);
SELECT @counter; -- 輸出 150
CALL Accumulate(@counter, 30);
SELECT @counter; -- 輸出 180示例 5:條件判斷 + 變量聲明
DELIMITER //
CREATE PROCEDURE EvaluateScore(IN p_score INT, OUT p_level VARCHAR(20))
BEGIN
IF p_score >= 90 THEN
SET p_level = '優(yōu)秀';
ELSEIF p_score >= 80 THEN
SET p_level = '良好';
ELSEIF p_score >= 60 THEN
SET p_level = '及格';
ELSE
SET p_level = '不及格';
END IF;
END //
DELIMITER ;
-- 調用
CALL EvaluateScore(85, @level);
SELECT @level; -- 輸出:良好示例 6:循環(huán) + 游標(遍歷結果集)
DELIMITER //
CREATE PROCEDURE BatchUpdateStatus()
BEGIN
DECLARE v_id INT;
DECLARE v_done INT DEFAULT 0;
-- 聲明游標
DECLARE cur CURSOR FOR
SELECT id FROM orders WHERE status = 'pending' AND create_time < DATE_SUB(NOW(), INTERVAL 30 DAY);
-- 聲明結束處理器
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = 1;
OPEN cur;
read_loop: LOOP
FETCH cur INTO v_id;
IF v_done = 1 THEN
LEAVE read_loop;
END IF;
-- 將超過30天的待處理訂單自動取消
UPDATE orders SET status = 'cancelled' WHERE id = v_id;
END LOOP;
CLOSE cur;
END //
DELIMITER ;
-- 調用
CALL BatchUpdateStatus();3.4 流程控制語句速查
| 語句 | 作用 | 語法 |
|---|---|---|
| IF | 條件判斷 | IF ... THEN ... ELSEIF ... ELSE ... END IF |
| CASE | 多分支選擇 | CASE WHEN ... THEN ... END CASE |
| WHILE | 前置條件循環(huán) | WHILE 條件 DO ... END WHILE |
| REPEAT | 后置條件循環(huán) | REPEAT ... UNTIL 條件 END REPEAT |
| LOOP | 無條件循環(huán) | LOOP ... END LOOP(配合 LEAVE 退出) |
| LEAVE | 退出循環(huán) | LEAVE 循環(huán)標簽 |
| ITERATE | 跳過本次循環(huán) | ITERATE 循環(huán)標簽(類似 continue) |
四、存儲過程中的事務與異常處理
4.1 事務控制
DELIMITER //
CREATE PROCEDURE TransferMoney(
IN p_from_id INT,
IN p_to_id INT,
IN p_amount DECIMAL(10,2),
OUT p_status VARCHAR(50)
)
BEGIN
-- 聲明異常處理:發(fā)生任何 SQL 異常時回滾
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SET p_status = '轉賬失?。喊l(fā)生異常';
END;
-- 余額校驗
DECLARE v_balance DECIMAL(10,2);
SELECT balance INTO v_balance FROM account WHERE id = p_from_id FOR UPDATE;
IF v_balance < p_amount THEN
SET p_status = '轉賬失?。河囝~不足';
ELSE
START TRANSACTION;
UPDATE account SET balance = balance - p_amount WHERE id = p_from_id;
UPDATE account SET balance = balance + p_amount WHERE id = p_to_id;
INSERT INTO transfer_log(from_id, to_id, amount, transfer_time)
VALUES (p_from_id, p_to_id, p_amount, NOW());
COMMIT;
SET p_status = '轉賬成功';
END IF;
END //
DELIMITER ;4.2 異常處理器類型
-- 遇到異常后繼續(xù)執(zhí)行后續(xù)語句
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET @err = 1;
-- 遇到異常后退出當前 BEGIN...END 塊
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SELECT '執(zhí)行出錯' AS error_msg;
END;
-- 處理特定錯誤碼
DECLARE CONTINUE HANDLER FOR 1062 -- 重復鍵錯誤
SET @duplicate = 1;五、存儲過程的管理操作
-- ========== 查看 ========== -- 查看當前數(shù)據庫所有存儲過程 SHOW PROCEDURE STATUS WHERE Db = 'your_database'; -- 查看某個存儲過程的創(chuàng)建語句 SHOW CREATE PROCEDURE TransferMoney; -- 從系統(tǒng)表查詢 SELECT * FROM information_schema.ROUTINES WHERE ROUTINE_SCHEMA = 'your_database' AND ROUTINE_TYPE = 'PROCEDURE'; -- ========== 修改 ========== -- MySQL 不支持 ALTER PROCEDURE 修改過程體 -- 只能先刪除再重新創(chuàng)建 DROP PROCEDURE IF EXISTS TransferMoney; -- 然后重新 CREATE PROCEDURE ... -- ========== 刪除 ========== DROP PROCEDURE IF EXISTS GetUserById;
六、游標(Cursor)詳解
6.1 什么是游標
游標(Cursor) 是數(shù)據庫提供的一種機制,用于對查詢結果集進行逐行處理。普通的 SELECT 語句一次性返回所有滿足條件的行,而游標則像一個"指針",可以在結果集上一行一行地移動,逐條讀取并處理數(shù)據。
通俗類比:
SELECT * FROM orders→ 把所有訂單數(shù)據一股腦全取出來- 游標 → 像超市收銀員,一件一件掃描商品,處理完一件再拿下一件
游標通常與存儲過程配合使用,在需要對每一行數(shù)據做差異化處理時發(fā)揮作用。
6.2 游標的使用四步驟
MySQL 中使用游標必須嚴格按照以下四個步驟進行:
① DECLARE → 聲明游標(綁定一條 SELECT 語句,此時不執(zhí)行) ② OPEN → 打開游標(執(zhí)行 SELECT,將結果集加載到內存) ③ FETCH → 逐行讀?。▽斍靶袛?shù)據存入變量,指針下移) ④ CLOSE → 關閉游標(釋放結果集占用的內存)
6.3 基礎語法
DELIMITER //
CREATE PROCEDURE cursor_demo()
BEGIN
-- 1. 聲明局部變量(用于存放每行數(shù)據)
DECLARE v_id INT;
DECLARE v_name VARCHAR(50);
DECLARE v_done INT DEFAULT 0; -- 游標結束標志
-- 2. 聲明游標(綁定 SELECT,注意:此時并不執(zhí)行查詢)
DECLARE cur CURSOR FOR
SELECT id, username FROM users WHERE status = 1;
-- 3. 聲明結束處理器(FETCH 到末尾觸發(fā) NOT FOUND,將 v_done 置 1)
-- 注意:HANDLER 必須聲明在游標之后
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = 1;
-- 4. 打開游標(此時才真正執(zhí)行 SELECT 查詢)
OPEN cur;
-- 5. 循環(huán)讀取
read_loop: LOOP
FETCH cur INTO v_id, v_name; -- 讀取當前行到變量,指針后移
IF v_done = 1 THEN -- 沒有更多行,退出循環(huán)
LEAVE read_loop;
END IF;
-- 處理當前行的業(yè)務邏輯
-- ...
END LOOP read_loop;
-- 6. 關閉游標(釋放資源)
CLOSE cur;
END //
DELIMITER ;?? DECLARE 的順序非常重要:MySQL 規(guī)定 DECLARE 語句必須按以下順序出現(xiàn),順序錯誤會報編譯錯誤:
- 局部變量(
DECLARE var TYPE) - 游標(
DECLARE cur CURSOR FOR) - 異常處理器(
DECLARE HANDLER)
6.4 游標的特性
| 特性 | 說明 |
|---|---|
| 只讀性 | MySQL 游標只能讀取數(shù)據,不能通過游標直接修改行(需單獨寫 UPDATE) |
| 單向滾動 | 只能向前逐行移動,不能后退或隨機跳轉到某一行 |
| 局部性 | 只能在存儲過程、存儲函數(shù)或觸發(fā)器內部使用,不能單獨執(zhí)行 |
| 內存占用 | 打開游標會將完整結果集加載到內存,結果集過大時存在 OOM 風險 |
6.5 實戰(zhàn)示例
示例 1:遍歷用戶列表自動寫通知日志
DELIMITER //
CREATE PROCEDURE sp_send_notification_log()
BEGIN
DECLARE v_user_id INT;
DECLARE v_email VARCHAR(100);
DECLARE v_done INT DEFAULT 0;
DECLARE cur CURSOR FOR
SELECT id, email FROM users WHERE is_active = 1;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = 1;
OPEN cur;
user_loop: LOOP
FETCH cur INTO v_user_id, v_email;
IF v_done = 1 THEN
LEAVE user_loop;
END IF;
INSERT INTO notification_log(user_id, email, send_time, content)
VALUES (v_user_id, v_email, NOW(), '系統(tǒng)有重要通知,請及時查看。');
END LOOP user_loop;
CLOSE cur;
END //
DELIMITER ;示例 2:游標 + 條件判斷 —— 差異化更新用戶積分
DELIMITER //
CREATE PROCEDURE sp_update_user_points()
BEGIN
DECLARE v_id INT;
DECLARE v_orders INT;
DECLARE v_done INT DEFAULT 0;
DECLARE cur CURSOR FOR
SELECT u.id, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'completed'
GROUP BY u.id;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = 1;
OPEN cur;
point_loop: LOOP
FETCH cur INTO v_id, v_orders;
IF v_done = 1 THEN
LEAVE point_loop;
END IF;
-- 根據完成訂單數(shù)量差異化增加積分
IF v_orders >= 100 THEN
UPDATE users SET points = points + 500 WHERE id = v_id;
ELSEIF v_orders >= 50 THEN
UPDATE users SET points = points + 200 WHERE id = v_id;
ELSEIF v_orders >= 10 THEN
UPDATE users SET points = points + 50 WHERE id = v_id;
END IF;
END LOOP point_loop;
CLOSE cur;
END //
DELIMITER ;示例 3:遍歷部門同步統(tǒng)計數(shù)據(游標 + 聚合查詢)
DELIMITER //
CREATE PROCEDURE sp_sync_department_stats()
BEGIN
DECLARE v_dept_id INT;
DECLARE v_emp_count INT;
DECLARE v_avg_salary DECIMAL(10,2);
DECLARE v_done INT DEFAULT 0;
DECLARE dept_cur CURSOR FOR
SELECT id FROM departments;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = 1;
OPEN dept_cur;
dept_loop: LOOP
FETCH dept_cur INTO v_dept_id;
IF v_done = 1 THEN
LEAVE dept_loop;
END IF;
-- 對每個部門單獨做聚合查詢
SELECT COUNT(*), IFNULL(AVG(salary), 0)
INTO v_emp_count, v_avg_salary
FROM employees
WHERE department_id = v_dept_id;
-- 更新統(tǒng)計表(存在則更新,不存在則插入)
INSERT INTO department_stats(dept_id, emp_count, avg_salary, updated_at)
VALUES (v_dept_id, v_emp_count, v_avg_salary, NOW())
ON DUPLICATE KEY UPDATE
emp_count = v_emp_count,
avg_salary = v_avg_salary,
updated_at = NOW();
END LOOP dept_loop;
CLOSE dept_cur;
END //
DELIMITER ;6.6 游標的性能問題與替代方案
游標的本質是逐行處理(Row-by-Row),而關系型數(shù)據庫的優(yōu)化器是為**集合操作(Set-based)**設計的,兩者背道而馳。
| 方案 | 適用場景 | 性能 |
|---|---|---|
| 游標逐行處理 | 每行邏輯復雜、無法用單條 SQL 表達 | 慢 |
| 集合 SQL(批量 UPDATE/INSERT) | 批量相同操作 | 快 |
| 分批處理(LIMIT + 循環(huán)) | 超大數(shù)據量的批量操作 | 較快 |
能用集合 SQL 解決的,絕不用游標:
-- ? 用游標逐行更新(慢,不推薦) -- 聲明游標 → 循環(huán) FETCH → 逐條 UPDATE ... -- ? 用一條集合 SQL 批量更新(快,推薦) UPDATE orders SET status = 'cancelled' WHERE status = 'pending' AND create_time < DATE_SUB(NOW(), INTERVAL 30 DAY);
游標合理的使用場景:
- 每行的處理邏輯確實無法用一條 SQL 表達(如需要調用存儲過程、做復雜條件分支)
- 數(shù)據量較?。◣浊У綆兹f行以內)
- ETL / 數(shù)據遷移中需要逐行加工轉換
七、存儲函數(shù)(Function)詳解
7.1 什么是存儲函數(shù)
存儲函數(shù)(Stored Function) 和存儲過程同樣是存儲在數(shù)據庫中的預編譯代碼塊。核心區(qū)別在于:存儲函數(shù)必須通過 RETURN 返回一個值,并且可以像 MySQL 內置函數(shù)(NOW()、LENGTH()、IFNULL() 等)一樣,直接嵌入 SQL 語句中使用。
-- 內置函數(shù)的使用方式 SELECT LENGTH(username), UPPER(email) FROM users; -- 自定義存儲函數(shù)可以做到完全一樣的事 SELECT fn_mask_phone(phone), fn_get_age_group(age) FROM users; SELECT * FROM users WHERE fn_is_vip(id) = 1;
7.2 基礎語法
DELIMITER //
CREATE FUNCTION 函數(shù)名(參數(shù)1 數(shù)據類型, 參數(shù)2 數(shù)據類型, ...)
RETURNS 返回值類型
[特性關鍵字]
BEGIN
-- 函數(shù)體(只有 IN 參數(shù),沒有 OUT/INOUT)
RETURN 返回值;
END //
DELIMITER ;特性關鍵字(至少聲明一個,MySQL 要求必須明確聲明函數(shù)特性):
| 關鍵字 | 含義 |
|---|---|
DETERMINISTIC | 相同輸入總是返回相同輸出(純函數(shù),如字符串處理、數(shù)學計算) |
NOT DETERMINISTIC | 相同輸入可能返回不同輸出(默認值,如使用了 NOW()、RAND()) |
NO SQL | 函數(shù)體中不包含任何 SQL 語句 |
READS SQL DATA | 函數(shù)體包含 SELECT 讀操作,但不修改數(shù)據 |
MODIFIES SQL DATA | 函數(shù)體包含 INSERT/UPDATE/DELETE 寫操作 |
?? 權限注意:MySQL 5.x 默認禁止創(chuàng)建存儲函數(shù)(因為在 binlog 開啟時,不確定性函數(shù)可能引發(fā)主從不一致)。需要 DBA 執(zhí)行:
SET GLOBAL log_bin_trust_function_creators = 1;
MySQL 8.0 對此限制已大幅放寬。
7.3 調用方式對比
-- 存儲過程:必須用 CALL,不能嵌入 SQL
CALL sp_get_user_by_id(1);
-- 存儲函數(shù):像內置函數(shù)一樣隨處可用
SELECT fn_mask_phone('13812345678'); -- 直接查詢
SELECT id, fn_mask_phone(phone) FROM users; -- SELECT 字段中
SELECT * FROM users WHERE fn_get_age_group(age) = '青年'; -- WHERE 條件中
INSERT INTO report SELECT fn_calc_score(id) FROM users; -- INSERT...SELECT 中7.4 實戰(zhàn)示例
示例 1:字符串處理函數(shù) —— 手機號中間四位脫敏
DELIMITER //
CREATE FUNCTION fn_mask_phone(p_phone VARCHAR(20))
RETURNS VARCHAR(20)
DETERMINISTIC
BEGIN
-- 13812345678 → 138****5678
IF p_phone IS NULL OR LENGTH(p_phone) != 11 THEN
RETURN p_phone; -- 不符合格式,原樣返回
END IF;
RETURN CONCAT(
LEFT(p_phone, 3),
'****',
RIGHT(p_phone, 4)
);
END //
DELIMITER ;
-- 使用:直接嵌入 SELECT
SELECT id, username, fn_mask_phone(phone) AS masked_phone FROM users;示例 2:業(yè)務函數(shù) —— 根據年齡返回用戶年齡段
DELIMITER //
CREATE FUNCTION fn_get_age_group(p_age INT)
RETURNS VARCHAR(10)
DETERMINISTIC
BEGIN
DECLARE v_group VARCHAR(10);
CASE
WHEN p_age < 18 THEN SET v_group = '未成年';
WHEN p_age < 30 THEN SET v_group = '青年';
WHEN p_age < 50 THEN SET v_group = '中年';
ELSE SET v_group = '老年';
END CASE;
RETURN v_group;
END //
DELIMITER ;
-- 使用:嵌入 ORDER BY 和 GROUP BY 也沒問題
SELECT fn_get_age_group(age) AS age_group, COUNT(*) AS cnt
FROM users
GROUP BY fn_get_age_group(age);示例 3:查詢函數(shù) —— 獲取用戶歷史訂單總金額
DELIMITER //
CREATE FUNCTION fn_get_user_total_amount(p_user_id INT)
RETURNS DECIMAL(12,2)
READS SQL DATA
BEGIN
DECLARE v_total DECIMAL(12,2) DEFAULT 0.00;
SELECT IFNULL(SUM(amount), 0.00)
INTO v_total
FROM orders
WHERE user_id = p_user_id
AND status = 'completed';
RETURN v_total;
END //
DELIMITER ;
-- 使用:在 WHERE 中篩選高價值用戶
SELECT id, username, fn_get_user_total_amount(id) AS total_amount
FROM users
WHERE fn_get_user_total_amount(id) > 10000
ORDER BY total_amount DESC;示例 4:遞歸函數(shù) —— 計算斐波那契數(shù)列
DELIMITER //
CREATE FUNCTION fn_fibonacci(n INT)
RETURNS BIGINT
DETERMINISTIC
BEGIN
IF n <= 0 THEN RETURN 0; END IF;
IF n = 1 THEN RETURN 1; END IF;
RETURN fn_fibonacci(n - 1) + fn_fibonacci(n - 2);
END //
DELIMITER ;
-- 使用前需設置最大遞歸深度(默認為0,即不允許遞歸)
SET max_sp_recursion_depth = 20;
SELECT fn_fibonacci(10); -- 輸出 557.5 存儲函數(shù)的管理
-- 查看所有存儲函數(shù) SHOW FUNCTION STATUS WHERE Db = 'your_database'; -- 查看函數(shù)定義 SHOW CREATE FUNCTION fn_mask_phone; -- 從系統(tǒng)表查詢 SELECT ROUTINE_NAME, ROUTINE_TYPE, DTD_IDENTIFIER, ROUTINE_DEFINITION FROM information_schema.ROUTINES WHERE ROUTINE_SCHEMA = 'your_database' AND ROUTINE_TYPE = 'FUNCTION'; -- 刪除函數(shù) DROP FUNCTION IF EXISTS fn_mask_phone;
7.6 存儲過程 vs 存儲函數(shù) 完整對比
| 對比維度 | 存儲過程(Procedure) | 存儲函數(shù)(Function) |
|---|---|---|
| 返回值 | 通過 OUT 參數(shù)返回,可有多個 | 必須有且只有一個 RETURN 返回值 |
| 調用方式 | CALL procedure_name() | SELECT function_name() |
| 能否嵌入 SQL | ? 不能嵌入 SELECT/WHERE/JOIN | ? 可以像內置函數(shù)一樣使用 |
| 參數(shù)類型 | IN / OUT / INOUT | 只有 IN |
| 能否控制事務 | ? 可以使用 COMMIT/ROLLBACK | ? 不能顯式控制事務 |
| 使用游標 | ? 支持 | ? 支持 |
| 側重點 | 執(zhí)行一系列操作(流程控制) | 計算并返回單個值(可復用計算邏輯) |
選擇原則:
- 需要做事情(執(zhí)行增刪改、控制事務、復雜流程)→ 存儲過程
- 需要算東西(計算一個值,嵌入到 SQL 中直接用)→ 存儲函數(shù)
八、觸發(fā)器(Trigger)詳解
8.1 什么是觸發(fā)器
觸發(fā)器(Trigger) 是與數(shù)據庫表關聯(lián)的特殊存儲程序。當表上發(fā)生特定的數(shù)據變更操作(INSERT、UPDATE、DELETE)時,觸發(fā)器會自動執(zhí)行,無需手動調用,完全由數(shù)據庫引擎驅動。
通俗類比:
- 存儲過程 → 你主動按開關開燈
- 觸發(fā)器 → 人體感應燈,有人進來自動亮
觸發(fā)器就像數(shù)據庫層的事件監(jiān)聽器 / 鉤子函數(shù)——就像 Java 中的 @EventListener,觸發(fā)器監(jiān)聽的是數(shù)據變更事件,一旦發(fā)生就自動響應。
8.2 觸發(fā)器的類型
MySQL 觸發(fā)器按兩個維度分類,兩兩組合共 6 種觸發(fā)時機:
按時機(WHEN):
| 時機 | 說明 | 常見用途 |
|---|---|---|
BEFORE | SQL 操作執(zhí)行之前觸發(fā) | 數(shù)據校驗、自動填充、阻止非法操作 |
AFTER | SQL 操作執(zhí)行之后觸發(fā) | 日志記錄、數(shù)據同步、統(tǒng)計更新 |
按操作(EVENT):INSERT / UPDATE / DELETE
8.3 基礎語法
DELIMITER //
CREATE TRIGGER trigger_name
{BEFORE | AFTER} {INSERT | UPDATE | DELETE}
ON table_name
FOR EACH ROW
BEGIN
-- 觸發(fā)器邏輯
END //
DELIMITER ;關鍵說明:
FOR EACH ROW:行級觸發(fā)器,每影響一行就觸發(fā)一次(MySQL 只支持行級,不支持語句級)- 觸發(fā)器名建議使用
trg_表名_時機_操作格式,如trg_users_after_insert
8.4 NEW 和 OLD 關鍵字
觸發(fā)器中通過 NEW 和 OLD 訪問數(shù)據變更前后的行:
| 觸發(fā)類型 | NEW(新數(shù)據) | OLD(舊數(shù)據) |
|---|---|---|
| INSERT | ? 新插入的行 | ? 不可用 |
| UPDATE | ? 更新后的新值 | ? 更新前的舊值 |
| DELETE | ? 不可用 | ? 被刪除的行 |
在 BEFORE 觸發(fā)器中,還可以通過 SET NEW.column = value 修改即將寫入數(shù)據庫的值:
-- 在 BEFORE INSERT 中預處理數(shù)據 SET NEW.username = LOWER(NEW.username); -- 用戶名統(tǒng)一轉小寫 SET NEW.create_time = NOW(); -- 自動填充創(chuàng)建時間
8.5 實戰(zhàn)示例
示例 1:AFTER INSERT —— 自動記錄新增日志
-- 操作日志表
CREATE TABLE operation_log (
id INT AUTO_INCREMENT PRIMARY KEY,
table_name VARCHAR(50) NOT NULL,
operation VARCHAR(20) NOT NULL,
record_id INT,
operate_time DATETIME NOT NULL,
detail TEXT
);
DELIMITER //
CREATE TRIGGER trg_users_after_insert
AFTER INSERT ON users
FOR EACH ROW
BEGIN
INSERT INTO operation_log(table_name, operation, record_id, operate_time, detail)
VALUES (
'users',
'INSERT',
NEW.id,
NOW(),
CONCAT('新增用戶:', NEW.username, ',郵箱:', NEW.email)
);
END //
DELIMITER ;示例 2:AFTER UPDATE —— 只記錄關鍵字段的變更
DELIMITER //
CREATE TRIGGER trg_users_after_update
AFTER UPDATE ON users
FOR EACH ROW
BEGIN
-- 只有 status 字段變更時才寫日志,避免無效日志
IF OLD.status != NEW.status THEN
INSERT INTO operation_log(table_name, operation, record_id, operate_time, detail)
VALUES (
'users',
'UPDATE',
NEW.id,
NOW(),
CONCAT('用戶[', NEW.username, ']狀態(tài)變更:', OLD.status, ' → ', NEW.status)
);
END IF;
END //
DELIMITER ;示例 3:BEFORE INSERT —— 數(shù)據校驗 + 自動填充
DELIMITER //
CREATE TRIGGER trg_orders_before_insert
BEFORE INSERT ON orders
FOR EACH ROW
BEGIN
-- 校驗:訂單金額不能為負數(shù)
IF NEW.amount <= 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '訂單金額必須大于0';
END IF;
-- 自動填充創(chuàng)建時間
IF NEW.create_time IS NULL THEN
SET NEW.create_time = NOW();
END IF;
-- 自動生成訂單號
IF NEW.order_no IS NULL OR NEW.order_no = '' THEN
SET NEW.order_no = CONCAT(
'ORD',
DATE_FORMAT(NOW(), '%Y%m%d%H%i%s'),
LPAD(NEW.user_id, 6, '0')
);
END IF;
END //
DELIMITER ;
SIGNAL語句:在觸發(fā)器或存儲過程中主動拋出錯誤,阻止當前操作并回滾事務。SQLSTATE '45000'是用戶自定義錯誤的標準狀態(tài)碼。
示例 4:AFTER DELETE —— 刪除前自動歸檔備份
DELIMITER //
CREATE TRIGGER trg_orders_after_delete
AFTER DELETE ON orders
FOR EACH ROW
BEGIN
-- 被刪除的數(shù)據自動歸檔到備份表
INSERT INTO orders_archive(
id, user_id, amount, status, create_time, archived_at
)
VALUES (
OLD.id,
OLD.user_id,
OLD.amount,
OLD.status,
OLD.create_time,
NOW()
);
END //
DELIMITER ;示例 5:BEFORE UPDATE —— 阻止非法狀態(tài)流轉
DELIMITER //
-- 訂單狀態(tài)只能單向流轉:pending → paid → shipped → completed
-- 已完成或已取消的訂單不允許再修改狀態(tài)
CREATE TRIGGER trg_orders_before_update
BEFORE UPDATE ON orders
FOR EACH ROW
BEGIN
IF OLD.status = 'completed' AND NEW.status != 'completed' THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '已完成的訂單不允許修改狀態(tài)';
END IF;
IF OLD.status = 'cancelled' THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '已取消的訂單不允許再操作';
END IF;
END //
DELIMITER ;8.6 觸發(fā)器的完整執(zhí)行流程
以執(zhí)行一條 UPDATE orders SET status='paid' WHERE id=1 為例: ┌──────────────────────────────────────────────────────────────────┐ │ 1. BEFORE UPDATE 觸發(fā)器執(zhí)行 │ │ → 可以通過 SET NEW.xxx 修改即將寫入的值 │ │ → 可以通過 SIGNAL 拋錯阻止本次操作(整個語句回滾) │ ├──────────────────────────────────────────────────────────────────┤ │ 2. 實際執(zhí)行 UPDATE 語句(數(shù)據寫入磁盤) │ ├──────────────────────────────────────────────────────────────────┤ │ 3. AFTER UPDATE 觸發(fā)器執(zhí)行 │ │ → 數(shù)據已確定寫入,適合做日志記錄、數(shù)據同步 │ │ → 若此處拋錯,整個事務(含 UPDATE)一起回滾 │ └──────────────────────────────────────────────────────────────────┘
MySQL 5.7+ 支持同一張表、同一事件有多個觸發(fā)器,按創(chuàng)建先后順序依次執(zhí)行。
8.7 觸發(fā)器的限制
| 限制 | 說明 |
|---|---|
| 不能調用有 OUT 參數(shù)的存儲過程 | 觸發(fā)器內部無法獲取存儲過程的輸出參數(shù) |
| 不能顯式控制事務 | 不能寫 START TRANSACTION / COMMIT / ROLLBACK,但觸發(fā)器與觸發(fā)它的語句在同一事務中 |
| 不能對同表做遞歸操作 | users 表的觸發(fā)器中不能再 UPDATE users(會無限遞歸報錯) |
| 不能使用動態(tài) SQL | 不能使用 PREPARE / EXECUTE 執(zhí)行動態(tài)語句 |
| 錯誤影響 | BEFORE 觸發(fā)器拋錯 → 阻止原操作;AFTER 觸發(fā)器拋錯 → 整個事務回滾 |
8.8 觸發(fā)器的管理
-- 查看當前數(shù)據庫所有觸發(fā)器
SHOW TRIGGERS FROM your_database;
-- 查看特定表的觸發(fā)器
SHOW TRIGGERS FROM your_database LIKE 'orders';
-- 查看觸發(fā)器定義
SHOW CREATE TRIGGER trg_users_after_insert;
-- 從系統(tǒng)表查詢(可獲取更詳細的信息)
SELECT TRIGGER_NAME, EVENT_MANIPULATION, EVENT_OBJECT_TABLE,
ACTION_TIMING, ACTION_STATEMENT
FROM information_schema.TRIGGERS
WHERE TRIGGER_SCHEMA = 'your_database';
-- 刪除觸發(fā)器
DROP TRIGGER IF EXISTS trg_users_after_insert;8.9 為什么現(xiàn)代開發(fā)不推薦濫用觸發(fā)器
| 問題 | 詳細說明 |
|---|---|
| 行為隱蔽 | 一條 INSERT 背后可能觸發(fā)多個觸發(fā)器,調用方完全不知情,Bug 難以追蹤 |
| 調試極難 | 觸發(fā)器自動執(zhí)行,日志里只顯示原始 SQL,觸發(fā)器內部的問題很難定位 |
| 性能陷阱 | 批量操作時每行都觸發(fā)一次(FOR EACH ROW),百萬行操作 = 百萬次觸發(fā)器調用 |
| 級聯(lián)風險 | 觸發(fā)器 A 修改表 B → 表 B 的觸發(fā)器 B 又觸發(fā)……級聯(lián)鏈路極難維護 |
| 業(yè)務邏輯分散 | 同一業(yè)務規(guī)則,一半在 Java Service,一半在數(shù)據庫觸發(fā)器,職責不清 |
| 測試困難 | 單元測試時很難 Mock 觸發(fā)器行為,集成測試成本高 |
推薦的替代方案:
- 審計日志 → 使用 AOP(
@Aspect)在 Service 層統(tǒng)一攔截記錄 - 數(shù)據校驗 → 放在 Java Bean Validation(
@NotNull、@Min等)或 Service 層 - 數(shù)據同步 → 使用消息隊列(MQ)做異步解耦,比觸發(fā)器更健壯
九、為什么現(xiàn)代開發(fā)中存儲過程用得越來越少?
這是面試中經常會被問到的問題。存儲過程在 2000~2010 年代被大量使用,但如今在互聯(lián)網項目中已經很少見了。原因如下:
9.1 微服務架構的興起
2005年的架構:
┌─────────┐ ┌──────────────────┐
│ 客戶端 │ ──→ │ 單體應用 + DB │ ← 業(yè)務邏輯大量放在存儲過程中
└─────────┘ └──────────────────┘
2020年的架構:
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 客戶端 │ ──→ │ 服務A │ │ 服務B │ │ 服務C │
└─────────┘ └────┬────┘ └────┬────┘ └────┬────┘
│ │ │
┌───┴───┐ ┌───┴───┐ ┌───┴───┐
│ DB-A │ │ DB-B │ │ DB-C │
└───────┘ └───────┘ └───────┘微服務架構下,業(yè)務邏輯必須放在服務層(Java/Go/Python),而不是數(shù)據庫中。因為:
- 跨服務的事務無法用數(shù)據庫存儲過程解決
- 每個服務獨立部署、獨立演進
9.2 可維護性差
| 問題 | 說明 |
|---|---|
| 調試困難 | 不像 Java 可以打斷點、單步調試,存儲過程只能通過 SELECT 打印中間變量 |
| 缺乏 IDE 支持 | 沒有代碼補全、沒有重構工具、沒有靜態(tài)分析 |
| 可讀性差 | SQL 本身不是面向對象語言,復雜邏輯寫起來冗長晦澀 |
| 測試困難 | 不能用 JUnit/Mockito 等框架進行單元測試 |
9.3 版本管理困難
- 存儲過程存在數(shù)據庫里,不在代碼倉庫中
- 無法用 Git 做版本控制、Code Review、分支管理
- 多人協(xié)作時容易沖突,且沖突難以發(fā)現(xiàn)
雖然可以把存儲過程的 SQL 文件放到 Git 里,但執(zhí)行和部署仍然需要額外的工具支持,遠不如 Java 代碼的 CI/CD 流程成熟。
9.4 數(shù)據庫可移植性差
-- MySQL 的存儲過程語法
DELIMITER //
CREATE PROCEDURE ...
BEGIN
DECLARE v INT;
...
END //
-- Oracle 的存儲過程語法(PL/SQL)
CREATE OR REPLACE PROCEDURE ...
IS
v NUMBER;
BEGIN
...
END;
-- SQL Server 的存儲過程語法(T-SQL)
CREATE PROCEDURE ...
AS
BEGIN
DECLARE @v INT;
...
END三種數(shù)據庫的語法完全不同。如果項目有更換數(shù)據庫的可能(如從 MySQL 遷移到 PostgreSQL),所有存儲過程都要重寫。
而 Java + MyBatis/JPA 的方案,換數(shù)據庫只需要改 SQL 方言配置,業(yè)務代碼基本不動。
9.5 水平擴展受限
- 應用層擴展:加幾臺服務器即可(無狀態(tài),容易擴展)
- 數(shù)據庫層擴展:讀寫分離、分庫分表都非常復雜
把業(yè)務邏輯放在存儲過程中 = 把壓力集中在數(shù)據庫上。而數(shù)據庫是整個系統(tǒng)中最難水平擴展的組件。
9.6 人才和協(xié)作問題
- 精通存儲過程開發(fā)的 DBA 人才稀缺
- 開發(fā)人員和 DBA 之間的協(xié)作成本高
- 出了 Bug,開發(fā)說是存儲過程的問題,DBA 說是調用方的問題——職責不清
9.7 ORM 框架的成熟
MyBatis、JPA/Hibernate、Spring Data 等 ORM 框架已經非常成熟:
// MyBatis —— 用 XML 或注解寫 SQL,邏輯在 Java 中
@Mapper
public interface UserMapper {
@Select("SELECT * FROM users WHERE id = #{id}")
User findById(int id);
}
// JPA —— 連 SQL 都不用寫
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByAgeGreaterThan(int age);
}這些框架讓 Java 代碼直接操作數(shù)據庫變得非常簡單,存儲過程的"封裝"優(yōu)勢不再明顯。
十、那什么時候還在用存儲過程?
雖然互聯(lián)網項目越來越少用,但存儲過程并沒有消亡。以下場景仍然活躍:
10.1 仍在使用的場景
| 場景 | 原因 |
|---|---|
| 傳統(tǒng)企業(yè)系統(tǒng)(ERP/銀行/保險) | 歷史遺留,存儲過程已運行多年,不敢輕易重構 |
| 批量數(shù)據處理 | 百萬/千萬級數(shù)據的批量更新,存儲過程比應用層逐條處理快得多 |
| 數(shù)據倉庫 / ETL | 數(shù)據清洗、加工、聚合,天然適合在數(shù)據庫端完成 |
| 復雜報表 | 涉及多表關聯(lián)、多級匯總的報表查詢 |
| DBA 運維腳本 | 數(shù)據庫巡檢、空間清理、權限管理等運維操作 |
| 安全性要求極高 | 不允許應用層直接訪問表,只能通過存儲過程操作數(shù)據 |
10.2 選擇建議
┌─────────────────────────────────────────────────────────────┐ │ 什么時候用存儲過程? │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ? 用: │ │ ? 大批量數(shù)據處理(ETL、定時任務) │ │ ? 對性能要求極致的數(shù)據庫操作 │ │ ? 需要嚴格權限控制,不允許直接操作表 │ │ ? 數(shù)據庫平臺固定,不考慮遷移 │ │ │ │ ? 不用: │ │ ? 常規(guī) CRUD 業(yè)務邏輯 │ │ ? 微服務架構項目 │ │ ? 需要頻繁迭代的業(yè)務 │ │ ? 需要跨數(shù)據庫平臺的項目 │ │ ? 團隊沒有熟悉存儲過程的 DBA │ │ │ └─────────────────────────────────────────────────────────────┘
十一、Java 中如何調用存儲過程
雖然現(xiàn)代項目不推薦大量使用存儲過程,但作為 Java 開發(fā)者,你仍然需要知道怎么調用它。
11.1 JDBC 原生調用
// 調用帶輸入輸出參數(shù)的存儲過程
try (Connection conn = dataSource.getConnection();
CallableStatement cs = conn.prepareCall("{CALL GetUserCount(?)}")) {
// 注冊輸出參數(shù)
cs.registerOutParameter(1, Types.INTEGER);
// 執(zhí)行
cs.execute();
// 獲取輸出參數(shù)
int count = cs.getInt(1);
System.out.println("用戶總數(shù):" + count);
}11.2 MyBatis 調用
<!-- Mapper XML -->
<select id="getUserCount" statementType="CALLABLE" resultType="map">
{CALL GetUserCount(#{count, mode=OUT, jdbcType=INTEGER})}
</select>// Mapper 接口
@Mapper
public interface UserMapper {
void getUserCount(Map<String, Object> params);
}
// 使用
Map<String, Object> params = new HashMap<>();
userMapper.getUserCount(params);
System.out.println(params.get("count"));11.3 Spring JdbcTemplate 調用
@Autowired
private JdbcTemplate jdbcTemplate;
public int getUserCount() {
SimpleJdbcCall call = new SimpleJdbcCall(jdbcTemplate)
.withProcedureName("GetUserCount");
Map<String, Object> result = call.execute();
return (Integer) result.get("p_count");
}11.4 JPA / Hibernate 調用
@Entity
@NamedStoredProcedureQuery(
name = "User.getUserCount",
procedureName = "GetUserCount",
parameters = {
@StoredProcedureParameter(mode = ParameterMode.OUT, name = "p_count", type = Integer.class)
}
)
public class User { ... }
// 調用
StoredProcedureQuery query = entityManager.createNamedStoredProcedureQuery("User.getUserCount");
query.execute();
Integer count = (Integer) query.getOutputParameterValue("p_count");十二、存儲過程的最佳實踐(如果你必須使用)
如果你的項目確實需要用到存儲過程,遵循以下規(guī)范可以減少后續(xù)的維護痛苦:
12.1 命名規(guī)范
-- 推薦:動詞 + 名詞,前綴表明類型 sp_transfer_money -- sp = stored procedure sp_get_user_by_id sp_batch_cancel_orders sp_calculate_monthly_report -- 不推薦 proc1 my_procedure test
12.2 編寫規(guī)范
DELIMITER //
CREATE PROCEDURE sp_example(
IN p_user_id INT, -- 參數(shù)用 p_ 前綴
OUT p_result VARCHAR(50)
)
COMMENT '示例存儲過程:簡要說明功能'
BEGIN
-- 局部變量用 v_ 前綴
DECLARE v_count INT DEFAULT 0;
DECLARE v_status VARCHAR(20);
-- 業(yè)務邏輯...
-- 1. 每個步驟加注釋
-- 2. 適當?shù)腻e誤處理
-- 3. 避免嵌套過深(建議不超過3層)
END //
DELIMITER ;12.3 其他建議
- 保持存儲過程短小:單個存儲過程不超過 200 行,太長就拆分
- 避免在存儲過程中寫業(yè)務邏輯:只做數(shù)據操作,不做業(yè)務判斷
- 一定要加異常處理:避免事務不提交也不回滾
- 把 DDL 文件納入版本管理:將所有存儲過程的 CREATE 語句放入項目的
sql/目錄 - 編寫變更日志:每次修改存儲過程,在注釋中記錄修改日期和原因
十三、面試高頻問題
存儲過程相關
Q1:什么是存儲過程?它的優(yōu)缺點是什么?
存儲過程是存儲在數(shù)據庫中的預編譯 SQL 語句集合,通過 CALL 語句調用。優(yōu)點是執(zhí)行效率高、減少網絡傳輸、增強安全性;缺點是可移植性差、調試困難、不利于版本管理。
Q2:IN、OUT、INOUT 參數(shù)的區(qū)別?
IN 是輸入參數(shù)(默認),調用方傳入值,過程內只讀;OUT 是輸出參數(shù),過程內賦值后調用方獲?。籌NOUT 是雙向參數(shù),調用方傳入、過程內修改、調用方再獲取結果。
Q3:為什么現(xiàn)在不推薦使用存儲過程?
微服務架構下業(yè)務邏輯必須在應用層;存儲過程難以調試、測試和版本管理;不同數(shù)據庫語法不兼容導致遷移成本高;把邏輯放在數(shù)據庫中不利于水平擴展;團隊協(xié)作成本高。
Q4:如何在 Java 中調用存儲過程?
可以用 JDBC 的
CallableStatement、MyBatis 的statementType="CALLABLE"、Spring 的SimpleJdbcCall、或 JPA 的@NamedStoredProcedureQuery。
游標相關
Q5:什么是游標?使用游標的四個步驟是什么?
游標是對查詢結果集進行逐行處理的數(shù)據庫機制。四個步驟:① DECLARE 聲明游標(綁定 SELECT 語句)② OPEN 打開游標(執(zhí)行查詢,加載結果集)③ FETCH 逐行讀?。▽斍靶写嫒胱兞浚羔樅笠疲?CLOSE 關閉游標(釋放內存)。
Q6:MySQL 游標有哪些特性和限制?
MySQL 游標是只讀的,不能通過游標直接修改行;是單向滾動的,只能向前逐行移動;只能在存儲過程/函數(shù)/觸發(fā)器內使用;打開游標會將完整結果集加載到內存,結果集過大時存在 OOM 風險。
Q7:游標存在性能問題,什么時候應該用游標,什么時候不用?
能用集合 SQL(如
UPDATE ... WHERE)解決的絕對不用游標。適合用游標的場景:每行邏輯復雜且確實無法用單條 SQL 表達、數(shù)據量較小(幾千到幾萬行)、ETL 中需要逐行加工轉換的場景。
Q8:MySQL 中 DECLARE 語句的順序有什么要求?
MySQL 規(guī)定
DECLARE語句必須按固定順序出現(xiàn),否則編譯報錯:① 先聲明局部變量(DECLARE var TYPE)② 再聲明游標(DECLARE cur CURSOR FOR)③ 最后聲明異常處理器(DECLARE HANDLER)。
存儲函數(shù)相關
Q9:存儲過程和存儲函數(shù)的區(qū)別?
對比項 存儲過程 存儲函數(shù) 調用方式 CALL sp_name()SELECT fn_name()返回值 OUT 參數(shù),可有多個 必須有且只有一個 RETURN 能否嵌入 SQL 不能 可以 事務控制 可以 不能顯式控制 側重點 執(zhí)行操作 計算返回值
Q10:創(chuàng)建存儲函數(shù)時 DETERMINISTIC 關鍵字是什么意思?
DETERMINISTIC表示相同輸入總是產生相同輸出(純函數(shù),如字符串處理、數(shù)學計算);NOT DETERMINISTIC(默認)表示相同輸入可能返回不同結果(如使用了NOW()、RAND())。在 binlog 開啟時,不確定性函數(shù)可能導致主從不一致,MySQL 5.x 需要設置log_bin_trust_function_creators = 1。
觸發(fā)器相關
Q11:什么是觸發(fā)器?MySQL 共有幾種觸發(fā)器?
觸發(fā)器是與表關聯(lián)的特殊存儲程序,當表發(fā)生 INSERT、UPDATE、DELETE 時自動執(zhí)行。按時機分
BEFORE和AFTER,按操作分INSERT/UPDATE/DELETE,兩兩組合共 6 種觸發(fā)時機。
Q12:觸發(fā)器中 NEW 和 OLD 關鍵字的區(qū)別?
INSERT 觸發(fā)器中只有
NEW(新插入的行);DELETE 觸發(fā)器中只有OLD(被刪除的行);UPDATE 觸發(fā)器中兩者都有(OLD是更新前,NEW是更新后)。在BEFORE觸發(fā)器中可以用SET NEW.col = val修改即將寫入的值。
Q13:為什么現(xiàn)代開發(fā)不推薦濫用觸發(fā)器?
觸發(fā)器隱式執(zhí)行不透明,Bug 難以追蹤;批量操作時每行都觸發(fā)一次,性能差;級聯(lián)觸發(fā)鏈路極難維護和調試;單元測試時很難 Mock 觸發(fā)器行為;業(yè)務邏輯分散在數(shù)據庫和應用層,職責不清。推薦用 AOP 替代審計日志,用 Bean Validation 替代數(shù)據校驗,用 MQ 替代數(shù)據同步。
Q14:BEFORE 觸發(fā)器和 AFTER 觸發(fā)器分別適合什么場景?
BEFORE:適合數(shù)據校驗(拋錯阻止非法操作)、自動填充字段(如創(chuàng)建時間)、防止非法狀態(tài)流轉。AFTER:適合寫操作日志、更新統(tǒng)計數(shù)據、實現(xiàn)刪除歸檔備份。
十四、總結
| 可編程對象 | 本質 | 調用方式 | 當前定位 |
|---|---|---|---|
| 存儲過程 | 執(zhí)行一組操作的 SQL 代碼塊 | CALL sp_name() | 批量處理、ETL、傳統(tǒng)企業(yè)系統(tǒng) |
| 游標 | 逐行遍歷結果集的指針 | 配合存儲過程/函數(shù)使用 | 每行邏輯差異化處理,優(yōu)先用集合 SQL |
| 存儲函數(shù) | 計算并返回單個值的代碼塊 | SELECT fn_name() | 可復用的計算邏輯,嵌入 SQL 中直接用 |
| 觸發(fā)器 | 數(shù)據變更時自動執(zhí)行的事件鉤子 | 自動觸發(fā),不能手動調用 | 嚴格限制使用,推薦用 AOP/MQ 替代 |
共同的歷史定位:這四者都是數(shù)據庫時代將業(yè)務邏輯下沉到數(shù)據庫的產物。在現(xiàn)代微服務架構中,它們的使用均大幅減少,業(yè)務邏輯應該回歸應用層,讓數(shù)據庫回歸它的本職工作——存儲和檢索數(shù)據。
| 場景 | 建議方案 |
|---|---|
| 審計日志 | AOP + 日志表,而非觸發(fā)器 |
| 數(shù)據校驗 | Bean Validation + Service 層,而非 BEFORE 觸發(fā)器 |
| 數(shù)據同步 | MQ 異步解耦,而非觸發(fā)器級聯(lián)操作 |
| 批量處理 | 存儲過程 / 存儲函數(shù) + 游標 |
| 常規(guī) CRUD | MyBatis / JPA + Service 層 Java 代碼 |
?? 推薦閱讀:如果你對 Java 后端技術棧感興趣,可以繼續(xù)閱讀本系列其他文章,涵蓋 JVM、Spring、Nacos 等核心知識點。
到此這篇關于MySQL 存儲過程、游標、存儲函數(shù)與觸發(fā)器示例詳解的文章就介紹到這了,更多相關mysql存儲過程、游標、觸發(fā)器內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
MySql中使用INSERT INTO語句更新多條數(shù)據的例子
這篇文章主要介紹了MySql中使用INSERT INTO語句更新多條數(shù)據的例子,MySQL的特有語法,需要的朋友可以參考下2014-06-06
MySQL 5.7.14 net start mysql 服務無法啟動-“NET HELPMSG 3534” 的奇怪問題
這篇文章主要介紹了MySQL 5.7.14 net start mysql 服務無法啟動-“NET HELPMSG 3534” 的奇怪問題,需要的朋友可以參考下2016-12-12
CentOS 7.0如何啟動多個MySQL實例教程(mysql-5.7.21)
這篇文章主要給大家介紹了關于CentOS 7.0如何啟動多個MySQL實例(mysql-5.7.21)的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起看看吧。2018-03-03

