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

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

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

本文將全面介紹 MySQL 存儲過程、游標(biāo)、存儲函數(shù)、觸發(fā)器的概念、語法、應(yīng)用場景以及在現(xiàn)代開發(fā)中的定位。這四者是 MySQL 可編程對象的核心,無論你是面試準(zhǔn)備還是項(xiàng)目實(shí)戰(zhàn),讀完這篇文章你將對它們建立起清晰、完整的認(rèn)知。

一、什么是存儲過程

1.1 概念定義

存儲過程(Stored Procedure) 是一組為了完成特定功能而預(yù)先編譯好的 SQL 語句集合,存儲在數(shù)據(jù)庫服務(wù)器中,客戶端通過指定存儲過程的名字并給出參數(shù)(如果有)來調(diào)用執(zhí)行。

用一個(gè)通俗的類比來理解:

  • 沒有存儲過程:你每次去餐廳都要一步一步告訴廚師——先放油、再放蒜、然后放肉、翻炒三分鐘……
  • 有存儲過程:你只需要說"來一份宮保雞丁",廚師已經(jīng)知道所有步驟了

本質(zhì)上,存儲過程就是數(shù)據(jù)庫端的"函數(shù)",把一段可重復(fù)使用的業(yè)務(wù)邏輯封裝起來,對外暴露一個(gè)調(diào)用接口。

1.2 核心特征

特征說明
預(yù)編譯創(chuàng)建時(shí)編譯一次,后續(xù)調(diào)用直接執(zhí)行編譯后的代碼
持久化存儲存儲在數(shù)據(jù)庫的系統(tǒng)表中,數(shù)據(jù)庫重啟后依然存在
參數(shù)化支持輸入(IN)、輸出(OUT)、輸入輸出(INOUT)三種參數(shù)模式
流程控制支持變量聲明、條件判斷、循環(huán)等編程語言的基本結(jié)構(gòu)
事務(wù)支持內(nèi)部可以使用事務(wù)控制(BEGIN / COMMIT / ROLLBACK)

1.3 存儲過程 vs 普通 SQL

┌─────────────────────────────────────────────────────────────┐
│                     普通 SQL 執(zhí)行流程                          │
├─────────────────────────────────────────────────────────────┤
│  客戶端 → 發(fā)送SQL語句 → 數(shù)據(jù)庫解析 → 編譯 → 優(yōu)化 → 執(zhí)行 → 返回  │
│  (每次都要重復(fù)上述全部步驟)                                    │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                   存儲過程執(zhí)行流程                              │
├─────────────────────────────────────────────────────────────┤
│  第一次:客戶端 → CALL → 解析 → 編譯 → 優(yōu)化 → 執(zhí)行 → 返回      │
│  后續(xù)次:客戶端 → CALL → 直接執(zhí)行已編譯代碼 → 返回               │
│  (省去了解析、編譯、優(yōu)化步驟)                                  │
└─────────────────────────────────────────────────────────────┘

二、存儲過程有什么用

2.1 五大核心作用

① 封裝復(fù)雜業(yè)務(wù)邏輯

將多條 SQL 語句和邏輯判斷封裝為一個(gè)整體,對外只暴露一個(gè)名字和參數(shù)列表,隱藏內(nèi)部實(shí)現(xiàn)細(xì)節(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í)行時(shí)被編譯,后續(xù)調(diào)用跳過解析和編譯階段
  • 減少了客戶端與數(shù)據(jù)庫之間的網(wǎng)絡(luò)往返(只傳一個(gè) CALL 命令,而不是一大段 SQL)

③ 增強(qiáng)安全性

  • 可以只授予用戶執(zhí)行存儲過程的權(quán)限,而不給予直接操作表的權(quán)限
  • 防止 SQL 注入:參數(shù)傳入存儲過程時(shí),不會被當(dāng)作 SQL 代碼執(zhí)行

④ 減少網(wǎng)絡(luò)傳輸

假設(shè)一個(gè)業(yè)務(wù)需要執(zhí)行 10 條 SQL,如果不用存儲過程需要 10 次網(wǎng)絡(luò)往返;用存儲過程只需要 1 次網(wǎng)絡(luò)調(diào)用。

⑤ 保證數(shù)據(jù)一致性

存儲過程內(nèi)部可以包含事務(wù)控制,保證一組操作要么全部成功,要么全部回滾。

2.2 典型應(yīng)用場景

場景說明
銀行轉(zhuǎn)賬扣款 + 加款 + 記流水,必須在事務(wù)中完成
批量數(shù)據(jù)處理定時(shí)結(jié)算、對賬、數(shù)據(jù)遷移
復(fù)雜報(bào)表統(tǒng)計(jì)多表關(guān)聯(lián) + 聚合計(jì)算 + 分組排序
權(quán)限控制DBA 封裝好存儲過程,開發(fā)人員只能調(diào)用
ETL 數(shù)據(jù)清洗數(shù)據(jù)倉庫中從源表加工到目標(biāo)表

三、快速入門

3.1 基礎(chǔ)語法

-- 修改結(jié)束符(因?yàn)榇鎯^程內(nèi)部有分號)
DELIMITER //
CREATE PROCEDURE 過程名([參數(shù)列表])
BEGIN
    -- SQL 語句和邏輯
END //
-- 恢復(fù)結(jié)束符
DELIMITER ;

為什么需要 DELIMITER?
MySQL 默認(rèn)用 ; 作為語句結(jié)束符。存儲過程體內(nèi)包含多個(gè) ;,如果不改結(jié)束符,MySQL 會在第一個(gè) ; 處就認(rèn)為語句結(jié)束了。所以我們臨時(shí)將結(jié)束符改為 //,定義完后再改回 ;

3.2 參數(shù)類型

存儲過程支持三種參數(shù)模式:

