爲何數據庫字段要使用NOT NULL?

最近剛入職新公司,發現數據庫設計有點小問題,數據庫字段不少沒有NOT NULL,對於強迫症晚期患者來講,簡直難以忍受,所以有了這篇文章。html

基於目前大部分的開發現狀來講,咱們都會把字段所有設置成NOT NULL而且給默認值的形式。mysql

一般,對於默認值通常這樣設置:sql

  1. 整形,咱們通常使用0做爲默認值。數據庫

  2. 字符串,默認空字符串緩存

  3. 時間,能夠默認1970-01-01 08:00:01,或者默認0000-00-00 00:00:00,可是鏈接參數要添加zeroDateTimeBehavior=convertToNull,建議的話仍是不要用這種默認的時間格式比較好數據庫設計

可是,考慮下緣由,爲何要設置成NOT NULL?編輯器

來自高性能Mysql中有這樣一段話:函數

儘可能避免NULL性能

不少表都包含可爲NULL(空值)的列,即便應用程序並不須要保存NULL也是如此,這是由於可爲NULL是列的默認屬性。一般狀況下最好指定列爲NOT NULL,除非真的須要存儲NULL值。測試

若是查詢中包含可爲NULL的列,對MySql來講更難優化,由於可爲NULL的列使得索引、索引統計和值比較都更復雜。可爲NULL的列會使用更多的存儲空間,在MySql裏也須要特殊處理。當可爲NULL的列被索引時,每一個索引記錄須要一個額外的字節,在MyISAM裏甚至還可能致使固定大小的索引(例如只有一個整數列的索引)變成可變大小的索引。

一般把可爲NULL的列改成NOT NULL帶來的性能提高比較小,因此(調優時)沒有必要首先在現有schema中查找並修改掉這種狀況,除非肯定這會致使問題。可是,若是計劃在列上建索引,就應該儘可能避免設計成可爲NULL的列。

固然也有例外,例如值得一提的是,InnoDB使用單獨的位(bit)存儲NULL值,因此對於稀疏數據有很好的空間效率。但這一點不適用於MyISAM。

書中的描述說了幾個主要問題,我這裏暫且拋開MyISAM的問題不談,這裏我針對InnoDB做爲考量條件。

  1. 若是不設置NOT NULL的話,NULL是列的默認值,若是不是自己須要的話,儘可能就不要使用NULL
  2. 使用NULL帶來更多的問題,好比索引、索引統計、值計算更加複雜,若是使用索引,就要避免列設置成NULL
  3. 若是是索引列,會帶來的存儲空間的問題,須要額外的特殊處理,還會致使更多的存儲空間佔用
  4. 對於稀疏數據又更好的空間效率,稀疏數據指的是不少值爲NULL,只有少數行的列有非NULL值的狀況

默認值

對於MySql而言,若是不主動設置爲NOT NULL的話,那麼插入數據的時候默認值就是NULL。

NULL和NOT NULL使用的空值表明的含義是不同,NULL能夠認爲這一列的值是未知的,空值則能夠認爲咱們知道這個值,只不過他是空的而已。

舉個例子,一張表中的某一條name字段是NULL,咱們能夠認爲不知道名字是什麼,反之若是是空字符串則能夠認爲咱們知道沒有名字,他就是一個空值

而對於大多數程序的狀況而言,沒有什麼特殊須要非要字段要NULL的吧,NULL值反而會對程序形成好比空指針的問題。

對於現狀大部分使用MyBatis的狀況來講,我建議使用默認生成的insertSelective方法或者純手動寫插入方法,能夠避免新增NOT NULL字段致使的默認值不生效或者插入報錯的問題。

值計算

聚合函數不許確

對於NULL值的列,使用聚合函數的時候會忽略NULL值。

如今咱們有一張表,name字段默認是NULL,此時對name進行count得出的結果是1,這個是錯誤的。

count(*)是對錶中的行數進行統計,count(name)則是對錶中非NULL的列進行統計。

=失效

對於NULL值的列,是不能使用=表達式進行判斷的,下面對name的查詢是不成立的,必須使用is NULL

與其餘值運算

NULL和其餘任何值進行運算都是NULL,包括表達式的值也是NULL。

