在現代企業級資料庫應用程式開發的技術領域中,數值運算能力是資料處理與分析的核心基礎設施。無論是財務系統的精確金額計算、銷售分析的統計指標運算、科學研究的複雜數學建模,還是商業智慧的即時數據處理,MySQL 提供的數學運算子與內建函式都扮演著關鍵角色。然而,許多開發者習慣性地將數值運算邏輯放置在應用程式層級處理,卻往往忽略了資料庫本身具備的強大數學計算能力。這種做法不僅增加了應用程式的複雜度與維護成本,更在處理大規模數據時產生了顯著的效能瓶頸與網路傳輸負擔。

MySQL 作為全球最廣泛使用的開源關聯式資料庫管理系統,其數學運算系統的設計理念著重於效能、精度與標準化三個核心面向。從最基礎的加減乘除四則運算,到進階的三角函式、對數運算與指數計算,MySQL 提供了完整且符合 SQL 標準的數學運算能力。這些運算子與函式不僅能夠在 SELECT 查詢中直接使用,更可以整合到複雜的資料轉換流程、聚合分析計算,以及儲存過程的業務邏輯中。善用這些內建功能,不僅能夠簡化應用程式的程式碼結構,更能在處理數百萬筆資料時獲得數十倍的效能提升。

在數值運算的實踐中,精度控制是一個經常被低估但卻至關重要的議題。浮點數的精度限制、捨入誤差的累積效應、以及不同數值型別之間的隱式轉換規則,都可能對計算結果產生微妙但關鍵的影響。在財務應用中,即使是小數點後幾位的誤差,在累積大量交易後也可能造成顯著的金額差異。因此,深入理解 MySQL 的數值型別系統、掌握 ROUND 與 TRUNCATE 等函式的精確行為、以及正確選擇 DECIMAL 與 FLOAT 等資料型別,都是建立可靠數值處理系統的必要基礎。

效能最佳化是數學運算應用中另一個不可忽視的面向。當數學函式被應用在包含索引的欄位上時,可能導致索引失效而引發全表掃描,這在大型資料表上會造成災難性的效能衰退。理解運算子的執行順序、掌握查詢最佳化器的運作機制、善用計算欄位的快取策略,以及適當使用 CTE 或子查詢來組織複雜計算,都是提升查詢效能的關鍵技巧。同時,在設計資料表結構時,預先計算常用的衍生欄位並建立適當的索引,也是常見的效能優化策略。

本文將系統化地探討 MySQL 數學運算系統的完整技術架構與實務應用。從基礎的四則運算子與模數運算的應用場景,到運算子優先順序與括號控制的精確機制;從絕對值、取整、四捨五入等基礎數值函式的實務應用,到指數、對數、三角函式等進階科學計算的完整解析;從企業級的財務計算、統計分析與金融建模案例,到效能優化策略與精度控制的最佳實踐。透過詳盡的程式碼範例與深入的技術剖析,協助讀者建立紮實的理論基礎與實務能力,最終能夠獨立設計並實作高效能、高精度且符合企業標準的資料庫數值處理系統。

MySQL 數學運算系統的架構理念與設計原則

在深入探討具體的運算子與函式之前,理解 MySQL 數學運算系統的整體架構與設計理念至關重要。這些運算能力並非孤立的功能模組,而是深度整合在 MySQL 的查詢處理引擎中,與資料型別系統、查詢最佳化器、索引機制等核心元件緊密協作。正確理解這些架構層級的設計原則,不僅有助於選擇適當的運算方式,更能在效能調校與問題診斷時提供關鍵洞察。

MySQL 的數學運算系統遵循 SQL 標準的基本規範,但也加入了許多實用的擴充功能。基礎運算子如加減乘除,其語法與大多數程式語言完全一致,這降低了學習曲線並提升了程式碼的可讀性。內建函式則採用標準的函式呼叫語法,參數傳遞遵循明確的型別轉換規則。這種設計讓開發者能夠將熟悉的數學概念直接應用到 SQL 查詢中,無需學習專門的語法結構。

在運算執行的層面,MySQL 查詢最佳化器會分析包含數學運算的查詢,嘗試找出最有效率的執行計畫。例如,當運算涉及常數時,最佳化器可能在查詢編譯階段就完成計算,避免在執行階段重複運算。當運算結果需要用於過濾條件時,最佳化器會評估是否能夠使用索引來加速查詢。然而,這些最佳化機制也有其限制,開發者需要理解哪些情況下運算會阻礙索引使用,以及如何調整查詢結構來獲得更好的效能。

數值型別系統是數學運算的基礎。MySQL 提供了多種數值型別,包括整數型別(TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT)、浮點數型別(FLOAT、DOUBLE)與定點數型別(DECIMAL)。不同型別在精度、儲存空間與運算效能上有著顯著差異。整數運算通常最快但不能表示小數,浮點數運算快速但有精度限制,定點數提供精確計算但運算較慢。在設計數值欄位時,必須根據業務需求權衡這些因素。

@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

package "MySQL 數學運算系統架構" {
  
  rectangle "查詢解析器" as Parser
  rectangle "查詢最佳化器" as Optimizer
  rectangle "執行引擎" as Executor
  
  rectangle "加法運算子" as Add
  rectangle "減法運算子" as Sub
  rectangle "乘法運算子" as Mul
  rectangle "除法運算子" as Div
  rectangle "整數除法 DIV" as IntDiv
  rectangle "模數運算 MOD" as Mod
  
  rectangle "絕對值 ABS()" as Abs
  rectangle "取整 CEILING/FLOOR" as Round1
  rectangle "四捨五入 ROUND" as Round2
  rectangle "指數運算 POW" as Exp
  rectangle "對數運算 LOG" as Log
  rectangle "三角函式 SIN/COS" as Trig
  rectangle "平方根 SQRT" as Sqrt
  
  rectangle "整數型別 INT" as IntType
  rectangle "浮點數型別 FLOAT" as FloatType
  rectangle "定點數型別 DECIMAL" as DecType
  
  database "儲存引擎" as SE
}

Parser --> Optimizer
Optimizer --> Executor

Executor --> Add
Executor --> Sub
Executor --> Mul
Executor --> Div

Executor --> Abs
Executor --> Round1
Executor --> Exp
Executor --> Log

Add --> IntType
Mul --> FloatType
Abs --> DecType

IntType --> SE
FloatType --> SE
DecType --> SE

note right of Optimizer
  最佳化策略:
  - 常數折疊
  - 運算式簡化
  - 索引使用評估
end note

note right of DecType
  型別轉換規則:
  - 整數 + 浮點數 → 浮點數
  - 整數 + DECIMAL → DECIMAL
  - FLOAT + DOUBLE → DOUBLE
end note

@enduml

在實務應用中,數學運算通常不會單獨出現,而是整合在更大的查詢邏輯中。運算結果可能用於 SELECT 子句的欄位計算、WHERE 子句的條件判斷、GROUP BY 子句的分組依據,或是 ORDER BY 子句的排序鍵值。理解運算在不同查詢子句中的執行時機與性能影響,對於撰寫高效率的 SQL 查詢至關重要。例如,在 WHERE 子句中對索引欄位進行運算會導致索引失效,但在 SELECT 子句中進行相同運算則不影響查詢的過濾效能。

基礎四則運算的深度應用與型別轉換機制

四則運算是所有數學計算的基礎,MySQL 使用標準的算術運算子符號來表示這些運算。加法使用 + 符號,減法使用 - 符號,乘法使用 * 符號,除法使用 / 符號。這些運算子不僅語法簡潔,更能與 SQL 的其他語法元素無縫整合,讓複雜的數值計算能夠以清晰易讀的方式表達。

