前些天有個粉絲在群裡發了一張圖片,成功引起了我的注意,圖片如下:
大致內容是在mysql中,一個表的自增id用完了,繼續插入到底會不會報錯?說實話,工作這麼多年,我還真沒遇到這種問題,也沒太想過,我的第一反應應該是會報錯,否則mysql也不會有那麼多類型和長度的限制;但是同時我也很疑惑,說不定mysql有什麼保護機制,可以自動升級類型之類的,接下來我將帶領大家去一層層揭開這個疑點。
我們可以想一下,資料庫為什麼需要主鍵?不需要主鍵行不行?資料庫三範式設計中,其中第二範式就明確要求表中每一行數據都需要被維一區分,這個時候主鍵就很必要了,比如一張顧客表,可以通過顧客id當作主鍵,訂單表可以通過訂單id當作主鍵,只要你認為能唯一標示這張表就可以;雖然我們並不總是都需要主鍵,但大多數資料庫設計人員都應保證他們創建的每個表有一個主鍵,以便於以後數據操縱和管理表中的任何列都可以作為主鍵,只要它滿足以下條件:
1、任何兩行都不具有相同的主鍵值
2、每個行都必須具有一個主鍵值(主鍵列不允許NULL值)
主鍵的底層是用索引來實現的,而索引主要分為聚簇索引和非聚簇索引,接下來我來介紹一下聚簇索引和非聚簇索引的區別。
聚簇索引聚簇索引不是一種單獨的索引類型,而是一種數據存儲方式。innodb的聚簇索引實際上在同一個結構中保存了B-tree索引和數據行。當表有聚簇索引時,數據行實際上是存儲在索引的葉子頁中。聚簇:表示數據行和相鄰的鍵值緊湊地存儲在一起。一個表只能有一個聚簇索引,聚簇索引如何存放記錄如圖:
聚簇索引的優點:
1.可以把相關數據保存在一起
2.數據訪問更快(聚集索引將索引和數據保存在同一個b-tree中)
3.使用覆蓋索引掃描的查詢可以直接使用頁節點中的主鍵值
聚簇索引的缺點:
1、聚簇數據提高了IO性能,如果數據全部放在內存中,則訪問的順序就沒那麼重要了
2、插入速度嚴重依賴插入順序。按主鍵的順序插入是速度最快的。但如果不是按照主鍵順序加載數據,則需在加載完成後最好使用optimize table重新組織一下表
3、更新聚簇索引列的代價很高。因為會強制innod將每個被更新的行移動到新的位置
4、基於聚簇索引的表在插入新行,或主鍵被更新導致需要移動行的時候,可能面臨頁分裂的問題。頁分裂會導致表佔用更多的磁碟空間。
5、聚簇索引可能導致全表掃描變慢,尤其是行比較稀疏,或由於頁分裂導致數據存儲不連續時。
非聚簇索引,又叫二級索引。二級索引的葉子節點中保存的不是指向行的物理指針,而是行的主鍵值。當通過二級索引查找行,存儲引擎需要在二級索引中找到相應的葉子節點,獲得行的主鍵值,然後使用主鍵去聚簇索引中查找數據行,這需要兩次B-Tree查找。
下表代表聚簇索引和非聚簇索引表的分布情況:
上一段我們提到,mysql的主鍵是使用的是聚簇索引,如果使用非自增主鍵(如果身份證號或學號等),由於每次插入主鍵的值近似於隨機,因此每次新紀錄都要被插到現有索引頁得中間某個位置,此時MySQL不得不為了將新記錄插到合適位置而移動數據,甚至目標頁面可能已經被回寫到磁碟上而從緩存中清掉,此時又要從磁碟上讀回來,這增加了很多開銷,同時頻繁的移動、分頁操作造成了大量的碎片,得到了不夠緊湊的索引結構,後續不得不通過OPTIMIZE TABLE來重建表並優化填充頁面。
如果表使用自增主鍵,那麼每次插入新的記錄,記錄就會順序添加到當前索引節點的後續位置,當一頁寫滿,就會自動開闢一個新的頁。總的來說就是可以提高查詢和插入的性能。
對InnoDB來說
1: 主鍵索引既存儲索引值,又在葉子節點中存儲行的數據,也就是說數據文件本身就是按照b+樹方式存放數據的。
2: 如果沒有定義主鍵,則會使用非空的UNIQUE鍵做主鍵 ; 如果沒有非空的UNIQUE鍵,則系統生成一個6位元組的rowid做主鍵;
聚簇索引中,N行形成一個頁(一頁通常大小為16K)。如果碰到不規則數據插入時,為了保持B+樹的平衡,會造成頻繁的頁分裂和頁旋轉,插入速度比較慢。所以聚簇索引的主鍵值應儘量是連續增長的值,而不是隨機值(不要用隨機字符串或UUID)。
故對於InnoDB的主鍵,儘量用整型,而且是遞增的整型。如果沒有特別的需要,請永遠使用一個與業務無關的自增欄位作為主鍵吧。
實踐是檢驗真理的唯一標準,既然對這塊知識點不是很清楚,那就動手做一下實驗。首先,我創建一張表,這個表只有1個欄位,分別是id , 如下:
create table t1(
`id` int(10) unsigned primary key not null auto_increment
) engine=InnoDB charset=utf8
我們定義這個表中的id 是 unsigned ,最大可以達到2的32次冪,如果這樣測試下來,半天也插不完,所以這裡有個小技巧,可以在創建表的時候,直接聲明auto_increment的初始值,我們修改下表結構:
create table t1(
`id` int(10) unsigned primary key not null auto_increment
) engine=InnoDB auto_increment=4294967295 charset=utf8
接下來,我們嘗試插入一條數據,得到了下面的結果。
mysql> insert into t1 values();
ERROR 2006 (HY000): MySQL server has gone away
No connection. Trying to reconnect...
Connection id: 8
Current database: demo
ERROR 1062 (23000): Duplicate entry '4294967295' for key 'PRIMARY'
說明,當再次插入時,使用的自增id還是4294967295,報主鍵衝突的錯誤。當然4294967295可以應付大部分的場景了,如果你的服務會經常性的插入,修改還是會有用完的風險,此時建議使用bigint
資料庫系統的設計者巧妙利用了磁碟預讀原理,將一個節點的大小設為等於一個頁,這樣每個節點只需要一次I/O就可以完全載入。為了達到這個目的,在實際實現B+Tree還需要使用如下技巧:每次新建節點時,直接申請一個頁的空間,這樣就保證一個節點物理上也存儲在一個頁裡,加之計算機存儲分配都是按頁對齊的,就實現了一個node只需一次I/O。B+Tree的節點都是按照鍵值的大小順序存放的,葉節點之間也通過指針連接起來,為了提高取數據時的效率。
使用自增id當主鍵,那麼每次插入新的記錄,記錄就會順序添加到當前索引節點的後續位置,當一頁寫滿,就會自動開闢一個新的頁。總的來說就是可以提高查詢和插入的性能。