在現代企業級應用程式開發的複雜生態系統中,資料庫不再僅僅是資料的儲存容器,而是承載著核心商業邏輯與複雜計算能力的關鍵基礎設施。MySQL 提供的自訂函式(Custom Functions)與儲存程式(Stored Procedures)正是實現資料庫層級程式化開發的兩大核心工具。這兩種資料庫物件讓開發者能夠將複雜的資料處理邏輯、商業規則驗證、以及多步驟的資料操作流程封裝在資料庫層級,透過標準化的介面提供給應用程式層呼叫。這種架構設計不僅大幅提升了程式碼的重用性與維護性,更在效能最佳化、網路傳輸成本控制、以及資料一致性保證等方面展現出顯著優勢。

然而,在實務應用中,許多開發團隊對於自訂函式與儲存程式的理解往往流於表面。常見的問題包括:不清楚何時應該使用函式、何時應該使用儲存程式;對於 DETERMINISTIC 等函式特性的含義與影響缺乏深入理解;在設定 SQL 資料存取特性時往往採用預設值而未經仔細評估;以及在效能調校時忽略了資料庫程式化物件可能成為瓶頸。這些問題不僅影響了系統的執行效率,更可能在資料量增長或併發需求提升時引發嚴重的效能問題。

從企業架構的角度來看,自訂函式與儲存程式的價值遠超過單純的程式碼封裝。它們構成了資料庫應用程式介面(Database API)的基礎,讓不同的應用程式模組、不同的程式語言實作,甚至不同的團隊開發的系統,都能透過一致的介面存取資料庫的核心功能。這種架構模式在大型企業的微服務架構、多語言技術棧、以及跨團隊協作場景中特別重要。當商業邏輯被封裝在資料庫層級時,任何應用程式層面的技術選型變更或重構,都不會影響到這些核心的資料處理邏輯,大幅降低了系統演進的風險與成本。

本文將從實務開發者的視角出發,系統性地探討 MySQL 自訂函式與儲存程式的完整技術體系。從最基礎的語法結構與建立流程,到進階的特性設定與效能最佳化策略;從 DELIMITER 機制的運作原理,到 DETERMINISTIC 特性對查詢最佳化器的影響;從簡單的查詢封裝,到複雜的交易控制與錯誤處理機制。透過豐富的實務案例、詳盡的程式碼註解、以及深入的技術剖析,協助讀者建立紮實的理論基礎與實務能力,最終能夠獨立設計並實作符合企業標準的高效能資料庫程式化架構。無論您是剛開始接觸資料庫程式化開發的新手,還是希望深化技術能力的資深開發者,都能從本文中獲得有價值的知識與經驗。

MySQL 自訂函式的核心概念與架構設計理念

自訂函式在 MySQL 的程式化開發體系中扮演著資料轉換與計算封裝的核心角色。從設計哲學來看,函式的概念源自數學中的函數定義,即給定特定的輸入值,經過一系列確定的計算過程,必然產生特定的輸出值。這種輸入輸出的明確對應關係,使得函式成為實現可重用計算邏輯的理想工具。在資料庫應用開發中,當某個計算邏輯需要在多個查詢、多個報表、甚至多個應用程式模組中重複使用時,將這個邏輯封裝成自訂函式,不僅能確保計算結果的一致性,更能大幅簡化程式碼的維護工作。

MySQL 自訂函式的設計遵循著嚴格的語法結構與執行規範。每個函式必須明確宣告其接受的參數型別、回傳值型別、以及函式的行為特性。這些宣告不僅是語法要求,更是 MySQL 查詢最佳化器理解函式行為的重要依據。當查詢最佳化器知道一個函式是確定性的(Deterministic)且只讀取資料(READS SQL DATA)時,它可以採取更積極的最佳化策略,例如快取函式的計算結果、重新排列查詢的執行順序,甚至在某些情況下將函式的執行提前到查詢計畫的早期階段。

在企業級應用開發中,自訂函式的價值體現在多個層面。首先是程式碼重用性的提升,當一個複雜的計算邏輯被封裝成函式後,所有需要這個計算的查詢都可以直接呼叫,避免了邏輯重複實作帶來的維護負擔。其次是計算一致性的保證,由於所有呼叫都使用相同的函式實作,不會因為不同開發者的理解差異而產生計算結果的不一致。第三是網路傳輸成本的降低,相較於將原始資料傳回應用程式層再進行計算,在資料庫層級完成計算能夠顯著減少需要傳輸的資料量。第四是安全性的強化,透過函式介面存取資料,可以隱藏底層資料表的結構細節,降低資料暴露的風險。

以下透過一個實際的人口統計查詢系統案例,深入理解自訂函式的建立過程與設計考量。假設正在開發一個全國人口統計分析平台,系統需要頻繁地根據縣市名稱查詢人口數據,並在各種報表與分析模組中使用這些數據。將這個查詢邏輯封裝成自訂函式,能夠為整個系統提供一致且高效的資料存取介面。

-- ============================================
-- 函式名稱: f_get_state_population
-- 功能說明: 根據縣市名稱查詢該縣市的人口總數
-- 輸入參數: state_param VARCHAR(100) - 縣市名稱
-- 回傳值: INT - 該縣市的人口數,若縣市不存在則回傳 NULL
-- 建立日期: 2024-01-15
-- 最後修改: 2024-11-28
-- 作者: 開發團隊
-- ============================================

-- 設定使用的資料庫
-- 確保在正確的資料庫結構描述下建立函式
USE population;

-- 修改 SQL 陳述式的分隔符號
-- MySQL 預設使用分號(;)作為陳述式結束符號
-- 但函式定義內部也會使用分號,會造成解析混淆
-- 因此暫時將分隔符號改為 // ,讓 MySQL 能夠完整解析函式定義
DELIMITER //

-- 建立自訂函式
-- CREATE FUNCTION: 函式建立的關鍵字
-- f_get_state_population: 函式名稱,建議使用 f_ 前綴標示這是 Function
-- 命名規範有助於在大型專案中快速識別資料庫物件的類型
CREATE FUNCTION f_get_state_population(
    state_param VARCHAR(100)     -- 輸入參數:縣市名稱
                                 -- VARCHAR(100) 提供足夠長度儲存台灣縣市名稱
                                 -- 參數命名使用 _param 後綴以區分變數作用域
)
RETURNS INT                      -- 宣告函式回傳值型別為整數
                                 -- 人口數使用 INT 型別足以涵蓋台灣縣市規模
DETERMINISTIC                    -- 確定性宣告:相同輸入必定產生相同輸出
                                 -- 此宣告讓 MySQL 可以快取函式結果
                                 -- 顯著提升重複查詢的效能
READS SQL DATA                   -- SQL 資料存取宣告:此函式只讀取資料
                                 -- 不執行任何 INSERT/UPDATE/DELETE 操作
                                 -- 讓 MySQL 能夠在複製環境中安全執行
COMMENT '查詢指定縣市的人口總數,用於統計分析與報表系統'
                                 -- 函式說明註解,便於系統維護與文件產生