在實務應用中,四則運算經常用於計算衍生欄位。例如在電子商務系統中,訂單金額需要根據商品單價與數量計算,折扣金額需要根據原價與折扣率計算,稅金需要根據小計與稅率計算。這些計算如果在應用程式層級處理,不僅增加了程式碼複雜度,更需要將大量原始資料從資料庫傳輸到應用程式,造成網路頻寬的浪費。將計算邏輯直接寫在 SQL 查詢中,可以讓資料庫伺服器完成計算後只傳回最終結果,大幅減少資料傳輸量。

-- 電子商務訂單金額計算的完整範例
-- 展示四則運算在實際業務場景中的綜合應用
SELECT
    order_id,                                        -- 訂單編號
    customer_name,                                    -- 客戶名稱
    
    -- 基本金額計算
    -- 小計 = 商品單價 × 購買數量
    unit_price,                                      -- 商品單價
    quantity,                                        -- 購買數量
    unit_price * quantity AS subtotal,               -- 小計金額
    
    -- 折扣計算
    -- 折扣金額 = 小計 × 折扣率
    discount_rate,                                   -- 折扣率(小數形式,如 0.15 表示 85 折)
    unit_price * quantity * discount_rate AS discount_amount,  -- 折扣金額
    
    -- 折扣後金額計算
    -- 方式一:小計 - 折扣金額
    unit_price * quantity - (unit_price * quantity * discount_rate) AS net_amount_1,
    -- 方式二:小計 × (1 - 折扣率) - 更簡潔的寫法
    unit_price * quantity * (1 - discount_rate) AS net_amount_2,
    
    -- 稅金計算
    -- 稅金 = 折扣後金額 × 稅率
    tax_rate,                                        -- 稅率(小數形式)
    unit_price * quantity * (1 - discount_rate) * tax_rate AS tax_amount,
    
    -- 最終應付金額
    -- 最終金額 = 折扣後金額 + 稅金
    -- 等同於:折扣後金額 × (1 + 稅率)
    unit_price * quantity * (1 - discount_rate) * (1 + tax_rate) AS final_total,
    
    -- 利潤計算(假設成本為售價的 60%)
    -- 利潤 = (售價 - 成本) × 數量 × (1 - 折扣率)
    (unit_price - unit_price * 0.6) * quantity * (1 - discount_rate) AS profit_amount

FROM orders
WHERE order_date >= '2024-01-01'                     -- 篩選 2024 年的訂單
    AND status = 'completed'                         -- 只計算已完成的訂單
ORDER BY final_total DESC;                           -- 按最終金額降序排列

型別轉換在四則運算中扮演著重要角色。當運算涉及不同型別的數值時,MySQL 會自動進行型別提升以確保計算的正確性。一般規則是將較低精度的型別轉換為較高精度的型別。例如,整數與浮點數相加時,整數會被轉換為浮點數。整數與 DECIMAL 相加時,整數會被轉換為 DECIMAL。FLOAT 與 DOUBLE 相加時,FLOAT 會被提升為 DOUBLE。這些隱式轉換通常是透明的,但在某些情況下可能導致精度損失或效能問題。

除法運算需要特別注意。MySQL 的 / 運算子會傳回浮點數結果,即使被除數和除數都是整數。這與某些程式語言(如早期的 Python 2)不同。如果需要整數除法,應該使用 DIV 運算子。此外,除以零在 MySQL 中不會產生錯誤,而是傳回 NULL。這種設計避免了查詢因為個別資料問題而完全失敗,但也需要開發者在使用結果時檢查 NULL 值。

-- 除法運算的細節與注意事項
SELECT
    -- 一般除法:傳回浮點數
    10 / 3 AS float_division,                        -- 結果:3.3333
    10.0 / 3.0 AS explicit_float,                    -- 結果:3.3333
    
    -- 整數除法:只傳回商的整數部分
    10 DIV 3 AS integer_division,                    -- 結果:3
    
    -- 模數運算:傳回餘數
    10 % 3 AS modulo,                                -- 結果:1
    10 MOD 3 AS modulo_alt,                          -- 結果:1
    
    -- 除以零的處理
    10 / 0 AS divide_by_zero,                        -- 結果:NULL
    10 DIV 0 AS integer_div_zero,                    -- 結果:NULL
    
    -- 實務應用:計算平均值並處理除以零
    total_amount,
    order_count,
    CASE 
        WHEN order_count = 0 THEN 0                  -- 避免除以零
        ELSE total_amount / order_count 
    END AS average_order_value,
    
    -- 或使用 NULLIF 函式
    total_amount / NULLIF(order_count, 0) AS avg_value_with_nullif

FROM sales_summary;

在進行大量數值計算時,運算順序的效率也值得考慮。雖然現代資料庫最佳化器通常能夠重新排列運算順序以提升效能,但開發者仍應該盡量將常數計算移到查詢外部,避免對每一列資料都重複進行相同的常數運算。例如,如果查詢中有 price * 1.08 這樣的運算(假設稅率固定為 8%),可以預先計算好 1.08 這個常數,或是在應用程式層級處理這類固定比率的計算。

模數運算的實務應用與循環分組技巧

模數運算(Modulo Operation)計算除法的餘數,是程式設計中極為實用的運算。MySQL 提供了兩種等價的語法:% 運算子與 MOD 函式。這兩種寫法在功能上完全相同,開發者可以根據個人偏好或團隊編碼規範選擇使用。模數運算在判斷奇偶性、實現循環分組、計算週期位置等場景中都有廣泛應用。

模數運算最直觀的應用是判斷數字的奇偶性。當一個數除以 2 的餘數為 0 時,該數為偶數;餘數為 1 時則為奇數。這個簡單的技巧在資料分析中特別有用,例如可以用來驗證數據的隨機分布情況,或是在測試環境中將數據分成兩組進行 A/B 測試。在某些業務場景中,奇偶性本身就具有特殊含義,例如某些彩票系統會根據號碼的奇偶性來劃分獎項類別。

-- 使用模數運算進行奇偶性判斷與統計分析
SELECT
    -- 基本奇偶性判斷
    number_value,
    number_value % 2 AS remainder,                   -- 餘數:0 或 1
    
    -- 使用 CASE 陳述式標記奇偶性
    CASE number_value % 2
        WHEN 0 THEN '偶數'
        WHEN 1 THEN '奇數'
    END AS parity_label,
    
    -- 更簡潔的寫法:使用 IF 函式
    IF(number_value % 2 = 0, '偶數', '奇數') AS parity_simple

FROM number_table;

-- 實務應用:分析訂單編號的奇偶分布
-- 如果訂單編號是隨機生成的,奇偶數應該大致相等
-- 如果分布不均,可能表示編號生成演算法有問題
SELECT
    CASE order_id % 2
        WHEN 0 THEN '偶數編號'
        ELSE '奇數編號'
    END AS order_type,
    COUNT(*) AS order_count,                         -- 訂單數量
    ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM orders), 2) AS percentage,  -- 百分比
    SUM(total_amount) AS total_sales,                -- 總銷售額
    AVG(total_amount) AS avg_order_value             -- 平均訂單金額

FROM orders
WHERE order_date >= '2024-01-01'
GROUP BY order_id % 2
ORDER BY order_type;

循環分組是模數運算的另一個重要應用。當需要將資料均勻分配到多個類別或處理器時,模數運算提供了簡單而有效的解決方案。例如,在分散式系統中,可以根據記錄 ID 的模數值將資料分配到不同的伺服器節點,實現負載平衡。在批次處理場景中,可以將大量任務分配給多個工作程序平行處理。這種分配方式保證了資料的均勻分布,避免某些節點或程序負載過重。

-- 循環分組:將任務均勻分配到多個處理器
SELECT
    task_id,
    task_name,
    priority,
    
    -- 方式一:分配到 4 個處理器(編號 0-3)
    task_id % 4 AS processor_id_zero_based,
    
    -- 方式二:分配到 4 個處理器(編號 1-4)
    -- 使用公式:(task_id - 1) % n + 1
    -- 這確保編號從 1 開始而不是 0
    (task_id - 1) % 4 + 1 AS processor_id_one_based,
    
    -- 根據分配結果統計每個處理器的負載
    COUNT(*) OVER (PARTITION BY (task_id - 1) % 4 + 1) AS tasks_per_processor

