mysql變長型別欄位varchar值更新變長或變短底層檔案儲存原理

月光冷鋒發表於2021-11-23
  為了搞清楚MySQL對於可變長度欄位值修改時,如何高效運算元據檔案的機制。之前一直模糊不清,網上也搜不到現成的答案。經過多方資料蒐集整理。寫出此文供大家一起參閱。由於涉及眾多非常底層的知識,我假設讀者已經對作業系統和磁碟存取有一定的基礎知識。文中如有疏漏,還請大佬指正。
  為了探究這個問題,我們要先來回顧一下我之前的一篇文章《檔案隨機或順序讀寫原理深入淺出》講的檔案儲存的底層原理知識。如下圖所示。一個檔案的資料是以塊為單位儲存到物理磁碟的隨機位置,這是由作業系統負責管理的,使用者程式無權決定。所以在檔案檢視層面我們連續儲存的資料,對映到物理磁碟層面就是隨機位置了。圖中是假設磁碟塊大小為32KB,則檔案對應的資料偏移地址儲存到對應的物理塊中示意圖。
  我們現在假設MySQL的一張表對應一個資料檔案。那麼表裡的資料按行儲存,則行與行之間的資料被緊密的連續填充到上圖“資料檔案檢視”中,並被以塊為單位隨機儲存到了物理磁碟上。這樣遍歷表時,就可以從檔案0地址開始依次讀取到所有資料,行與行之間的間隔符就是普通的換行符。每一行中的欄位都按照表結構定義中的欄位長度讀取即可。這裡先假設表中的欄位都是定長型別的。這樣就不會有什麼問題。即便是更新某個欄位的值,則可以直接使用隨機檔案讀取方法定位到欄位的偏移地址寫入新值即可覆蓋舊值。
  那麼現在問題來了,如果表中某欄位是可變長型別的如varchar(50)。資料插入時假設值為“月光冷鋒”,後面有個更新操作,需要將值改為“月光冷鋒的部落格”。此時會發生什麼呢?由於是可變長度的欄位型別,檔案中該行的該欄位實際佔用空間就是四個字元,而不是50個字元。且行之間資料緊密排列儲存。現在值要變成7個字元,則我們無法通過簡單的隨機檔案存取定位到該欄位覆蓋舊值,這樣會出現嚴重的問題,會覆蓋其他欄位或行的值。一種古老的檔案修改方法是,先將要修改的位置後面的資料都轉移到一個臨時檔案中,然後修改現在的位置處的值,最後再把臨時檔案的內容拼接回原檔案,從而實現修改操作。顯然這種方式無法應用到頻繁更新的資料庫上。這種方式代價巨大,不過大多數檔案都是隻讀的,極少更新。所以應用也挺廣泛的。比如音訊、視訊檔案。
  那麼MySQL是如何解決這類問題的呢?首先我們如果把表資料檔案看成一個可以任意發揮的平臺,我們不在像其他型別檔案那樣直接將資料一個挨著一個緊密的寫入檔案的偏移地址中。MySQL使用頁這個概念重新對檔案檢視劃分,也就是一個頁對應一段檔案的偏移地址。如下圖所示。MySQL頁大小預設16kB,剛好對應圖中兩個頁對應到一個物理塊32kb大小。MySQL的頁使用雙向連結串列方式組織。

 

  下面我們來看下如果可變長度varchar型別的欄位值變長了,檔案裡的資料該怎麼儲存呢?因為表中資料剛開始插入時,可變長度欄位值都是根據實際長度儲存下來的,且行與行之間資料也是緊密連續存放在檔案地址中的(再次強調一下,不是連續存放在物理磁碟上的)。那麼現在值變長了,原來的位置無法擴充套件出新的空間出來,所以無法覆蓋存放到原來的位置上。此時MySQL就會使用頁分裂的方法擴充套件欄位變長的空間。
  比如假設行10的資料存放在頁③的位置,如下圖2所示。行11也是存放在頁③的位置,且他們兩行都把頁③填滿了。現在更新行10的某個變長欄位值,由“月光冷鋒”改成“月光冷鋒的部落格”。MySQL將新建立一個頁⑥出來(相應的原資料檔案也要變大增長),把原來頁③的內容,行10和行11的資料重新生成排列一遍儲存到頁③和頁⑥中,同時將原來的頁連結串列結構重新修改其前驅和後繼頁節點的指標就OK了。如下圖3所示。
 圖2         圖3                                                                          
  MySQL就是通過這種技巧,實現了修改資料檔案時,不必像傳統修改檔案那樣付出昂貴代價。這種方式雖然解決了修改檔案時避免大規模移動資料的弊端,但是讀取這些資料時,卻無法像傳統存取方式那樣,直接從檔案偏移地址0開始順序讀取。而是要根據頁的連結串列結構順序讀取。需要不斷的計算和移動檔案偏移量指標,好在這個過程不會花費多少代價。但是會帶來另外一個比較嚴重的問題就是頁空洞,也稱為碎片。上面行11有部分欄位資料已經轉移到了頁⑥中,顯然頁⑥是沒有存滿的。行12是存在頁④中的,這樣就產生了碎片問題,浪費了檔案的一些地址空間,這些空洞存的都是特殊佔位符,也要佔據真實的物理磁碟空間。隨著更新刪除操作越來越多,碎片也會越來越多,所以有必要定期進行表的碎片整理,這樣可以收縮表檔案佔據的磁碟空間。也可以降低頁連結串列的長度,從而節省一些定址操作代價。
  如果可變長欄位值由大變小,則原來的欄位值地址空間足夠了,也就不需要新加頁了,只需要重新整理排列一下當前更新的行資料即可。使得變成欄位的值佔用實際空間即可。至於留下的頁碎片問題,MySQL也有相應的機制做合併優化操作。我這裡不做深究。
 
 

相關文章