BEGIN
    -- 宣告函式內部使用的區域變數
    -- DECLARE 關鍵字建立函式作用域內的臨時變數
    -- 變數命名使用 _var 後綴以明確標示其為變數
    DECLARE population_var INT DEFAULT NULL;
                                 -- 初始化為 NULL,若查詢無結果則保持此值
                                 -- DEFAULT 子句確保變數有明確的初始狀態

    -- 執行查詢並將結果存入變數
    -- SELECT ... INTO 語法是 MySQL 中將查詢結果賦值給變數的標準方式
    -- 這種寫法預期且要求查詢只回傳單一筆資料
    -- 若查詢結果為空,變數保持 NULL 值
    -- 若查詢回傳多筆資料,會產生錯誤 "Subquery returns more than 1 row"
    SELECT population INTO population_var
    FROM state_population               -- 人口統計資料表
    WHERE state = state_param;          -- 使用輸入參數進行精確比對
                                        -- 建議在 state 欄位建立索引以提升查詢效能

    -- 回傳查詢結果
    -- RETURN 關鍵字結束函式執行並回傳指定值
    -- 函式執行到 RETURN 陳述式時立即結束,不會繼續執行後續程式碼
    RETURN population_var;
    
    -- 注意:如果縣市不存在,此函式會回傳 NULL
    -- 呼叫端需要處理 NULL 值的情況,或使用 IFNULL/COALESCE 函式提供預設值
END //

-- 恢復預設的分隔符號
-- 函式建立完成後,將分隔符改回標準的分號
-- 這樣後續的 SQL 陳述式可以正常使用分號作為結束符號
DELIMITER ;

-- ============================================
-- 函式使用範例與說明
-- ============================================

-- 範例 1: 直接呼叫函式取得特定縣市的人口數
-- 函式可以在 SELECT 清單中直接使用,如同內建函式一樣
SELECT f_get_state_population('台北市') AS taipei_population;
-- 結果: taipei_population
--       2700000

-- 範例 2: 在 WHERE 子句中使用函式進行條件篩選
-- 這個查詢會找出所有人口數超過台北市的縣市
-- MySQL 會先執行 f_get_state_population('台北市') 取得基準值
-- 然後用這個值與資料表中的 population 欄位進行比較
SELECT
    state AS 縣市名稱,
    population AS 人口數,
    -- 計算與台北市的人口比例
    ROUND(population / f_get_state_population('台北市') * 100, 2) AS 相對台北市人口比例
FROM state_population
WHERE population > f_get_state_population('台北市')
      -- 注意:在 WHERE 子句中對非索引欄位使用函式不影響效能
      -- 但如果對 population 欄位使用函式(如 WHERE f_something(population) > xxx)
      -- 則可能導致索引失效
ORDER BY population DESC;

-- 範例 3: 在聚合查詢中使用函式
-- 計算所有縣市人口數與台北市的平均比例
SELECT
    COUNT(*) AS 統計縣市數,
    ROUND(AVG(population / f_get_state_population('台北市')), 2) AS 平均人口比例,
    ROUND(MIN(population / f_get_state_population('台北市')), 2) AS 最小人口比例,
    ROUND(MAX(population / f_get_state_population('台北市')), 2) AS 最大人口比例
FROM state_population
WHERE population > 100000;           -- 只統計人口超過 10 萬的縣市

-- 範例 4: 使用 IFNULL 處理函式可能回傳的 NULL 值
-- 如果查詢的縣市不存在,函式會回傳 NULL
-- 使用 IFNULL 可以提供預設值,避免後續計算出現問題
SELECT
    '高雄市' AS 查詢縣市,
    IFNULL(f_get_state_population('高雄市'), 0) AS 人口數,
    CASE
        WHEN f_get_state_population('高雄市') IS NULL THEN '資料不存在'
        WHEN f_get_state_population('高雄市') > 2000000 THEN '特大城市'
        WHEN f_get_state_population('高雄市') > 1000000 THEN '大城市'
        ELSE '中小城市'
    END AS 城市規模分類;

從這個詳細的範例可以看出,自訂函式的建立涉及多個關鍵要素。首先是 DELIMITER 機制的正確使用,這是 MySQL 能夠正確解析包含多個陳述式的函式定義的基礎。其次是函式特性的明確宣告,DETERMINISTIC 告訴 MySQL 這個函式的輸出完全由輸入決定,READS SQL DATA 則說明函式只進行資料讀取。這些宣告不僅影響查詢最佳化器的決策,也關係到函式在主從複製環境中的執行行為。第三是區域變數的使用,透過 DECLARE 宣告的變數只在函式內部有效,不會與其他函式或外部變數產生衝突。第四是 SELECT INTO 語法的正確使用,這種賦值方式簡潔高效,但需要注意其對查詢結果筆數的要求。

在實務應用中,函式的價值不僅體現在程式碼的重用性,更重要的是它建立了一個抽象層,將資料存取的實作細節與應用邏輯分離。當底層資料表結構需要調整時,只要函式的介面保持不變,應用程式層的程式碼就不需要修改。這種架構模式在大型系統的演進過程中特別有價值,能夠大幅降低系統變更的風險與成本。

@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 16
skinparam minClassWidth 100

title MySQL 自訂函式完整執行流程

participant "應用程式" as App
participant "MySQL 查詢解析器" as Parser
participant "查詢最佳化器" as Optimizer
participant "函式快取系統" as Cache
participant "函式執行引擎" as FuncEngine
participant "資料庫資料表" as Table

App -> Parser: 發送包含函式呼叫的 SQL 查詢
note right of App
  SELECT city, 
    f_get_state_population(city) 
  FROM cities
end note

Parser -> Parser: 解析 SQL 語法結構
Parser -> Parser: 識別函式呼叫

Parser -> Optimizer: 傳遞解析後的查詢樹
Optimizer -> Optimizer: 檢查函式特性設定

alt 函式標記為 DETERMINISTIC
  Optimizer -> Cache: 檢查是否有快取結果
  
  alt 快取中存在相同參數的結果
    Cache -> Optimizer: 回傳快取的計算結果
    note right of Cache
      快取命中
      直接使用快取值
      大幅提升效能
    end note
  else 快取中不存在結果
    Optimizer -> FuncEngine: 執行函式邏輯
    FuncEngine -> FuncEngine: 宣告區域變數
    FuncEngine -> Table: 執行 SELECT INTO 查詢
    Table -> FuncEngine: 回傳查詢結果
    FuncEngine -> FuncEngine: 將結果賦值給變數
    FuncEngine -> FuncEngine: 執行 RETURN 陳述式
    FuncEngine -> Cache: 將結果存入快取
    FuncEngine -> Optimizer: 回傳函式計算結果
  end
else 函式標記為 NOT DETERMINISTIC
  Optimizer -> FuncEngine: 每次都執行函式邏輯
  note right of Optimizer
    非確定性函式
    不使用快取機制
    每次都重新計算
  end note
  FuncEngine -> Table: 執行資料查詢
  Table -> FuncEngine: 回傳查詢結果
  FuncEngine -> Optimizer: 回傳計算結果
end

Optimizer -> Parser: 將函式結果代入原始查詢
Parser -> App: 回傳最終查詢結果

note over App, Table
  完整執行流程展示了 MySQL 如何處理自訂函式
  特別注意 DETERMINISTIC 特性對快取機制的影響
  正確設定函式特性能夠顯著提升查詢效能
end note

@enduml

這個詳細的時序圖完整展示了 MySQL 執行自訂函式的內部機制。從應用程式發送查詢開始,經過語法解析、函式識別、特性檢查、快取查詢,到實際執行函式邏輯並回傳結果的每一個關鍵步驟都清楚呈現。特別值得注意的是,當函式被正確標記為 DETERMINISTIC 時,MySQL 會啟用快取機制,對於相同參數的重複呼叫可以直接使用快取結果,避免重複執行資料庫查詢,這在處理大量資料或高併發查詢時能夠帶來顯著的效能提升。相反地,如果函式被標記為 NOT DETERMINISTIC,則每次呼叫都會重新執行完整的函式邏輯,確保結果的即時性但犧牲了快取帶來的效能優勢。