FROM task_queue
WHERE status = 'pending'                             -- 只分配待處理的任務
ORDER BY processor_id_one_based, priority DESC;      -- 按處理器編號和優先順序排序

-- 實務應用:資料庫分片策略
-- 根據使用者 ID 將資料分散到不同的資料庫分片
SELECT
    user_id,
    username,
    email,
    
    -- 計算應該儲存在哪個分片
    -- 假設有 16 個分片
    user_id % 16 AS shard_id,
    
    -- 生成分片的連線字串
    CONCAT('shard_', user_id % 16, '.database.com') AS shard_host

FROM users
WHERE registration_date >= '2024-01-01'
ORDER BY shard_id, user_id;

模數運算在處理週期性資料時也特別有用。許多業務場景具有週期性特徵,例如一週七天、一年十二個月、一天二十四小時等。使用模數運算可以將連續的時間序列轉換為週期位置,便於進行週期性分析或預測。例如,分析星期幾的銷售模式、計算月份中的某一天、或是判斷小時的尖峰離峰時段。

-- 使用模數運算進行週期性分析
SELECT
    transaction_date,
    transaction_time,
    amount,
    
    -- 計算星期幾(0=週日, 1=週一, ..., 6=週六)
    DAYOFWEEK(transaction_date) - 1 AS day_of_week_num,
    
    -- 計算是一年中的第幾天
    DAYOFYEAR(transaction_date) AS day_of_year,
    
    -- 計算是一年中的第幾週(以 7 天為一週期)
    CEIL(DAYOFYEAR(transaction_date) / 7) AS week_of_year,
    
    -- 使用模數判斷是週期的第幾天(0-6)
    DAYOFYEAR(transaction_date) % 7 AS week_position,
    
    -- 判斷是否為工作日(週一到週五)
    CASE 
        WHEN (DAYOFWEEK(transaction_date) - 1) BETWEEN 1 AND 5 THEN '工作日'
        ELSE '週末'
    END AS day_type,
    
    -- 計算小時(0-23)
    HOUR(transaction_time) AS hour_of_day,
    
    -- 判斷營業時段
    CASE 
        WHEN HOUR(transaction_time) BETWEEN 9 AND 11 THEN '上午時段'
        WHEN HOUR(transaction_time) BETWEEN 12 AND 14 THEN '午餐時段'
        WHEN HOUR(transaction_time) BETWEEN 15 AND 17 THEN '下午時段'
        WHEN HOUR(transaction_time) BETWEEN 18 AND 20 THEN '晚餐時段'
        ELSE '其他時段'
    END AS business_period

FROM transactions
WHERE transaction_date >= '2024-01-01'
ORDER BY transaction_date, transaction_time;

-- 統計每週各天的平均交易量
SELECT
    CASE (DAYOFWEEK(transaction_date) - 1)
        WHEN 0 THEN '週日'
        WHEN 1 THEN '週一'
        WHEN 2 THEN '週二'
        WHEN 3 THEN '週三'
        WHEN 4 THEN '週四'
        WHEN 5 THEN '週五'
        WHEN 6 THEN '週六'
    END AS weekday,
    COUNT(*) AS transaction_count,
    SUM(amount) AS total_amount,
    AVG(amount) AS avg_amount

FROM transactions
WHERE transaction_date >= '2024-01-01'
GROUP BY (DAYOFWEEK(transaction_date) - 1)
ORDER BY (DAYOFWEEK(transaction_date) - 1);

在使用模數運算時需要注意符號的處理。MySQL 中模數運算的結果符號與被除數相同。也就是說,7 % 3 的結果是 1,而 -7 % 3 的結果是 -1。這與某些程式語言的行為可能不同,在處理可能為負數的資料時需要特別留意。如果需要確保結果永遠為正,可以使用 ((value % modulus) + modulus) % modulus 這樣的公式。

運算子優先順序與括號控制的精確機制

在包含多個運算子的複雜數學運算式中,運算子的優先順序決定了計算的執行順序。MySQL 遵循標準的數學運算優先順序規則,但在撰寫複雜的 SQL 查詢時,明確使用括號來控制運算順序不僅能確保計算的正確性,更能大幅提升程式碼的可讀性。錯誤的運算順序可能導致微妙但嚴重的計算錯誤,特別是在財務或科學計算等對精度要求極高的場景中。

MySQL 的數學運算子優先順序由高到低排列如下:首先是乘法(*)、除法(/)、整數除法(DIV)與模數運算(%, MOD),這些運算子具有相同的優先順序;其次是加法(+)與減法(-),同樣具有相同的優先順序。當運算式中出現多個相同優先順序的運算子時,MySQL 會按照從左到右的順序依序計算。括號的優先順序最高,括號內的運算式會優先於所有運算子執行。

@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 運算子優先順序與計算順序影響

package "錯誤的計算順序" {
  rectangle "salary + bonus * tax_rate" as expr1 {
    rectangle "步驟 1\nbonus * tax_rate = 1200" as step1a
    rectangle "步驟 2\nsalary + 1200 = 51200" as step1b
    
    step1a -down-> step1b
    
    note right of step1b
      錯誤結果:51200
      
      假設:
      salary = 50000
      bonus = 10000
      tax_rate = 0.12
      
      問題:
      只對 bonus 計算了稅金
      沒有對 salary 計算稅金
    end note
  }
}

package "正確的計算順序" {
  rectangle "(salary + bonus) * tax_rate" as expr2 {
    rectangle "步驟 1\nsalary + bonus = 60000" as step2a
    rectangle "步驟 2\n60000 * tax_rate = 7200" as step2b
    
    step2a -down-> step2b
    
    note right of step2b
      正確結果:7200
      
      計算邏輯:
      1. 先計算總收入
      2. 再計算總收入的稅金
      
      這才是正確的
      稅金計算方式
    end note
  }
}

@enduml

在實務應用中,稅金計算是一個經典的優先順序陷阱案例。假設我們需要計算員工的應繳稅金,稅金應該是(基本薪資 + 獎金)× 稅率。如果查詢寫成 salary + bonus * tax_rate,由於乘法優先於加法,實際計算順序會是 salary + (bonus * tax_rate),這會產生完全錯誤的結果。正確的寫法應該是 (salary + bonus) * tax_rate,使用括號明確指定先計算總收入再乘以稅率。

-- 運算子優先順序的陷阱與正確寫法
SELECT
    employee_id,
    employee_name,
    salary,
    bonus,
    tax_rate,
    
    -- 錯誤寫法:未使用括號,運算順序錯誤
    -- 計算過程:salary + (bonus * tax_rate)
    -- 假設 salary=50000, bonus=10000, tax_rate=0.12
    -- 結果:50000 + 1200 = 51200 (錯誤!)
    salary + bonus * tax_rate AS wrong_calculation,
    
    -- 正確寫法一:使用括號明確指定運算順序
    -- 計算過程:(salary + bonus) * tax_rate
    -- 結果:(50000 + 10000) * 0.12 = 7200 (正確)
    (salary + bonus) * tax_rate AS correct_tax,
    
    -- 正確寫法二:分步計算,提高可讀性
    -- 先計算總收入,再計算稅金
    salary + bonus AS gross_income,
    (salary + bonus) * tax_rate AS tax_amount,
    
    -- 計算稅後淨收入
    -- 淨收入 = 總收入 - 稅金 = 總收入 × (1 - 稅率)
    (salary + bonus) * (1 - tax_rate) AS net_income

FROM payroll
WHERE department = 'Engineering';

