第17期:索引設計(主鍵設計)

image

表的主鍵指的針對一張表中的一列或者多列,其結果必須能標識表中每行記錄的惟一性。InnoDB 表是索引組織表,主鍵既是數據也是索引。mysql

主鍵的設計原則

  1. 對空間佔用要小

上一篇咱們介紹過 InnoDB 主鍵的存儲方式,主鍵佔用空間越小,每一個索引頁裏存放的鍵值越多,這樣一次性放入內存的數據也就越多。sql

  1. 最好是有必定的排序屬性

如 INT32 類型來作主鍵,數值有嚴格的排序,那新記錄的插入只要往原先數據頁後面添加新記錄或者在數據頁後新增空頁來填充記錄便可,這樣有嚴格排序的主鍵寫入速度也會很是快。數據庫

  1. 數據類型爲整形

數據類型早就已經講過,按照前兩點的需求,最理想的固然是選擇整數類型,好比 int32 unsigned。數據順序增加,要麼是數據庫本身生成,要麼是業務自動生成。數據庫設計

1、與業務無關的屬性作主鍵

1.1 自增字段作主鍵

這是 MySQL 最推薦的方式。通常用 INT32 能夠知足大部分場景,單庫單表能夠最大保存 42 億行記錄;含有自增字段的新增記錄會順序添加到當前索引節點的後續位置直到數據頁寫滿爲止,再寫新頁。這樣會極大程度的減小數據頁的隨機 IO。
用自增字段作主鍵可能須要注意兩個問題:
第一個問題:MySQL 原生自增鍵拆分
若是隨着數據後期增加,有拆庫拆表預期,能夠考慮用 INT64;MySQL 原生支持拆庫拆表的自增主鍵,經過自增步長與起始值來肯定。最少要有 2 個 MySQL 節點,每一個節點自增步長爲 2,假設 server_id 分別爲 1,2,那自增起始值也能夠是 1,2。假設下面是第 1 個 MySQL 節點,設置好了步長和起始值後,表 tmp 插入三行,每行嚴格按照設置的方式插入數據。函數

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 並不能保證其餘的值不衝突,好比插入一條節點 2 的值,也能成功插入,MySQL 默認對這塊沒有什麼約束,最好是數據入庫前就校驗好。性能

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 自增鍵合併
這個問題通常牽扯到老的系統改造升級,好比多個分部老系統數據要向新系統合並,那以前每一個分部的自增主鍵不能簡單的合併,可能會有主鍵衝突。舉個例子,假設武漢市每一個區都有本身的醫保數據,而且之前每一個區都是本身獨立設計的數據庫,如今醫保要升級爲全市統一,以市爲單位設計新的數據庫模型。
武昌的數據以下,對應表 n1,優化

mysql> select  * from n1;
+----+
| id |
+----+
|  1 |
|  2 |
|  3 |
+----+
3 rows in set (0.00 sec)

漢陽的數據以下,對應表 n2,ui

mysql> select * from n2;
+----+
| id |
+----+
|  1 |
|  2 |
|  3 |
+----+
3 rows in set (0.00 sec)

因爲以前兩個區數據庫設計的人都沒有考慮之後合併的事情,因此每一個區的表都有本身獨立的自增主鍵,
考慮這樣創建一張彙總表 n3,有新的自增 ID,而且設計導入老系統的 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)

這樣進行彙總, 應用代碼可能不太肯定怎麼鏈接老的數據,這張表缺乏一個 old_id 到原始表名的映射。
那基於原始表 ID 與原始表名的映射關係創建一個多值索引。好比如下例子:spa

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)
);

固然,關於數據彙總遷移的話題,討論篇幅太長,不在本節範圍。

1.2 UUID 作主鍵

UUID 和自增主鍵同樣,能保證主鍵的惟一性。可是天生無序、隨機產生、佔用空間大。在 MySQL 裏,用 char(36) 來存儲 UUID,沒有專門的 UUID 數據類型,相似這樣的字符串: ‘7985847c-7d59-11ea-8add-080027c52750’。因爲 InnoDB 表的特性,應該避免用 char(36) 保存原始 UUID 的方式作表主鍵。
雖然 UUID 無序,且存在空間浪費,但天生隨機這個優勢可否利用上?
MySQL 提供瞭如下的優化方法來讓原始 UUID 能夠被用於表主鍵:
函數 uuid_to_bin
MySQL 提供了函數 uuid_to_bin,把 UUID 字符串變爲 16 個字節的二進制串。相似於某些數據庫(好比 POSTGRESQL)的 UUID 類型。函數 uuid_to_bin 返回數據類型爲 varbinary(16)。
例如表 t_binary,

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)

函數 uuid_short
varbinary(16) 依然是無序的,爲此 MySQL 還提供了一個函數 uuid_short,用來生成相似 UUID 的全局 ID,結果爲 INT64。具體計算方式以下:
(server_id & 255) << 56 + (server_startup_time_in_seconds << 24) + incremented_variable++;

  • server_id & 255:佔 1 個字節;
  • server_startup_time_in_seconds:佔 4 個字節;
  • incremented_variable: 佔 3 個字節。