CREATE PROCEDURE 過程名(
    IN    輸入?yún)?shù)名  數(shù)據(jù)類型,    -- 調(diào)用方傳入,過程內(nèi)只讀
    OUT   輸出參數(shù)名  數(shù)據(jù)類型,    -- 過程內(nèi)寫入,調(diào)用方獲取
    INOUT 雙向參數(shù)名  數(shù)據(jù)類型     -- 調(diào)用方傳入,過程內(nèi)修改后調(diào)用方可獲取
)
模式方向類比
IN調(diào)用方 → 存儲過程函數(shù)的入?yún)?/td>
OUT存儲過程 → 調(diào)用方函數(shù)的返回值
INOUT雙向傳遞引用傳遞

3.3 快速實(shí)戰(zhàn) 6 例

示例 1:最簡單的存儲過程(無參數(shù))

DELIMITER //
CREATE PROCEDURE HelloWorld()
BEGIN
    SELECT 'Hello, Stored Procedure!' AS message;
END //
DELIMITER ;
-- 調(diào)用
CALL HelloWorld();

輸出:

+---------------------------+
| message                   |
+---------------------------+
| Hello, Stored Procedure!  |
+---------------------------+

示例 2:帶輸入?yún)?shù)(IN)—— 根據(jù) ID 查用戶

DELIMITER //
CREATE PROCEDURE GetUserById(IN p_id INT)
BEGIN
    SELECT id, username, email, create_time
    FROM users
    WHERE id = p_id;
END //
DELIMITER ;
-- 調(diào)用
CALL GetUserById(1);
CALL GetUserById(5);

示例 3:帶輸出參數(shù)(OUT)—— 統(tǒng)計(jì)用戶數(shù)量

DELIMITER //
CREATE PROCEDURE GetUserCount(OUT p_count INT)
BEGIN
    SELECT COUNT(*) INTO p_count FROM users;
END //
DELIMITER ;
-- 調(diào)用
CALL GetUserCount(@total);
SELECT @total AS user_count;

注意@total 是 MySQL 的用戶變量(Session 級別),用 @ 前綴聲明,可以在同一個(gè)會話中跨語句使用。

示例 4:輸入輸出參數(shù)(INOUT)—— 累加計(jì)數(shù)器

DELIMITER //
CREATE PROCEDURE Accumulate(INOUT p_counter INT, IN p_increment INT)
BEGIN
    SET p_counter = p_counter + p_increment;
END //
DELIMITER ;
-- 調(diào)用
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 ;
-- 調(diào)用
CALL EvaluateScore(85, @level);
SELECT @level;  -- 輸出:良好

示例 6:循環(huán) + 游標(biāo)(遍歷結(jié)果集)

DELIMITER //
CREATE PROCEDURE BatchUpdateStatus()
BEGIN
    DECLARE v_id INT;
    DECLARE v_done INT DEFAULT 0;
    -- 聲明游標(biāo)
    DECLARE cur CURSOR FOR
        SELECT id FROM orders WHERE status = 'pending' AND create_time < DATE_SUB(NOW(), INTERVAL 30 DAY);
    -- 聲明結(jié)束處理器
    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天的待處理訂單自動(dòng)取消
        UPDATE orders SET status = 'cancelled' WHERE id = v_id;
    END LOOP;
    CLOSE cur;
END //
DELIMITER ;
-- 調(diào)用
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)標(biāo)簽
ITERATE跳過本次循環(huán)ITERATE 循環(huán)標(biāo)簽(類似 continue)

四、存儲過程中的事務(wù)與異常處理

4.1 事務(wù)控制

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 異常時(shí)回滾
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        SET p_status = '轉(zhuǎn)賬失?。喊l(fā)生異常';
    END;
    -- 余額校驗(yàn)
    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 = '轉(zhuǎn)賬失?。河囝~不足';
    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 = '轉(zhuǎn)賬成功';
    END IF;
END //
DELIMITER ;

4.2 異常處理器類型

-- 遇到異常后繼續(xù)執(zhí)行后續(xù)語句
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET @err = 1;
-- 遇到異常后退出當(dāng)前 BEGIN...END 塊
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
    ROLLBACK;
    SELECT '執(zhí)行出錯(cuò)' AS error_msg;
END;
-- 處理特定錯(cuò)誤碼
DECLARE CONTINUE HANDLER FOR 1062  -- 重復(fù)鍵錯(cuò)誤
    SET @duplicate = 1;

五、存儲過程的管理操作