user表第二條記錄age是NULL,因此+1以後仍是NULL,name是NULL,進行concat運算以後結果仍是NULL。

能夠再看下下面的例子,任何和NULL進行運算的話得出的結果都會是NULL,想象下你設計的某個字段若是是NULL還不當心進行各類運算,最後得出的結果。。。

distinct、group by、order by

對於distinctgroup by來講,全部的NULL值都會被視爲相等,對於order by來講升序NULL會排在最前

其餘問題

表中只有一條有名字的記錄,此時查詢名字!=a預期的結果應該是想查出來剩餘的兩條記錄,會發現與預期結果不匹配。

索引問題

爲了驗證NULL字段對索引的影響,分別對nameage添加索引。

關於網上不少說若是NULL那麼不能使用索引的說法,這個描述其實並不許確,根據引用官方文檔[3]裏描述,使用is NULL和範圍查詢都是能夠和正常同樣使用索引的,實際驗證的結果好像也是這樣,看如下例子。

而後接着咱們往數據庫中繼續插入一些數據進行測試,當NULL列值變多以後發現索引失效了。

咱們知道,一個查詢SQL執行大概是這樣的流程:

首先鏈接器負責鏈接到指定的數據庫上,接着看看查詢緩存中是否有這條語句,若是有就直接返回結果。

若是緩存沒有命中的話,就須要分析器來對SQL語句進行語法和詞法分析,判斷SQL語句是否合法。

如今來到優化器,就會選擇使用什麼索引比較合理,SQL語句具體怎麼執行的方案就肯定下來了。

最後執行器負責執行語句、有無權限進行查詢,返回執行結果。

從上面的簡單測試結果其實能夠看到,索引列存在NULL就會存在書中所說的致使優化器在作索引選擇的時候更復雜,更加難以優化。

存儲空間

數據庫中的一行記錄在最終磁盤文件中也是以行的方式來存儲的,對於InnoDB來講,有4種行存儲格式:REDUNDANTCOMPACTDYNAMICCOMPRESSED

InnoDB的默認行存儲格式是COMPACT,存儲格式以下所示,虛線部分表明可能不必定會存在。

變長字段長度列表:有多個字段則以逆序存儲,咱們只有一個字段全部不考慮那麼多,存儲格式是16進制,若是沒有變長字段就不須要這一部分了。

NULL值列表:用來存儲咱們記錄中值爲NULL的狀況,若是存在多個NULL值那麼也是逆序存儲,而且必須是8bit的整數倍,若是不夠8bit,則高位補0。1表明是NULL,0表明不是NULL。若是都是NOT NULL那麼這個就存在了。

ROW_ID:一行記錄的惟一標誌,沒有指定主鍵的時候自動生成的ROW_ID做爲主鍵。

TRX_ID:事務ID。

ROLL_PRT:回滾指針。

最後就是每列的值。

爲了說明清楚這個存儲格式的問題,我弄張表來測試,這張表只有c1字段是NOT NULL,其餘都是能夠爲NULL的。

可變字段長度列表c1c3字段值長度分別爲1和2,因此長度轉換爲16進制是0x01 0x02,逆序以後就是0x02 0x01

NULL值列表:由於存在容許爲NULL的列,因此c2,c3,c4分別爲010,逆序以後仍是同樣,同時高位補0滿8位,結果是00000010

其餘字段咱們暫時無論他,最後第一條記錄的結果就是,固然這裏咱們就不考慮編碼以後的結果了。

這樣就是一個完整的數據行數據的格式,反之,若是咱們把全部字段都設置爲NOT NULL,而且插入一條數據a,bb,ccc,dddd的話,存儲格式應該這樣:

雖然咱們發現NULL自己並不會佔用存儲空間,可是若是存在NULL的話就會多佔用一個字節的標誌位的空間。

文章參考文檔:

  1. https://dev.mysql.com/doc/refman/8.0/en/problems-with-null.html
  2. https://dev.mysql.com/doc/refman/8.0/en/working-with-null.html
  3. https://dev.mysql.com/doc/refman/5.6/en/is-null-optimization.html
  4. https://dev.mysql.com/doc/refman/5.6/en/innodb-row-format.html
  5. https://www.cnblogs.com/zhoujinyi/articles/2726462.html
相關文章
相關標籤/搜索