-- 更複雜的範例:計算員工的實際到手金額
-- 考慮多種扣除項目:勞保、健保、退休金、稅金
SELECT
    employee_id,
    employee_name,
    base_salary,
    allowance,                                       -- 津貼
    bonus,
    
    -- 步驟 1:計算稅前總收入
    (base_salary + allowance + bonus) AS gross_total,
    
    -- 步驟 2:計算各項扣除
    -- 勞保費用 = (基本薪資 + 津貼) × 勞保費率
    (base_salary + allowance) * labor_insurance_rate AS labor_insurance,
    
    -- 健保費用 = (基本薪資 + 津貼) × 健保費率  
    (base_salary + allowance) * health_insurance_rate AS health_insurance,
    
    -- 退休金提撥 = (基本薪資 + 津貼) × 退休金費率
    (base_salary + allowance) * pension_rate AS pension,
    
    -- 所得稅 = (總收入 - 各項保險) × 稅率
    -- 注意:括號的層次很重要
    ((base_salary + allowance + bonus) - 
     ((base_salary + allowance) * labor_insurance_rate) -
     ((base_salary + allowance) * health_insurance_rate)) * tax_rate AS income_tax,
    
    -- 步驟 3:計算實際到手金額
    -- 到手金額 = 總收入 - 勞保 - 健保 - 退休金 - 所得稅
    (base_salary + allowance + bonus) -
    ((base_salary + allowance) * labor_insurance_rate) -
    ((base_salary + allowance) * health_insurance_rate) -
    ((base_salary + allowance) * pension_rate) -
    (((base_salary + allowance + bonus) - 
      ((base_salary + allowance) * labor_insurance_rate) -
      ((base_salary + allowance) * health_insurance_rate)) * tax_rate) AS take_home_pay

FROM payroll
WHERE payment_month = '2024-01';

上面的查詢雖然計算正確,但可讀性較差且容易出錯。在實務中,遇到這種複雜計算時,建議使用 CTE(Common Table Expression)或子查詢來分步計算,將複雜的運算式分解為多個較簡單的步驟。這不僅提升了可讀性,也便於偵錯與維護。

-- 使用 CTE 改善複雜計算的可讀性
-- 將計算過程分解為多個清晰的步驟
WITH salary_base AS (
    -- 步驟 1:計算基礎薪資項目
    SELECT
        employee_id,
        employee_name,
        base_salary,
        allowance,
        bonus,
        labor_insurance_rate,
        health_insurance_rate,
        pension_rate,
        tax_rate,
        base_salary + allowance + bonus AS gross_total,
        base_salary + allowance AS taxable_base
    FROM payroll
    WHERE payment_month = '2024-01'
),
deductions AS (
    -- 步驟 2:計算各項扣除金額
    SELECT
        employee_id,
        employee_name,
        gross_total,
        taxable_base,
        tax_rate,
        taxable_base * labor_insurance_rate AS labor_insurance,
        taxable_base * health_insurance_rate AS health_insurance,
        taxable_base * pension_rate AS pension
    FROM salary_base
),
final_calculation AS (
    -- 步驟 3:計算稅金與最終金額
    SELECT
        employee_id,
        employee_name,
        gross_total,
        labor_insurance,
        health_insurance,
        pension,
        (gross_total - labor_insurance - health_insurance) * tax_rate AS income_tax
    FROM deductions
)
-- 步驟 4:彙總所有結果
SELECT
    employee_id,
    employee_name,
    gross_total,
    labor_insurance,
    health_insurance,
    pension,
    income_tax,
    gross_total - labor_insurance - health_insurance - pension - income_tax AS take_home_pay,
    ROUND((gross_total - labor_insurance - health_insurance - pension - income_tax) / gross_total * 100, 2) AS take_home_percentage

FROM final_calculation
ORDER BY take_home_pay DESC;

在撰寫包含複雜數學運算的 SQL 查詢時,應該遵循以下最佳實踐:第一,即使運算子優先順序正確,也應該使用括號來明確表達意圖,提高程式碼的可讀性。第二,當運算式過於複雜時,考慮使用 CTE 或子查詢分步計算,而不是試圖在一個運算式中完成所有計算。第三,為計算結果使用有意義的欄位別名,讓後續使用這些結果的查詢更容易理解。第四,在註解中說明複雜運算式的計算邏輯,特別是涉及業務規則的計算公式。

數值處理函式的深度應用:絕對值與取整運算

數值處理函式是 MySQL 數學運算系統中最常用的一組函式,它們提供了對數值進行各種轉換與調整的能力。這些函式在資料清理、統計分析、財務計算等場景中都有廣泛應用。深入理解這些函式的行為特性與適用場景,對於撰寫正確且高效的數值處理邏輯至關重要。

ABS() 函式計算數值的絕對值,即該數值與零的距離,結果永遠是非負數。這個函式在計算差異、誤差或偏差時特別有用。在統計分析中,絕對誤差常用來評估預測模型的準確度。在財務應用中,絕對值可以用來計算價格波動幅度而不考慮漲跌方向。在幾何計算中,絕對值是計算距離的基礎。

-- ABS() 函式的深度應用範例
-- 案例一:分析預測準確度
SELECT
    product_id,
    product_name,
    actual_sales,                                    -- 實際銷售量
    forecast_sales,                                  -- 預測銷售量
    
    -- 原始誤差(可能為正或負)
    actual_sales - forecast_sales AS raw_error,
    
    -- 絕對誤差(永遠為正)
    ABS(actual_sales - forecast_sales) AS absolute_error,
    
    -- 相對誤差百分比
    -- 公式:|實際值 - 預測值| / 實際值 × 100%
    ROUND(ABS(actual_sales - forecast_sales) / actual_sales * 100, 2) AS error_percentage,
    
    -- 準確度評級
    CASE
        WHEN ABS(actual_sales - forecast_sales) / actual_sales <= 0.05 THEN '優秀(誤差≤5%)'
        WHEN ABS(actual_sales - forecast_sales) / actual_sales <= 0.10 THEN '良好(誤差≤10%)'
        WHEN ABS(actual_sales - forecast_sales) / actual_sales <= 0.20 THEN '尚可(誤差≤20%)'
        ELSE '需改進(誤差>20%)'
    END AS accuracy_rating

FROM sales_forecast
WHERE forecast_month = '2024-01'
ORDER BY ABS(actual_sales - forecast_sales) / actual_sales;  -- 按準確度排序

-- 案例二:股票價格波動分析
SELECT
    stock_code,
    trade_date,
    open_price,                                      -- 開盤價
    close_price,                                     -- 收盤價
    high_price,                                      -- 最高價
    low_price,                                       -- 最低價
    
    -- 日漲跌幅(可能為正或負)
    close_price - open_price AS price_change,
    
    -- 日波動幅度(絕對值)
    ABS(close_price - open_price) AS volatility,
    
    -- 日振幅(最高價與最低價的差距)
    high_price - low_price AS daily_range,
    
    -- 波動率百分比
    ROUND(ABS(close_price - open_price) / open_price * 100, 2) AS volatility_pct,
    
    -- 振幅百分比
    ROUND((high_price - low_price) / open_price * 100, 2) AS range_pct,
    
    -- 波動類型分類
    CASE
        WHEN ABS(close_price - open_price) / open_price >= 0.05 THEN '高波動'
        WHEN ABS(close_price - open_price) / open_price >= 0.02 THEN '中波動'
        ELSE '低波動'
    END AS volatility_type

FROM stock_daily_prices
WHERE trade_date >= '2024-01-01'
ORDER BY ABS(close_price - open_price) / open_price DESC  -- 按波動率降序
LIMIT 20;                                            -- 顯示波動最大的 20 檔股票

取整函式包括 CEILING()(向上取整)與 FLOOR()(向下取整)兩個函式。CEILING() 傳回大於或等於指定數值的最小整數,而 FLOOR() 傳回小於或等於指定數值的最大整數。這兩個函式在需要整數結果的場景中非常有用,例如計算需要的包裝箱數量、可以購買的完整商品數、或是分頁計算等。

