MySQL 中主键的几种表设计组合的实际应用效果
了解主鍵、外鍵、索引
主鍵
主鍵的主要作用是保證表的完整、保證表數(shù)據(jù)行的唯一性質(zhì),
① 業(yè)務(wù)主鍵(自然主鍵):在數(shù)據(jù)庫表中把具有業(yè)務(wù)邏輯含義的字段作為主鍵,稱為“自然主鍵(Natural Key)”。
自然主鍵的含義就是原始數(shù)據(jù)中存在的不重復(fù)字段,直接使用成為主鍵字段。這種方式對(duì)業(yè)務(wù)的耦合太強(qiáng),一般不會(huì)使用。
② 邏輯主鍵(代理主鍵):在數(shù)據(jù)庫表中采用一個(gè)與當(dāng)前表中邏輯信息無關(guān)的字段作為其主鍵,稱為“代理主鍵”。
邏輯主鍵提供了一個(gè)與當(dāng)前表數(shù)據(jù)邏輯無關(guān)的字段作為主鍵,邏輯主鍵被廣泛使用在業(yè)務(wù)表、數(shù)據(jù)表,一般有幾種生成方式:uuid、自增。其中使用最多的是自增,邏輯主鍵成功的避免了主鍵與數(shù)據(jù)表關(guān)聯(lián)耦合的問題,與業(yè)務(wù)主鍵不同的是,業(yè)務(wù)主鍵的數(shù)據(jù)一旦發(fā)生更改,那么那個(gè)系統(tǒng)中關(guān)于主鍵的所有信息都需要連帶修改,這是不可避免的,并且這個(gè)更改是隨業(yè)務(wù)需求的增量而不斷的增加、膨脹。而邏輯主鍵與應(yīng)用耦合度低,它與數(shù)據(jù)無任何必要的關(guān)系,你可以只關(guān)心:第一條數(shù)據(jù);而不用關(guān)心:名字是a的那條數(shù)據(jù)。某一天名字改成b, 你還是只關(guān)心:第一條數(shù)據(jù)。
業(yè)務(wù)的更改幾乎是不可避免的,前期任何產(chǎn)品經(jīng)理言之鑿鑿的不修改論調(diào)都是不可靠、不切實(shí)際的。我們必須考慮主鍵數(shù)據(jù)在更改的情況下,數(shù)據(jù)能否平穩(wěn)度過危機(jī)。
③ 復(fù)合主鍵(聯(lián)合主鍵):通過兩個(gè)或者多個(gè)字段的組合作為主鍵。
復(fù)合主鍵可以說是業(yè)務(wù)主鍵的升級(jí)版本,通常一個(gè)業(yè)務(wù)字段不能夠確定一條數(shù)據(jù)的唯一性,例如 張三的身份證是34123322, 張三這種大眾名稱100%會(huì)出現(xiàn)重復(fù)。我們可以用姓名 + 身份證的方式表示主鍵,聲明一個(gè)唯一的記錄。
有時(shí)候,復(fù)合主鍵是復(fù)雜的。姓名+身份證 不一定能表示不重復(fù),雖然身份證在17年消除了重復(fù)的問題,但是之前的數(shù)據(jù)呢?可能我們需要新增一個(gè)地址作為聯(lián)合主鍵,例如 姓名 + 身份證 + 聯(lián)系地址確認(rèn)一個(gè)人的身份。在其他的業(yè)務(wù)中,例如訪問控制,用戶 + 終端 + 終端類型 + 站點(diǎn) + 頁面 + 時(shí)間,可能六個(gè)字段的聯(lián)合才能夠去確定一個(gè)字段的唯一性,這另復(fù)雜度陡升。
另外如果其他表要與該表關(guān)聯(lián)則需要引用復(fù)合主鍵的所有字段,這就不單純是性能問題了,還有存儲(chǔ)空間的問題了,當(dāng)然你也可以認(rèn)為這是合理的數(shù)據(jù)冗余,方便查詢,但是感覺有點(diǎn)得不償失。
使用復(fù)合主鍵的原因可能是:對(duì)于關(guān)系表來說必須關(guān)聯(lián)兩個(gè)實(shí)體表的主鍵,才能表示它們之間的關(guān)系,那么可以把這兩個(gè)主鍵聯(lián)合組成復(fù)合主鍵即可。
如果兩個(gè)實(shí)體存在多個(gè)關(guān)系,可以再加一個(gè)順序字段聯(lián)合組成復(fù)合主鍵,但是這樣就會(huì)引入業(yè)務(wù)主鍵的弊端。當(dāng)然也可以另外對(duì)這個(gè)關(guān)系表添加一個(gè)邏輯主鍵,避免了業(yè)務(wù)主鍵的弊端,同時(shí)也方便其他表對(duì)它的引用。
外鍵
外鍵是一種約束,表與表的關(guān)聯(lián)約束,例如a表依賴關(guān)聯(lián)b表的某個(gè)字段,你可以設(shè)置a表字段外鍵關(guān)聯(lián)到b表的字段,將兩張表強(qiáng)制關(guān)聯(lián)起來,這時(shí)候產(chǎn)生兩個(gè)效果
① 表 b 無法被刪除,你必須先刪除a表
② 新增的數(shù)據(jù)必須與表b某行關(guān)聯(lián)
這對(duì)某些需要強(qiáng)耦合的業(yè)務(wù)操作來說很有必要,但、 要強(qiáng)調(diào)但是,外鍵約束我認(rèn)為,不可濫用,沒有合適的理由支撐它的使用的話,將導(dǎo)致業(yè)務(wù)強(qiáng)制耦合。另外對(duì)開發(fā)人員不夠友好。使用外鍵一定不能超過3表相互。否則將引出很多的麻煩而不得不取消外鍵。
索引
索引用于快速找出在某個(gè)列中有一特定值的行,不使用索引,MySQL必須從第一條記錄開始讀完整個(gè)表,直到找出相關(guān)的行,表越大,查詢數(shù)據(jù)所花費(fèi)的時(shí)間就越多,如果表中查詢的列有一個(gè)索引,MySQL能夠快速到達(dá)一個(gè)位置去搜索數(shù)據(jù)文件,而不必查看所有數(shù)據(jù),那么將會(huì)節(jié)省很大一部分時(shí)間。
例如:有一張person表,其中有2W條記錄,記錄著2W個(gè)人的信息。有一個(gè)Phone的字段記錄每個(gè)人的電話號(hào)碼,現(xiàn)在想要查詢出電話號(hào)碼為xxxx的人的信息。
如果沒有索引,那么將從表中第一條記錄一條條往下遍歷,直到找到該條信息為止。
如果有了索引,那么會(huì)將該P(yáng)hone字段,通過一定的方法進(jìn)行存儲(chǔ),好讓查詢?cè)撟侄紊系男畔r(shí),能夠快速找到對(duì)應(yīng)的數(shù)據(jù),而不必在遍歷2W條數(shù)據(jù)了。其中MySQL中的索引的存儲(chǔ)類型有兩種BTREE、HASH。也就是用樹或者Hash值來存儲(chǔ)該字段,要知道其中詳細(xì)是如何查找的,就需要會(huì)算法的知識(shí)了。我們現(xiàn)在只需要知道索引的作用,功能是什么就行。
優(yōu)點(diǎn):
1、所有的MySql列類型(字段類型)都可以被索引,也就是可以給任意字段設(shè)置索引
2、大大加快數(shù)據(jù)的查詢速度
缺點(diǎn):
1、創(chuàng)建索引和維護(hù)索引要耗費(fèi)時(shí)間,并且隨著數(shù)據(jù)量的增加所耗費(fèi)的時(shí)間也會(huì)增加
2、索引也需要占空間,我們知道數(shù)據(jù)表中的數(shù)據(jù)也會(huì)有最大上線設(shè)置的,如果我們有大量的索引,索引文件可能會(huì)比數(shù)據(jù)文件更快達(dá)到上線值
3、當(dāng)對(duì)表中的數(shù)據(jù)進(jìn)行增加、刪除、修改時(shí),索引也需要?jiǎng)討B(tài)的維護(hù),降低了數(shù)據(jù)的維護(hù)速度。
使用原則:
索引需要合理的使用。
1、對(duì)經(jīng)常更新的表就避免對(duì)其進(jìn)行過多的索引,對(duì)經(jīng)常用于查詢的字段應(yīng)該創(chuàng)建索引,
2、數(shù)據(jù)量小的表最好不要使用索引,因?yàn)橛捎跀?shù)據(jù)較少,可能查詢?nèi)繑?shù)據(jù)花費(fèi)的時(shí)間比遍歷索引的時(shí)間還要短,索引就可能不會(huì)產(chǎn)生優(yōu)化效果。
3、在一同值少的列上(字段上)不要建立索引,比如在學(xué)生表的"性別"字段上只有男,女兩個(gè)不同值。相反的,在一個(gè)字段上不同值較多可是建立索引。
測試主鍵的影響力
為了說明業(yè)務(wù)主鍵、邏輯主鍵、復(fù)合主鍵對(duì)數(shù)據(jù)表的影響力,博主使用java生成四組測試數(shù)據(jù),首先準(zhǔn)備表結(jié)構(gòu)為:
`id`?int(10)?UNSIGNED?NOT?NULL?AUTO_INCREMENT,??--?自增`dt`?varchar(40)?CHARACTER?SET?utf8?COLLATE?utf8_general_ci?NULL?DEFAULT?NULL,?????--?使用uuid模擬不同的id`name`?varchar(255)?CHARACTER?SET?utf8?COLLATE?utf8_general_ci?NULL?DEFAULT?NULL,??--?隨機(jī)名稱`age`?int(10)?NULL?DEFAULT?NULL,???--?隨機(jī)數(shù)生成年齡`key`?varchar(40)?CHARACTER?SET?utf8?COLLATE?utf8_general_ci?NULL?DEFAULT?NULL,??--?唯一標(biāo)識(shí)?使用uuid測試PRIMARY?KEY?(`id`)?USING?BTREE?--?設(shè)置主鍵將生成四組千萬條的數(shù)據(jù):
自增主鍵 test_primary_a
自增主鍵 有索引 test_primary_d
無主鍵 無索引 test_primary_b
復(fù)合主鍵 無索引 test_primary_c
使用java, spring boot + mybatis每次批量一萬條數(shù)據(jù),插入一千次,記錄每次插入時(shí)間,總插入時(shí)間:
mybatis代碼:
<insert?id="insertTestData">insert?into?test_primary_${code}?(`dt`,`name`,`age`,`key`)?values<foreach?collection="items"?item="item"??index=?"index"?separator?=",">(#{item.dt},#{item.name},#{item.age},#{item.key})</foreach>java代碼,使用了mybatis插件提供的事務(wù)處理:
@Transactional(readOnly?=?false)public?Object?testPrimary?(String?type)?{HashMap?result?=?new?HashMap();//?記錄總耗時(shí)?開始時(shí)間long?start?=?new?Date().getTime();//?記錄總耗時(shí)?插入條數(shù)int?len?=?0;try{String[]?names?=?{"趙一",?"錢二",?"張三"?,?"李四",?"王五",?"宋六",?"陳七",?"孫八",?"歐陽九"?,?"徐10"};for?(int?w?=?0;?w?<?1000;?w++)?{//?記錄萬條耗時(shí)long?startMil?=?new?Date().getTime();ArrayList<HashMap>?items?=?new?ArrayList<>();for?(int?i?=?0;?i?<?10000;?i++)?{String?dt?=?StringUtils.uuid();String?key?=?StringUtils.uuid();int?age?=?(int)((Math.random()?*?9?+?1)?*?10);?//?隨機(jī)兩位String?name?=?names[(int)(Math.random()?*?9?+?1)];HashMap?item?=?new?HashMap<>();item.put("dt",?dt);item.put("key",?key);item.put("age",?age);item.put("name",?name);items.add(item);}len?+=?tspTagbodyMapper.insertTestData(items,?type);long?endMil?=?new?Date().getTime();//?萬條最終耗時(shí)result.put(w,?endMil?-?startMil);}long?end?=?new?Date().getTime();//?總耗時(shí)result.put("all",?end?-?start);result.put("len",?len);return?result;}?catch?(Exception?e)?{System.out.println(e.toString());result.put("e",?e.toString());}return?result;}最終生成的數(shù)據(jù)表情況:
1、自增主鍵 test_primary_a ---------- 數(shù)據(jù)長度 960MB
62分鐘插入一千萬條數(shù)據(jù) 平均一萬條數(shù)據(jù)插入 4秒
2、自增主鍵 有索引 test_primary_d 數(shù)據(jù)長度 1GB 索引長度 1.36GB
75分鐘插入一千萬條數(shù)據(jù) 平均一萬條數(shù)據(jù)插入 4.5秒
3、無主鍵 無索引 test_primary_b ----------- 數(shù)據(jù)長度 960MB
65分鐘插入一千萬條數(shù)據(jù) 平均一萬條數(shù)據(jù)插入 4.2秒
4、復(fù)合主鍵 無索引 test_primary_c ----------- 數(shù)據(jù)長度 1.54GB
219分鐘插入一千萬條數(shù)據(jù) 平均一萬條數(shù)據(jù)插入 8秒, 這里有一個(gè)問題, 復(fù)合主鍵的數(shù)據(jù)插入耗時(shí)是線性增長的,當(dāng)數(shù)據(jù)小于100萬 插入時(shí)常在五秒左右, 當(dāng)數(shù)據(jù)變大,插入時(shí)長無限變大,在1000萬條數(shù)據(jù)時(shí),平均插入一萬數(shù)據(jù)秒數(shù)已經(jīng)達(dá)到15秒了。
查詢速度
注意索引的建立時(shí)以name字段為開頭,索引的生效第一個(gè)條件必須是name
簡單查詢:
select name,age from test_primary_a where age=20 -- 自增主鍵 無索引 結(jié)果條數(shù)11萬 平均3.5秒
select name,age from test_primary_a where name='張三' and age=20 -- 自增主鍵 有索引 結(jié)果條數(shù)11萬 平均650豪秒
select name,age from test_primary_b where age=20 -- 無主鍵 無索引 結(jié)果條數(shù)11萬 平均7秒
select name,age from test_primary_c where age=20 -- 聯(lián)合主鍵 無索引 結(jié)果條數(shù)11萬 平均4.5秒
稍復(fù)雜條件:
select name,age,key,dt from test_primary_a where age=20 and (name='王五' or name = '張三') and dt like '%abc%' -- 自增主鍵 無索引 結(jié)果條數(shù)198 平均4.2秒
select dt,name,age,key?from test_primary_d where (name='王五' or name = '張三') and age=20 and dt like '%abc%' -- 自增主鍵 有索引 結(jié)果條數(shù)204 平均650豪秒
select name,age,key,dt from test_primary_d where age=20 and (name='王五' or name = '張三') and dt like '%abc%' -- 無主鍵 無索引 結(jié)果條數(shù)194 平均5.9秒
select name,age,key,dt from test_primary_c where age=20 and (name='王五' or name = '張三') and dt like '%abc%' -- 聯(lián)合主鍵 無索引 結(jié)果條數(shù)11萬 平均5秒
這樣的語句更夸張一點(diǎn):
select name,age,dt from test_primary_c where dt like '%0000%' and name='張三' -- 聯(lián)合主鍵 無索引 結(jié)果條數(shù)359 平均8秒
select name,age,dt from test_primary_c where dt like '%0000%' and name='張三' -- 自增主鍵 有索引 結(jié)果條數(shù)400 平均1秒
初步結(jié)論
從實(shí)際應(yīng)用中可以看出:用各主鍵的對(duì)比,在導(dǎo)入速度上,在前期百萬數(shù)據(jù)時(shí),各表表現(xiàn)一致,在百萬數(shù)據(jù)以后,復(fù)合主鍵的新增時(shí)長將線性增長,應(yīng)該是因?yàn)槊恳粭l新增都需要判斷是否重復(fù),而數(shù)據(jù)量一旦增大,每次新增都需要全表篩查。
另外一點(diǎn),邏輯主鍵 + 索引的方式占用空間一共2.4G, 復(fù)合主鍵占用1.54G 相差大約1個(gè)G , 但是實(shí)際查詢效果看起來索引更勝一籌,只要查詢方法得當(dāng),索引應(yīng)該是當(dāng)前的首選。
最后,關(guān)于復(fù)合主鍵的作用?我想應(yīng)該是在業(yè)務(wù)主鍵字段不超過2-3個(gè)的情況下,需要確保數(shù)據(jù)維度的唯一性,采取復(fù)合主鍵加上限制。
總結(jié)
以上是生活随笔為你收集整理的MySQL 中主键的几种表设计组合的实际应用效果的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java中的享元设计模式,涨姿势了!
- 下一篇: 何谓 SQL 注入,这个漫画告诉你!