-- ========== 查看 ==========
-- 查看當(dāng)前數(shù)據(jù)庫所有存儲過程
SHOW PROCEDURE STATUS WHERE Db = 'your_database';
-- 查看某個(gè)存儲過程的創(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;

六、游標(biāo)(Cursor)詳解

6.1 什么是游標(biāo)

游標(biāo)(Cursor) 是數(shù)據(jù)庫提供的一種機(jī)制,用于對查詢結(jié)果集進(jìn)行逐行處理。普通的 SELECT 語句一次性返回所有滿足條件的行,而游標(biāo)則像一個(gè)"指針",可以在結(jié)果集上一行一行地移動(dòng),逐條讀取并處理數(shù)據(jù)。

通俗類比:

  • SELECT * FROM orders → 把所有訂單數(shù)據(jù)一股腦全取出來
  • 游標(biāo) → 像超市收銀員,一件一件掃描商品,處理完一件再拿下一件

游標(biāo)通常與存儲過程配合使用,在需要對每一行數(shù)據(jù)做差異化處理時(shí)發(fā)揮作用。

6.2 游標(biāo)的使用四步驟

MySQL 中使用游標(biāo)必須嚴(yán)格按照以下四個(gè)步驟進(jìn)行:

① DECLARE  →  聲明游標(biāo)(綁定一條 SELECT 語句,此時(shí)不執(zhí)行)
② OPEN     →  打開游標(biāo)(執(zhí)行 SELECT,將結(jié)果集加載到內(nèi)存)
③ FETCH    →  逐行讀取(將當(dāng)前行數(shù)據(jù)存入變量,指針下移)
④ CLOSE    →  關(guān)閉游標(biāo)(釋放結(jié)果集占用的內(nèi)存)

6.3 基礎(chǔ)語法

DELIMITER //
CREATE PROCEDURE cursor_demo()
BEGIN
    -- 1. 聲明局部變量(用于存放每行數(shù)據(jù))
    DECLARE v_id   INT;
    DECLARE v_name VARCHAR(50);
    DECLARE v_done INT DEFAULT 0;  -- 游標(biāo)結(jié)束標(biāo)志
    -- 2. 聲明游標(biāo)(綁定 SELECT,注意:此時(shí)并不執(zhí)行查詢)
    DECLARE cur CURSOR FOR
        SELECT id, username FROM users WHERE status = 1;
    -- 3. 聲明結(jié)束處理器(FETCH 到末尾觸發(fā) NOT FOUND,將 v_done 置 1)
    --    注意:HANDLER 必須聲明在游標(biāo)之后
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = 1;
    -- 4. 打開游標(biāo)(此時(shí)才真正執(zhí)行 SELECT 查詢)
    OPEN cur;
    -- 5. 循環(huán)讀取
    read_loop: LOOP
        FETCH cur INTO v_id, v_name;  -- 讀取當(dāng)前行到變量,指針后移
        IF v_done = 1 THEN            -- 沒有更多行,退出循環(huán)
            LEAVE read_loop;
        END IF;
        -- 處理當(dāng)前行的業(yè)務(wù)邏輯
        -- ...
    END LOOP read_loop;
    -- 6. 關(guān)閉游標(biāo)(釋放資源)
    CLOSE cur;
END //
DELIMITER ;

?? DECLARE 的順序非常重要:MySQL 規(guī)定 DECLARE 語句必須按以下順序出現(xiàn),順序錯(cuò)誤會報(bào)編譯錯(cuò)誤:

  1. 局部變量(DECLARE var TYPE
  2. 游標(biāo)(DECLARE cur CURSOR FOR
  3. 異常處理器(DECLARE HANDLER

6.4 游標(biāo)的特性

特性說明
只讀性MySQL 游標(biāo)只能讀取數(shù)據(jù),不能通過游標(biāo)直接修改行(需單獨(dú)寫 UPDATE)
單向滾動(dòng)只能向前逐行移動(dòng),不能后退或隨機(jī)跳轉(zhuǎn)到某一行
局部性只能在存儲過程、存儲函數(shù)或觸發(fā)器內(nèi)部使用,不能單獨(dú)執(zhí)行
內(nèi)存占用打開游標(biāo)會將完整結(jié)果集加載到內(nèi)存,結(jié)果集過大時(shí)存在 OOM 風(fēng)險(xiǎn)

6.5 實(shí)戰(zhàn)示例

示例 1:遍歷用戶列表自動(dòng)寫通知日志

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)有重要通知,請及時(shí)查看。');
    END LOOP user_loop;
    CLOSE cur;
END //
DELIMITER ;

示例 2:游標(biāo) + 條件判斷 —— 差異化更新用戶積分

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;
        -- 根據(jù)完成訂單數(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)計(jì)數(shù)據(jù)(游標(biāo) + 聚合查詢)

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;
        -- 對每個(gè)部門單獨(dú)做聚合查詢
        SELECT COUNT(*), IFNULL(AVG(salary), 0)
        INTO v_emp_count, v_avg_salary
        FROM employees
        WHERE department_id = v_dept_id;
        -- 更新統(tǒng)計(jì)表(存在則更新,不存在則插入)
        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 游標(biāo)的性能問題與替代方案

游標(biāo)的本質(zhì)是逐行處理(Row-by-Row),而關(guān)系型數(shù)據(jù)庫的優(yōu)化器是為**集合操作(Set-based)**設(shè)計(jì)的,兩者背道而馳。

方案適用場景性能
游標(biāo)逐行處理每行邏輯復(fù)雜、無法用單條 SQL 表達(dá)
集合 SQL(批量 UPDATE/INSERT)批量相同操作
分批處理(LIMIT + 循環(huán))超大數(shù)據(jù)量的批量操作較快

能用集合 SQL 解決的,絕不用游標(biāo)

-- ? 用游標(biāo)逐行更新(慢,不推薦)
-- 聲明游標(biāo) → 循環(huán) FETCH → 逐條 UPDATE ...
-- ? 用一條集合 SQL 批量更新(快,推薦)
UPDATE orders
SET status = 'cancelled'
WHERE status = 'pending'
  AND create_time < DATE_SUB(NOW(), INTERVAL 30 DAY);

游標(biāo)合理的使用場景

  • 每行的處理邏輯確實(shí)無法用一條 SQL 表達(dá)(如需要調(diào)用存儲過程、做復(fù)雜條件分支)
  • 數(shù)據(jù)量較?。◣浊У綆兹f行以內(nèi))
  • ETL / 數(shù)據(jù)遷移中需要逐行加工轉(zhuǎn)換

七、存儲函數(shù)(Function)詳解

7.1 什么是存儲函數(shù)

存儲函數(shù)(Stored Function) 和存儲過程同樣是存儲在數(shù)據(jù)庫中的預(yù)編譯代碼塊。核心區(qū)別在于:存儲函數(shù)必須通過 RETURN 返回一個(gè)值,并且可以像 MySQL 內(nèi)置函數(shù)(NOW()LENGTH()、IFNULL() 等)一樣,直接嵌入 SQL 語句中使用。

-- 內(nèi)置函數(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 基礎(chǔ)語法

DELIMITER //
CREATE FUNCTION 函數(shù)名(參數(shù)1 數(shù)據(jù)類型, 參數(shù)2 數(shù)據(jù)類型, ...)
RETURNS 返回值類型
[特性關(guān)鍵字]
BEGIN
    -- 函數(shù)體(只有 IN 參數(shù),沒有 OUT/INOUT)
    RETURN 返回值;
END //
DELIMITER ;

特性關(guān)鍵字(至少聲明一個(gè),MySQL 要求必須明確聲明函數(shù)特性):

關(guān)鍵字含義
DETERMINISTIC相同輸入總是返回相同輸出(純函數(shù),如字符串處理、數(shù)學(xué)計(jì)算)
NOT DETERMINISTIC相同輸入可能返回不同輸出(默認(rèn)值,如使用了 NOW()、RAND()
NO SQL函數(shù)體中不包含任何 SQL 語句
READS SQL DATA函數(shù)體包含 SELECT 讀操作,但不修改數(shù)據(jù)
MODIFIES SQL DATA函數(shù)體包含 INSERT/UPDATE/DELETE 寫操作

?? 權(quán)限注意:MySQL 5.x 默認(rèn)禁止創(chuàng)建存儲函數(shù)(因?yàn)樵?binlog 開啟時(shí),不確定性函數(shù)可能引發(fā)主從不一致)。需要 DBA 執(zhí)行:

SET GLOBAL log_bin_trust_function_creators = 1;

MySQL 8.0 對此限制已大幅放寬。

7.3 調(diào)用方式對比

-- 存儲過程:必須用 CALL,不能嵌入 SQL
CALL sp_get_user_by_id(1);
-- 存儲函數(shù):像內(nèi)置函數(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 實(shí)戰(zhàn)示例

示例 1:字符串處理函數(shù) —— 手機(jī)號中間四位脫敏

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è)務(wù)函數(shù) —— 根據(jù)年齡返回用戶年齡段

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 中篩選高價(jià)值用戶
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ù) —— 計(jì)算斐波那契數(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 ;
-- 使用前需設(shè)置最大遞歸深度(默認(rèn)為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ù)返回,可有多個(gè)必須有且只有一個(gè) RETURN 返回值
調(diào)用方式CALL procedure_name()SELECT function_name()
能否嵌入 SQL? 不能嵌入 SELECT/WHERE/JOIN? 可以像內(nèi)置函數(shù)一樣使用
參數(shù)類型IN / OUT / INOUT只有 IN
能否控制事務(wù)? 可以使用 COMMIT/ROLLBACK? 不能顯式控制事務(wù)
使用游標(biāo)? 支持? 支持
側(cè)重點(diǎn)執(zhí)行一系列操作(流程控制)計(jì)算并返回單個(gè)值(可復(fù)用計(jì)算邏輯)

選擇原則

  • 需要做事情(執(zhí)行增刪改、控制事務(wù)、復(fù)雜流程)→ 存儲過程
  • 需要算東西(計(jì)算一個(gè)值,嵌入到 SQL 中直接用)→ 存儲函數(shù)

八、觸發(fā)器(Trigger)詳解

8.1 什么是觸發(fā)器

觸發(fā)器(Trigger) 是與數(shù)據(jù)庫表關(guān)聯(lián)的特殊存儲程序。當(dāng)表上發(fā)生特定的數(shù)據(jù)變更操作(INSERT、UPDATE、DELETE)時(shí),觸發(fā)器會自動(dòng)執(zhí)行,無需手動(dòng)調(diào)用,完全由數(shù)據(jù)庫引擎驅(qū)動(dòng)。

通俗類比:

  • 存儲過程 → 你主動(dòng)按開關(guān)開燈
  • 觸發(fā)器 → 人體感應(yīng)燈,有人進(jìn)來自動(dòng)

觸發(fā)器就像數(shù)據(jù)庫層的事件監(jiān)聽器 / 鉤子函數(shù)——就像 Java 中的 @EventListener,觸發(fā)器監(jiān)聽的是數(shù)據(jù)變更事件,一旦發(fā)生就自動(dòng)響應(yīng)。

8.2 觸發(fā)器的類型

MySQL 觸發(fā)器按兩個(gè)維度分類,兩兩組合共 6 種觸發(fā)時(shí)機(jī):

按時(shí)機(jī)(WHEN)

時(shí)機(jī)說明常見用途
BEFORESQL 操作執(zhí)行之前觸發(fā)數(shù)據(jù)校驗(yàn)、自動(dòng)填充、阻止非法操作
AFTERSQL 操作執(zhí)行之后觸發(fā)日志記錄、數(shù)據(jù)同步、統(tǒng)計(jì)更新

按操作(EVENT)INSERT / UPDATE / DELETE

8.3 基礎(chǔ)語法

DELIMITER //
CREATE TRIGGER trigger_name
{BEFORE | AFTER} {INSERT | UPDATE | DELETE}
ON table_name
FOR EACH ROW
BEGIN
    -- 觸發(fā)器邏輯
END //
DELIMITER ;

關(guān)鍵說明:

  • FOR EACH ROW:行級觸發(fā)器,每影響一行就觸發(fā)一次(MySQL 只支持行級,不支持語句級)
  • 觸發(fā)器名建議使用 trg_表名_時(shí)機(jī)_操作 格式,如 trg_users_after_insert

8.4 NEW 和 OLD 關(guān)鍵字

觸發(fā)器中通過 NEWOLD 訪問數(shù)據(jù)變更前后的行:

觸發(fā)類型NEW(新數(shù)據(jù))OLD(舊數(shù)據(jù))
INSERT? 新插入的行? 不可用
UPDATE? 更新后的新值? 更新前的舊值
DELETE? 不可用? 被刪除的行

BEFORE 觸發(fā)器中,還可以通過 SET NEW.column = value 修改即將寫入數(shù)據(jù)庫的值:

-- 在 BEFORE INSERT 中預(yù)處理數(shù)據(jù)
SET NEW.username   = LOWER(NEW.username);   -- 用戶名統(tǒng)一轉(zhuǎn)小寫
SET NEW.create_time = NOW();                 -- 自動(dòng)填充創(chuàng)建時(shí)間

8.5 實(shí)戰(zhàn)示例

示例 1:AFTER INSERT —— 自動(dòng)記錄新增日志

-- 操作日志表
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 —— 只記錄關(guān)鍵字段的變更

DELIMITER //
CREATE TRIGGER trg_users_after_update
AFTER UPDATE ON users
FOR EACH ROW
BEGIN
    -- 只有 status 字段變更時(shí)才寫日志,避免無效日志
    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ù)據(jù)校驗(yàn) + 自動(dòng)填充

DELIMITER //
CREATE TRIGGER trg_orders_before_insert
BEFORE INSERT ON orders
FOR EACH ROW
BEGIN
    -- 校驗(yàn):訂單金額不能為負(fù)數(shù)
    IF NEW.amount <= 0 THEN
        SIGNAL SQLSTATE '45000'
            SET MESSAGE_TEXT = '訂單金額必須大于0';
    END IF;
    -- 自動(dòng)填充創(chuàng)建時(shí)間
    IF NEW.create_time IS NULL THEN
        SET NEW.create_time = NOW();
    END IF;
    -- 自動(dòng)生成訂單號
    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ā)器或存儲過程中主動(dòng)拋出錯(cuò)誤,阻止當(dāng)前操作并回滾事務(wù)。SQLSTATE '45000' 是用戶自定義錯(cuò)誤的標(biāo)準(zhǔn)狀態(tài)碼。

示例 4:AFTER DELETE —— 刪除前自動(dòng)歸檔備份

DELIMITER //
CREATE TRIGGER trg_orders_after_delete
AFTER DELETE ON orders
FOR EACH ROW
BEGIN
    -- 被刪除的數(shù)據(jù)自動(dòng)歸檔到備份表
    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)流轉(zhuǎn)