若是知足如下條件,那這個值就一定是惟一的

  1. server_id 惟一而且對函數 uuid_short() 的調用次數不超過每秒 16777216 次,也就是 2^24。因此通常狀況下,uuid_short 函數能保證結果惟一。
  2. uuid_short 函數生成的 ID 只需一個輕量級的 mutex 來保護,這點比自增 ID 須要的 auto-inc 表鎖更省資源,生成結果確定更加快速。

下面表 t_uuid_short 演示瞭如何用這個函數。

mysql> create table t_uuid_short  (id bigint unsigned primary key,r1 int, key idx_r1(r1));
Query OK, 0 rows affected (0.06 sec)

mysql> insert into t_uuid_short values(uuid_short(),1),(uuid_short(),2)
Query OK, 2 rows affected (0.02 sec)
Records: 2  Duplicates: 0  Warnings: 0

mysql> select * from t_uuid_short;
+----------------------+------+
| id                   | r1   |
+----------------------+------+
| 16743984358464946177 |    1 |
| 16743984358464946178 |    2 |
+----------------------+------+
2 rows in set (0.00 sec)

能夠看到 uuid_short 生成的數據是基於 INT64 有序的,因此這塊能夠看作是自增 ID 的一個補充優化,若是每秒調用次數少於 16777216,推薦用 uuid_short,而非自增 ID。
說了那麼多,仍是簡單驗證下上面的結論,作個小實驗。
如下實驗涉及到四張表:

  • 新建 t_uuid: uuid 爲主鍵
  • 表 t_binary:varbinary(16) 爲主鍵
  • 表 t_uuid_short:bigint 爲主鍵
  • 新建表 t_id:自增 ID 爲主鍵

正如以前的預期,寫性能差別按從最差到最好排列依次爲:t_uuid; t_binary;t_id;t_uuid_short。咱們來實驗下是否和預期相符。
新增的兩張表結構:

mysql> create table t_uuid(id char(36) primary key, r1 int, key idx_r1(r1));
Query OK, 0 rows affected (0.06 sec)

mysql> create table t_id (id bigint auto_increment primary key, r1 int, key idx_r1(r1));
Query OK, 0 rows affected (0.08 sec)

簡單寫了一個存儲過程,分別給這些表造 30W 條記錄。

DELIMITER $$

CREATE

  PROCEDURE `ytt`.`sp_insert_data`(
  f_tbname VARCHAR(64),
  f_number INT UNSIGNED
  )

    BEGIN
    DECLARE i INT UNSIGNED DEFAULT 0; 
    SET @@autocommit=0;
    IF f_tbname = 't_uuid' THEN
      SET @stmt = CONCAT('insert into t_uuid values (uuid(),ceil(rand()*100));');
   ELSEIF f_tbname = 't_binary' THEN
     SET @stmt = CONCAT('insert into t_binary values(uuid_to_bin(uuid()),ceil(rand()*100));');
    ELSEIF f_tbname = 't_uuid_short' THEN
     SET @stmt = CONCAT('insert into t_uuid_short values(uuid_short(),ceil(rand()*100));');
    ELSEIF f_tbname = 't_id' THEN
      SET @stmt = CONCAT('insert into t_id(r1) values(ceil(rand()*100));');
    END IF;
    
    WHILE i < f_number
    DO 
      PREPARE s1 FROM @stmt;
      EXECUTE s1;
      SET i = i + 1;
      IF MOD(i,50) = 0 THEN
       COMMIT;
      END IF;
    END WHILE;
    COMMIT;
    DROP PREPARE s1;
SET @@autocommit=1;
    END$$
    
 DELIMITER ;

接下來分別調用存儲過程,結果和預期一致。t_uuid 時間最長,t_uuid_short 時間最短。

mysql> call sp_insert_data('t_uuid',300000);
Query OK, 0 rows affected (5 min 23.33 sec)

mysql> call sp_insert_data('t_binary',300000);
Query OK, 0 rows affected (4 min 48.92 sec)

mysql> call sp_insert_data('t_id',300000);
Query OK, 0 rows affected (3 min 40.38 sec)

mysql> call sp_insert_data('t_uuid_short',300000);
Query OK, 0 rows affected (3 min 9.94 sec)

2、與業務有關的屬性作主鍵。

主鍵的設計要求可讀性很強,相似學生學號(入學年份+所屬系+所讀專業),購物訂單編碼等。其實很是不建議主鍵用這樣有實際意義的業務字段。能夠新建一個自增主鍵或者 uuid_short() 函數字段,實際業務字段非主鍵設計,變爲普通惟一索引。好比表 n5:

mysql> create table n5(
        id int unsigned auto_increment primary key, 
        userno int unsigned ,
        unique key udx_userno(userno)
        );
Query OK, 0 rows affected (0.08 sec)

用 userno(用戶編碼)來作主鍵,若是在業務端數據已經錯誤,好比可能因爲老師緣由錄入錯誤數據,或者是業務系統的 BUG 致使錄入數據有誤, 那不只要對錄入表的主鍵作更改(這但是聚簇索引),還要更改依賴這張表的全部子表,這實際上是一個很大的工程。可是若是有與業務不相關的主鍵,只須要更改業務字段(二級索引)就能夠,不須要更改依賴這張表的子表。
關於 MySQL 主鍵的設計思路大體介紹到此,有問題歡迎留言,歡迎指正本篇任何不足之處。


關於 MySQL 的技術內容,大家還有什麼想知道的嗎?趕忙留言告訴小編吧!
image

相關文章
相關標籤/搜索