最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

MySQL 存儲過程、游標、存儲函數(shù)與觸發(fā)器最佳實踐

 更新時間:2026年05月27日 10:50:17   作者:callNull  
本文詳細介紹了MySQL存儲過程、游標、存儲函數(shù)及觸發(fā)器的概念、語法、應用場景及現(xiàn)代開發(fā)中的定位,涵蓋從基礎語法到高級用法,全方位解析數(shù)據庫可編程對象,感興趣的朋友一起看看吧

本文將全面介紹 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),順序錯誤會報編譯錯誤:

  1. 局部變量(DECLARE var TYPE
  2. 游標(DECLARE cur CURSOR FOR
  3. 異常處理器(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);  -- 輸出 55

7.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)

時機說明常見用途
BEFORESQL 操作執(zhí)行之前觸發(fā)數(shù)據校驗、自動填充、阻止非法操作
AFTERSQL 操作執(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ā)器中通過 NEWOLD 訪問數(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í)行。按時機分 BEFOREAFTER,按操作分 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ī) CRUDMyBatis / JPA + Service 層 Java 代碼

?? 推薦閱讀:如果你對 Java 后端技術棧感興趣,可以繼續(xù)閱讀本系列其他文章,涵蓋 JVM、Spring、Nacos 等核心知識點。

到此這篇關于MySQL 存儲過程、游標、存儲函數(shù)與觸發(fā)器示例詳解的文章就介紹到這了,更多相關mysql存儲過程、游標、觸發(fā)器內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • MySql中使用INSERT INTO語句更新多條數(shù)據的例子

    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” 的奇怪問題

    這篇文章主要介紹了MySQL 5.7.14 net start mysql 服務無法啟動-“NET HELPMSG 3534” 的奇怪問題,需要的朋友可以參考下
    2016-12-12
  • mysql外鍵基本功能與用法詳解

    mysql外鍵基本功能與用法詳解

    這篇文章主要介紹了mysql外鍵基本功能與用法,結合實例形式詳細分析了mysql外鍵的基本概念、功能、用法及操作注意事項,需要的朋友可以參考下
    2020-04-04
  • delete?in子查詢不走索引問題分析

    delete?in子查詢不走索引問題分析

    這篇文章主要為大家介紹了delete?in子查詢不走索引的問題分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-07-07
  • CentOS 7.0如何啟動多個MySQL實例教程(mysql-5.7.21)

    CentOS 7.0如何啟動多個MySQL實例教程(mysql-5.7.21)

    這篇文章主要給大家介紹了關于CentOS 7.0如何啟動多個MySQL實例(mysql-5.7.21)的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起看看吧。
    2018-03-03
  • 一個mysql死鎖場景實例分析

    一個mysql死鎖場景實例分析

    這篇文章主要給大家實例分析了一個mysql死鎖場景的相關資料,文中通過示例代碼介紹的非常詳細,對大家學習或者使用mysql具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧
    2019-05-05
  • MySQL的ALTER TABLE命令的使用解讀

    MySQL的ALTER TABLE命令的使用解讀

    這篇文章主要介紹了MySQL的ALTER TABLE命令的使用,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2025-05-05
  • mysql sql語句總結

    mysql sql語句總結

    mysql sql語句總結,都是一些比較實用簡單的語句。一定要掌握的。
    2009-11-11
  • MySQL一文搞懂行級鎖經典版

    MySQL一文搞懂行級鎖經典版

    文章詳細介紹了MySQL中行級鎖的原理,包括InnoDB和MyISAM引擎的區(qū)別、記錄鎖(RecordLock)、間隙鎖(GapLock)和臨鍵鎖(Next-KeyLock)的定義與特性,以及如何通過`SELECT ... FOR UPDATE`語句來實現(xiàn)鎖定讀和避免幻讀問題,感興趣的朋友跟隨小編一起看看吧
    2025-12-12
  • MySQL修改默認端口失敗的常見原因及解決方案

    MySQL修改默認端口失敗的常見原因及解決方案

    文章詳細介紹了MySQL配置文件修改為3307端口后重啟失敗的常見原因,并提供了相應的解決方案,關鍵步驟包括查看錯誤日志、檢查端口占用、權限問題、配置文件語法等,此外,還特別提到了Linux和Windows系統(tǒng)中可能遇到的特定問題和解決方法,需要的朋友可以參考下
    2025-12-12

最新評論

稷山县| 闻喜县| 建德市| 南漳县| 建瓯市| 朝阳县| 宁南县| 长乐市| 紫金县| 望都县| 巴中市| 迁安市| 潍坊市| 临夏县| 赤壁市| 中宁县| 伊金霍洛旗| 昌平区| 长泰县| 娄烦县| 伽师县| 鸡泽县| 治多县| 教育| 宁化县| 许昌县| 阳朔县| 伊宁县| 巴楚县| 宜城市| 芮城县| 阳东县| 望奎县| 苍溪县| 万宁市| 广汉市| 醴陵市| 咸丰县| 彰化县| 鹤壁市| 铜鼓县|