DELIMITER //
-- 訂單狀態(tài)只能單向流轉(zhuǎn):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 拋錯(cuò)阻止本次操作(整個(gè)語句回滾)              │
├──────────────────────────────────────────────────────────────────┤
│  2. 實(shí)際執(zhí)行 UPDATE 語句(數(shù)據(jù)寫入磁盤)                            │
├──────────────────────────────────────────────────────────────────┤
│  3. AFTER UPDATE 觸發(fā)器執(zhí)行                                       │
│     → 數(shù)據(jù)已確定寫入,適合做日志記錄、數(shù)據(jù)同步                      │
│     → 若此處拋錯(cuò),整個(gè)事務(wù)(含 UPDATE)一起回滾                    │
└──────────────────────────────────────────────────────────────────┘

MySQL 5.7+ 支持同一張表、同一事件有多個(gè)觸發(fā)器,按創(chuàng)建先后順序依次執(zhí)行。

8.7 觸發(fā)器的限制

限制說明
不能調(diào)用有 OUT 參數(shù)的存儲過程觸發(fā)器內(nèi)部無法獲取存儲過程的輸出參數(shù)
不能顯式控制事務(wù)不能寫 START TRANSACTION / COMMIT / ROLLBACK,但觸發(fā)器與觸發(fā)它的語句在同一事務(wù)中
不能對同表做遞歸操作users 表的觸發(fā)器中不能再 UPDATE users(會無限遞歸報(bào)錯(cuò))
不能使用動(dòng)態(tài) SQL不能使用 PREPARE / EXECUTE 執(zhí)行動(dòng)態(tài)語句
錯(cuò)誤影響BEFORE 觸發(fā)器拋錯(cuò) → 阻止原操作;AFTER 觸發(fā)器拋錯(cuò) → 整個(gè)事務(wù)回滾