需要特別注意的是,這兩個函式對正數與負數的處理方式可能與直覺不同。對於正數,CEILING() 會向更大的方向取整,FLOOR() 會向更小的方向取整。對於負數,CEILING() 會向零的方向取整(變得更大),FLOOR() 會向負無窮的方向取整(變得更小)。例如,CEILING(3.2) = 4,CEILING(-3.2) = -3;FLOOR(3.8) = 3,FLOOR(-3.8) = -4。

-- CEILING() 與 FLOOR() 的深度應用範例
-- 案例一:物流配送計算
SELECT
    order_id,
    product_name,
    order_quantity,                                  -- 訂購數量
    box_capacity,                                    -- 每箱容量
    
    -- 計算需要的箱數(無條件進位)
    -- 即使只超出一件也需要一整箱
    CEILING(order_quantity / box_capacity) AS boxes_needed,
    
    -- 計算完整裝滿的箱數(無條件捨去)
    FLOOR(order_quantity / box_capacity) AS full_boxes,
    
    -- 計算最後一箱的件數
    order_quantity % box_capacity AS items_in_last_box,
    
    -- 計算總容量(所需箱數 × 每箱容量)
    CEILING(order_quantity / box_capacity) * box_capacity AS total_capacity,
    
    -- 計算空間利用率
    ROUND(order_quantity / (CEILING(order_quantity / box_capacity) * box_capacity) * 100, 2) AS utilization_rate

FROM shipping_orders
WHERE order_date >= '2024-01-01'
ORDER BY boxes_needed DESC;

-- 案例二:財務預算分配
SELECT
    department_name,
    annual_budget,                                   -- 年度預算
    months_remaining,                                -- 剩餘月份
    
    -- 每月可用預算(精確值)
    annual_budget / months_remaining AS exact_monthly,
    
    -- 每月可用預算(無條件捨去)
    -- 確保不會超支
    FLOOR(annual_budget / months_remaining) AS safe_monthly,
    
    -- 每月可用預算(無條件進位)
    -- 確保預算充足
    CEILING(annual_budget / months_remaining) AS generous_monthly,
    
    -- 如果使用 FLOOR,最後會剩餘的金額
    annual_budget - (FLOOR(annual_budget / months_remaining) * months_remaining) AS remaining_if_floor,
    
    -- 如果使用 CEILING,需要額外增加的金額
    (CEILING(annual_budget / months_remaining) * months_remaining) - annual_budget AS additional_if_ceiling

FROM department_budgets
WHERE fiscal_year = 2024;

-- 案例三:分頁計算
SELECT
    total_records,                                   -- 總記錄數
    page_size,                                       -- 每頁顯示筆數
    
    -- 計算總頁數(無條件進位)
    -- 即使最後一頁只有一筆記錄也算一頁
    CEILING(total_records / page_size) AS total_pages,
    
    -- 計算完整頁數(無條件捨去)
    FLOOR(total_records / page_size) AS full_pages,
    
    -- 計算最後一頁的記錄數
    CASE 
        WHEN total_records % page_size = 0 THEN page_size
        ELSE total_records % page_size
    END AS last_page_records,
    
    -- 判斷是否有不完整的最後一頁
    IF(total_records % page_size = 0, '完整分頁', '有不完整頁') AS pagination_status

FROM pagination_settings;

在選擇使用 CEILING() 或 FLOOR() 時,需要根據具體的業務需求來決定。在需要確保資源充足的場景(如包裝箱數量、預算分配),通常使用 CEILING() 以避免不足。在需要確保不超出限制的場景(如可購買數量、可分配時段),通常使用 FLOOR() 以避免超出。在某些情況下,可能需要兩者配合使用,例如先用 FLOOR() 計算完整單位數量,再用模數運算處理剩餘部分。

四捨五入與截斷函式的精度控制策略

在處理小數數值時,控制小數位數是一個常見且重要的需求。MySQL 提供了 ROUND() 與 TRUNCATE() 兩個函式來達成這個目的,但它們的處理邏輯有本質上的差異。ROUND() 按照四捨五入規則調整數值,而 TRUNCATE() 直接截斷指定位數之後的小數,不進行任何捨入。正確選擇使用哪個函式,對於確保計算結果的正確性至關重要,特別是在財務計算等對精度要求極高的場景中。

ROUND() 函式執行四捨五入運算,其語法為 ROUND(number, decimals)。第一個參數是要處理的數值,第二個參數指定要保留的小數位數。當第二個參數為正數時,表示保留小數點後幾位;當第二個參數為 0 或省略時,表示四捨五入到整數;當第二個參數為負數時,表示向左進位到整數部分的指定位數。

-- ROUND() 函式的全面應用解析
SELECT
    -- 基本四捨五入:保留不同小數位數
    ROUND(9.87654321, 4) AS four_decimals,          -- 結果:9.8765
    ROUND(9.87654321, 3) AS three_decimals,         -- 結果:9.877 (7 進位)
    ROUND(9.87654321, 2) AS two_decimals,           -- 結果:9.88 (7 進位)
    ROUND(9.87654321, 1) AS one_decimal,            -- 結果:9.9 (8 進位)
    ROUND(9.87654321, 0) AS no_decimal,             -- 結果:10 (9 進位)
    ROUND(9.87654321) AS default_round,             -- 結果:10 (預設為 0 位小數)
    
    -- 臨界值的四捨五入行為
    ROUND(9.5) AS half_up,                          -- 結果:10 (5 進位)
    ROUND(9.4) AS below_half,                       -- 結果:9 (4 捨去)
    ROUND(8.5) AS even_half,                        -- 結果:9 (5 進位)
    
    -- 負數的四捨五入
    ROUND(-9.5) AS negative_half,                   -- 結果:-10
    ROUND(-9.4) AS negative_below,                  -- 結果:-9
    
    -- 向整數部分進位(負數第二參數)
    ROUND(1234.5678, -1) AS round_tens,             -- 結果:1230 (向十位進位)
    ROUND(1234.5678, -2) AS round_hundreds,         -- 結果:1200 (向百位進位)
    ROUND(1289.5678, -2) AS round_hundreds_up,      -- 結果:1300 (89 進位到 100)
    ROUND(1234.5678, -3) AS round_thousands;        -- 結果:1000 (向千位進位)

-- 實務案例一:商品價格的顯示格式化
SELECT
    product_id,
    product_name,
    cost_price,                                      -- 成本價(精確值)
    markup_rate,                                     -- 加成率
    
    -- 計算建議售價(成本 × (1 + 加成率))
    cost_price * (1 + markup_rate) AS exact_price,
    
    -- 四捨五入到整數(適用於低價商品)
    ROUND(cost_price * (1 + markup_rate)) AS rounded_to_integer,
    
    -- 四捨五入到十位(製造心理價格,如 290、390)
    ROUND(cost_price * (1 + markup_rate), -1) AS rounded_to_tens,
    
    -- 四捨五入到小數點後 2 位(標準貨幣格式)
    ROUND(cost_price * (1 + markup_rate), 2) AS standard_price,
    
    -- 計算與原始價格的差異
    ROUND(cost_price * (1 + markup_rate), 2) - (cost_price * (1 + markup_rate)) AS rounding_diff

FROM products
WHERE category = 'Electronics'
ORDER BY product_id;

TRUNCATE() 函式則採用完全不同的處理方式,它直接截斷指定位數之後的數字,不進行任何捨入判斷。其語法為 TRUNCATE(number, decimals),參數含義與 ROUND() 相同。TRUNCATE() 的行為更加可預測,不會因為數值的細微差異而產生不同的進位結果,這在某些需要一致性處理的場景中特別重要。

