聊聊數(shù)據(jù)庫建表的15個小技巧
前言
對于后端開發(fā)同學(xué)來說,訪問數(shù)據(jù)庫,是代碼中必不可少的一個環(huán)節(jié)。
系統(tǒng)中收集到用戶的核心數(shù)據(jù),為了安全性,我們一般會存儲到數(shù)據(jù)庫,比如:mysql,oracle等。
后端開發(fā)的日常工作,需要不斷的建庫和建表,來滿足業(yè)務(wù)需求。
通常情況下,建庫的頻率比建表要低很多,所以,我們這篇文章主要討論建表相關(guān)的內(nèi)容。
如果我們在建表的時候不注意細(xì)節(jié),等后面系統(tǒng)上線之后,表的維護(hù)成本變得非常高,而且很容易踩坑。
今天就跟大家一起聊聊,數(shù)據(jù)庫建表的15個小技巧,希望對你會有所幫助。
一、名字
建表的時候,給表、字段和索引起個好名字,真的太重要了。
1、見名知意
名字就像表、字段和索引的一張臉,可以給人留下第一印象。
好的名字,言簡意賅,見名知意,讓人心情愉悅,能夠提高溝通和維護(hù)成本。
壞的名字,模擬兩可,不知所云。而且顯得雜亂無章,看得讓人抓狂。
反例:
用戶名稱字段定義成:yong_hu_ming、用戶_name、name、user_name_123456789
你看了可能會一臉懵逼,這是什么騷操作?
正例:
用戶名稱字段定義成:user_name
溫馨提醒一下,名字也不宜過長,盡量控制在?
?30?
?個字符以內(nèi)。
2、大小寫
名字盡量都用小寫字母,因?yàn)閺囊曈X上,小寫字母更容易讓人讀懂。
反例:
字段名:PRODUCT_NAME、PRODUCT_name
全部大寫,看起來有點(diǎn)不太直觀。而一部分大寫,一部分小寫,讓人看著更不爽。
正例:
字段名:product_name
名字還是使用全小寫字母,看著更舒服。
3、分隔符
很多時候,名字為了讓人好理解,有可能會包含多個單詞。
那么,多個單詞間的分隔符該用什么呢?
反例:
字段名:productname、productName、product name、product@name
單詞間沒有分隔,或者單詞間用駝峰標(biāo)識,或者單詞間用空格分隔,或者單詞間用@分隔,這幾種方式都不太建議。
正例:
字段名:product_name
強(qiáng)烈建議大家在單詞間用_分隔。
4、表名
對于表名,在言簡意賅,見名知意的基礎(chǔ)之上,建議帶上業(yè)務(wù)前綴。
如果是訂單相關(guān)的業(yè)務(wù)表,可以在表名前面加個前綴:order_。
例如:order_pay、order_pay_detail等。
如果是商品相關(guān)的業(yè)務(wù)表,可以在表名前面加個前綴:product_。
例如:product_spu,product_sku等。
這樣做的好處是為了方便歸類,把相同業(yè)務(wù)的表,可以非常快速的聚集到一起。
另外,還有有個好處是,如果哪天有非訂單的業(yè)務(wù),比如:金融業(yè)務(wù),也需要建一個名字叫做pay的表,可以取名:finance_pay,就能非常輕松的區(qū)分。
這樣就不會出現(xiàn)同名表的情況。
5、字段名稱
字段名稱是開發(fā)人員發(fā)揮空間最大,但也最容易發(fā)生混亂的地方。
比如有些表,使用flag表示狀態(tài),另外的表用status表示狀態(tài)。
可以統(tǒng)一一下,使用status表示狀態(tài)。
如果一個表使用了另一個表的主鍵,可以在另一張表的名后面,加_id或_sys_no,例如:
在product_sku表中有個字段,是product_spu表的主鍵,這時候可以取名:product_spu_id或product_spu_sys_no。
還有創(chuàng)建時間,可以統(tǒng)一成:create_time,修改時間統(tǒng)一成:update_time。
刪除狀態(tài)固定為:delete_status。
其實(shí)還有很多公共字段,在不同的表之間,可以使用全局統(tǒng)一的命名規(guī)則,定義成相同的名稱,以便于大家好理解。
6、索引名
在數(shù)據(jù)庫中,索引有很多種,包括:主鍵、普通索引、唯一索引、聯(lián)合索引等。
每張表的主鍵只有一個,一般使用:id或者sys_no命名。
普通索引和聯(lián)合索引,其實(shí)是一類。在建立該類索引時,可以加ix_前綴,比如:ix_product_status。
唯一索引,可以加ux_前綴,比如:ux_product_code。
二、字段類型
在設(shè)計表時,我們在選擇字段類型時,可發(fā)揮空間很大。
時間格式的數(shù)據(jù)有:date、datetime和timestamp等等可以選擇。
字符類型的數(shù)據(jù)有:varchar、char、text等可以選擇。
數(shù)字類型的數(shù)據(jù)有:int、bigint、smallint、tinyint等可以選擇。
說實(shí)話,選擇很多,有時候是一件好事,也可能是一件壞事。
如何選擇一個合適的字段類型,變成了我們不得不面對的問題。
如果字段類型選大了,比如:原本只有1-10之間的10個數(shù)字,結(jié)果選了bigint,它占8個字節(jié)。
其實(shí),1-10之間的10個數(shù)字,每個數(shù)字1個字節(jié)就能保存,選擇tinyint更為合適。
這樣會白白浪費(fèi)7個字節(jié)的空間。
如果字段類型擇小了,比如:一個18位的id字段,選擇了int類型,最終數(shù)據(jù)會保存失敗。
所以選擇一個合適的字段類型,還是非常重要的一件事情。
以下原則可以參考一下:
- 盡可能選擇占用存儲空間小的字段類型,在滿足正常業(yè)務(wù)需求的情況下,從小到大,往上選。
- 如果字符串長度固定,或者差別不大,可以選擇char類型。如果字符串長度差別較大,可以選擇varchar類型。
- 是否字段,可以選擇bit類型。
- 枚舉字段,可以選擇tinyint類型。
- 主鍵字段,可以選擇bigint類型。
- 金額字段,可以選擇decimal類型。
- 時間字段,可以選擇timestamp或datetime類型。
三、字段長度
前面我們已經(jīng)定義好了字段名稱,選擇了合適的字段類型,接下來,需要重點(diǎn)關(guān)注的是字段長度了。
比如:varchar(20),biginit(20)等。
那么問題來了,varchar代表的是字節(jié)長度,還是字符長度呢?
答:在mysql中除了varchar和char是代表字符長度之外,其余的類型都是代表字節(jié)長度。
biginit(n) 這個n表示什么意思呢?
假如我們定義的字段類型和長度是:bigint(4),bigint實(shí)際長度是8個字節(jié)。
現(xiàn)在有個數(shù)據(jù)a=1,a顯示4個字節(jié),所以在不滿4個字節(jié)時前面填充0(前提是該字段設(shè)置了zerofill屬性),比如:0001。
當(dāng)滿了4個字節(jié)時,比如現(xiàn)在數(shù)據(jù)是a=123456,它會按照實(shí)際的長度顯示,比如:123456。
但需要注意的是,有些mysql客戶端即使?jié)M了4個字節(jié),也可能只顯示4個字節(jié)的內(nèi)容,比如會顯示成:1234。
所以bigint(4),這里的4表示顯示的長度為4個字節(jié),實(shí)際長度還是占8個字節(jié)。
四、字段個數(shù)
我們在建表的時候,一定要對字段個數(shù)做一些限制。
我之前見過有人創(chuàng)建的表,有幾十個,甚至上百個字段,表中保存的數(shù)據(jù)非常大,查詢效率很低。
如果真有這種情況,可以將一張大表拆成多張小表,這幾張表的主鍵相同。
建議每表的字段個數(shù),不要超過20個。
五、主鍵
在創(chuàng)建表時,一定要創(chuàng)建主鍵。
因?yàn)橹麈I自帶了主鍵索引,相比于其他索引,主鍵索引的查詢效率最高,因?yàn)樗恍枰乇怼?/p>
此外,主鍵還是天然的唯一索引,可以根據(jù)它來判重。
在單個數(shù)據(jù)庫中,主鍵可以通過AUTO_INCREMENT,設(shè)置成自動增長的。
但在分布式數(shù)據(jù)庫中,特別是做了分庫分表的業(yè)務(wù)庫中,主鍵最好由外部算法(比如:雪花算法)生成,它能夠保證生成的id是全局唯一的。
除此之外,主鍵建議保存跟業(yè)務(wù)無關(guān)的值,減少業(yè)務(wù)耦合性,方便今后的擴(kuò)展。
不過我也見過,有些一對一的表關(guān)系,比如:用戶表和用戶擴(kuò)展表,在保存數(shù)據(jù)時是一對一的關(guān)系。
這樣,用戶擴(kuò)展表的主鍵,可以直接保存用戶表的主鍵。
六、存儲引擎
在mysql8以前的版本,默認(rèn)的存儲引擎是myslam,而mysql8以后的版本,默認(rèn)的存儲引擎變成了innodb。
之前我們還在創(chuàng)建表時,還一直糾結(jié)要選哪種存儲引擎?
myslam的索引和數(shù)據(jù)分開存儲,而有利于查詢,但它不支持事務(wù)和外鍵等功能。
而innodb雖說查詢性能,稍微弱一點(diǎn),但它支持事務(wù)和外鍵等,功能更強(qiáng)大一些。
以前的建議是:讀多寫少的表,用myslam存儲引擎。而寫多讀多的表,用innodb。
但雖說mysql對innodb存儲引擎性能的不斷優(yōu)化,現(xiàn)在myslam和innodb查詢性能相差已經(jīng)越來越小。
所以,建議我們在使用mysql8以后的版本時,直接使用默認(rèn)的innodb存儲引擎即可,無需額外修改存儲引擎。
七、NOT NULL
在創(chuàng)建字段時,需要選擇該字段是否允許為NULL。
我們在定義字段時,應(yīng)該盡可能明確該字段NOT NULL。
為什么呢?
我們主要以innodb存儲引擎為例,myslam存儲引擎沒啥好說的。
主要有以下原因:
- 在innodb中,需要額外的空間存儲null值,需要占用更多的空間。
- null值可能會導(dǎo)致索引失效。
- null值只能用is null或者is not null判斷,用=號判斷永遠(yuǎn)返回false。
因此,建議我們在定義字段時,能定義成NOT NULL,就定義成NOT NULL。
但如果某個字段直接定義成NOT NULL,萬一有些地方忘了給該字段寫值,就會insert不了數(shù)據(jù)。
這也算合理的情況。
但有一種情況是,系統(tǒng)有新功能上線,新增了字段。上線時一般會先執(zhí)行sql腳本,再部署代碼。
由于老代碼中,不會給新字段賦值,則insert數(shù)據(jù)時,也會報錯。
由此,非常有必要給NOT NULL的字段設(shè)置默認(rèn)值,特別是后面新增的字段。
例如:
alter table product_sku add column brand_id int(10) not null default 0;
八、外鍵
在mysql中,是存在外鍵的。
外鍵存在的主要作用是:保證數(shù)據(jù)的一致性和完整性。
例如:
create table class (
id int(10) primary key auto_increment,
cname varchar(15)
);
有個班級表class。
然后有個student表:
create table student(
id int(10) primary key auto_increment,
name varchar(15) not null,
gender varchar(10) not null,
cid int,
foreign key(cid) references class(id)
);
其中student表中的cid字段,保存的class表的id,這時通過foreign key增加了一個外鍵。
這時,如果你直接通過student表的id刪除數(shù)據(jù),會報異常:
a foreign key constraint fails
必須要先刪除class表對于的cid那條數(shù)據(jù),再刪除student表的數(shù)據(jù)才行,這樣能夠保證數(shù)據(jù)的一致性和完整性。
順便說一句:只有存儲引擎是innodb時,才能使用外鍵。
如果只有兩張表的關(guān)聯(lián)還好,但如果有十幾張表都建了外鍵關(guān)聯(lián),每刪除一次主表,都需要同步刪除十幾張子表,很顯然性能會非常差。
因此,互聯(lián)網(wǎng)系統(tǒng)中,一般建議不使用外鍵。因?yàn)檫@類系統(tǒng)更多的是為了性能考慮,寧可犧牲一點(diǎn)數(shù)據(jù)一致性和完整性。
除了外鍵之外,存儲過程和觸發(fā)器也不太建議使用,他們都會影響性能。
九、索引
在建表時,除了指定主鍵索引之外,還需要創(chuàng)建一些普通索引。
例如:
create table product_sku(
id int(10) primary key auto_increment,
spu_id int(10) not null,
brand_id int(10) not null,
name varchar(15) not null
);
在創(chuàng)建商品表時,使用spu_id(商品組表)和brand_id(品牌表)的id。
像這類保存其他表id的情況,可以增加普通索引:
create table product_sku (
id int(10) primary key auto_increment,
spu_id int(10) not null,
brand_id int(10) not null,
name varchar(15) not null,
KEY `ix_spu_id` (`spu_id`) USING BTREE,
KEY `ix_brand_id` (`brand_id`) USING BTREE
);
后面查表的時候,效率更高。
但索引字段也不能建的太多,可能會影響保存數(shù)據(jù)的效率,因?yàn)樗饕枰~外的存儲空間。
建議單表的索引個數(shù)不要超過:5個。
如果在建表時,發(fā)現(xiàn)索引個數(shù)超過5個了,可以刪除部分普通索引,改成聯(lián)合索引。
順便說一句:在創(chuàng)建聯(lián)合索引的時候,需要使用注意最左匹配原則,不然,建的聯(lián)合索引效率可能不高。
對于數(shù)據(jù)重復(fù)率非常高的字段,比如:狀態(tài),不建議單獨(dú)創(chuàng)建普通索引。因?yàn)榧词辜恿怂饕绻鹠ysql發(fā)現(xiàn)全表掃描效率更高,可能會導(dǎo)致索引失效。
如果你對索引失效問題比較感興趣,可以看看我的另一篇文章《聊聊索引失效的10種場景,太坑了》,里面有非常詳細(xì)的介紹。
十、時間字段
時間字段的類型,我們可以選擇的范圍還是比較多的,目前mysql支持:date、datetime、timestamp、varchar等。
varchar類型可能是為了跟接口保持一致,接口中的時間類型是String。
但如果哪天我們要通過時間范圍查詢數(shù)據(jù),效率會非常低,因?yàn)檫@種情況沒法走索引。
date類型主要是為了保存日期,比如:2020-08-20,不適合保存日期和時間,比如:2020-08-20 12:12:20。
而datetime和timestamp類型更適合我們保存日期和時間。
但它們有略微區(qū)別。
- timestamp:用4個字節(jié)來保存數(shù)據(jù),它的取值范圍為1970-01-01 00:00:01 UTC ~ 2038-01-19 03:14:07。此外,它還跟時區(qū)有關(guān)。
- datetime:用8個字節(jié)來保存數(shù)據(jù),它的取值范圍為1000-01-01 00:00:00 ~ 9999-12-31 23:59:59。它跟時區(qū)無關(guān)。
優(yōu)先推薦使用datetime類型保存日期和時間,可以保存的時間范圍更大一些。
溫馨提醒一下,在給時間字段設(shè)置默認(rèn)值是,建議不要設(shè)置成:0000-00-00 00:00:00,不然查詢表時可能會因?yàn)檗D(zhuǎn)換不了,而直接報錯。
十一、金額字段
mysql中有多個字段可以表示浮點(diǎn)數(shù):float、double、decimal等。
而float和double可能會丟失精度,因此推薦大家使用decimal類型保存金額。
一般我們是這樣定義浮點(diǎn)數(shù)的:decimal(m,n)。
其中n是指小數(shù)的長度,而m是指整數(shù)加小數(shù)的總長度。
假如我們定義的金額類型是這樣的:decimal(10,2),則表示整數(shù)長度是8位,并且保留2位小數(shù)。
十二、唯一索引
唯一索引在我們實(shí)際工作中,使用頻率相當(dāng)高。
你可以給單個字段,加唯一索引,比如:組織機(jī)構(gòu)code。
也可以給多個字段,加一個聯(lián)合的唯一索引,比如:分類編號、單位、規(guī)格等。
單個的唯一索引還好,但如果是聯(lián)合的唯一索引,字段值出現(xiàn)null時,則唯一性約束可能會失效。
創(chuàng)建唯一索引時,相關(guān)字段一定不能包含null值,否則唯一性會失效。
十三、字符集
mysql中支持的字符集有很多,常用的有:latin1、utf-8、utf8mb4、GBK等。
這4種字符集情況如下:
latin1容易出現(xiàn)亂碼問題,在實(shí)際項(xiàng)目中使用比較少。
而GBK支持中文,但不支持國際通用字符,在實(shí)際項(xiàng)目中使用也不多。
從目前來看,mysql的字符集使用最多的還是:utf-8和utf8mb4。
其中utf-8占用3個字節(jié),比utf8mb4的4個字節(jié),占用更小的存儲空間。
但utf-8有個問題:即無法存儲emoji表情,因?yàn)閑moji表情一般需要4個字節(jié)。
由此,使用utf-8字符集,保存emoji表情時,數(shù)據(jù)庫會直接報錯。
所以,建議在建表時字符集設(shè)置成:utf8mb4,會省去很多不必要的麻煩。
十四、排序規(guī)則
不知道,你關(guān)注過沒,在mysql中創(chuàng)建表時,有個COLLATE參數(shù)可以設(shè)置。
例如:
CREATE TABLE `order` (
`id` bigint NOT NULL AUTO_INCREMENT,
`code` varchar(20) COLLATE utf8mb4_bin NOT NULL,
`name` varchar(30) COLLATE utf8mb4_bin NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `un_code` (`code`),
KEY `un_code_name` (`code`,`name`) USING BTREE,
KEY `idx_name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
它是用來設(shè)置排序規(guī)則的。
字符排序規(guī)則跟字符集有關(guān),比如:字符集如果是utf8mb4,則字符排序規(guī)則也是以:utf8mb4_開頭的,常用的有:utf8mb4_general_ci、utf8mb4_bin等。
其中utf8mb4_general_ci排序規(guī)則,對字母的大小寫不敏感。說得更直白一點(diǎn),就是不區(qū)分大小寫。
而utf8mb4_bin排序規(guī)則,對字符大小寫敏感,也就是區(qū)分大小寫。
說實(shí)話,這一點(diǎn)還是非常重要的。
假如order表中現(xiàn)在有一條記錄,name的值是大寫的YOYO,但我們用小寫的yoyo去查,例如:
select * from order where name='yoyo';
如果字符排序規(guī)則是utf8mb4_general_ci,則可以查出大寫的YOYO的那條數(shù)據(jù)。
如果字符排序規(guī)則是utf8mb4_bin,則查不出來。
由此,字符排序規(guī)則一定要根據(jù)實(shí)際的業(yè)務(wù)場景選擇,否則容易出現(xiàn)問題。
十五、大字段
我們在創(chuàng)建表時,對一些特殊字段,要額外關(guān)注,比如:大字段,即占用較多存儲空間的字段。
比如:用戶的評論,這就屬于一個大字段,但這個字段可長可短。
但一般會對評論的總長度做限制,比如:最多允許輸入500個字符。
如果直接定義成text類型,可能會浪費(fèi)存儲空間,所以建議將這類字段定義成varchar類型的存儲效率更高。
當(dāng)然,我還見過更大的字段,即該字段直接保存合同數(shù)據(jù)。
一個合同可能會占幾Mb。
在mysql中保存這種數(shù)據(jù),從系統(tǒng)設(shè)計的角度來說,本身就不太合理。
像合同這種非常大的數(shù)據(jù),可以保存到mongodb中,然后在mysql的業(yè)務(wù)表中,保存mongodb表的id。