8.8 觸發(fā)器的管理

-- 查看當(dāng)前數(shù)據(jù)庫所有觸發(fā)器
SHOW TRIGGERS FROM your_database;
-- 查看特定表的觸發(fā)器
SHOW TRIGGERS FROM your_database LIKE 'orders';
-- 查看觸發(fā)器定義
SHOW CREATE TRIGGER trg_users_after_insert;
-- 從系統(tǒng)表查詢(可獲取更詳細(xì)的信息)
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ā)器

問題詳細(xì)說明
行為隱蔽一條 INSERT 背后可能觸發(fā)多個(gè)觸發(fā)器,調(diào)用方完全不知情,Bug 難以追蹤
調(diào)試極難觸發(fā)器自動(dòng)執(zhí)行,日志里只顯示原始 SQL,觸發(fā)器內(nèi)部的問題很難定位
性能陷阱批量操作時(shí)每行都觸發(fā)一次(FOR EACH ROW),百萬行操作 = 百萬次觸發(fā)器調(diào)用
級聯(lián)風(fēng)險(xiǎn)觸發(fā)器 A 修改表 B → 表 B 的觸發(fā)器 B 又觸發(fā)……級聯(lián)鏈路極難維護(hù)
業(yè)務(wù)邏輯分散同一業(yè)務(wù)規(guī)則,一半在 Java Service,一半在數(shù)據(jù)庫觸發(fā)器,職責(zé)不清
測試?yán)щy單元測試時(shí)很難 Mock 觸發(fā)器行為,集成測試成本高

推薦的替代方案

  • 審計(jì)日志 → 使用 AOP(@Aspect)在 Service 層統(tǒng)一攔截記錄
  • 數(shù)據(jù)校驗(yàn) → 放在 Java Bean Validation(@NotNull、@Min 等)或 Service 層
  • 數(shù)據(jù)同步 → 使用消息隊(duì)列(MQ)做異步解耦,比觸發(fā)器更健壯

九、為什么現(xiàn)代開發(fā)中存儲過程用得越來越少?

這是面試中經(jīng)常會被問到的問題。存儲過程在 2000~2010 年代被大量使用,但如今在互聯(lián)網(wǎng)項(xiàng)目中已經(jīng)很少見了。原因如下:

9.1 微服務(wù)架構(gòu)的興起