-- TRUNCATE() 函式的全面應用解析
SELECT
    -- 基本截斷:保留不同小數位數
    TRUNCATE(9.87654321, 4) AS four_decimals,       -- 結果:9.8765
    TRUNCATE(9.87654321, 3) AS three_decimals,      -- 結果:9.876 (不進位)
    TRUNCATE(9.87654321, 2) AS two_decimals,        -- 結果:9.87 (不進位)
    TRUNCATE(9.87654321, 1) AS one_decimal,         -- 結果:9.8 (不進位)
    TRUNCATE(9.87654321, 0) AS no_decimal,          -- 結果:9 (不進位)
    
    -- 與 ROUND() 的對比
    TRUNCATE(9.99999, 0) AS truncate_no_round,      -- 結果:9 (不進位)
    ROUND(9.99999, 0) AS round_will_round,          -- 結果:10 (進位)
    
    -- 負數的截斷
    TRUNCATE(-9.8, 0) AS negative_truncate,         -- 結果:-9
    TRUNCATE(-9.2, 0) AS negative_truncate_2,       -- 結果:-9
    
    -- 向整數部分截斷(負數第二參數)
    TRUNCATE(1234.5678, -1) AS truncate_tens,       -- 結果:1230
    TRUNCATE(1289.5678, -2) AS truncate_hundreds,   -- 結果:1200 (不進位到 1300)
    TRUNCATE(1999.9999, -3) AS truncate_thousands;  -- 結果:1000 (不進位到 2000)

-- 實務案例二:稅金計算的精度控制
-- 不同國家/地區對稅金的捨入規定可能不同
SELECT
    invoice_id,
    subtotal,                                        -- 小計金額
    tax_rate,                                        -- 稅率
    
    -- 精確稅金(未捨入)
    subtotal * tax_rate AS exact_tax,
    
    -- 方式一:四捨五入(常見於多數國家)
    ROUND(subtotal * tax_rate, 2) AS tax_rounded,
    
    -- 方式二:無條件捨去(某些國家的規定)
    TRUNCATE(subtotal * tax_rate, 2) AS tax_truncated,
    
    -- 方式三:無條件進位(對政府最有利)
    CEILING(subtotal * tax_rate * 100) / 100 AS tax_ceiling,
    
    -- 計算不同方式的差異
    ROUND(subtotal * tax_rate, 2) - TRUNCATE(subtotal * tax_rate, 2) AS diff_round_truncate,
    
    -- 最終金額比較
    subtotal + ROUND(subtotal * tax_rate, 2) AS total_with_round,
    subtotal + TRUNCATE(subtotal * tax_rate, 2) AS total_with_truncate

FROM invoices
WHERE invoice_date >= '2024-01-01'
ORDER BY subtotal DESC;

在選擇使用 ROUND() 或 TRUNCATE() 時,需要考慮業務規則與法規要求。在財務計算中,利息與股息的計算通常使用四捨五入,而稅金的計算則可能因國家而異,有些採用四捨五入,有些則規定無條件捨去或進位。在科學計算中,為了避免捨入誤差累積,有時會使用銀行家捨入法(Banker’s Rounding),即當捨入位剛好為 5 時,根據前一位的奇偶性決定進位方向,但 MySQL 的 ROUND() 不支援這種方法。

另一個重要的考量是精度損失的累積。當多次進行捨入運算時,每次的捨入誤差會累積,可能導致最終結果與理論值有較大偏差。在這種情況下,建議盡可能延後捨入的時機,在所有計算完成後才進行最終的捨入。或是使用更高精度的中間計算,只在最後呈現時才捨入到所需精度。

-- 展示捨入誤差累積的影響
WITH RECURSIVE price_changes AS (
    -- 初始價格
    SELECT 
        1 AS iteration,
        100.00 AS price,
        100.00 AS precise_price
    
    UNION ALL
    
    -- 模擬 10 次價格調整,每次增加 3.33%
    SELECT
        iteration + 1,
        ROUND(price * 1.0333, 2) AS price,           -- 每次都捨入
        precise_price * 1.0333 AS precise_price      -- 保持精確值
    FROM price_changes
    WHERE iteration < 10
)
SELECT
    iteration AS 調整次數,
    price AS 每次捨入後價格,
    precise_price AS 精確價格,
    ROUND(precise_price, 2) AS 最終捨入價格,
    price - ROUND(precise_price, 2) AS 累積誤差,
    ROUND((price - ROUND(precise_price, 2)) / ROUND(precise_price, 2) * 100, 4) AS 誤差百分比

FROM price_changes
ORDER BY iteration;

-- 這個查詢展示了重複捨入造成的誤差累積
-- 正確做法是保持精確計算,只在最後捨入

指數對數與三角函式的科學計算應用

進階的數學函式為 MySQL 提供了科學計算與工程應用的能力。指數函式、對數函式與三角函式在金融建模、統計分析、物理計算與幾何運算等領域都有廣泛應用。雖然這些函式在一般的業務應用中使用頻率較低,但在特定領域卻是不可或缺的工具。深入理解這些函式的數學意義與應用場景,能夠讓開發者在面對複雜計算需求時游刃有餘。

指數函式包括 EXP()(自然指數,即 e 的次方)與 POW()/POWER()(任意底數的冪次方)。EXP() 函式在連續複利計算、指數成長模型等場景中特別有用。POW() 函式則更加通用,可以計算任意數的任意次方,包括分數次方(用於計算根號)與負數次方(用於計算倒數)。

-- 指數函式的深度應用
-- 案例一:複利計算
SELECT
    principal AS 本金,
    annual_rate AS 年利率,
    years AS 投資年數,
    
    -- 連續複利:A = P × e^(rt)
    -- 當複利次數趨近無窮大時的極限
    principal * EXP(annual_rate * years) AS 連續複利終值,
    principal * EXP(annual_rate * years) - principal AS 連續複利利息,
    
    -- 年複利:A = P × (1 + r)^t
    principal * POW(1 + annual_rate, years) AS 年複利終值,
    principal * POW(1 + annual_rate, years) - principal AS 年複利利息,
    
    -- 月複利:A = P × (1 + r/12)^(12t)
    principal * POW(1 + annual_rate/12, 12 * years) AS 月複利終值,
    
    -- 日複利:A = P × (1 + r/365)^(365t)
    principal * POW(1 + annual_rate/365, 365 * years) AS 日複利終值,
    
    -- 比較不同複利方式的差異
    principal * EXP(annual_rate * years) - 
        principal * POW(1 + annual_rate, years) AS 連續與年複利差額

FROM (
    SELECT 10000 AS principal, 0.05 AS annual_rate, 10 AS years
) AS investment_scenarios;

-- 案例二:人口成長模型
-- 指數成長模型:P(t) = P₀ × e^(rt)
SELECT
    year,
    initial_population AS 初始人口,
    growth_rate AS 年成長率,
    year - base_year AS 經過年數,
    
    -- 使用指數模型預測人口
    ROUND(initial_population * EXP(growth_rate * (year - base_year))) AS 預測人口,
    
    -- 計算成長倍數
    ROUND(EXP(growth_rate * (year - base_year)), 2) AS 成長倍數,
    
    -- 計算年均成長率(基於實際數據)
    ROUND(POW(actual_population / initial_population, 1.0 / (year - base_year)) - 1, 4) AS 實際年均成長率

FROM population_projections
WHERE year BETWEEN 2020 AND 2050;

對數函式是指數函式的反函式,在解方程、數據變換與統計分析中有重要應用。MySQL 提供了多種對數函式:LOG()/LN() 計算自然對數(以 e 為底),LOG10() 計算常用對數(以 10 為底),LOG2() 計算二進位對數(以 2 為底),以及 LOG(base, number) 計算任意底數的對數。

-- 對數函式的深度應用
-- 案例一:計算投資翻倍所需時間
-- 使用 Rule of 72 的精確版本
SELECT
    annual_rate AS 年利率,
    annual_rate * 100 AS 年利率百分比,
    
    -- 翻倍所需年數:t = ln(2) / ln(1 + r)
    -- 或更直接:t = LOG(2, 1 + r)
    LOG(2) / LOG(1 + annual_rate) AS 翻倍年數_自然對數,
    LOG(2, 1 + annual_rate) AS 翻倍年數_直接計算,
    
    -- Rule of 72 近似值
    72 / (annual_rate * 100) AS Rule_of_72近似值,
    
    -- 計算誤差
    ABS(72 / (annual_rate * 100) - LOG(2) / LOG(1 + annual_rate)) AS 近似誤差

