資料庫開發中,函式、預存程序、觸發器和事件排程器是實現複雜業務邏輯與自動化管理的核心工具。有效運用這些資料庫物件可以大幅提升系統效能、確保資料完整性,並簡化應用程式開發的複雜度。函式能夠封裝特定的運算邏輯並回傳結果,預存程序則可將複雜的SQL操作序列化為可重複使用的模組。觸發器提供了資料變更的自動化回應機制,確保資料一致性與業務規則的執行。事件排程器則賦予資料庫自主執行定期任務的能力,實現從資料清理到報表生成的各種自動化需求。
理解這些資料庫物件的差異與適用場景是有效運用的關鍵。函式著重於計算與回傳值,適合用於SELECT陳述式中的運算。預存程序則更靈活,可以執行複雜的流程控制、處理多個結果集,並修改資料庫狀態。觸發器作為事件驅動的機制,在資料變更時自動執行,無需應用程式明確呼叫。事件排程器則按照時間排程執行任務,類似作業系統的定時任務,但完全在資料庫層面運作。本文將深入探討這些資料庫物件的設計原理、實作技巧與最佳實務,涵蓋語法細節、錯誤處理、效能考量,以及實務應用場景,協助讀者建構高效且可維護的資料庫系統。
資料庫函式的設計與實作
資料庫函式是一種可以接受參數、執行特定運算並回傳單一值的資料庫物件。與預存程序相比,函式的主要特點在於它必須回傳一個值,且可以直接在SELECT陳述式或其他SQL表達式中使用。這使得函式特別適合封裝常用的計算邏輯,例如日期轉換、字串處理、數學運算等。函式的設計應遵循單一職責原則,每個函式只負責一個明確定義的計算任務,這樣不僅提高了程式碼的可讀性,也便於測試與維護。
建立函式時需要明確指定函式名稱、輸入參數與回傳型態。參數可以是各種SQL資料型態,包括整數、字串、日期等。回傳型態同樣可以是任何有效的SQL資料型態。函式主體包含實際的運算邏輯,通常以RETURN陳述式結束,回傳計算結果。在MySQL中,函式的定義需要使用DELIMITER指令暫時改變陳述式結束符號,因為函式主體內部會包含分號,若不改變結束符號會導致SQL解析錯誤。
-- 暫時改變陳述式結束符號
-- 因為函式定義內部會使用分號,需要使用不同的結束符號
DELIMITER //
-- 建立計算年資的函式
-- 接受員工入職日期作為參數,回傳以年為單位的年資
CREATE FUNCTION calculate_years_of_service(
-- 輸入參數: 員工入職日期
hire_date DATE
)
-- 回傳型態: 整數,代表完整年數
RETURNS INT
-- 標記函式為確定性函式
-- 相同輸入必定產生相同輸出
DETERMINISTIC
-- 標記函式不修改SQL資料
-- 這是一個純粹的讀取操作
READS SQL DATA
BEGIN
-- 宣告區域變數儲存計算結果
-- 使用DECLARE陳述式定義變數型態
DECLARE years INT;
-- 使用TIMESTAMPDIFF函式計算年數
-- YEAR表示以年為單位計算差異
-- CURDATE()取得當前日期
SET years = TIMESTAMPDIFF(YEAR, hire_date, CURDATE());
-- 回傳計算出的年資
RETURN years;
END//
-- 恢復預設的陳述式結束符號
DELIMITER ;
-- 使用函式範例: 查詢所有員工的年資
-- 函式可以直接在SELECT陳述式中使用
SELECT
employee_id AS '員工編號',
CONCAT(last_name, first_name) AS '姓名',
hire_date AS '入職日期',
-- 呼叫自訂函式計算年資
calculate_years_of_service(hire_date) AS '年資(年)'
FROM employee
-- 篩選年資超過五年的資深員工
WHERE calculate_years_of_service(hire_date) >= 5
ORDER BY hire_date ASC;
這個函式範例展示了函式定義的完整結構。DETERMINISTIC關鍵字標記函式為確定性函式,意味著相同的輸入必定產生相同的輸出,這對查詢最佳化器很重要。READS SQL DATA標記函式會讀取資料庫資料但不會修改,這也是最佳化的重要資訊。函式主體內使用DECLARE宣告區域變數,透過SET指派計算結果,最後以RETURN回傳值。在實際使用時,函式可以像內建函式一樣在SELECT陳述式、WHERE子句等任何需要表達式的地方使用。
函式設計需要注意幾個重要原則。首先是避免在函式內執行資料修改操作,雖然技術上可行,但這會使函式的行為難以預測,特別是在複雜查詢中使用時。其次是注意效能影響,如果函式內包含複雜的查詢或運算,在處理大量資料時可能成為效能瓶頸。第三是適當使用DETERMINISTIC與SQL DATA ACCESS屬性,這些宣告能協助資料庫最佳化器產生更高效的執行計畫。
@startuml
!define PLANTUML_FORMAT svg
!theme _none_
skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 14
skinparam minClassWidth 100
title 資料庫函式執行流程
|SQL查詢執行|
start
:應用程式提交SQL查詢;
note right
SELECT陳述式中
包含自訂函式呼叫
end note
:SQL解析器解析查詢;
:識別函式呼叫;
|函式執行|
:載入函式定義;
note right
從資料字典中
讀取函式元資料
end note
:傳遞輸入參數;
:配置區域變數記憶體空間;
:執行函式主體邏輯;
partition "函式內部處理" {
:執行SQL陳述式;
:進行數學運算;
:字串處理操作;
:日期時間計算;
}
:計算回傳值;
:釋放區域變數;
|結果回傳|
:函式回傳計算結果;
:結果代入原始查詢;
:繼續執行查詢其他部分;
:產生最終結果集;
:回傳結果至應用程式;
stop
@enduml
這個流程圖展示了資料庫函式從呼叫到執行完成的完整過程。當SQL查詢中包含函式呼叫時,資料庫系統首先解析查詢識別函式。接著載入函式定義,配置必要的記憶體空間給區域變數,然後執行函式主體的邏輯。函式執行完成後回傳結果,這個結果會被代入原始查詢繼續處理。理解這個流程有助於設計高效的函式,例如避免在函式內執行昂貴的操作,或是在大量資料處理時考慮使用其他方式替代函式呼叫。
預存程序的設計與實作
預存程序是一段預先編譯並儲存在資料庫中的SQL程式碼,可以接受輸入參數、執行複雜的業務邏輯,並透過輸出參數或結果集回傳資料。與函式相比,預存程序更加靈活,它可以包含多個SQL陳述式、流程控制結構、錯誤處理機制,並且可以修改資料庫狀態。預存程序的主要優勢在於將業務邏輯封裝在資料庫層,減少應用程式與資料庫之間的網路往返,同時提供了程式碼重用與集中管理的便利性。
預存程序的設計應該考慮模組化原則。複雜的業務流程可以分解為多個預存程序,每個程序負責一個明確的功能模組。程序之間可以相互呼叫,形成層次化的程式架構。這種設計不僅提高了程式碼的可維護性,也使得測試與除錯變得更加容易。在定義預存程序時,需要仔細設計參數介面,包括輸入參數、輸出參數與輸入輸出參數的合理使用。
-- 暫時改變陳述式結束符號
DELIMITER //
-- 建立員工薪資調整預存程序
-- 根據績效評等調整員工薪資並記錄調整歷程
CREATE PROCEDURE adjust_employee_salary(
-- 輸入參數: 員工編號
IN emp_id INT,
-- 輸入參數: 績效評等 (1-5分)
IN performance_rating INT,
-- 輸出參數: 調整後的薪資
OUT new_salary DECIMAL(15, 2),
-- 輸出參數: 調整百分比
OUT adjustment_percentage DECIMAL(5, 2)
)
BEGIN
-- 宣告區域變數
-- 儲存員工當前薪資
DECLARE current_salary DECIMAL(15, 2);
-- 儲存薪資調整金額
DECLARE adjustment_amount DECIMAL(15, 2);
-- 錯誤處理旗標
DECLARE error_flag INT DEFAULT 0;
-- 宣告錯誤處理器
-- 當發生SQL異常時設定錯誤旗標
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
BEGIN
SET error_flag = 1;
END;
-- 開始交易
-- 確保所有操作具有原子性
START TRANSACTION;
-- 查詢員工當前薪資
-- 使用SELECT INTO將查詢結果指派給變數
SELECT salary INTO current_salary
FROM employee
WHERE employee_id = emp_id
-- 使用FOR UPDATE鎖定該筆記錄
-- 防止並行修改導致的資料不一致
FOR UPDATE;
-- 根據績效評等計算調整百分比
-- 使用CASE陳述式實現多分支邏輯
SET adjustment_percentage = CASE
WHEN performance_rating = 5 THEN 10.00 -- 傑出: 調薪10%
WHEN performance_rating = 4 THEN 7.00 -- 優良: 調薪7%
WHEN performance_rating = 3 THEN 5.00 -- 符合期望: 調薪5%
WHEN performance_rating = 2 THEN 2.00 -- 待改善: 調薪2%
WHEN performance_rating = 1 THEN 0.00 -- 不符期望: 不調薪
ELSE 0.00 -- 無效評等: 不調薪
END;
-- 計算調整金額
-- 當前薪資乘以調整百分比
SET adjustment_amount = current_salary * (adjustment_percentage / 100);
-- 計算調整後的新薪資
SET new_salary = current_salary + adjustment_amount;
-- 更新員工薪資
-- 只有在沒有發生錯誤時才執行
IF error_flag = 0 THEN
UPDATE employee
SET salary = new_salary,
last_salary_review_date = CURDATE()
WHERE employee_id = emp_id;
-- 記錄薪資調整歷程
-- 寫入稽核表以供後續追蹤
INSERT INTO salary_adjustment_history (
employee_id,
adjustment_date,
old_salary,
new_salary,
adjustment_percentage,
performance_rating
) VALUES (
emp_id,
NOW(),
current_salary,
new_salary,
adjustment_percentage,
performance_rating
);
-- 提交交易
-- 所有變更永久生效
COMMIT;
ELSE
-- 發生錯誤時回復交易
-- 所有變更都不會生效
ROLLBACK;
-- 設定輸出參數為NULL表示失敗
SET new_salary = NULL;
SET adjustment_percentage = NULL;
END IF;
END//
DELIMITER ;
-- 呼叫預存程序範例
-- 使用使用者變數接收輸出參數
CALL adjust_employee_salary(
1001, -- 員工編號
4, -- 績效評等
@result_salary, -- 接收新薪資的變數
@result_pct -- 接收調整百分比的變數
);
-- 查詢輸出參數的值
-- 檢視程序執行結果
SELECT
@result_salary AS '調整後薪資',
@result_pct AS '調整百分比';
這個預存程序範例展示了多個重要概念。首先是參數的使用,IN參數用於接收輸入,OUT參數用於回傳結果。程序內使用DECLARE宣告區域變數,這些變數的作用域僅限於程序內部。錯誤處理透過DECLARE HANDLER實現,當發生SQL異常時自動執行指定的處理邏輯。交易控制確保了操作的原子性,要麼所有變更都成功,要麼全部回復。CASE陳述式提供了多分支的條件判斷,SELECT INTO用於將查詢結果指派給變數。
預存程序的進階應用包括使用遊標處理結果集。遊標允許程序逐列處理查詢結果,這在需要對每筆資料執行複雜邏輯時特別有用。遊標的使用涉及宣告、開啟、提取資料與關閉四個基本步驟。雖然遊標提供了強大的處理能力,但也會帶來效能開銷,因此應該謹慎使用,優先考慮基於集合的操作方式。
DELIMITER //
-- 建立使用遊標的預存程序
-- 計算各部門的薪資統計並產生報表
CREATE PROCEDURE generate_department_salary_report()
BEGIN
-- 宣告變數儲存遊標提取的資料
DECLARE dept_id INT;
DECLARE dept_name VARCHAR(100);
DECLARE emp_count INT;
DECLARE avg_salary DECIMAL(15, 2);
DECLARE total_salary DECIMAL(15, 2);
-- 宣告結束旗標
-- 當遊標沒有更多資料時設為TRUE
DECLARE done INT DEFAULT FALSE;
-- 宣告遊標
-- 定義要處理的資料集
DECLARE dept_cursor CURSOR FOR
SELECT
d.department_id,
d.department_name,
COUNT(e.employee_id) AS employee_count,
AVG(e.salary) AS average_salary,
SUM(e.salary) AS total_salary
FROM department d
LEFT JOIN employee e ON d.department_id = e.department_id
GROUP BY d.department_id, d.department_name
ORDER BY d.department_name;
-- 宣告NOT FOUND處理器
-- 當遊標沒有更多資料時觸發
DECLARE CONTINUE HANDLER FOR NOT FOUND
SET done = TRUE;
-- 清空報表表格
-- 移除舊的報表資料
TRUNCATE TABLE department_salary_report;
-- 開啟遊標
-- 執行SELECT查詢並準備資料集
OPEN dept_cursor;
-- 開始遊標迴圈
read_loop: LOOP
-- 提取下一列資料
-- 將欄位值指派給對應變數
FETCH dept_cursor INTO
dept_id,
dept_name,
emp_count,
avg_salary,
total_salary;
-- 檢查是否已處理完所有資料
IF done THEN
-- 離開迴圈
LEAVE read_loop;
END IF;
-- 處理當前資料列
-- 計算額外的統計資訊
-- 將結果寫入報表表格
INSERT INTO department_salary_report (
department_id,
department_name,
employee_count,
average_salary,
total_salary,
salary_range,
report_date
) VALUES (
dept_id,
dept_name,
emp_count,
avg_salary,
total_salary,
-- 計算薪資範圍等級
CASE
WHEN avg_salary >= 100000 THEN '高薪資部門'
WHEN avg_salary >= 60000 THEN '中薪資部門'
ELSE '一般薪資部門'
END,
CURDATE()
);
END LOOP;
-- 關閉遊標
-- 釋放資源
CLOSE dept_cursor;
-- 回傳處理結果
SELECT '報表生成完成' AS '狀態訊息';
END//
DELIMITER ;
這個範例展示了遊標的完整使用流程。遊標宣告時指定要處理的SELECT查詢,OPEN開啟遊標執行查詢,FETCH在迴圈中逐列提取資料,最後CLOSE關閉遊標釋放資源。NOT FOUND處理器在遊標沒有更多資料時設定旗標,配合LOOP迴圈實現對所有資料的處理。遊標適合用於需要逐列執行複雜邏輯的場景,但在大量資料處理時應該評估效能影響。
@startuml
!define PLANTUML_FORMAT svg
!theme _none_
skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 14
skinparam minClassWidth 100
title 預存程序遊標處理流程
|程序執行|
start
:程序被呼叫;
:宣告變數與遊標;
|遊標操作|
:開啟遊標;
note right
執行SELECT查詢
建立結果集
準備資料提取
end note
:初始化迴圈;
repeat
:提取下一列資料;
note right
FETCH INTO
將欄位值指派給變數
end note
if (還有資料?) then (是)
:處理當前資料列;
partition "資料處理邏輯" {
:執行業務運算;
:資料驗證檢查;
:寫入處理結果;
}
else (否)
:設定結束旗標;
note right
NOT FOUND處理器
自動觸發
end note
endif
repeat while (未到達結束) is (繼續)
:關閉遊標;
note right
釋放遊標資源
清理記憶體
end note
:回傳處理結果;
stop
@enduml
觸發器的設計與應用
觸發器是一種特殊的預存程序,當資料表發生特定的資料變更事件時自動執行。觸發器無需應用程式明確呼叫,而是由資料庫系統在偵測到觸發條件時自動啟動。這種機制特別適合實作資料完整性約束、自動化稽核追蹤、衍生資料的同步更新等需求。觸發器可以設定在資料修改之前或之後執行,分別稱為BEFORE觸發器與AFTER觸發器。BEFORE觸發器適合用於資料驗證與預處理,AFTER觸發器則適合用於稽核記錄與後續處理。
觸發器的設計需要特別注意幾個原則。首先是保持觸發器邏輯簡潔,避免在觸發器中執行複雜的運算或查詢,因為觸發器會在每次資料變更時執行,複雜的邏輯會嚴重影響效能。其次是避免觸發器之間的連鎖反應,一個觸發器修改資料可能觸發另一個觸發器,形成難以預測的執行鏈。第三是謹慎使用BEFORE觸發器修改NEW值,雖然技術上可行,但可能使資料變更行為變得不透明。
DELIMITER //
-- 建立庫存自動更新觸發器
-- 當訂單明細新增時自動扣減庫存
CREATE TRIGGER tr_order_detail_ai
AFTER INSERT ON order_detail
FOR EACH ROW
BEGIN
-- 宣告區域變數
-- 儲存當前庫存數量
DECLARE current_stock INT;
-- 查詢產品當前庫存
SELECT stock_quantity INTO current_stock
FROM product
WHERE product_id = NEW.product_id;
-- 檢查庫存是否充足
-- 這個檢查在AFTER觸發器中主要用於警示
IF current_stock < NEW.quantity THEN
-- 記錄庫存不足警告
INSERT INTO inventory_alert (
product_id,
alert_type,
alert_message,
alert_datetime
) VALUES (
NEW.product_id,
'INSUFFICIENT_STOCK',
CONCAT('訂單 ', NEW.order_id, ' 的商品庫存不足'),
NOW()
);
END IF;
-- 扣減庫存數量
-- 即使庫存不足也執行扣減
-- 允許負庫存以便後續處理
UPDATE product
SET stock_quantity = stock_quantity - NEW.quantity,
last_updated = NOW()
WHERE product_id = NEW.product_id;
-- 記錄庫存異動
INSERT INTO inventory_transaction (
product_id,
transaction_type,
quantity_change,
reference_type,
reference_id,
transaction_datetime
) VALUES (
NEW.product_id,
'SALES',
-NEW.quantity, -- 負數表示庫存減少
'ORDER',
NEW.order_id,
NOW()
);
END//
DELIMITER ;
-- 建立訂單取消觸發器
-- 當訂單明細刪除時自動回復庫存
CREATE TRIGGER tr_order_detail_ad
AFTER DELETE ON order_detail
FOR EACH ROW
BEGIN
-- 回復庫存數量
-- 將之前扣減的數量加回去
UPDATE product
SET stock_quantity = stock_quantity + OLD.quantity,
last_updated = NOW()
WHERE product_id = OLD.product_id;
-- 記錄庫存回復
INSERT INTO inventory_transaction (
product_id,
transaction_type,
quantity_change,
reference_type,
reference_id,
transaction_datetime
) VALUES (
OLD.product_id,
'RETURN',
OLD.quantity, -- 正數表示庫存增加
'ORDER_CANCEL',
OLD.order_id,
NOW()
);
END//
DELIMITER ;
這些觸發器範例展示了INSERT與DELETE事件的自動化處理。在NEW與OLD兩個特殊記錄中,NEW代表插入或更新後的新值,OLD代表刪除或更新前的舊值。觸發器可以存取這些值來執行相應的邏輯。在實務上觸發器常用於維護衍生資料、執行複雜的業務規則,以及建立完整的稽核追蹤。
觸發器的另一個重要應用是實作複雜的資料驗證。雖然CHECK約束可以實作簡單的驗證,但對於需要查詢其他表格或執行複雜運算的驗證,BEFORE觸發器是更好的選擇。透過在資料實際寫入前檢查並在必要時拋出錯誤,可以確保資料庫中永遠只包含有效的資料。
DELIMITER //
-- 建立員工薪資驗證觸發器
-- 確保薪資調整符合業務規則
CREATE TRIGGER tr_employee_salary_bu
BEFORE UPDATE ON employee
FOR EACH ROW
BEGIN
-- 宣告區域變數
DECLARE salary_increase DECIMAL(15, 2);
DECLARE increase_percentage DECIMAL(5, 2);
DECLARE max_allowed_increase DECIMAL(5, 2);
DECLARE min_salary DECIMAL(15, 2);
DECLARE max_salary DECIMAL(15, 2);
-- 只有在薪資欄位被修改時才執行驗證
IF NEW.salary != OLD.salary THEN
-- 計算薪資增加金額與百分比
SET salary_increase = NEW.salary - OLD.salary;
SET increase_percentage = (salary_increase / OLD.salary) * 100;
-- 查詢職位的薪資範圍
SELECT
min_salary_range,
max_salary_range
INTO
min_salary,
max_salary
FROM job_position
WHERE position_id = NEW.position_id;
-- 驗證1: 新薪資不可低於職位最低薪資
IF NEW.salary < min_salary THEN
-- 使用SIGNAL拋出錯誤
-- 阻止資料變更
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '新薪資低於職位最低薪資標準';
END IF;
-- 驗證2: 新薪資不可超過職位最高薪資
IF NEW.salary > max_salary THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '新薪資超過職位最高薪資標準';
END IF;
-- 驗證3: 單次調薪幅度不可超過20%
-- 避免異常的薪資調整
IF ABS(increase_percentage) > 20 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = CONCAT(
'薪資調整幅度 ',
ROUND(increase_percentage, 2),
'% 超過允許範圍 (±20%)'
);
END IF;
-- 驗證4: 降薪需要有特殊原因
-- 防止意外的薪資降低
IF salary_increase < 0 AND NEW.salary_adjustment_reason IS NULL THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '薪資調降必須提供調整原因';
END IF;
END IF;
END//
DELIMITER ;
這個BEFORE觸發器展示了複雜的資料驗證邏輯。透過查詢相關表格取得驗證所需的參考資料,然後執行多項檢查。當發現違反業務規則的情況時,使用SIGNAL陳述式拋出錯誤,這會導致觸發觸發器的UPDATE陳述式失敗並回復。這種機制確保了資料庫層面的資料完整性,即使應用程式存在漏洞也無法寫入無效資料。
@startuml
!define PLANTUML_FORMAT svg
!theme _none_
skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 14
skinparam minClassWidth 100
title 觸發器執行機制
|資料變更操作|
start
:應用程式提交DML陳述式;
note right
INSERT / UPDATE / DELETE
資料修改操作
end note
:資料庫系統解析陳述式;
|BEFORE觸發器階段|
if (存在BEFORE觸發器?) then (是)
:載入BEFORE觸發器定義;
:執行BEFORE觸發器邏輯;
partition "BEFORE觸發器處理" {
:存取OLD/NEW值;
:執行資料驗證;
:修改NEW值(可選);
if (驗證通過?) then (是)
:繼續執行;
else (否)
#pink:SIGNAL拋出錯誤;
:交易回復;
stop
endif
}
endif
|實際資料變更|
:執行實際的資料修改;
note right
寫入資料到表格
更新索引
檢查約束條件
end note
if (資料修改成功?) then (是)
|AFTER觸發器階段|
if (存在AFTER觸發器?) then (是)
:載入AFTER觸發器定義;
:執行AFTER觸發器邏輯;
partition "AFTER觸發器處理" {
:存取OLD/NEW值;
:更新衍生資料;
:寫入稽核記錄;
:發送通知訊息;
}
endif
:變更成功完成;
stop
else (否)
#pink:變更失敗;
:交易回復;
stop
endif
@enduml
事件排程器的設計與應用
事件排程器是MySQL提供的定時任務執行機制,允許資料庫在指定的時間或按照特定的時間間隔自動執行SQL陳述式或預存程序。這個功能類似作業系統的cron或Windows任務排程器,但完全運作在資料庫層面,不需要外部排程工具。事件排程器適合用於定期的資料維護任務、報表生成、資料彙總、過期資料清理等自動化需求。使用事件排程器可以簡化系統架構,減少對外部排程工具的依賴。
事件排程器的使用需要先確認該功能已啟用。在MySQL中,事件排程器由event_scheduler系統變數控制。事件的定義包括執行時間表、要執行的SQL陳述式或程序,以及其他設定如是否在完成後保留事件定義。時間表可以是一次性的在特定時間執行,或是週期性的按照固定間隔重複執行。
-- 檢查事件排程器狀態
-- 確認事件排程器是否已啟用
SHOW VARIABLES LIKE 'event_scheduler';
-- 啟用事件排程器
-- 需要SUPER權限
SET GLOBAL event_scheduler = ON;
-- 建立每日資料備份事件
-- 每天凌晨2點執行完整資料備份
DELIMITER //
CREATE EVENT evt_daily_backup
-- 設定執行排程
-- EVERY 1 DAY表示每天執行一次
ON SCHEDULE EVERY 1 DAY
-- 設定開始時間為今天的凌晨2點
STARTS (TIMESTAMP(CURRENT_DATE) + INTERVAL 2 HOUR)
-- 加入註解說明事件用途
COMMENT '每日凌晨2點執行資料備份'
DO
BEGIN
-- 宣告區域變數
DECLARE backup_status VARCHAR(50);
DECLARE backup_file VARCHAR(255);
-- 生成備份檔案名稱
-- 包含日期時間戳記以便識別
SET backup_file = CONCAT(
'backup_',
DATE_FORMAT(NOW(), '%Y%m%d_%H%i%s'),
'.sql'
);
-- 呼叫備份預存程序
-- 實際的備份邏輯封裝在程序中
CALL sp_create_database_backup(backup_file, backup_status);
-- 記錄備份執行狀態
INSERT INTO backup_history (
backup_datetime,
backup_file,
backup_status,
backup_type
) VALUES (
NOW(),
backup_file,
backup_status,
'DAILY'
);
-- 清理超過30天的舊備份記錄
DELETE FROM backup_history
WHERE backup_datetime < DATE_SUB(NOW(), INTERVAL 30 DAY);
END//
DELIMITER ;
-- 建立過期資料清理事件
-- 每週日凌晨3點清理過期的日誌資料
CREATE EVENT evt_cleanup_old_logs
ON SCHEDULE EVERY 1 WEEK
-- 指定在週日執行
STARTS (TIMESTAMP(CURRENT_DATE) + INTERVAL
(7 - WEEKDAY(CURRENT_DATE)) DAY + INTERVAL 3 HOUR)
COMMENT '每週日凌晨3點清理過期日誌'
DO
BEGIN
-- 刪除超過90天的系統日誌
DELETE FROM system_log
WHERE log_datetime < DATE_SUB(NOW(), INTERVAL 90 DAY);
-- 刪除超過180天的稽核記錄
DELETE FROM audit_log
WHERE audit_datetime < DATE_SUB(NOW(), INTERVAL 180 DAY);
-- 刪除超過365天的錯誤日誌
DELETE FROM error_log
WHERE error_datetime < DATE_SUB(NOW(), INTERVAL 365 DAY);
-- 記錄清理執行資訊
INSERT INTO maintenance_log (
maintenance_type,
maintenance_datetime,
records_affected
) VALUES (
'LOG_CLEANUP',
NOW(),
ROW_COUNT()
);
END;
-- 建立一次性事件
-- 在特定時間執行資料遷移任務
CREATE EVENT evt_data_migration
-- AT指定精確的執行時間
ON SCHEDULE AT '2025-12-31 23:00:00'
-- ON COMPLETION PRESERVE保留事件定義
-- 即使執行完成也不刪除
ON COMPLETION PRESERVE
COMMENT '年度資料遷移至歷史表'
DO
BEGIN
-- 將本年度的交易資料遷移至歷史表
INSERT INTO transaction_history
SELECT * FROM transaction
WHERE transaction_date < '2025-01-01';
-- 刪除已遷移的資料
DELETE FROM transaction
WHERE transaction_date < '2025-01-01';
-- 最佳化歷史表
OPTIMIZE TABLE transaction_history;
-- 記錄遷移完成
INSERT INTO migration_log (
migration_type,
migration_datetime,
records_migrated
) VALUES (
'ANNUAL_ARCHIVE',
NOW(),
ROW_COUNT()
);
END;
這些事件範例展示了不同類型的排程任務。週期性事件使用EVERY子句指定重複間隔,可以是秒、分、時、日、週、月等單位。STARTS子句指定事件首次執行的時間,如果省略則立即開始執行。一次性事件使用AT子句指定精確的執行時間點。ON COMPLETION PRESERVE選項決定事件執行完成後是否保留定義,預設會刪除已完成的一次性事件。
事件的管理包括查詢、修改、啟用停用與刪除等操作。透過INFORMATION_SCHEMA資料庫可以查詢所有事件的詳細資訊,包括執行排程、上次執行時間、下次執行時間等。ALTER EVENT可以修改現有事件的定義,包括排程、執行的SQL陳述式等。事件可以暫時停用而不刪除定義,這在需要暫停某些維護任務時很有用。
-- 查詢所有事件的資訊
-- 從INFORMATION_SCHEMA取得事件元資料
SELECT
event_name AS '事件名稱',
event_definition AS '事件定義',
interval_value AS '間隔值',
interval_field AS '間隔單位',
starts AS '開始時間',
ends AS '結束時間',
status AS '狀態',
last_executed AS '上次執行',
event_comment AS '說明'
FROM information_schema.EVENTS
WHERE event_schema = DATABASE()
ORDER BY event_name;
-- 暫時停用事件
-- 不刪除事件定義
ALTER EVENT evt_daily_backup DISABLE;
-- 重新啟用事件
ALTER EVENT evt_daily_backup ENABLE;
-- 修改事件執行時間
-- 變更為每天凌晨3點執行
ALTER EVENT evt_daily_backup
ON SCHEDULE EVERY 1 DAY
STARTS (TIMESTAMP(CURRENT_DATE) + INTERVAL 3 HOUR);
-- 刪除事件
-- 永久移除事件定義
DROP EVENT IF EXISTS evt_data_migration;
事件排程器在使用時需要注意幾個要點。首先是確保事件排程器服務已啟用,如果服務停止則所有事件都不會執行。其次是事件執行時間的計算,特別是涉及夏令時間或時區變更時。第三是事件的錯誤處理,事件執行失敗通常不會阻止後續的執行,但會記錄在錯誤日誌中,需要定期檢查。第四是並行執行的控制,如果前一次執行尚未完成而下次執行時間已到,可能產生並行執行的情況,需要在事件邏輯中考慮這種可能性。
@startuml
!define PLANTUML_FORMAT svg
!theme _none_
skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 14
skinparam minClassWidth 100
title 事件排程器執行架構
|事件排程器守護程序|
start
:event_scheduler服務啟動;
note right
資料庫啟動時
自動載入事件定義
end note
:載入所有啟用的事件;
:計算下次執行時間;
repeat
:等待至下個執行時間點;
if (到達執行時間?) then (是)
:識別需執行的事件;
fork
:事件A執行;
partition "事件執行" {
:建立獨立連線;
:執行事件主體SQL;
:記錄執行狀態;
:關閉連線;
}
fork again
:事件B執行;
fork again
:事件C執行;
end fork
note right
多個事件可能
同時執行
end note
:更新上次執行時間;
:計算下次執行時間;
if (週期性事件?) then (是)
:保留事件定義;
else (一次性事件)
if (PRESERVE設定?) then (是)
:保留事件定義;
:標記為已完成;
else (否)
:刪除事件定義;
endif
endif
endif
repeat while (服務執行中) is (繼續)
:event_scheduler服務停止;
stop
@enduml
資料庫物件管理最佳實務
在實際的資料庫開發與維護中,有效管理函式、預存程序、觸發器與事件是確保系統穩定性與可維護性的關鍵。這些資料庫物件雖然提供了強大的功能,但如果使用不當也可能帶來維護困難、效能問題,甚至資料不一致等風險。建立完善的管理規範與遵循最佳實務,能夠最大化這些工具的價值同時避免潛在的問題。
首要的最佳實務是保持程式碼的可讀性與可維護性。所有資料庫物件都應該有清晰的命名規範,例如函式以fn_開頭,預存程序以sp_開頭,觸發器以tr_開頭,事件以evt_開頭。名稱應該明確反映物件的功能,避免使用過於簡短或含糊的命名。每個物件都應該包含詳細的註解,說明其用途、參數意義、回傳值含義,以及任何重要的業務邏輯或注意事項。這些文件對於後續的維護與除錯至關重要。
版本控制是另一個重要的實務。所有資料庫物件的定義SQL腳本都應該納入版本控制系統,與應用程式原始碼一同管理。當需要修改現有物件時,應該保留修改歷史,包括修改時間、修改者、修改原因等資訊。在部署變更到生產環境前,必須在測試環境中充分驗證,確保變更不會造成非預期的副作用。資料庫變更的部署應該是自動化且可回復的,使用資料庫遷移工具可以大幅降低人為錯誤的風險。
錯誤處理與日誌記錄是確保系統可靠性的基礎。預存程序應該包含適當的錯誤處理機制,透過DECLARE HANDLER捕捉並處理異常。重要的操作應該記錄到稽核表,包括執行時間、執行者、操作內容與結果。當發生錯誤時,除了在錯誤處理器中記錄錯誤資訊,還應該考慮是否需要發送警示通知相關人員。對於事件排程器執行的任務,由於沒有即時的使用者互動,完善的日誌記錄更是不可或缺。
效能監控與最佳化同樣重要。定期檢視資料庫物件的執行統計資訊,識別執行頻繁或耗時較長的物件,進行針對性的最佳化。觸發器應該保持簡潔,避免在觸發器中執行複雜的查詢或運算。如果業務邏輯確實複雜,可以考慮將主要邏輯移到預存程序中,觸發器只負責呼叫程序。對於大量資料的處理,應該評估是否適合使用遊標,或是改用基於集合的操作方式。
安全性考量也不容忽視。資料庫物件的權限應該遵循最小權限原則,只授予必要的執行權限。預存程序可以作為安全的資料存取介面,應用程式透過呼叫程序來操作資料,而不是直接執行動態SQL,這可以有效防止SQL注入攻擊。對於包含敏感操作的程序,應該實施額外的存取控制,例如記錄所有執行紀錄以供稽核。
資料庫函式、預存程序、觸發器與事件排程器構成了資料庫程式設計的完整工具集。函式提供了封裝計算邏輯的能力,預存程序實現了複雜業務流程的模組化,觸發器確保了資料完整性與自動化回應,事件排程器則賦予了資料庫自主執行任務的能力。透過深入理解這些工具的特性與適用場景,遵循最佳實務進行設計與實作,可以建構出高效、可靠且易於維護的資料庫系統。隨著雲端原生資料庫與無伺服器運算的發展,這些傳統的資料庫物件正在與新技術整合,形成更靈活且強大的資料處理架構。對於資料庫開發者而言,掌握這些核心概念並持續關注技術演進,將是構建現代化資料系統的關鍵能力。