MySQL-1
MySQL 為什麼會被叫做「MySQL」、又為什麼現在官網寫的是「Oracle MySQL」,這背後其實是一連串的公司併購故事,跟資料庫技術本身沒什麼關係
MySQL AB 開發,創辦人是 Michael "Monty" Widenius、David Axmark。Monty 有一個女兒叫做 My Widenius,所以 MySQL = My + SQL,也就是「My 的 SQL 資料庫」。當時 Oracle 跟 MySQL 還是競爭對手。Java 也是 Sun 的資產,所以 Java、MySQL 一度都在 Sun 旗下。Java、MySQL、Solaris、VirtualBox 一整批技術資產。現在的 MySQL 8.0,核心幾乎都是用 C++ 寫的;最早期是以 C 為主,後來才逐漸轉成 C++。如果去下載 MySQL 的 source code(GitHub 或 Oracle 官方的 source),會看到大量的 .cc、.cpp、.h 檔案,這一段就是把「一條 SQL 從進來到跑出結果」的內部分層畫出來,看看哪些環節是 C++ 的地盤。
flowchart TD
Client["Client"] --> Connector["mysql.exe / Connector"]
Connector --> Parser["SQL Parser(C++)"]
Parser --> Optimizer["Optimizer(C++)"]
Optimizer --> Executor["Executor(C++)"]
Executor --> API["Storage Engine API"]
API --> InnoDB["InnoDB(C / C++)"]
API --> MyISAM["MyISAM(C++)"]
SQL Parser 解析 SQL、Optimizer 決定 Execution Plan、Executor 真正執行,一路到 Join 演算法、Cost Model、B+ Tree 操作,這些核心邏輯全部都是 C++ 寫的;C 主要只留在最底層、最早期就存在的一些相容性程式碼。
建了兩個 index,明明其中一個明顯比較好,MySQL 卻選了比較差的那個——這是 Oracle 官方有紀錄的一個真實 Bug(#36817),它很有代表性,反映的是 Optimizer 的 Cost Model 不夠成熟,而不是某一行程式寫錯。
問題是這樣:2008 年有人回報,若今天有兩個 index A、B,是先後建在資料庫裡的,就算某個情境下使用 B 可以避免 filesort,MySQL 還是可能會選擇 A,原因只是因為 A 比較早被建立。也就是說,只要改變 CREATE INDEX 的先後順序,Execution Plan 就跟著變了——舊版 MySQL Optimizer 因為成本估算能力不足,可能受到 Index Metadata(通常就是建立順序)的影響,因此選到並非最佳的 Index。
實例如下圖,即使 origin -> price 會是更合理的 index 選擇,也會因為建立時間較晚而不被選中:

Bug #36817:Index 建立順序會影響 Optimizer 的選擇結果。filesort 的索引增加對應的成本(cost)。ORDER BY,仍然存在因索引建立順序導致選錯 Index 的案例,代表根因並未完全解決。Verified 狀態,沒有被標成 Fixed 或 Closed。Cost Model 在早期版本還不夠成熟,才會讓「建立順序」這種跟查詢效率無關的 Metadata,意外影響到 Execution Plan 的選擇。
大約從 MySQL 5.5 開始,Optimizer 針對這類問題做了不少改善,例如更好的 Cost Model、更好的統計資訊(statistics)、更好的 ORDER BY 最佳化、更好的 index 選擇邏輯,所以後續 MySQL 5.5、5.6、5.7、8.0 的 Optimizer 也持續在演進。
實際修改的位置是 sql_optimizer.cc 附近的 Cost Calculation,概念大致如下(僅為示意,並非原始碼):
cost = rows * row_cost;- 完全沒有把「是否需要額外排序」算進成本裡
cost = rows * row_cost;if (need_filesort) cost += filesort_penalty;
同一份 Role 欄位資料,換一個資料庫,取最大最小值、排序的結果居然完全不一樣——問題出在 MySQL 特有的 ENUM 型別,它的底層儲存方式跟排序邏輯根本是兩套不同的規則,而 MSSQL 因為沒有這個型別,反而規則單純很多。
MySQL 支援 ENUM(列舉)資料型別,這是一種字串物件,值必須從建立資料表時就定義好的清單中挑選。欄位的值只能是清單裡的其中一個(或是空白、NULL),否則寫入會直接失敗。雖然看起來、寫起來都是字串,但 MySQL 在底層其實會自動編碼成整數索引來節省空間——如果選項數量在 1 到 255 個之間,一筆資料只需要 1 個位元組的儲存空間。
- 欄位本身就列出所有合法值,具備自我說明性(Self-documenting)
- 能直接擋掉不在清單內的亂碼輸入,不用額外加
CHECK約束
- 未來要新增/修改列舉選項得靠
ALTER TABLE,在大型或高流量資料表上改起來比較麻煩 - 排序依據不夠直覺(下面會展開說明),實務上不少人改用
TINYINT搭配對照表、或直接存VARCHAR
讓我們看一下原子能影片中提及的例子:


ENUM 底層是用整數索引儲存的(建立時排在第一個的選項索引為 1,第二個為 2,依此類推),但 MAX() / MIN() 會以字串的字典順序(Alphabetical order)來比大小,並不是照定義的先後順序。若想依「定義的先後順序」取最大/最小值,必須把欄位 + 0 強制轉換成底層的數字索引才行。反過來,對 ENUM 欄位做 ORDER BY 時,MySQL 預設卻是照「定義列舉時的先後順序」排序,不是英文字母——同一個欄位,兩種操作各套用一套規則,邏輯本身並不一致。
MSSQL 沒有 ENUM 這個型別,欄位本質上就是 VARCHAR(搭配 CHECK 約束)或另外建一張數字對照表,規則反而單純很多:
- 底層用整數索引儲存,但只是儲存方式,不代表操作也照這個順序
MAX()/MIN()依字串字典順序比大小ORDER BY卻依「定義時的先後順序」排序,跟MAX()/MIN()的邏輯不一樣
- 方案 A:欄位存
VARCHAR(搭配CHECK),MAX()/MIN()一樣依字典順序比大小 - 方案 B:另建數字代碼對照表,直接對數字欄位取
MAX()/MIN(),最符合邏輯也最有效率 ORDER BY不管ASC還是DESC,一律照英文字母/字典順序排序,沒有例外規則
方案 A 的字典順序範例:
1 | -- 假設 Role 內有 'admin', 'editor', 'user' |
方案 B 的數字代碼範例(例如 1=admin, 2=editor, 3=user):
1 | -- 取得目前使用者中,權限代碼最大與最小的值 |
ORDER BY 一律依字典順序,沒有例外:
1 | -- 狀況 A:字母 A 到 Z 排序,取第 1 筆 |
想像資料庫裡有一個「守門員」,只要有人對某張表做新增、修改或刪除,它就會自動跳出來多做一些事——不用後端程式特別呼叫,這就是 MySQL 的 Trigger(觸發器):一種會在資料變更的「之前」或「之後」自動執行的預存程序。
BEFORE:在資料實際寫入或變更之前執行,常用於欄位值的檢查、攔截或自動修正AFTER:在資料實際寫入或變更之後執行,常用於同步更新其他表、寫入 Log 紀錄
INSERT、UPDATE或DELETE三選一,對應要攔截的資料變更動作
- 綁定在特定的某一張資料表(Table)上,離開這張表就不會觸發
NEW 代表即將寫入或更新後的新資料(可用於 INSERT 和 UPDATE);OLD 代表被修改前或被刪除的舊資料(可用於 UPDATE 和 DELETE)。
假設有一張 users 表,如果使用者寫入年齡小於 0,自動將其修正為 0:
1 | DELIMITER // |
當使用者的資料被修改時,自動把舊資料和新資料寫入到另一張 user_logs 表中備份:
1 | DELIMITER // |
- 確保資料完整性:不管後端程式(PHP、Java、Node.js)怎麼寫,只要進到資料庫都會被強制執行,是最後一道防線
- 自動化維護:不需要在後端程式碼裡重複寫入 Log、同步更新其他表的邏輯
- 隱藏邏輯、難以排查 Bug:後端開發者看程式碼以為只做了一次
INSERT,卻不知道資料庫底層偷偷跑了 5 個 Trigger,出錯時很難追蹤 - 效能隱憂:Trigger 是
FOR EACH ROW(逐筆觸發),一口氣更新 10 萬筆資料,Trigger 就會被執行 10 萬次,可能導致資料庫瞬間卡死
Inserted 和 Deleted 虛擬表,而不是 NEW/OLD)與 MySQL 有些微不同。
MySql Bug
例如我們今天建立一個 trigger,用來監聽服裝表格的異動,如果服裝資料被修改,trigger 會正常被觸發,但是若修改是被動發生的,例如它儲存買家的 ID 作為 fk,因為買家被刪掉了導致她變成 NULL,這種被動修改在 MYSQL 中就不會觸發 trigger

Postgres
官方文件直接舉
1 | ON UPDATE CASCADE |
這類 FK 自動造成的修改,affected table 上相關 trigger 會被觸發
1 | DELETE Buyer |
也就是 PostgreSQL 會把 Buyer 被 DELETE 和它產生的 Clothes 被 UPDATE 看成同一個 SQL command 所引發的一連串資料異動
MSSQL
SQL Server 在這點也會處理 cascade 造成的 trigger。
Microsoft 文件明確說明 cascading referential actions 會觸發 AFTER UPDATE / AFTER DELETE triggers。
流程基本上是:
1 | DELETE Buyer |
而且 SQL Server 的順序設計得很明確是 referential cascade action 與 constraint check 成功完成之後,相關 AFTER trigger 才執行