FROM (
    SELECT 0.03 AS annual_rate
    UNION ALL SELECT 0.05
    UNION ALL SELECT 0.07
    UNION ALL SELECT 0.10
    UNION ALL SELECT 0.12
) AS interest_rates;

-- 案例二:對數尺度的數據轉換
-- 處理跨越多個數量級的數據
SELECT
    product_id,
    product_name,
    unit_sales,                                      -- 原始銷售量
    
    -- 對數轉換(處理偏態分布)
    LOG10(unit_sales) AS log10_sales,
    LN(unit_sales) AS ln_sales,
    
    -- 計算數量級
    FLOOR(LOG10(unit_sales)) AS order_of_magnitude,
    
    -- 分類數量級
    CASE 
        WHEN LOG10(unit_sales) < 2 THEN '小量(< 100)'
        WHEN LOG10(unit_sales) < 3 THEN '中量(100-999)'
        WHEN LOG10(unit_sales) < 4 THEN '大量(1K-9.9K)'
        WHEN LOG10(unit_sales) < 5 THEN '超大量(10K-99.9K)'
        ELSE '極大量(≥100K)'
    END AS sales_category

FROM product_sales
WHERE sales_year = 2024
ORDER BY unit_sales DESC;

三角函式在幾何計算、物理模擬與位置運算中有重要應用。MySQL 提供了完整的三角函式支援,包括 SIN()(正弦)、COS()(餘弦)、TAN()(正切)及其反函式 ASIN()、ACOS()、ATAN()。需要注意的是,這些函式的參數與傳回值都使用弧度為單位,如果需要使用角度,必須使用 RADIANS() 與 DEGREES() 函式進行轉換。

-- 三角函式的深度應用
-- 案例一:計算兩個地理座標之間的距離
-- 使用 Haversine 公式
DELIMITER //

CREATE FUNCTION calculate_distance(
    lat1 DECIMAL(10, 6),                             -- 起點緯度
    lon1 DECIMAL(10, 6),                             -- 起點經度
    lat2 DECIMAL(10, 6),                             -- 終點緯度
    lon2 DECIMAL(10, 6)                              -- 終點經度
)
RETURNS DECIMAL(10, 2)                               -- 傳回距離(公里)
DETERMINISTIC
COMMENT '使用 Haversine 公式計算兩點間的球面距離'
BEGIN
    DECLARE earth_radius DECIMAL(10, 2) DEFAULT 6371.0;  -- 地球平均半徑(公里)
    DECLARE dlat DECIMAL(10, 6);                     -- 緯度差
    DECLARE dlon DECIMAL(10, 6);                     -- 經度差
    DECLARE a DECIMAL(20, 10);                       -- Haversine 公式中間值
    DECLARE c DECIMAL(20, 10);                       -- 角距離
    
    -- 將角度轉換為弧度並計算差值
    SET dlat = RADIANS(lat2 - lat1);
    SET dlon = RADIANS(lon2 - lon1);
    
    -- 計算 Haversine 公式
    -- a = sin²(Δφ/2) + cos φ₁ × cos φ₂ × sin²(Δλ/2)
    SET a = POW(SIN(dlat / 2), 2) + 
            COS(RADIANS(lat1)) * COS(RADIANS(lat2)) * 
            POW(SIN(dlon / 2), 2);
    
    -- c = 2 × atan2(√a, √(1−a))
    SET c = 2 * ATAN2(SQRT(a), SQRT(1 - a));
    
    -- 距離 = 地球半徑 × 角距離
    RETURN ROUND(earth_radius * c, 2);
END//

DELIMITER ;

-- 使用距離計算函式
SELECT
    store_id,
    store_name,
    store_latitude,
    store_longitude,
    
    -- 計算與台北 101 的距離
    -- 台北 101 座標:25.0340° N, 121.5645° E
    calculate_distance(
        25.0340, 121.5645,
        store_latitude, store_longitude
    ) AS distance_from_taipei_101,
    
    -- 分類距離範圍
    CASE
        WHEN calculate_distance(25.0340, 121.5645, store_latitude, store_longitude) < 5 THEN '5公里內'
        WHEN calculate_distance(25.0340, 121.5645, store_latitude, store_longitude) < 10 THEN '5-10公里'
        WHEN calculate_distance(25.0340, 121.5645, store_latitude, store_longitude) < 20 THEN '10-20公里'
        ELSE '20公里以上'
    END AS distance_category

FROM store_locations
ORDER BY distance_from_taipei_101;

效能優化策略與最佳實踐建議

在使用 MySQL 數學運算子與函式時,效能考量與最佳實踐至關重要。不當的使用方式可能導致查詢效能大幅下降,特別是在處理大型資料表時。理解這些效能陷阱並掌握相應的優化策略,是撰寫高效率 SQL 查詢的關鍵技能。

索引失效是數學函式使用中最常見的效能問題。當在 WHERE 子句中對索引欄位使用函式時,MySQL 無法使用該欄位上的索引,導致全表掃描。例如,WHERE ROUND(price) = 100 會阻止使用 price 欄位上的索引,即使該欄位有索引也無濟於事。正確的做法是將函式應用在常數側,或是改用範圍查詢來取代函式運算。

-- 效能陷阱:對索引欄位使用函式
-- 不良實踐範例
EXPLAIN SELECT * FROM products
WHERE ROUND(price) = 100;
-- 執行計畫會顯示 type: ALL,表示全表掃描
-- 即使 price 欄位有索引也無法使用

-- 優化方式一:將函式移到常數側
-- 確定價格範圍後使用範圍查詢
EXPLAIN SELECT * FROM products
WHERE price >= 99.5 AND price < 100.5;
-- 執行計畫會顯示 type: range,可以使用索引
-- 這個查詢的結果與上面相同,但效能大幅提升

-- 優化方式二:使用預計算欄位
-- 如果經常需要對 ROUND(price) 進行查詢
-- 可以建立一個儲存 ROUND(price) 結果的欄位
ALTER TABLE products ADD COLUMN price_rounded INT GENERATED ALWAYS AS (ROUND(price)) STORED;
CREATE INDEX idx_price_rounded ON products(price_rounded);

-- 現在可以直接查詢預計算欄位
EXPLAIN SELECT * FROM products
WHERE price_rounded = 100;
-- 可以有效使用索引

-- 效能對比測試
-- 創建測試資料
CREATE TABLE price_test (
    id INT AUTO_INCREMENT PRIMARY KEY,
    price DECIMAL(10, 2),
    INDEX idx_price (price)
);

INSERT INTO price_test (price)
SELECT RAND() * 1000
FROM (SELECT 1 UNION ALL SELECT 2) t1,
     (SELECT 1 UNION ALL SELECT 2) t2,
     (SELECT 1 UNION ALL SELECT 2) t3,
     (SELECT 1 UNION ALL SELECT 2) t4,
     (SELECT 1 UNION ALL SELECT 2) t5,
     (SELECT 1 UNION ALL SELECT 2) t6,
     (SELECT 1 UNION ALL SELECT 2) t7,
     (SELECT 1 UNION ALL SELECT 2) t8,
     (SELECT 1 UNION ALL SELECT 2) t9,
     (SELECT 1 UNION ALL SELECT 2) t10;
-- 生成約 1024 筆測試資料

-- 不良查詢(使用函式)
SET @start_time = NOW(6);
SELECT COUNT(*) FROM price_test WHERE ROUND(price) = 500;
SET @func_time = TIMESTAMPDIFF(MICROSECOND, @start_time, NOW(6));