2005年的架構(gòu):
┌─────────┐      ┌──────────────────┐
│  客戶端  │ ──→  │   單體應(yīng)用 + DB   │  ← 業(yè)務(wù)邏輯大量放在存儲過程中
└─────────┘      └──────────────────┘
2020年的架構(gòu):
┌─────────┐      ┌─────────┐  ┌─────────┐  ┌─────────┐
│  客戶端  │ ──→  │ 服務(wù)A   │  │ 服務(wù)B   │  │ 服務(wù)C   │
└─────────┘      └────┬────┘  └────┬────┘  └────┬────┘
                      │            │            │
                  ┌───┴───┐   ┌───┴───┐   ┌───┴───┐
                  │  DB-A  │  │  DB-B  │  │  DB-C  │
                  └───────┘   └───────┘   └───────┘

微服務(wù)架構(gòu)下,業(yè)務(wù)邏輯必須放在服務(wù)層(Java/Go/Python),而不是數(shù)據(jù)庫中。因?yàn)椋?/p>

  • 跨服務(wù)的事務(wù)無法用數(shù)據(jù)庫存儲過程解決
  • 每個(gè)服務(wù)獨(dú)立部署、獨(dú)立演進(jìn)

9.2 可維護(hù)性差

問題說明
調(diào)試?yán)щy不像 Java 可以打斷點(diǎn)、單步調(diào)試,存儲過程只能通過 SELECT 打印中間變量
缺乏 IDE 支持沒有代碼補(bǔ)全、沒有重構(gòu)工具、沒有靜態(tài)分析
可讀性差SQL 本身不是面向?qū)ο笳Z言,復(fù)雜邏輯寫起來冗長晦澀
測試?yán)щy不能用 JUnit/Mockito 等框架進(jìn)行單元測試

9.3 版本管理困難

  • 存儲過程存在數(shù)據(jù)庫里,不在代碼倉庫中
  • 無法用 Git 做版本控制、Code Review、分支管理
  • 多人協(xié)作時(shí)容易沖突,且沖突難以發(fā)現(xiàn)

雖然可以把存儲過程的 SQL 文件放到 Git 里,但執(zhí)行和部署仍然需要額外的工具支持,遠(yuǎn)不如 Java 代碼的 CI/CD 流程成熟。

9.4 數(shù)據(jù)庫可移植性差

-- 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ù)據(jù)庫的語法完全不同。如果項(xiàng)目有更換數(shù)據(jù)庫的可能(如從 MySQL 遷移到 PostgreSQL),所有存儲過程都要重寫。

Java + MyBatis/JPA 的方案,換數(shù)據(jù)庫只需要改 SQL 方言配置,業(yè)務(wù)代碼基本不動(dòng)。

9.5 水平擴(kuò)展受限

  • 應(yīng)用層擴(kuò)展:加幾臺服務(wù)器即可(無狀態(tài),容易擴(kuò)展)
  • 數(shù)據(jù)庫層擴(kuò)展:讀寫分離、分庫分表都非常復(fù)雜

把業(yè)務(wù)邏輯放在存儲過程中 = 把壓力集中在數(shù)據(jù)庫上。而數(shù)據(jù)庫是整個(gè)系統(tǒng)中最難水平擴(kuò)展的組件。

9.6 人才和協(xié)作問題

  • 精通存儲過程開發(fā)的 DBA 人才稀缺
  • 開發(fā)人員和 DBA 之間的協(xié)作成本高
  • 出了 Bug,開發(fā)說是存儲過程的問題,DBA 說是調(diào)用方的問題——職責(zé)不清

9.7 ORM 框架的成熟

MyBatis、JPA/Hibernate、Spring Data 等 ORM 框架已經(jīng)非常成熟:

// 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ù)據(jù)庫變得非常簡單,存儲過程的"封裝"優(yōu)勢不再明顯。

十、那什么時(shí)候還在用存儲過程?

雖然互聯(lián)網(wǎng)項(xiàng)目越來越少用,但存儲過程并沒有消亡。以下場景仍然活躍:

10.1 仍在使用的場景

場景原因
傳統(tǒng)企業(yè)系統(tǒng)(ERP/銀行/保險(xiǎn))歷史遺留,存儲過程已運(yùn)行多年,不敢輕易重構(gòu)
批量數(shù)據(jù)處理百萬/千萬級數(shù)據(jù)的批量更新,存儲過程比應(yīng)用層逐條處理快得多
數(shù)據(jù)倉庫 / ETL數(shù)據(jù)清洗、加工、聚合,天然適合在數(shù)據(jù)庫端完成
復(fù)雜報(bào)表涉及多表關(guān)聯(lián)、多級匯總的報(bào)表查詢
DBA 運(yùn)維腳本數(shù)據(jù)庫巡檢、空間清理、權(quán)限管理等運(yùn)維操作
安全性要求極高不允許應(yīng)用層直接訪問表,只能通過存儲過程操作數(shù)據(jù)

10.2 選擇建議

┌─────────────────────────────────────────────────────────────┐
│              什么時(shí)候用存儲過程?                               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ? 用:                                                    │
│     ? 大批量數(shù)據(jù)處理(ETL、定時(shí)任務(wù))                           │
│     ? 對性能要求極致的數(shù)據(jù)庫操作                               │
│     ? 需要嚴(yán)格權(quán)限控制,不允許直接操作表                        │
│     ? 數(shù)據(jù)庫平臺固定,不考慮遷移                               │
│                                                             │
│  ? 不用:                                                   │
│     ? 常規(guī) CRUD 業(yè)務(wù)邏輯                                     │
│     ? 微服務(wù)架構(gòu)項(xiàng)目                                          │
│     ? 需要頻繁迭代的業(yè)務(wù)                                      │
│     ? 需要跨數(shù)據(jù)庫平臺的項(xiàng)目                                  │
│     ? 團(tuán)隊(duì)沒有熟悉存儲過程的 DBA                              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

十一、Java 中如何調(diào)用存儲過程