儲存程式的深度應用與交易控制機制

儲存程式在 MySQL 程式化開發體系中扮演著複雜業務邏輯封裝與多步驟資料操作協調的關鍵角色。與自訂函式的設計理念不同,儲存程式並非追求數學函數般的純粹性與確定性,而是著重於處理需要多個資料操作步驟、包含複雜控制流程、以及要求交易一致性保證的實務場景。在企業級應用開發中,訂單處理流程、庫存管理系統、財務結算作業、以及批次資料處理任務,都是儲存程式的典型應用場景。

從架構設計的角度來看,儲存程式提供了一個完整的程式執行環境,支援變數宣告、條件判斷、循環控制、錯誤處理、以及交易管理等完整的程式設計能力。這使得複雜的商業邏輯可以完全在資料庫層級實作,而無需在應用程式層與資料庫層之間進行多次往返通訊。這種架構不僅降低了網路延遲的影響,更重要的是能夠利用資料庫的交易機制,確保多步驟操作的原子性,即所有操作要麼全部成功提交,要麼全部回滾撤銷,避免了部分成功導致的資料不一致問題。

儲存程式的參數機制比函式更加靈活,支援三種參數方向:IN 參數用於接收輸入資料、OUT 參數用於回傳輸出結果、以及 INOUT 參數可以同時接收輸入並回傳輸出。這種設計讓儲存程式能夠在單一呼叫中完成複雜的資料交換,不僅能夠執行操作,更能夠回傳執行狀態、錯誤訊息、以及計算結果等多個資訊。相較於函式只能回傳單一標量值的限制,儲存程式的這種能力使其更適合處理企業級應用的複雜需求。

以下透過一個實際的人口統計資料更新系統案例,深入理解儲存程式的建立過程、交易控制機制、以及錯誤處理策略。這個案例展示了如何在單一儲存程式中整合資料驗證、資料更新、日誌記錄、以及完整的錯誤處理邏輯,構成一個完整且可靠的資料操作流程。

-- ============================================
-- 程式名稱: p_update_state_population
-- 功能說明: 更新指定縣市的人口數,並記錄操作日誌
-- 輸入參數: 
--   state_param VARCHAR(100) - 縣市名稱
--   new_population INT - 新的人口數值
-- 輸出參數:
--   operation_result VARCHAR(200) - 操作執行結果訊息
-- 建立日期: 2024-01-15
-- 最後修改: 2024-11-28
-- 作者: 開發團隊
-- 重要提示: 此程式使用交易控制,確保資料一致性
-- ============================================

-- 修改陳述式分隔符號
-- 儲存程式通常包含更複雜的邏輯與更多的陳述式
-- 需要使用非標準分隔符號來避免解析衝突
DELIMITER //

-- 建立儲存程式
-- CREATE PROCEDURE: 程式建立的關鍵字
-- p_update_state_population: 程式名稱,建議使用 p_ 前綴標示這是 Procedure
-- 這種命名慣例在大型專案中有助於快速識別資料庫物件類型
CREATE PROCEDURE p_update_state_population(
    IN state_param VARCHAR(100),      -- IN: 輸入參數,只能讀取不能修改
                                      -- 用於接收外部傳入的縣市名稱
    IN new_population INT,             -- IN: 新的人口數值
                                      -- 必須為正整數,程式內部會進行驗證
    OUT operation_result VARCHAR(200)  -- OUT: 輸出參數,用於回傳執行結果
                                      -- 呼叫端可以透過使用者變數接收這個值
)
MODIFIES SQL DATA                      -- SQL 資料存取宣告:此程式會修改資料
                                      -- 相較於 READS SQL DATA,這表示會執行
                                      -- INSERT/UPDATE/DELETE 等修改操作
COMMENT '更新縣市人口數並記錄操作日誌,支援交易回滾與錯誤處理'
BEGIN
    -- 宣告錯誤處理器
    -- MySQL 的錯誤處理機制類似其他程式語言的 try-catch
    -- DECLARE HANDLER 定義當特定條件發生時的處理邏輯
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        -- 當任何 SQL 錯誤發生時,這個處理器會被自動觸發
        -- EXIT 表示處理完錯誤後會退出程式
        -- 相對地,CONTINUE 表示處理完錯誤後會繼續執行
        
        -- 回滾所有尚未提交的變更
        -- 確保資料庫維持一致性狀態
        ROLLBACK;
        
        -- 設定錯誤訊息給輸出參數
        -- 呼叫端可以透過這個訊息了解錯誤原因
        SET operation_result = CONCAT(
            '更新失敗:系統錯誤 - ',
            state_param,
            ' - 請聯繫系統管理員'
        );
    END;
    
    -- 宣告區域變數用於存取中間結果
    DECLARE old_population INT DEFAULT NULL;
    DECLARE current_user_name VARCHAR(100);
    
    -- 取得當前資料庫使用者名稱
    -- 用於記錄是誰執行了這個操作
    SET current_user_name = CURRENT_USER();

    -- 開始交易
    -- START TRANSACTION 標記交易的起點
    -- 從這裡開始的所有資料修改操作都會被納入交易範圍
    -- 直到執行 COMMIT(提交)或 ROLLBACK(回滾)才會結束交易
    START TRANSACTION;
    
    -- 輸入參數驗證
    -- 在執行實際的資料操作前,先驗證輸入是否合法
    IF state_param IS NULL OR LENGTH(TRIM(state_param)) = 0 THEN
        -- 縣市名稱為空的錯誤處理
        ROLLBACK;                     -- 回滾交易
        SET operation_result = '錯誤:縣市名稱不能為空';
        -- 使用 LEAVE 語句可以提前退出程式
        -- 但這裡我們使用條件判斷的結構來控制流程
    ELSEIF new_population IS NULL OR new_population <= 0 THEN
        -- 人口數不合法的錯誤處理
        ROLLBACK;
        SET operation_result = '錯誤:人口數必須為正整數';
    ELSE
        -- 輸入驗證通過,執行實際的資料操作
        
        -- 檢查縣市是否存在於資料庫中
        -- 使用 EXISTS 子查詢進行存在性檢查,效能優於 COUNT(*)
        IF EXISTS (
            SELECT 1 FROM state_population 
            WHERE state = state_param
        ) THEN
            -- 縣市存在,執行更新操作
            
            -- 先查詢並保存舊的人口數
            -- 這個資訊將用於記錄操作日誌
            SELECT population INTO old_population
            FROM state_population
            WHERE state = state_param;
            
            -- 執行人口數更新
            -- UPDATE 陳述式修改現有記錄
            UPDATE state_population
            SET 
                population = new_population,    -- 更新為新的人口數
                last_updated = NOW()            -- 記錄更新時間戳記
            WHERE state = state_param;
            
            -- 新增操作日誌記錄
            -- 記錄誰在什麼時間對哪個縣市的人口數做了什麼變更
            -- 這對於資料稽核與問題追蹤非常重要
            INSERT INTO population_update_log (
                state,                          -- 縣市名稱
                old_population,                 -- 更新前的人口數
                new_population,                 -- 更新後的人口數
                update_time,                    -- 更新時間
                operator,                       -- 操作人員
                change_amount                   -- 變化量
            ) VALUES (
                state_param,
                old_population,
                new_population,
                NOW(),
                current_user_name,
                new_population - old_population  -- 計算變化量
            );
            
            -- 所有操作都成功完成,提交交易
            -- COMMIT 將所有變更永久寫入資料庫
            COMMIT;
            
            -- 設定成功訊息
            SET operation_result = CONCAT(
                '成功更新 ',
                state_param,
                ' 的人口數從 ',
                old_population,
                ' 變更為 ',
                new_population,
                ' (變化: ',
                (new_population - old_population),
                ')'
            );
            
        ELSE
            -- 縣市不存在的錯誤處理
            -- 回滾交易並設定錯誤訊息
            ROLLBACK;
            SET operation_result = CONCAT(
                '錯誤:縣市「',
                state_param,
                '」不存在於資料庫中,請先新增該縣市資料'
            );
        END IF;
    END IF;
    
    -- 程式執行到此處自動結束
    -- 注意:所有的路徑都已經明確處理了 COMMIT 或 ROLLBACK
    -- 確保交易不會處於未決狀態
