對於MySQL的一些個規範,某些公司建表規範中有一項要求是所有欄位非空,意味著沒有值的時候存儲一個預設值。其實所有欄位非空這麼說應該是絕對了,應該說是儘可能非空,某些情況下不可能給出一個預設值。那麼這條要求,是基於哪些考慮因素,存儲空間?相關增刪查改操作的性能?亦或是其他考慮?該理論到底有沒有道理或 ...
對於MySQL的一些個規範,某些公司建表規範中有一項要求是所有欄位非空,意味著沒有值的時候存儲一個預設值。其實所有欄位非空這麼說應該是絕對了,應該說是儘可能非空,某些情況下不可能給出一個預設值。
那麼這條要求,是基於哪些考慮因素,存儲空間?相關增刪查改操作的性能?亦或是其他考慮?該理論到底有沒有道理或者可行性,本文就個人的理解,做一個粗淺的分析。
1,基於存儲的考慮
這裡對存儲的分析要清楚MySQL數據行的存儲格式,這裡直接從這篇文章白嫖一部分結論,文章里分析的非常清楚(其實也是參考《MySQL技術內容Innodb存儲引擎》)。
對於預設的Dynamic或者Compact格式的數據行結構,其行結構格式如下:
|變長欄位長度列表(1~2位元組)|NULL標誌位(1位元組)|記錄頭信息(5位元組)|RowID(6位元組)|事務ID(6位元組)|回滾指針(7位元組)|row content
1,對於變長欄位,當相關的欄位值為NULL時,相關欄位不會占用存儲空間。NULL值沒有存儲,不占空間,但是需要一個標誌位(一行一個)。
2,對於變長欄位,相關欄位要求NOT NULL,存儲成''的時候,也不占用空間,如果一個表中所有的字典都NOT NULL,行頭不需要NULL的標誌位
3,所有欄位都是定長,不管是否要求為NOT NULL,都不需要標誌位,同時不需要存儲變長列長度
鑒於null值和非空(not null default '')兩種情況,如果一個欄位存儲的內容是空,也就是什麼都沒有,前者存儲為null,後者存儲為空字元串'',兩者欄位內容本身存儲空間大小是一樣的。
但是如果一個表中存儲在可空欄位的情況下,其對應的數據行的頭部,都需要一個1位元組的NULL標誌位,這個就決定了存儲同樣的數據,如果允許為null,相比not null的情況下,每行多了一個位元組的存儲空間的。
這個因素或者就是某些公司或者個人堅持“所有表禁止null欄位”這個信仰的原因之一(個人持否定態度,可以嘗試將資料庫中所有的欄位都至為not null 然後default一個值後會不會雞飛狗跳)。
這裡不再去做“微觀”的分析,直接從“巨集觀”的角度來看一下差異。
測試demo
直接創建結構一致,但是一個表欄位not null,一個表欄位為null,然後使用存儲此過程,兩張表同時按照null值與非null值1:10的比例寫入數據,也就是說每10行數據中1行數據欄位為null的方式寫入600W行數據。
CREATE TABLE a ( id INT AUTO_INCREMENT, c2 VARCHAR(50) NOT NULL DEFAULT '', c3 VARCHAR(50) NOT NULL DEFAULT '', PRIMARY KEY (id) ); CREATE TABLE b ( id INT AUTO_INCREMENT, c2 VARCHAR(50), c3 VARCHAR(50), PRIMARY KEY (id) ); CREATE DEFINER=`root`@`%` PROCEDURE `create_test_data`( IN `loop_cnt` INT ) LANGUAGE SQL NOT DETERMINISTIC CONTAINS SQL SQL SECURITY DEFINER COMMENT '' BEGIN DECLARE v2 , v3 VARCHAR(36); START TRANSACTION; while loop_cnt>0 do SET v2 = UUID(); SET v3 = UUID(); if (loop_cnt MOD 10) = 0 then INSERT INTO a (c2,c3) VALUES(DEFAULT,DEFAULT); INSERT INTO b (c2,c3) VALUES(DEFAULT,DEFAULT); else INSERT INTO a (c2,c3) VALUES (v2,v3); INSERT INTO b (c2,c3) VALUES (v2,v3); END if ; SET loop_cnt=loop_cnt-1; END while; COMMIT;View Code
a,b兩張表生產完全一致的數據。
查看占用的存儲空間情況,從information_schema.TABLES中查詢這兩個表的存儲信息
1,一個位元組的差別,體現在avg_row_length,a表因為所有的欄位都是not null,因此相比b表,每行節省了每行節省了一個位元組的存儲
2,總得空間的差別:a表662683648/1024/1024=631.98437500MB,b表666877952/1024/1024=635.98437500MB,
也當前情況下,600W行數據有4MB的差異,差異在1%之內,其實實際情況下,欄位多,table size更大的的時候,這個差異會遠遠小於1%。
就存儲空間來說,你跟我說1T的資料庫你在乎1GB的存儲空間,隨便一點數據/索引碎片空間,一點預留空間,垃圾文件空間,無用索引空間……,都遠遠大於可為空帶來的額外這一點差異。
2,增刪查改的效率
讀寫操作對比,通過連續讀寫一個範圍之內的數據,來對比a,b兩張表在讀上面的情況。
2.1.)首先buffer pool是遠大於table size的,因此不用擔心物理IO引起的差異,目前兩張表的數據完全都存在與buffer pool中。
2.1.)讀測試操作放在MySQL實例機器上,因此網路不穩定引起的差異可以忽略。
增刪查改的差異與存儲空間的差異類似,甚至更小,因為單行相差1個位元組,放大到600W+才能看到一個5MB級別的差異,增刪查改的話,各種測試下來,沒有發現有明顯的差異
#!/usr/bin/env python3 import pymysql import time mysql_conn_conf = {'host': '127.0.0.1', 'port': 3306, 'user': 'root', 'password': '******', 'db': 'db01'} def mysql_read(table_name): conn = pymysql.connect(host=mysql_conn_conf['host'], port=mysql_conn_conf['port'], database=mysql_conn_conf['db'],user=mysql_conn_conf['user'],password = mysql_conn_conf['password']) cursor = conn.cursor() try: cursor.execute(''' select id,c2,c3 from {0} where id>3888888 and id<3889999;'''.format(table_name)) row = cursor.fetchall() except pymysql.Error as e: print("mysql execute error:", e) cursor.close() conn.close() def mysql_write(loop,table_name): conn = pymysql.connect(host=mysql_conn_conf['host'], port=mysql_conn_conf['port'], database=mysql_conn_conf['db'],user=mysql_conn_conf['user'],password = mysql_conn_conf['password']) cursor = conn.cursor() try: if loop%10 == 0: cursor.execute(''' insert into {0}} (c2,c3) values(DEFAULT,DEFAULT)'''.format(table_name)) else: cursor.execute(''' insert into {1}} (c2,c3) values(uuid(),uuid())'''.format(table_name)) except pymysql.Error as e: print("mysql execute error:", e) cursor.close() conn.commit() conn.close() if __name__ == '__main__': time_start = time.time() loop=10 while loop>0: mysql_write(loop) loop = loop-1 time_end = time.time() time_c= time_end - time_start print('time cost', time_c, 's')View Code
3,相關欄位上的語義解析和邏輯考慮
這一點就觀點差異就太多了,也是最容易引起口水或者爭議的了。
1,對於字元類型,NULL就是不存在,‘’就是空,不存在和空本身就不是一回事,不太認同一定要NOT NULL,然後給出預設值。
2,對於字元類型,任何資料庫中,NULL都是不等於NULL的,因為在處理相關欄位上進行join或者where篩選的時候,是不需要考慮連接雙方都為NULL的情況的,一旦用''替代了NULL,''是等於''的,此時就會出現與存儲NULL完全不用的語義
3,對於字元類型,一旦將相關欄位default成'',如何區分''與空字元串,比如備註欄位,不允許為NULL,default成‘’,那麼怎麼區分,NULL表達的空和預設值的空字元串''
4,對於相關的查詢操作,如果允許為NULL,篩選非NULL值就是where *** is not null,語義上很清晰直觀,一旦用欄位非空,預設成'',會使用where *** <>''這種看起來超級噁心的寫法,究竟要表達什麼,語義上就已經開始模糊了
5,對於時間類型,絕大多數時候是不允許有預設值的,預設多少合適,當前時間合適麽,千禧年2000合適麽,2008年北京奧運會開幕時間合適麽?
6,對於數值類型,比如int,比如decimal,在可空的情況下,如果禁止為NULL,預設給多少合適,0合適嗎?-1合適嗎?-9999999……合適嗎?10086合適嗎?1024合適嗎?說實話,預設多少都不合適,NULL自身就是最合適的。
個人觀點很明確,除非有特殊的需求要求一個欄位絕對不能出現NULL值的情況,正常情況下,該NULL就NULL。
如果NULL沒有存在的意義,乾脆資料庫就不要存在這個NULL就好了,事實上,哪個資料庫沒有NULL類型?
當然也不排除,某些DBA為了顯得自己專業,弄出來一些莫須有的東西,現在就是有一種風氣,在資料庫上能提出來的限制條件越多,越有優越感。
個人認識有限,數據實話,非常想知道“所有欄位非空”會帶來什麼其他哪些正面的影響,以及如何衡量這個正面的因素,還有,你們真的做到了,可以禁止整個實例下所有的庫表中的欄位禁止可空(nullable)?