表的主鍵指的針對一張表中的一列或者多列,其結果必須能標識表中每行記錄的唯一性。 表是索引組織表,主鍵既是數(shù)據(jù)也是索引。
主鍵的設計原則
1. 對空間占用要小
上一篇我們介紹過 主鍵的存儲方式,主鍵占用空間越小,每個索引頁里存放的鍵值越多,這樣一次性放入內(nèi)存的數(shù)據(jù)也就越多。
2. 最好是有一定的排序屬性
如 INT32 類型來做主鍵表可以沒有主鍵嗎,數(shù)值有嚴格的排序,那新記錄的插入只要往原先數(shù)據(jù)頁后面添加新記錄或者在數(shù)據(jù)頁后新增空頁來填充記錄即可,這樣有嚴格排序的主鍵寫入速度也會非常快。
3. 數(shù)據(jù)類型為整形
數(shù)據(jù)類型早就已經(jīng)講過,按照前兩點的需求,最理想的當然是選擇整數(shù)類型,比如 int32 。數(shù)據(jù)順序增長,要么是數(shù)據(jù)庫自己生成,要么是業(yè)務自動生成。
一、與業(yè)務無關的屬性做主鍵
1.1 自增字段做主鍵
這是 MySQL 最推薦的方式。一般用 INT32 可以滿足大部分場景,單庫單表可以最大保存 42 億行記錄;含有自增字段的新增記錄會順序添加到當前索引節(jié)點的后續(xù)位置直到數(shù)據(jù)頁寫滿為止,再寫新頁。這樣會極大程度地減少數(shù)據(jù)頁的隨機 IO。
用自增字段做主鍵可能需要注意兩個問題:
第一個問題:MySQL 原生自增鍵拆分
如果隨著數(shù)據(jù)后期增長,有拆庫拆表預期,可以考慮用 INT64;MySQL 原生支持拆庫拆表的自增主鍵表可以沒有主鍵嗎,通過自增步長與起始值來確定。最少要有 2 個 MySQL 節(jié)點,每個節(jié)點自增步長為 2,假設 分別為 1,2,那自增起始值也可以是 1,2。假設下面是第 1 個 MySQL 節(jié)點,設置好了步長和起始值后,表 tmp 插入三行,每行嚴格按照設置的方式插入數(shù)據(jù)。
mysql>?set?@@auto_increment_increment=2;
Query?OK,?0?rows?affected?(0.00?sec)
mysql>?set?@@auto_increment_offset=1;
Query?OK,?0?rows?affected?(0.00?sec)
mysql>?insert?into?tmp?values(null),(null),(null);
Query?OK,?3?rows?affected?(0.01?sec)
Records:?3??Duplicates:?0??Warnings:?0
mysql>?select?*?from?tmp;
+----+
|?id?|
+----+
|??1?|
|??3?|
|??5?|
+----+
3?rows?in?set?(0.00?sec)
但是這塊 MySQL 并不能保證其他的值不沖突,比如插入一條節(jié)點 2 的值,也能成功插入,MySQL 默認對這塊沒有什么約束,最好是數(shù)據(jù)入庫前就校驗好。
mysql>?insert?into?tmp?values(2);
Query?OK,?1?row?affected?(0.02?sec)
mysql>?select?*?from?tmp;
+----+
|?id?|
+----+
|??1?|
|??2?|
|??3?|
|??5?|
+----+