END //

-- 恢復預設分隔符號
DELIMITER ;

-- ============================================
-- 儲存程式使用範例與說明
-- ============================================

-- 範例 1: 基本的單一縣市人口更新
-- 宣告使用者變數用於接收輸出參數
-- MySQL 使用者變數以 @ 符號開頭,可以在多個陳述式之間保持值
SET @result_message = '';

-- 呼叫儲存程式
-- CALL 關鍵字用於執行儲存程式
-- 參數依序傳入:縣市名稱、新人口數、輸出變數
CALL p_update_state_population('台北市', 2700000, @result_message);

-- 查詢執行結果
-- 使用者變數會保留程式執行後的輸出值
SELECT @result_message AS 執行結果;
-- 預期結果: 成功更新 台北市 的人口數從 2650000 變更為 2700000 (變化: 50000)

-- 範例 2: 批次更新多個縣市的人口數
-- 這個範例展示如何在應用程式層面迴圈呼叫儲存程式
-- 實務上這個邏輯通常會放在應用程式的循環結構中

-- 更新新北市
CALL p_update_state_population('新北市', 4030000, @result_message);
SELECT '新北市' AS 縣市, @result_message AS 更新結果;

-- 更新桃園市
CALL p_update_state_population('桃園市', 2280000, @result_message);
SELECT '桃園市' AS 縣市, @result_message AS 更新結果;

-- 更新台中市
CALL p_update_state_population('台中市', 2820000, @result_message);
SELECT '台中市' AS 縣市, @result_message AS 更新結果;

-- 範例 3: 錯誤處理測試
-- 測試縣市不存在的情況
CALL p_update_state_population('不存在的縣市', 1000000, @result_message);
SELECT @result_message AS 錯誤處理結果;
-- 預期結果: 錯誤:縣市「不存在的縣市」不存在於資料庫中,請先新增該縣市資料

-- 測試人口數無效的情況
CALL p_update_state_population('台北市', -1000, @result_message);
SELECT @result_message AS 輸入驗證結果;
-- 預期結果: 錯誤:人口數必須為正整數

-- 範例 4: 查詢操作日誌驗證交易完整性
-- 檢視今天所有的人口數更新記錄
SELECT
    state AS 縣市,
    old_population AS 更新前人口,
    new_population AS 更新後人口,
    change_amount AS 變化量,
    CASE
        WHEN change_amount > 0 THEN '增加'
        WHEN change_amount < 0 THEN '減少'
        ELSE '無變化'
    END AS 變化類型,
    update_time AS 更新時間,
    operator AS 操作人員
FROM population_update_log
WHERE DATE(update_time) = CURDATE()   -- 只顯示今天的記錄
ORDER BY update_time DESC;

這個詳細的儲存程式範例展示了企業級資料操作的標準模式。首先是完整的錯誤處理機制,透過 DECLARE HANDLER 捕獲所有 SQL 錯誤並進行適當處理,確保即使發生異常也不會讓資料庫處於不一致狀態。其次是嚴謹的輸入驗證,在執行任何實際的資料操作前,先驗證所有輸入參數的合法性,這是防禦性程式設計的重要實踐。第三是交易控制的正確使用,START TRANSACTION、COMMIT 與 ROLLBACK 的配合使用確保了多步驟操作的原子性。第四是完整的操作日誌記錄,不僅記錄了資料的變更,更記錄了操作時間、操作人員等稽核資訊,這在企業環境中對於合規要求與問題追蹤都至關重要。

從這個案例可以清楚看出儲存程式與自訂函式的本質差異。儲存程式不追求函式那樣的純粹性與確定性,而是著重於處理複雜的業務流程。它可以修改資料庫內容、可以執行多個不同的操作、可以根據不同條件採取不同的處理路徑、可以透過多個輸出參數回傳豐富的執行資訊。這些特性使得儲存程式成為實作企業級複雜業務邏輯的理想選擇。

@startuml
!define DISABLE_LINK
!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 16
skinparam minClassWidth 100

title 儲存程式交易控制與錯誤處理完整流程

|應用程式|
start
:準備呼叫儲存程式;
:宣告使用者變數接收輸出;

|MySQL 執行引擎|
:接收 CALL 陳述式;
:載入程式定義;
:初始化執行環境;
:設定輸入參數值;

:註冊 SQLEXCEPTION 處理器;
note right
  一旦註冊,任何 SQL 錯誤
  都會觸發此處理器
end note

:執行 START TRANSACTION;
note right
  交易起點
  後續所有操作都在
  交易保護範圍內
end note

:輸入參數驗證;

if (參數是否合法?) then (否)
  :執行 ROLLBACK;
  :設定錯誤訊息;
  :程式執行完畢;
  stop
else (是)
  :檢查縣市是否存在;
  
  if (縣市是否存在?) then (否)
    :執行 ROLLBACK;
    :設定錯誤訊息;
    :程式執行完畢;
    stop
  else (是)
    :查詢舊人口數;
    
    :執行 UPDATE 更新人口;
    note right
      如果 UPDATE 失敗
      錯誤處理器會被觸發
    end note
    
    if (UPDATE 是否成功?) then (否)
      |錯誤處理器|
      :觸發 SQLEXCEPTION 處理器;
      :執行 ROLLBACK;
      :設定錯誤訊息;
      :程式執行完畢;
      stop
    else (是)
      |MySQL 執行引擎|
      :INSERT 操作日誌記錄;
      
      if (INSERT 是否成功?) then (否)
        |錯誤處理器|
        :觸發錯誤處理器;
        :ROLLBACK 整個交易;
        note right
          UPDATE 也會被撤銷
          確保資料一致性
        end note
        :程式執行完畢;
        stop
      else (是)
        |MySQL 執行引擎|
        :執行 COMMIT;
        note right
          交易提交
          所有變更永久生效
        end note
        :設定成功訊息;
      endif
    endif
  endif
endif

:將輸出參數值寫入使用者變數;
:程式執行完畢;

|應用程式|
:接收執行結果;
:顯示操作訊息;

stop

@enduml

這個詳細的活動圖完整展示了儲存程式執行過程中的交易控制與錯誤處理機制。從交易的開始(START TRANSACTION)到結束(COMMIT 或 ROLLBACK),每一個關鍵決策點都清楚標示。特別值得注意的是錯誤處理器的作用,當任何 SQL 操作發生錯誤時,處理器會自動被觸發,執行 ROLLBACK 撤銷所有尚未提交的變更,確保資料庫不會因為部分操作失敗而陷入不一致狀態。這種機制在處理涉及多個資料表、多個步驟的複雜業務邏輯時特別重要,是企業級應用系統可靠性的重要保證。

函式特性的深度解析與查詢最佳化策略

