MySQL枚舉類型的“八宗罪”
MySQL的 枚舉(ENUM)類型 是程序員群體中的一個討論熱點。乍一看,我們可以通過枚舉類型,很好地將記錄值限制在允許范圍內(nèi)。一個典型的例子是,一個具有字段名稱為“大陸板塊”的數(shù)據(jù)表:每一個國家位于一個大陸板塊,而這些大陸板塊不太可能經(jīng)常變化。當然,或許一天北美板塊會與亞洲板塊碰撞形成北美亞,但即便你的數(shù)據(jù)庫能夠延續(xù)使用到那個時候,起碼你也不需要研討怎么去重構你的數(shù)據(jù)表,那將是當時的開發(fā)者要做的工作。
言歸正傳。如果,使用ENUM是唯一 一個,能夠代表某一個國家屬于哪個大陸板塊的選擇,那我們大可進行下一步,去爭辯諸如NoSQL的優(yōu)劣、Git和SVN孰強孰弱、你喜歡的框架有哪些缺點這些其他的問題。但這里有一個普遍適用于實現(xiàn)枚舉的最佳實踐:
維基百科 是這樣描述關系表的:
…這是一種將可知的枚舉數(shù)據(jù)分離出來的表。例如,一個關系型數(shù)據(jù)庫的倉庫數(shù)據(jù),倉庫里面的“物件”有可能會有一個“狀態(tài)”的字段記錄已經(jīng)聲明的值,例如:“已售,預定,售罄”。在極簡的數(shù)據(jù)庫設計當中,這些值都會在獨立的關系表“狀態(tài)”中存儲,以此滿足范式(database normalization)。
所以,關系表也可以滿足枚舉的實現(xiàn)。下面就來看看,ENUM的”八宗罪“到底是什么:
1. 數(shù)據(jù)被錯誤對待
男、女;先生、夫人、小姐;非洲、亞洲,等等。這些人們使用作為ENUM類型字段的短詞稱為數(shù)據(jù)。當你使用一個ENUM類型字段, 技術上看,是你將數(shù)據(jù)抽離出來 (對應到實際數(shù)據(jù)表時), 放到一個獨立的地位(一種數(shù)據(jù)庫的元數(shù)據(jù),具有精確定義字段)。 這不同與約束數(shù)據(jù)類型,如我們通常的做法:數(shù)值型字段只能存儲整型數(shù)據(jù),或者日期型字段不能為空——這些都沒有問題,而且還十分重要。使用ENUM類型字段時,我們實際上是保存部分數(shù)據(jù) 去作為 這個數(shù)據(jù)模型的一個特征信息。簡而言之, ENUM類型字段破壞了范式要求。這也許看起來十分“學院派”或“迂腐陳舊”,但這正是以下各種“罪行”的源頭。
2. 更改ENUM類型字段,代價很昂貴
永恒不變的是, 每次你創(chuàng)建ENUM類型字段的時候都說:“這個字段不可能變的”。人類普遍欠缺顧全大局的能力,預測上更是糟糕,其如研發(fā)部的新產(chǎn)品線、貴司新的航運方案、北美板塊碰撞亞洲板塊。
使用ALTER TABLE去修改整個數(shù)據(jù)表的ENUM類型字段,是十分耗費資源的。如果將ENUM('red', 'blue', 'black') 改為 ENUM('red', 'blue', 'white'), MySQL 需要重構整個數(shù)據(jù)表,并且檢索 所有數(shù)據(jù)去檢查 'black'這個無效值。 MySQL 是真的蠢,它確實會在你每次增加一個新的ENUM值時都這么做的?。▊餮晕磥頃幚鞥NUM類型字段的效率問題,但我對其受重視程度深表懷疑。)
全表重構在小型數(shù)據(jù)表中可能沒有那么痛苦,但在海量數(shù)據(jù)的情況下可能會導致資源被鎖死很長很長一段時間。如果你使用關系表去替代ENUM類型字段,改變枚舉集合只不過是使用INSERT、UPDATE和DELETE,對比來看真是滑稽。
很重要的一點,當更改ENUM類型字段的枚舉集合時,MySQL會轉(zhuǎn)換任意已有但不存在于新的枚舉集合中的記錄值為''(空的字符串)。使用關系表,在更改和刪除枚舉集合時會靈活很多(下面會提到)。
3. 幾乎無法給關聯(lián)數(shù)據(jù)添加額外的屬性
至今都沒有一個可以更加明智地改變ENUM類型字段的方法,這也是我們的常態(tài)。在我們的“國家、大陸板塊”例子中, 更改“國土面積”會出現(xiàn)什么情況?我們沒有預料到這個屬性, 但也要既來之則安之。使用關系表設計,我們可以輕易地拓展“大陸板塊”這個數(shù)據(jù)表,各種方式為其增加我們想要的數(shù)據(jù)和字段 。ENUM?快別說了。
另一種極妙的靈活性體現(xiàn)在關系表的拓展便捷性上。一個簡單的標記位字段即可表示這個“枚舉值”是否可用。所以,當你的公司不打算銷售黑色的裝飾品了,你只需在“黑色”所對應的_isdiscontinued字段中做個標記即可。而且你依然可以查詢到已售的顏色(譯者:指的是,ENUM的修改會導致原有,而現(xiàn)在已經(jīng)沒有的值變?yōu)榭兆址?,?shù)據(jù)失去了部分特征),同時你那些黑色裝飾品的訂單依然可統(tǒng)可計哦!ENUM,你要不要試試?
4. 獲取ENUM全部可能值,很麻煩
一個很常見的需求是,將數(shù)據(jù)庫中存在的數(shù)據(jù)顯示在可拖拽列表中,例如:
選擇顏色:
紅 藍 黑
如果這些數(shù)值存儲在一個名為‘colors’的數(shù)據(jù)表里,你所要做的僅僅是:SELECT * FROM colors,這樣即可動態(tài)地令數(shù)據(jù)地顯示在可拖拽列表中。你可以添加或者改變color關系表中的顏色,并且,你那酷炫訂單的顏色可選項會自動更新,真了不起。 (譯:此處所舉例子,應等同于:“通過后臺管理,可以限定前端用戶某類型數(shù)據(jù)的可選項。”這樣的功能。)
回到ENUM上:你要如何獲取全部的枚舉值?你當然可以使用ENUM值搭配DISTINCT去查詢(譯:即是查詢ENUM值互相不相同的數(shù)據(jù),等于利用DISTINCT的唯一性去查詢ENUM),但這樣也只會返回確實使用過,并存在于數(shù)據(jù)表ENUM字段可選值中的ENUM值,而不是所有可能的值。你也可以查詢INFORMATION_SCHEMA然后通過代碼解析返回的數(shù)據(jù),去找到你想要的ENUM的所有值,但這完全是多此一舉。事實上,我依然沒有發(fā)現(xiàn),有任何兼顧了優(yōu)雅與原生的SQL方式,可以獲取ENUM類型字段的所有值。
5. ENUM類型字段所提供的優(yōu)化有限
通常使用ENUM的正當理由,不外乎“優(yōu)化”二字,譬如,性能提升,簡化模型與高可讀性。
那我們從性能上看。你可以在未優(yōu)化的數(shù)據(jù)庫中做很多匪夷所思而夸張的事,但是大多情況是,在數(shù)據(jù)達到一定規(guī)模前,都不會出現(xiàn)影響性能的情況,并且通常我們的產(chǎn)品遠未達到那個尺度規(guī)模。有一點需要注意的是,因為數(shù)據(jù)庫開發(fā)者們都熱衷于令自己的設計可以達到完備的范式,并且只會在遇到性能問題時才會考慮反范式。如果你擔心使用關系表會導致變慢,可以在同一基準下測試不同方式下的表現(xiàn),再進行考慮。切勿先入為主地認為關聯(lián)查詢會成為瓶頸,可能有時并非如此。(可參照 evidence to support that ENUM isn't always appreciably faster than alternatives.)
另一個關于ENUM優(yōu)化方式的說法是,ENUM可以有效減少數(shù)據(jù)庫中的數(shù)據(jù)表外鍵。不可置否,使用外鍵相當于是將很多不同的盒子以線相連,而且在大型系統(tǒng)中,范式設計已可降低對人類的理解能力界限、復雜型查詢的要求。但是,我們?yōu)槭裁磿O計模型,為什么要將模型抽象化以便我們能夠理解它。去試試做一個新數(shù)據(jù)模型圖或者ER圖,并且忽略一些小細節(jié)和關系表。有時候使用ENUM確實如看上去那般簡單,但事實上你在心里需要想著一個隱式的關系表,所以并沒有看上去那般簡單。
6. ENUM值在其他數(shù)據(jù)表中不可直接復用
當你(在數(shù)據(jù)表中)創(chuàng)建了一個帶值的ENUM字段,在其他數(shù)據(jù)表中無法直接復用這個ENUM。而當有了關系表,相同應用形式下,可以在其他多個數(shù)據(jù)表中復用。當改變關系表中的一個數(shù)據(jù),其他多個數(shù)據(jù)表也會得到響應。
ENUM類型字段的分離,將使你能在多個數(shù)據(jù)表中復用相同的ENUM值(需要保持一致性)。
7. ENUM類型字段有顯然陷阱
假設你設置了一個字段“color”ENUM('blue', 'black', 'red') ,這時你想INSERT一行數(shù)據(jù),但“color”字段是 'purple', MySQL 會將不合法的值變?yōu)?'' (空字符串)。 處理上沒問題, 但如果我們使用的是帶外鍵的關系表, 那么我們的數(shù)據(jù)能因健壯性而更加可靠。
同樣,MySQL 會為ENUM值關聯(lián)枚舉索引,并且在使用中會錯誤地調(diào)用到索引而不是ENUM值,反之亦然。
想象一下:
- CREATE TABLE test (foobar ENUM('0', '1', '2'));
- mysql> INSERT INTO test VALUES ('1'), (1);
- Query OK, 2 rows affected (0.00 sec)
- Records: 2 Duplicates: 0 Warnings: 0
- mysql> SELECT * FROM test;
- +--------+
- | foobar |
- +--------+
- | 1 |
- | 0 |
- +--------+
- 2 rows in set (0.00 sec)
我們插入了 '1' (字符串),并且不小心插入了 1 (沒有引號,數(shù)值型)。 MySQL 會將我們地數(shù)值型數(shù)據(jù)當作是枚舉索引去處理(并沒有錯,但會令人混淆),根據(jù)索引可知,ENUM字段的第一個值為 0 。(譯:枚舉索引由 1 開始)
8. ENUM 的移植性不佳
ENUM類型不是SQL標準,屬于MySQL,而其他DBMS不一定有原生的支持。 PostgreSQL, MariaDB,與Drizzle (后面那兩個就MySQL的分支), 我只知道這三個是支持的ENUM的。如果某個人打算將數(shù)據(jù)庫遷移, 那么他就要花費更多的步驟去處理你那些“精妙”的ENUM字段了,相信他會“更愛你”。如果(那個人)是你, 你可會發(fā)現(xiàn)自己當時真是“聰明夠了”。通常來說,數(shù)據(jù)庫遷移不會經(jīng)常發(fā)生,并且,由于所有人都會假設遷移數(shù)據(jù)庫的過程中,必然要出亂子,因此成為“第八宗罪”。
幾時適合使用ENUM:
1. 當你需要存儲的是準確、不變的值時
大陸板塊就是最好的例子,定義十分準確。另一個常用例子是稱謂:先生、夫人、小姐,或者是撲克的花色:方塊、梅花、紅心、黑桃。但是,即便是這些例子,有時也需要去拓展值的范圍(例如有人需要你稱呼“陳醫(yī)生”而不是“陳先生”的時候,或者你的撲克游戲里面需要用到小丑牌)。
2. 你永遠不需要存儲額外的關聯(lián)信息
用回撲克牌的例子。撲克游戲老少咸宜,依賴的規(guī)則是梅花和黑桃為黑色,方塊和紅心為紅色(例如,尤克牌)。如果我們需要為花色關聯(lián)額外的信息,例如顏色,那將如何?如果我們使用關系表,那我們只需要在關系表中新增字段即可,小事一樁。如果我們使用ENUM去表示花色,那我們就很難去準確的表示花色于顏色的關聯(lián)了,如此我們只能在應用層上去達成這種關聯(lián)。
3. ENUM值的數(shù)量大于2個并少于20個
如果你的ENUM值只有兩個,你完全可以將ENUM換成更加高效的TINYINT(1)或者更更高效的BIT(1)(MySQL5.0.3及以上)。例如: gender ENUM('male', 'female') 可以變換為: is_male BIT(1). 當你只有兩個選項時,完全能以布爾值 true/false,結合字段名字中的“is”關鍵詞來區(qū)分。至于20個的上限設定,沒錯,ENUM事實上可以保存多達65535個值,但求你千萬別試。超過二十個值會變得很累贅,超過50個必然難于管理與使用。
如果你無論如何都要用ENUM:
1. ENUM值千萬不要使用數(shù)值型
ENUM定義為字符型數(shù)據(jù)是有原因的。并不是說你使用數(shù)值型字段類型去存儲數(shù)字是錯誤的,但有充足的證據(jù)顯示,MySQL內(nèi)部機制使用數(shù)字去引用索引(參考 上面的第七條)。反正不要在ENUM中存儲數(shù)字,OK?
2. 考慮使用嚴格模式
啟用嚴格模式,至少在你插入一個不存在的ENUM值時會報告錯誤。否則,只會簡單地出現(xiàn)一個警告,繼而該值被設置為一個空字符串""(枚舉索引為0)。抄筆記:如果你設置了IGNORE,錯誤依然會被忽略。
結論
從開發(fā)、維護的角度去做有意義的事,性能問題出現(xiàn)時再考慮優(yōu)化——普遍而言,使用關系表抑或是使用ENUM類型,爭議不斷。
性能瓶頸(這個概念)被濫用已是不爭事實。 開發(fā)者們浪費了大量的時間去思考它、擔心它,(例如)非關鍵代碼上的運行速度。這些對效率的苛求,給調(diào)試與維護造成了很大的負面影響。我們理應忽略那小部分的效率,就拿(達到)97%(效率)而言,過早的優(yōu)化是萬惡之源。