4?rows?in?set?(0.00?sec)
第二個問題:MySQL 自增鍵合并
這個問題一般牽扯到老的系統(tǒng)改造升級,比如多個分部老系統(tǒng)數(shù)據(jù)要向新系統(tǒng)合并,那之前每個分部的自增主鍵不能簡單地合并,可能會有主鍵沖突。舉個例子,假設武漢市每個區(qū)都有自己的醫(yī)保數(shù)據(jù),并且以前每個區(qū)都是自己獨立設計的數(shù)據(jù)庫,現(xiàn)在醫(yī)保要升級為全市統(tǒng)一,以市為單位設計新的數(shù)據(jù)庫模型。
武昌的數(shù)據(jù)如下,對應表 n1,
mysql>?select??*?from?n1;
+----+
|?id?|
+----+
|??1?|
|??2?|
|??3?|
+----+
3?rows?in?set?(0.00?sec)
漢陽的數(shù)據(jù)如下,對應表 n2,
mysql>?select?*?from?n2;
+----+
|?id?|
+----+
|??1?|
|??2?|
|??3?|
+----+
3?rows?in?set?(0.00?sec)
由于之前兩個區(qū)數(shù)據(jù)庫設計的人都沒有考慮以后合并的事情,所以每個區(qū)的表都有自己獨立的自增主鍵,
考慮這樣建立一張匯總表 n3,有新的自增 ID,并且設計導入老系統(tǒng)的 ID。
mysql>?create?table?n3?(id?int?auto_increment?primary?key,?old_id?int);
Query?OK,?0?rows?affected?(0.07?sec)
mysql>?insert?into?n3?(old_id)?select?*?from?n1?union?all?select?*?from?n2;
Query?OK,?6?rows?affected?(0.01?sec)
Records:?6??Duplicates:?0??Warnings:?0
mysql>?select?*?from?n3;
+----+--------+
|?id?|?old_id?|
+----+--------+
|??1?|??????1?|
|??2?|??????2?|
|??3?|??????3?|
|??4?|??????1?|
|??5?|??????2?|
|??6?|??????3?|
+----+--------+
6?rows?in?set?(0.00?sec)
這樣進行匯總, 應用代碼可能不太確定怎么連接老的數(shù)據(jù),這張表缺少一個 到原始表名的映射。
那基于原始表 ID 與原始表名的映射關系建立一個多值索引。比如以下例子:
mysql>?create?table?n4(old_id?int,?old_name?varchar(64),primary?key(old_id,old_name));
Query?OK,?0?rows?affected?(0.05?sec)
mysql>?insert?into?n4?select?id?,'n1'?from?n1?union?all?select?id,'n2'?from?n2;

Query?OK,?6?rows?affected?(0.02?sec)
Records:?6??Duplicates:?0??Warnings:?0
mysql>?select?*?from?n4;
+--------+----------+
|?old_id?|?old_name?|
+--------+----------+
|??????1?|?n1???????|
|??????1?|?n2???????|
|??????2?|?n1???????|
|??????2?|?n2???????|
|??????3?|?n1???????|
|??????3?|?n2???????|
+--------+----------+
6?rows?in?set?(0.00?sec)
最終表結構,結合前面兩張表 n3 和 n4,建立一個包含新的自增字段主鍵,原來表 ID,原來表名的新表:
create table n5(
id?int?unsigned?auto_increment?primary?key,
old_id?int,
old_name?varchar(64),
unique?key?udx_old_id_old_name?(old_id,old_name)
);
當然,關于數(shù)據(jù)匯總遷移的話題,討論篇幅太長,不在本節(jié)范圍。
1.2 UUID 做主鍵
UUID 和自增主鍵一樣,能保證主鍵的唯一性。但是天生無序、隨機產(chǎn)生、占用空間大。在 MySQL 里,用 char(36) 來存儲 UUID,沒有專門的 UUID 數(shù)據(jù)類型,類似這樣的字符串: ‘-7d59-11ea-8add-’。由于 表的特性,應該避免用 char(36) 保存原始 UUID 的方式做表主鍵。
雖然 UUID 無序,且存在空間浪費,但天生隨機這個優(yōu)點能否利用上?
MySQL 提供了以下的優(yōu)化方法來讓原始 UUID 可以被用于表主鍵:
函數(shù)
MySQL 提供了函數(shù) ,把 UUID 字符串變?yōu)?16 個字節(jié)的二進制串。類似于某些數(shù)據(jù)庫(比如 )的 UUID 類型。函數(shù) 返回數(shù)據(jù)類型為 (16)。
例如表 ,
mysql>?create?table?t_binary(id?varbinary(16)?primary?key,r1?int,?key?idx_r1(r1));
Query?OK,?0?rows?affected?(0.07?sec)
mysql>?insert?into?t_binary?values?(uuid_to_bin(uuid()),1),(uuid_to_bin(uuid()),2);
Query?OK,?2?rows?affected?(0.01?sec)
Records:?2??Duplicates:?0??Warnings:?0
mysql>?select?*?from?t_binary;
+------------------------------------+------+
|?id?????????????????????????????????|?r1???|
+------------------------------------+------+
|?0x412234A77DEF11EA9AF9080027C52750?|????1?|
|?0x412236E27DEF11EA9AF9080027C52750?|????2?|
+------------------------------------+------+
2?rows?in?set?(0.00?sec)
函數(shù)
(16) 依然是無序的,為此 MySQL 還提供了一個函數(shù) ,用來生成類似 UUID 的全局 ID,結果為 INT64。具體計算方式如下:
( & 255)