在 MySQL 自訂函式的設計與實作中,函式特性(Function Characteristics)的正確設定是影響系統效能與資料一致性的關鍵因素。這些特性不僅是語法要求,更是 MySQL 查詢最佳化器理解函式行為、做出最佳化決策的重要依據。深入理解每種特性的含義、影響,以及如何根據函式的實際行為選擇適當的特性設定,是每個資料庫開發者必須掌握的核心技能。

MySQL 要求在建立函式時必須至少明確宣告兩類特性:確定性特性(Deterministic Characteristics)與 SQL 資料存取特性(SQL Data Access Characteristics)。這個要求背後有深刻的技術考量。確定性特性描述了函式的輸出與輸入之間的關係,告訴 MySQL 這個函式是否會因為外部因素(如當前時間、隨機數、系統狀態)而產生不同的輸出。SQL 資料存取特性則說明了函式與資料庫資料之間的互動方式,是純計算、只讀取、還是會修改資料。這些資訊讓查詢最佳化器能夠做出更明智的決策,例如是否可以快取函式結果、是否可以重新排列執行順序、在複製環境中如何處理函式呼叫等。

確定性特性的核心概念源自數學中的函數定義。一個確定性函式(DETERMINISTIC Function)承諾:在相同的輸入參數與相同的資料庫狀態下,這個函式必然產生相同的輸出。這個保證對於查詢最佳化器極為重要,因為它允許 MySQL 採取積極的最佳化策略。例如,當同一個確定性函式在查詢中被多次呼叫且參數相同時,MySQL 可以只執行一次函式並快取結果,後續呼叫直接使用快取值。這在處理大型資料集或複雜查詢時能夠帶來數倍甚至數十倍的效能提升。

-- ============================================
-- 確定性特性的正確使用範例
-- ============================================

-- 範例 1: 典型的確定性函式
-- 這個函式根據縣市名稱查詢人口數
-- 只要資料庫內容不變,相同的輸入必然產生相同的輸出
DELIMITER //

CREATE FUNCTION f_get_population_deterministic(
    city_name VARCHAR(100)
)
RETURNS INT
DETERMINISTIC                    -- 確定性宣告
READS SQL DATA                   -- 只讀取資料
COMMENT '確定性人口查詢函式,相同輸入產生相同輸出'
BEGIN
    DECLARE pop_count INT;
    
    -- 從靜態的參考資料表查詢
    -- 這種查詢結果在資料未更新時保持穩定
    SELECT population INTO pop_count
    FROM city_reference              -- 假設這是一個變動不頻繁的參考資料表
    WHERE city = city_name;
    
    -- 使用 IFNULL 避免回傳 NULL
    -- 提供預設值 0 確保回傳值的確定性
    RETURN IFNULL(pop_count, 0);
END //

DELIMITER ;

-- 範例 2: 必須使用 NOT DETERMINISTIC 的情況 - 時間相關
-- 這個函式的輸出取決於當前時間,即使參數相同也可能回傳不同結果
DELIMITER //

CREATE FUNCTION f_get_current_quarter_population(
    city_name VARCHAR(100)
)
RETURNS INT
NOT DETERMINISTIC                -- 非確定性宣告,因為輸出隨時間改變
READS SQL DATA
COMMENT '查詢當前季度的人口數,輸出隨季度變化'
BEGIN
    DECLARE pop_count INT;
    DECLARE current_quarter INT;
    DECLARE current_year INT;
    
    -- 計算當前季度與年份
    -- NOW() 函式的回傳值會隨時間改變
    SET current_quarter = QUARTER(NOW());
    SET current_year = YEAR(NOW());
    
    -- 根據當前季度查詢對應的人口數
    SELECT population INTO pop_count
    FROM quarterly_population
    WHERE city = city_name
      AND quarter = current_quarter
      AND year = current_year;
    
    RETURN IFNULL(pop_count, 0);
    
    -- 注意:即使 city_name 參數相同
    -- 在不同的季度呼叫這個函式會得到不同的結果
    -- 因此必須宣告為 NOT DETERMINISTIC
END //

DELIMITER ;

-- 範例 3: 必須使用 NOT DETERMINISTIC 的情況 - 包含隨機性
-- 這個函式使用隨機數產生器,每次呼叫都可能回傳不同結果
DELIMITER //

CREATE FUNCTION f_get_random_city()
RETURNS VARCHAR(100)
NOT DETERMINISTIC                -- 非確定性,因為包含隨機選擇
READS SQL DATA
COMMENT '隨機選取一個縣市,每次呼叫可能回傳不同結果'
BEGIN
    DECLARE city_name VARCHAR(100);
    
    -- 使用 RAND() 隨機選取一個縣市
    -- RAND() 是非確定性函式,每次呼叫產生不同的隨機數
    SELECT city INTO city_name
    FROM city_reference
    ORDER BY RAND()              -- 隨機排序
    LIMIT 1;
    
    RETURN city_name;
    
    -- 即使沒有參數,每次呼叫也可能得到不同結果
    -- 因此必須宣告為 NOT DETERMINISTIC
END //

DELIMITER ;

-- 範例 4: 錯誤設定確定性特性的危險
-- 這個範例展示錯誤設定特性可能導致的問題
DELIMITER //

-- 錯誤範例:將非確定性函式錯誤標記為 DETERMINISTIC
CREATE FUNCTION f_wrong_deterministic_example()
RETURNS DATETIME
DETERMINISTIC                    -- 錯誤!此函式實際上是非確定性的
NO SQL
COMMENT '錯誤範例:不應該將 NOW() 標記為確定性'
BEGIN
    -- NOW() 每次呼叫回傳不同時間
    -- 但函式被標記為 DETERMINISTIC
    -- 可能導致 MySQL 錯誤快取結果
    RETURN NOW();
END //

DELIMITER ;

-- 測試錯誤設定的影響
SELECT 
    f_wrong_deterministic_example() AS 第一次呼叫,
    SLEEP(2),                           -- 等待 2 秒
    f_wrong_deterministic_example() AS 第二次呼叫;
-- 如果 MySQL 快取了第一次的結果,兩次呼叫可能回傳相同時間
-- 這明顯不符合函式的預期行為

-- 正確的寫法應該是:
DROP FUNCTION IF EXISTS f_wrong_deterministic_example;

DELIMITER //

CREATE FUNCTION f_correct_nondeterministic_example()
RETURNS DATETIME
NOT DETERMINISTIC                -- 正確:明確宣告為非確定性
NO SQL
COMMENT '正確範例:NOW() 函式應標記為非確定性'
BEGIN
    RETURN NOW();
END //

DELIMITER ;

SQL 資料存取特性定義了函式與資料庫資料之間的互動方式。MySQL 提供四種不同的宣告選項,每種都對應不同的使用場景與最佳化策略。NO SQL 表示函式完全不包含 SQL 陳述式,是純粹的計算邏輯;CONTAINS SQL 表示包含 SQL 語法但不讀寫資料;READS SQL DATA 表示會執行 SELECT 查詢但不修改資料;MODIFIES SQL DATA 表示會執行 INSERT、UPDATE 或 DELETE 等修改操作。正確選擇這些特性不僅影響查詢最佳化,也關係到函式在主從複製環境中的行為。

-- ============================================
-- SQL 資料存取特性的完整解析
-- ============================================

-- 特性 1: NO SQL - 純計算函式
-- 適用於不涉及任何資料庫操作的純計算邏輯
DELIMITER //