雖然現(xiàn)代項(xiàng)目不推薦大量使用存儲過程,但作為 Java 開發(fā)者,你仍然需要知道怎么調(diào)用它。

11.1 JDBC 原生調(diào)用

// 調(diào)用帶輸入輸出參數(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 調(diào)用

<!-- 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 調(diào)用

@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 調(diào)用

@Entity
@NamedStoredProcedureQuery(
    name = "User.getUserCount",
    procedureName = "GetUserCount",
    parameters = {
        @StoredProcedureParameter(mode = ParameterMode.OUT, name = "p_count", type = Integer.class)
    }
)
public class User { ... }
// 調(diào)用
StoredProcedureQuery query = entityManager.createNamedStoredProcedureQuery("User.getUserCount");
query.execute();
Integer count = (Integer) query.getOutputParameterValue("p_count");

十二、存儲過程的最佳實(shí)踐(如果你必須使用)

如果你的項(xiàng)目確實(shí)需要用到存儲過程,遵循以下規(guī)范可以減少后續(xù)的維護(hù)痛苦:

12.1 命名規(guī)范

-- 推薦:動(dòng)詞 + 名詞,前綴表明類型
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è)務(wù)邏輯...
    -- 1. 每個(gè)步驟加注釋
    -- 2. 適當(dāng)?shù)腻e(cuò)誤處理
    -- 3. 避免嵌套過深(建議不超過3層)
END //
DELIMITER ;

12.3 其他建議

  • 保持存儲過程短小:單個(gè)存儲過程不超過 200 行,太長就拆分
  • 避免在存儲過程中寫業(yè)務(wù)邏輯:只做數(shù)據(jù)操作,不做業(yè)務(wù)判斷
  • 一定要加異常處理:避免事務(wù)不提交也不回滾
  • 把 DDL 文件納入版本管理:將所有存儲過程的 CREATE 語句放入項(xiàng)目的 sql/ 目錄
  • 編寫變更日志:每次修改存儲過程,在注釋中記錄修改日期和原因

十三、面試高頻問題

存儲過程相關(guān)

Q1:什么是存儲過程?它的優(yōu)缺點(diǎn)是什么?

存儲過程是存儲在數(shù)據(jù)庫中的預(yù)編譯 SQL 語句集合,通過 CALL 語句調(diào)用。優(yōu)點(diǎn)是執(zhí)行效率高、減少網(wǎng)絡(luò)傳輸、增強(qiáng)安全性;缺點(diǎn)是可移植性差、調(diào)試?yán)щy、不利于版本管理。

Q2:IN、OUT、INOUT 參數(shù)的區(qū)別?

IN 是輸入?yún)?shù)(默認(rèn)),調(diào)用方傳入值,過程內(nèi)只讀;OUT 是輸出參數(shù),過程內(nèi)賦值后調(diào)用方獲??;INOUT 是雙向參數(shù),調(diào)用方傳入、過程內(nèi)修改、調(diào)用方再獲取結(jié)果。

Q3:為什么現(xiàn)在不推薦使用存儲過程?

微服務(wù)架構(gòu)下業(yè)務(wù)邏輯必須在應(yīng)用層;存儲過程難以調(diào)試、測試和版本管理;不同數(shù)據(jù)庫語法不兼容導(dǎo)致遷移成本高;把邏輯放在數(shù)據(jù)庫中不利于水平擴(kuò)展;團(tuán)隊(duì)協(xié)作成本高。

Q4:如何在 Java 中調(diào)用存儲過程?

可以用 JDBC 的 CallableStatement、MyBatis 的 statementType="CALLABLE"、Spring 的 SimpleJdbcCall、或 JPA 的 @NamedStoredProcedureQuery。

游標(biāo)相關(guān)

Q5:什么是游標(biāo)?使用游標(biāo)的四個(gè)步驟是什么?

游標(biāo)是對查詢結(jié)果集進(jìn)行逐行處理的數(shù)據(jù)庫機(jī)制。四個(gè)步驟:① DECLARE 聲明游標(biāo)(綁定 SELECT 語句)② OPEN 打開游標(biāo)(執(zhí)行查詢,加載結(jié)果集)③ FETCH 逐行讀?。▽?dāng)前行存入變量,指針后移)④ CLOSE 關(guān)閉游標(biāo)(釋放內(nèi)存)。

Q6:MySQL 游標(biāo)有哪些特性和限制?

MySQL 游標(biāo)是只讀的,不能通過游標(biāo)直接修改行;是單向滾動(dòng)的,只能向前逐行移動(dòng);只能在存儲過程/函數(shù)/觸發(fā)器內(nèi)使用;打開游標(biāo)會將完整結(jié)果集加載到內(nèi)存,結(jié)果集過大時(shí)存在 OOM 風(fēng)險(xiǎn)。

Q7:游標(biāo)存在性能問題,什么時(shí)候應(yīng)該用游標(biāo),什么時(shí)候不用?

能用集合 SQL(如 UPDATE ... WHERE)解決的絕對不用游標(biāo)。適合用游標(biāo)的場景:每行邏輯復(fù)雜且確實(shí)無法用單條 SQL 表達(dá)、數(shù)據(jù)量較?。◣浊У綆兹f行)、ETL 中需要逐行加工轉(zhuǎn)換的場景。

Q8:MySQL 中 DECLARE 語句的順序有什么要求?

MySQL 規(guī)定 DECLARE 語句必須按固定順序出現(xiàn),否則編譯報(bào)錯(cuò):① 先聲明局部變量(DECLARE var TYPE)② 再聲明游標(biāo)(DECLARE cur CURSOR FOR)③ 最后聲明異常處理器(DECLARE HANDLER)。

存儲函數(shù)相關(guān)

Q9:存儲過程和存儲函數(shù)的區(qū)別?