-- 優化查詢(範圍查詢)
SET @start_time = NOW(6);
SELECT COUNT(*) FROM price_test WHERE price >= 499.5 AND price < 500.5;
SET @range_time = TIMESTAMPDIFF(MICROSECOND, @start_time, NOW(6));

-- 顯示效能差異
SELECT 
    @func_time AS 函式查詢微秒,
    @range_time AS 範圍查詢微秒,
    @func_time / @range_time AS 效能比值;

計算欄位的重複使用是另一個重要的效能考量。如果相同的計算需要在查詢的多個地方使用,重複計算會浪費 CPU 資源。使用子查詢、CTE 或衍生表來一次計算並多次使用,可以顯著提升效能。

-- 不良實踐:重複計算
SELECT
    order_id,
    quantity * unit_price AS subtotal,
    quantity * unit_price * tax_rate AS tax,
    quantity * unit_price * (1 + tax_rate) AS total,
    -- quantity * unit_price 被重複計算了 3 次
    CASE
        WHEN quantity * unit_price > 1000 THEN 'VIP'
        ELSE 'Regular'
    END AS customer_type
FROM orders;

-- 優化方式一:使用 CTE
WITH calculated_orders AS (
    SELECT
        order_id,
        quantity,
        unit_price,
        tax_rate,
        quantity * unit_price AS subtotal  -- 只計算一次
    FROM orders
)
SELECT
    order_id,
    subtotal,
    subtotal * tax_rate AS tax,           -- 重複使用 subtotal
    subtotal * (1 + tax_rate) AS total,
    CASE
        WHEN subtotal > 1000 THEN 'VIP'
        ELSE 'Regular'
    END AS customer_type
FROM calculated_orders;

-- 優化方式二:使用衍生表
SELECT
    order_id,
    subtotal,
    subtotal * tax_rate AS tax,
    subtotal * (1 + tax_rate) AS total,
    CASE
        WHEN subtotal > 1000 THEN 'VIP'
        ELSE 'Regular'
    END AS customer_type
FROM (
    SELECT
        order_id,
        quantity * unit_price AS subtotal,
        tax_rate
    FROM orders
) AS calculated_orders;

數值型別的選擇對效能與精度都有重要影響。整數運算通常比浮點數運算快,浮點數運算比 DECIMAL 運算快,但精度會依序降低。在設計資料表時,應該根據實際需求選擇適當的型別。

-- 數值型別的效能與精度比較
-- 創建測試表
CREATE TABLE numeric_types_test (
    id INT AUTO_INCREMENT PRIMARY KEY,
    value_int INT,                                   -- 整數型別
    value_decimal DECIMAL(10, 2),                    -- 定點數型別
    value_double DOUBLE                              -- 浮點數型別
);

-- 插入相同的數值使用不同型別儲存
INSERT INTO numeric_types_test (value_int, value_decimal, value_double)
SELECT 
    FLOOR(RAND() * 1000000),
    FLOOR(RAND() * 1000000) + RAND(),
    FLOOR(RAND() * 1000000) + RAND()
FROM (SELECT 1 UNION ALL SELECT 2) t1,
     (SELECT 1 UNION ALL SELECT 2) t2,
     (SELECT 1 UNION ALL SELECT 2) t3,
     (SELECT 1 UNION ALL SELECT 2) t4,
     (SELECT 1 UNION ALL SELECT 2) t5,
     (SELECT 1 UNION ALL SELECT 2) t6,
     (SELECT 1 UNION ALL SELECT 2) t7,
     (SELECT 1 UNION ALL SELECT 2) t8,
     (SELECT 1 UNION ALL SELECT 2) t9,
     (SELECT 1 UNION ALL SELECT 2) t10;

-- 效能測試:求和運算
SET @start_time = NOW(6);
SELECT SUM(value_int) FROM numeric_types_test;
SET @int_time = TIMESTAMPDIFF(MICROSECOND, @start_time, NOW(6));

SET @start_time = NOW(6);
SELECT SUM(value_decimal) FROM numeric_types_test;
SET @decimal_time = TIMESTAMPDIFF(MICROSECOND, @start_time, NOW(6));

SET @start_time = NOW(6);
SELECT SUM(value_double) FROM numeric_types_test;
SET @double_time = TIMESTAMPDIFF(MICROSECOND, @start_time, NOW(6));

-- 顯示效能比較
SELECT
    @int_time AS 整數型別微秒,
    @decimal_time AS DECIMAL型別微秒,
    @double_time AS DOUBLE型別微秒,
    ROUND(@decimal_time / @int_time, 2) AS DECIMAL相對整數倍數,
    ROUND(@double_time / @int_time, 2) AS DOUBLE相對整數倍數;

最後,建立適當的最佳實踐準則對於長期維護高效能的數值處理系統至關重要。這包括:在 WHERE 子句中避免對索引欄位使用函式;使用 CTE 或子查詢來組織複雜計算,避免重複運算;選擇適當的數值型別以平衡效能與精度;在財務計算中使用 DECIMAL 而非 FLOAT;為常用的計算結果建立物化視圖或預計算欄位;定期分析查詢的執行計畫,識別效能瓶頸;以及在生產環境部署前進行充分的效能測試。

總結與企業級數值處理體系建議

本文全面剖析了 MySQL 數學運算系統的完整技術架構與實務應用,從基礎的四則運算到進階的科學計算函式,從運算子優先順序的控制機制到數值精度的管理策略,從企業級的實務案例到效能優化的最佳實踐。這些知識與技能共同構成了在 MySQL 環境中進行高效能數值處理的完整體系。

在基礎運算層面,四則運算與模數運算是日常開發中最常使用的功能。理解運算子優先順序並善用括號來明確表達計算邏輯,能夠避免微妙但嚴重的計算錯誤。模數運算在奇偶性判斷、循環分組與週期性分析等場景中展現出獨特價值。在撰寫複雜運算式時,使用 CTE 或子查詢來分步計算,不僅能提升可讀性,更能避免重複運算帶來的效能損失。

在數值處理層面,絕對值、取整與四捨五入函式為各種精度控制需求提供了完整解決方案。ABS() 函式在誤差分析與波動計算中不可或缺,CEILING() 與 FLOOR() 函式在資源分配與數量計算中扮演重要角色,而 ROUND() 與 TRUNCATE() 函式則在精度控制上提供了不同的處理策略。正確選擇使用哪個函式,需要深入理解業務需求與法規要求。

在進階計算層面,指數函式、對數函式與三角函式為科學計算與工程應用提供了必要支援。這些函式雖然在一般業務應用中使用頻率較低,但在金融建模、統計分析、位置運算等特定領域卻是不可或缺的工具。深入理解這些函式的數學意義與應用場景,能夠讓開發者在面對複雜計算需求時更有信心。

在效能優化層面,避免在 WHERE 子句中對索引欄位使用函式、善用預計算欄位與物化視圖、選擇適當的數值型別,以及使用 CTE 組織複雜計算,這些策略共同構成了高效能數值處理的基礎。在設計資料表結構時預先考慮查詢模式,在撰寫查詢時關注執行計畫,這些習慣能夠確保系統在面對大規模數據時依然保持良好效能。

建立企業級的數值處理體系需要系統化的方法與持續的改進。建議開發團隊建立統一的數值處理規範,明確定義不同場景下應該使用的函式與型別;建立完整的測試套件,驗證數值計算的正確性與精度;定期進行效能審查,識別並優化效能瓶頸;以及建立知識庫,記錄常見問題與最佳實踐。透過這些措施,能夠建立起可靠、高效且易於維護的數值處理系統,為企業的數位轉型提供堅實的技術支撐。

掌握這些技術後,開發者將能夠充分發揮 MySQL 數學運算系統的強大能力,在資料庫層級完成複雜的數值計算,減少應用程式的複雜度,提升整體系統的效能與可維護性。持續學習與實踐,關注 MySQL 社群的最新發展,將進一步提升數值處理的專業能力,為更複雜的應用場景做好準備。