CREATE FUNCTION f_calculate_percentage_no_sql(
    part_value DECIMAL(10,2),
    total_value DECIMAL(10,2)
)
RETURNS DECIMAL(5,2)
DETERMINISTIC                    -- 純計算是確定性的
NO SQL                           -- 不包含任何 SQL 陳述式
COMMENT '計算百分比,純數學運算不涉及資料庫'
BEGIN
    -- 純粹的數學計算
    -- 不涉及任何資料庫表格的查詢或操作
    
    -- 處理除以零的情況
    IF total_value = 0 THEN
        RETURN 0;
    END IF;
    
    -- 計算百分比並四捨五入到小數點後兩位
    RETURN ROUND((part_value / total_value) * 100, 2);
    
    -- 因為完全不涉及資料庫操作
    -- MySQL 可以更積極地快取這個函式的結果
END //

DELIMITER ;

-- 特性 2: CONTAINS SQL - 包含 SQL 但不存取資料
-- 適用於包含 SQL 語法結構但不實際讀寫資料的邏輯
DELIMITER //

CREATE FUNCTION f_validate_city_name_contains_sql(
    city_name VARCHAR(100)
)
RETURNS BOOLEAN
DETERMINISTIC
CONTAINS SQL                     -- 包含 SQL 語法但不存取資料表
COMMENT '驗證縣市名稱格式,使用 SQL 語法但不查詢資料庫'
BEGIN
    -- 這個函式只進行邏輯判斷
    -- 雖然使用了 IF、CASE 等 SQL 控制結構
    -- 但不實際存取任何資料庫表格
    
    -- 檢查是否為 NULL 或空字串
    IF city_name IS NULL OR LENGTH(TRIM(city_name)) = 0 THEN
        RETURN FALSE;
    END IF;
    
    -- 檢查長度是否合理
    IF LENGTH(city_name) > 50 THEN
        RETURN FALSE;
    END IF;
    
    -- 檢查是否包含非法字元
    -- 這裡只是範例,實際驗證邏輯可能更複雜
    IF city_name REGEXP '[0-9]' THEN
        -- 縣市名稱不應包含數字
        RETURN FALSE;
    END IF;
    
    -- 通過所有驗證
    RETURN TRUE;
END //

DELIMITER ;

-- 特性 3: READS SQL DATA - 只讀取資料
-- 這是最常用的特性,適用於查詢資料但不修改的函式
DELIMITER //

CREATE FUNCTION f_calculate_population_density_reads_sql(
    city_name VARCHAR(100)
)
RETURNS DECIMAL(10,2)
DETERMINISTIC
READS SQL DATA                   -- 只讀取資料,不進行修改
COMMENT '計算人口密度,需要查詢資料庫但不修改資料'
BEGIN
    DECLARE pop INT;
    DECLARE area DECIMAL(10,2);
    DECLARE density DECIMAL(10,2);
    
    -- 從資料庫讀取人口數與面積
    -- 這是典型的只讀操作
    SELECT population, area_km2
    INTO pop, area
    FROM city_statistics
    WHERE city = city_name;
    
    -- 進行計算但不寫回資料庫
    -- 計算結果只是函式的回傳值
    IF area > 0 THEN
        SET density = pop / area;
    ELSE
        SET density = 0;
    END IF;
    
    RETURN ROUND(density, 2);
    
    -- 注意:雖然這個函式讀取了資料庫
    -- 但它不會修改任何資料
    -- 因此適合標記為 READS SQL DATA
END //

DELIMITER ;

-- 特性 4: MODIFIES SQL DATA - 修改資料
-- 注意:在函式中修改資料通常不是最佳實踐
-- 建議改用儲存程式來處理需要修改資料的邏輯
DELIMITER //

-- 這個範例僅用於說明 MODIFIES SQL DATA 的使用
-- 實務上應該使用儲存程式而非函式來修改資料
CREATE FUNCTION f_increment_query_counter_modifies_sql(
    city_name VARCHAR(100)
)
RETURNS INT
NOT DETERMINISTIC                -- 修改資料的函式通常是非確定性的
MODIFIES SQL DATA                -- 會執行 INSERT/UPDATE/DELETE
COMMENT '增加查詢計數器 - 不建議在函式中修改資料'
BEGIN
    -- 更新查詢計數
    UPDATE city_query_log
    SET query_count = query_count + 1,
        last_query_time = NOW()
    WHERE city = city_name;
    
    -- 如果記錄不存在,插入新記錄
    IF ROW_COUNT() = 0 THEN
        INSERT INTO city_query_log (city, query_count, last_query_time)
        VALUES (city_name, 1, NOW());
    END IF;
    
    -- 回傳當前的查詢次數
    RETURN (SELECT query_count FROM city_query_log WHERE city = city_name);
    
    -- 重要提示:
    -- 1. 在函式中修改資料可能影響複製環境的一致性
    -- 2. 函式中的資料修改不易進行錯誤處理與交易控制
    -- 3. 建議改用儲存程式來處理資料修改邏輯
END //

DELIMITER ;

在選擇函式特性時,需要仔細評估函式的實際行為。一個常見的錯誤是過於保守地使用預設值或較寬鬆的設定。例如,將一個實際上是確定性的函式標記為 NOT DETERMINISTIC,雖然不會造成邏輯錯誤,但會失去 MySQL 快取機制帶來的效能優勢。相反地,將非確定性函式錯誤標記為 DETERMINISTIC,則可能導致 MySQL 使用過期的快取結果,產生邏輯錯誤。因此,在設定函式特性時,需要根據函式的實際行為進行準確判斷,既不能過於保守也不能過於樂觀。

@startuml
!define DISABLE_LINK
!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 16
skinparam minClassWidth 100

title 函式特性選擇完整決策流程

start

:開始建立自訂函式;

partition "確定性特性評估" {
  :分析函式行為;
  
  if (函式是否使用 NOW()\nCURDATE() CURTIME() 等\n時間相關函式?) then (是)
    :必須使用\nNOT DETERMINISTIC;
    note right
      時間函式的回傳值
      會隨執行時間改變
    end note
  elseif (函式是否使用 RAND()\nUUID() 等隨機函式?) then (是)
    :必須使用\nNOT DETERMINISTIC;
    note right
      隨機函式每次執行
      產生不同結果
    end note
  elseif (函式是否讀取會變動的\n系統狀態或連線資訊?) then (是)
    :必須使用\nNOT DETERMINISTIC;
    note right
      例如: CURRENT_USER()
      CONNECTION_ID() 等
    end note
  elseif (函式是否查詢\n頻繁更新的資料表?) then (是)
    :建議使用\nNOT DETERMINISTIC;
    note right
      雖然從技術上可能是確定性
      但資料變化頻繁時
      快取可能導致過時結果
    end note
  else (否)
    :可以使用\nDETERMINISTIC;
    note right
      相同輸入與資料庫狀態
      必定產生相同輸出
      啟用快取機制
    end note
  endif
}

partition "SQL 資料存取特性選擇" {
  :評估資料庫互動方式;
  
  if (函式是否完全不包含\nSQL 陳述式?) then (是)
    :使用 NO SQL;
    note right
      純計算邏輯
      不涉及資料庫操作
      最高的快取與最佳化機會
    end note
  elseif (函式是否包含 SQL 語法\n但不讀寫資料表?) then (是)
    :使用 CONTAINS SQL;
    note right
      使用 IF CASE 等
      但不執行 SELECT
    end note
  elseif (函式是否會執行\nINSERT UPDATE DELETE?) then (是)
    :使用 MODIFIES SQL DATA;
    note right
      會修改資料庫內容
      但建議改用儲存程式
    end note
    :發出警告:\n考慮改用儲存程式;
  else (只執行 SELECT)
    :使用 READS SQL DATA;
    note right
      只讀取資料
      不修改任何內容
      最常用的設定
    end note
  endif
}