對比項(xiàng)存儲過程存儲函數(shù)
調(diào)用方式CALL sp_name()SELECT fn_name()
返回值OUT 參數(shù),可有多個(gè)必須有且只有一個(gè) RETURN
能否嵌入 SQL不能可以
事務(wù)控制可以不能顯式控制
側(cè)重點(diǎn)執(zhí)行操作計(jì)算返回值

Q10:創(chuàng)建存儲函數(shù)時(shí) DETERMINISTIC 關(guān)鍵字是什么意思?

DETERMINISTIC 表示相同輸入總是產(chǎn)生相同輸出(純函數(shù),如字符串處理、數(shù)學(xué)計(jì)算);NOT DETERMINISTIC(默認(rèn))表示相同輸入可能返回不同結(jié)果(如使用了 NOW()RAND())。在 binlog 開啟時(shí),不確定性函數(shù)可能導(dǎo)致主從不一致,MySQL 5.x 需要設(shè)置 log_bin_trust_function_creators = 1

觸發(fā)器相關(guān)

Q11:什么是觸發(fā)器?MySQL 共有幾種觸發(fā)器?

觸發(fā)器是與表關(guān)聯(lián)的特殊存儲程序,當(dāng)表發(fā)生 INSERT、UPDATE、DELETE 時(shí)自動(dòng)執(zhí)行。按時(shí)機(jī)分 BEFOREAFTER,按操作分 INSERT/UPDATE/DELETE,兩兩組合共 6 種觸發(fā)時(shí)機(jī)。

Q12:觸發(fā)器中 NEW 和 OLD 關(guān)鍵字的區(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 難以追蹤;批量操作時(shí)每行都觸發(fā)一次,性能差;級聯(lián)觸發(fā)鏈路極難維護(hù)和調(diào)試;單元測試時(shí)很難 Mock 觸發(fā)器行為;業(yè)務(wù)邏輯分散在數(shù)據(jù)庫和應(yīng)用層,職責(zé)不清。推薦用 AOP 替代審計(jì)日志,用 Bean Validation 替代數(shù)據(jù)校驗(yàn),用 MQ 替代數(shù)據(jù)同步。

Q14:BEFORE 觸發(fā)器和 AFTER 觸發(fā)器分別適合什么場景?

BEFORE:適合數(shù)據(jù)校驗(yàn)(拋錯(cuò)阻止非法操作)、自動(dòng)填充字段(如創(chuàng)建時(shí)間)、防止非法狀態(tài)流轉(zhuǎn)。AFTER:適合寫操作日志、更新統(tǒng)計(jì)數(shù)據(jù)、實(shí)現(xiàn)刪除歸檔備份。

十四、總結(jié)

可編程對象本質(zhì)調(diào)用方式當(dāng)前定位
存儲過程執(zhí)行一組操作的 SQL 代碼塊CALL sp_name()批量處理、ETL、傳統(tǒng)企業(yè)系統(tǒng)
游標(biāo)逐行遍歷結(jié)果集的指針配合存儲過程/函數(shù)使用每行邏輯差異化處理,優(yōu)先用集合 SQL
存儲函數(shù)計(jì)算并返回單個(gè)值的代碼塊SELECT fn_name()可復(fù)用的計(jì)算邏輯,嵌入 SQL 中直接用
觸發(fā)器數(shù)據(jù)變更時(shí)自動(dòng)執(zhí)行的事件鉤子自動(dòng)觸發(fā),不能手動(dòng)調(diào)用嚴(yán)格限制使用,推薦用 AOP/MQ 替代

共同的歷史定位:這四者都是數(shù)據(jù)庫時(shí)代將業(yè)務(wù)邏輯下沉到數(shù)據(jù)庫的產(chǎn)物。在現(xiàn)代微服務(wù)架構(gòu)中,它們的使用均大幅減少,業(yè)務(wù)邏輯應(yīng)該回歸應(yīng)用層,讓數(shù)據(jù)庫回歸它的本職工作——存儲和檢索數(shù)據(jù)。

場景建議方案
審計(jì)日志AOP + 日志表,而非觸發(fā)器
數(shù)據(jù)校驗(yàn)Bean Validation + Service 層,而非 BEFORE 觸發(fā)器
數(shù)據(jù)同步MQ 異步解耦,而非觸發(fā)器級聯(lián)操作
批量處理存儲過程 / 存儲函數(shù) + 游標(biāo)
常規(guī) CRUDMyBatis / JPA + Service 層 Java 代碼

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

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

相關(guān)文章

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

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

    這篇文章主要介紹了MySql中使用INSERT INTO語句更新多條數(shù)據(jù)的例子,MySQL的特有語法,需要的朋友可以參考下
    2014-06-06
  • MySQL 5.7.14 net start mysql 服務(wù)無法啟動(dòng)-“NET HELPMSG 3534” 的奇怪問題

    MySQL 5.7.14 net start mysql 服務(wù)無法啟動(dòng)-“NET HELPMSG 3534” 的奇怪問題

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

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

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

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

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

    CentOS 7.0如何啟動(dòng)多個(gè)MySQL實(shí)例教程(mysql-5.7.21)

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

    一個(gè)mysql死鎖場景實(shí)例分析

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

    MySQL的ALTER TABLE命令的使用解讀

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

    mysql sql語句總結(jié)

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

    MySQL一文搞懂行級鎖經(jīng)典版

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

    MySQL修改默認(rèn)端口失敗的常見原因及解決方案

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

最新評論

汪清县| 阜宁县| 鹿邑县| 六安市| 霍邱县| 视频| 西峡县| 黎城县| 顺平县| 苍梧县| 武清区| 恭城| 大新县| 登封市| 工布江达县| 鹿泉市| 会东县| 桦甸市| 奎屯市| 青海省| 长兴县| 镇宁| 洛浦县| 泸州市| 连云港市| 开封市| 巫溪县| 齐齐哈尔市| 永泰县| 仲巴县| 兴安盟| 陇川县| 子长县| 甘南县| 垫江县| 峡江县| 上高县| 宁津县| 北海市| 黑龙江省| 潜江市|