:完成特性設定;

:建立函式;

partition "驗證與測試" {
  :測試函式行為;
  :驗證特性設定是否正確;
  
  if (實際行為與宣告\n是否一致?) then (否)
    :修正特性設定;
    :重新建立函式;
  else (是)
    :函式建立完成;
  endif
}

stop

@enduml

這個完整的決策流程圖系統性地展示了如何根據函式的實際行為選擇適當的特性設定。從確定性特性的評估開始,逐一檢查函式是否使用時間函式、隨機函式、或查詢頻繁變動的資料。然後評估 SQL 資料存取特性,判斷函式是純計算、只包含控制邏輯、只讀取資料、還是會修改資料。每個決策點都提供了清晰的判斷標準與相應的特性選擇建議。遵循這個流程,開發者能夠為函式選擇最適當的特性設定,既確保邏輯正確性,又能充分發揮 MySQL 查詢最佳化器的能力。

效能最佳化策略與企業級開發最佳實踐

在將自訂函式與儲存程式部署到生產環境之前,效能考量與最佳化策略必須成為設計過程中的核心要素。一個在開發環境中表現良好的函式,在面對生產環境的大量資料與高併發負載時,可能成為整個系統的效能瓶頸。因此,需要從多個維度系統性地思考如何設計高效能的資料庫程式化物件,包括函式呼叫成本的控制、索引使用的最佳化、快取機制的善用、以及效能監控與診斷的完整策略。

函式呼叫本身存在固定的執行成本。每次函式被呼叫時,MySQL 需要執行一系列操作:解析函式定義、建立執行環境、準備參數、執行函式邏輯、清理執行環境。當函式在處理數百萬筆資料的查詢中被重複呼叫時,這些看似微小的成本會累積成顯著的效能影響。更嚴重的是,如果函式內部包含複雜的查詢邏輯或多次資料庫存取,每次呼叫的成本會進一步放大。因此,在設計函式時,需要仔細評估函式的呼叫頻率與執行成本之間的平衡。

-- ============================================
-- 效能最佳化範例:減少函式呼叫次數
-- ============================================

-- 效能問題範例:在大型查詢中重複呼叫多個函式
-- 這個查詢的效能可能不佳,因為每筆資料都要呼叫三個函式
SELECT
    city,
    population,
    -- 呼叫函式 1:取得平均氣溫
    f_get_average_temperature(city) AS 平均氣溫,
    -- 呼叫函式 2:取得平均雨量
    f_get_average_rainfall(city) AS 平均雨量,
    -- 呼叫函式 3:計算舒適度指數
    -- 這個函式內部又呼叫了前兩個函式
    f_calculate_comfort_index(
        f_get_average_temperature(city),
        f_get_average_rainfall(city)
    ) AS 舒適度指數
FROM city_reference
WHERE population > 100000;

-- 問題分析:
-- 1. 如果 city_reference 有 100 筆記錄
-- 2. 每筆記錄都會呼叫 f_get_average_temperature 和 f_get_average_rainfall
-- 3. 共執行 200 次函式呼叫,每次都可能涉及資料庫查詢
-- 4. 如果函式未標記為 DETERMINISTIC,無法使用快取
-- 5. 總執行時間 = 100 × (呼叫時間1 + 呼叫時間2 + 呼叫時間3)

-- 最佳化方案 1:使用 JOIN 預先取得資料,減少函式呼叫
-- 將資料查詢從函式內部移到主查詢的 JOIN 中
SELECT
    cr.city,
    cr.population,
    cd.avg_temperature AS 平均氣溫,
    cd.avg_rainfall AS 平均雨量,
    -- 只呼叫一次計算函式,參數直接使用 JOIN 取得的資料
    f_calculate_comfort_index(
        cd.avg_temperature,
        cd.avg_rainfall
    ) AS 舒適度指數
FROM city_reference cr
JOIN climate_data cd ON cr.city = cd.city
WHERE cr.population > 100000;

-- 優勢分析:
-- 1. 氣溫和雨量資料透過 JOIN 一次性取得,不需要函式呼叫
-- 2. 只有舒適度計算需要呼叫函式,且參數不涉及資料庫查詢
-- 3. 如果 climate_data 表格有適當索引,JOIN 效能優異
-- 4. 總執行時間 = JOIN 時間 + 100 × 計算時間(純數學運算)

-- 最佳化方案 2:確保函式被標記為 DETERMINISTIC
-- 將純計算函式正確標記,啟用快取機制
DELIMITER //

CREATE FUNCTION f_calculate_comfort_index_optimized(
    temperature DECIMAL(5,2),
    rainfall DECIMAL(8,2)
)
RETURNS DECIMAL(5,2)
DETERMINISTIC                    -- 啟用結果快取
NO SQL                           -- 純計算,不存取資料庫
COMMENT '最佳化版本:純計算舒適度指數,支援快取'
BEGIN
    DECLARE comfort_value DECIMAL(5,2);
    
    -- 基於溫度和降雨量的舒適度計算公式
    -- 25°C 時舒適度最高,降雨越少舒適度越高
    SET comfort_value = (
        (25 - ABS(temperature - 25)) * 0.4 +  -- 溫度因子
        (100 - rainfall) * 0.01 * 0.6          -- 降雨因子
    );
    
    -- 確保回傳值在 0-100 範圍內
    IF comfort_value < 0 THEN
        RETURN 0;
    ELSEIF comfort_value > 100 THEN
        RETURN 100;
    ELSE
        RETURN ROUND(comfort_value, 2);
    END IF;
END //

DELIMITER ;

-- ============================================
-- 效能最佳化範例:避免在 WHERE 子句中對索引欄位使用函式
-- ============================================

-- 效能問題範例:對索引欄位使用函式導致索引失效
-- 即使 population 欄位有索引,這個查詢也可能執行全表掃描
SELECT *
FROM city_reference
WHERE f_calculate_population_density(city, population, area_km2) > 1000;

-- 問題分析:
-- 1. WHERE 子句中對 population 使用了函式
-- 2. MySQL 無法使用 population 欄位上的索引
-- 3. 必須掃描全表,對每筆記錄執行函式計算
-- 4. 如果資料表有百萬筆記錄,效能會非常差

-- 最佳化方案 1:將函式移到 SELECT 清單,WHERE 使用原始欄位
-- 讓 WHERE 子句可以使用索引進行快速過濾
SELECT
    city,
    population,
    area_km2,
    f_calculate_population_density(city, population, area_km2) AS population_density
FROM city_reference
WHERE population / area_km2 > 1000;  -- 直接計算,可使用索引
-- 注意:這裡假設 (population / area_km2) 的計算方式與函式內部相同

-- 優勢分析:
-- 1. WHERE 子句使用原始欄位的計算,可以利用索引
-- 2. 先透過索引快速過濾出符合條件的記錄
-- 3. 只對過濾後的記錄執行函式(在 SELECT 清單中)
-- 4. 大幅減少函式執行次數

-- 最佳化方案 2:建立計算欄位並建立索引
-- 這是最根本的解決方案,但需要修改資料表結構
ALTER TABLE city_reference
ADD COLUMN population_density DECIMAL(10,2)
GENERATED ALWAYS AS (population / area_km2) STORED;

-- 為計算欄位建立索引
CREATE INDEX idx_population_density
ON city_reference(population_density);

-- 現在查詢可以充分利用索引
SELECT city, population, area_km2, population_density
FROM city_reference
WHERE population_density > 1000;

-- 優勢分析:
-- 1. population_density 是預先計算並儲存的
-- 2. 有專門的索引支援快速查詢
-- 3. 不需要在查詢時重複計算
-- 4. 是效能最佳但需要額外儲存空間的方案

-- ============================================
-- 效能最佳化範例:使用 CTE 避免重複計算
-- ============================================

-- 效能問題範例:相同的計算在查詢中重複執行
SELECT
    order_id,
    quantity,
    unit_price,
    tax_rate,
    -- 計算小計(第 1 次)
    quantity * unit_price AS subtotal,
    -- 計算稅金(第 2 次計算小計)
    quantity * unit_price * tax_rate AS tax,
    -- 計算總計(第 3 次計算小計)
    quantity * unit_price * (1 + tax_rate) AS total,
    -- 判斷客戶類型(第 4 次計算小計)
    CASE
        WHEN quantity * unit_price > 10000 THEN 'VIP'
        WHEN quantity * unit_price > 5000 THEN 'Premium'
        ELSE 'Regular'
    END AS customer_type
FROM orders
WHERE order_date >= '2024-01-01';

-- 問題分析:
-- 1. quantity * unit_price 被計算了 4 次
-- 2. 雖然是簡單運算,但在大量資料時累積成本顯著
-- 3. 程式碼可讀性差,維護困難

-- 最佳化方案:使用 CTE 一次計算,多次使用
WITH calculated_orders AS (
    -- 第一階段:計算基礎值
    SELECT
        order_id,
        quantity,
        unit_price,
        tax_rate,
        quantity * unit_price AS subtotal  -- 只計算一次
    FROM orders
    WHERE order_date >= '2024-01-01'
)
-- 第二階段:使用預先計算的值
SELECT
    order_id,
    quantity,
    unit_price,
    tax_rate,
    subtotal,
    subtotal * tax_rate AS tax,           -- 重複使用 subtotal
    subtotal * (1 + tax_rate) AS total,   -- 重複使用 subtotal
    CASE
        WHEN subtotal > 10000 THEN 'VIP'
        WHEN subtotal > 5000 THEN 'Premium'
        ELSE 'Regular'
    END AS customer_type
FROM calculated_orders;

-- 優勢分析:
-- 1. quantity * unit_price 只計算一次
-- 2. 後續所有使用都直接參考 subtotal
-- 3. 程式碼結構清晰,易於理解和維護
-- 4. 效能提升與可讀性提升兼得

在生產環境中監控函式效能是持續最佳化的基礎。MySQL 的 Performance Schema 提供了豐富的效能監控能力,可以追蹤每個函式的執行次數、總執行時間、平均執行時間等關鍵指標。透過定期分析這些指標,可以識別出效能瓶頸所在,針對性地進行最佳化。

-- ============================================
-- 效能監控與診斷
-- ============================================

-- 步驟 1:啟用 Performance Schema 的函式監控
-- 這個設定通常在 MySQL 設定檔中配置,或在執行階段啟用
UPDATE performance_schema.setup_instruments
SET ENABLED = 'YES', TIMED = 'YES'
WHERE NAME LIKE 'statement/sp/%';

-- 步驟 2:查詢函式執行統計資訊
-- 找出執行最頻繁和最耗時的函式
SELECT
    OBJECT_SCHEMA AS 資料庫,
    OBJECT_NAME AS 函式名稱,
    OBJECT_TYPE AS 物件類型,
    COUNT_EXECUTE AS 執行次數,
    SUM_TIMER_WAIT / 1000000000000 AS 總執行時間秒,
    AVG_TIMER_WAIT / 1000000000000 AS 平均執行時間秒,
    MIN_TIMER_WAIT / 1000000000000 AS 最小執行時間秒,
    MAX_TIMER_WAIT / 1000000000000 AS 最大執行時間秒
FROM performance_schema.events_statements_summary_by_program
WHERE OBJECT_TYPE = 'FUNCTION'
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 20;

-- 步驟 3:識別需要最佳化的函式
-- 找出執行次數多且平均耗時較長的函式
SELECT
    OBJECT_NAME AS 需要最佳化的函式,
    COUNT_EXECUTE AS 執行次數,
    ROUND(AVG_TIMER_WAIT / 1000000000000, 4) AS 平均執行時間秒,
    ROUND(SUM_TIMER_WAIT / 1000000000000, 2) AS 總耗時秒,
    -- 計算該函式佔總體執行時間的比例
    ROUND(
        (SUM_TIMER_WAIT * 100.0) / 
        (SELECT SUM(SUM_TIMER_WAIT) 
         FROM performance_schema.events_statements_summary_by_program 
         WHERE OBJECT_TYPE = 'FUNCTION'
        ), 2
    ) AS 佔總體時間百分比
FROM performance_schema.events_statements_summary_by_program
WHERE OBJECT_TYPE = 'FUNCTION'
  AND COUNT_EXECUTE > 1000              -- 執行次數超過 1000 次
  AND AVG_TIMER_WAIT > 10000000         -- 平均執行時間超過 0.01 秒
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;

-- 步驟 4:清除統計資訊以進行新的測試
-- 在最佳化後,清除舊的統計資訊以評估改進效果
TRUNCATE TABLE performance_schema.events_statements_summary_by_program;

總結與企業級資料庫程式化開發體系建議

本文系統性地探討了 MySQL 自訂函式與儲存程式的完整技術體系,從基礎的語法結構與建立流程,到進階的特性設定與效能最佳化策略,提供了全面而深入的技術解析。透過豐富的實務案例與詳盡的程式碼註解,展示了如何在企業級應用開發中正確設計、實作並部署這兩種資料庫程式化物件,建立高效能、高可維護性且符合企業標準的資料庫應用程式介面。

在技術實踐層面,自訂函式與儲存程式各有其適用場景與設計考量。函式適合封裝確定性的計算邏輯與資料轉換,可以直接嵌入 SQL 查詢中使用,是實現可重用資料處理邏輯的理想工具。儲存程式則更適合處理複雜的業務流程,支援多步驟資料操作、交易控制與完整的錯誤處理機制,是實作企業級商業邏輯的核心手段。正確理解兩者的差異與各自的優勢,才能在實務開發中做出最適切的技術選擇。

函式特性的正確設定是發揮 MySQL 查詢最佳化能力的關鍵。DETERMINISTIC 特性的準確宣告能夠啟用快取機制,在高併發場景下帶來數倍的效能提升。SQL 資料存取特性的適當選擇不僅影響查詢最佳化器的決策,也關係到函式在複製環境中的行為。開發者需要根據函式的實際行為仔細評估,既不能過於保守而失去最佳化機會,也不能過於樂觀而導致邏輯錯誤或資料不一致。

效能最佳化必須成為資料庫程式化開發的核心考量。從減少函式呼叫次數、避免索引失效、善用快取機制,到建立完整的效能監控體系,每個環節都可能成為系統效能的決定因素。在大規模資料處理與高併發訪問的企業環境中,即使是微小的效能差異也會被放大成顯著的影響。因此,效能測試與監控應該成為開發流程的標準環節,而非事後補救的措施。

展望未來,資料庫程式化開發將朝向更精細的邏輯封裝、更智慧的查詢最佳化、以及更完善的開發工具鏈方向發展。MySQL 持續演進中的新版本將提供更強大的函式特性控制、更細緻的效能監控工具、以及更完善的錯誤處理機制。對於企業開發團隊而言,及早掌握這些核心技術,建立紮實的資料庫程式化開發基礎,將在未來的技術競爭中保持優勢,為企業的數位轉型提供堅實的技術支撐。