MySQL伺服器上 存儲引擎 負責對錶中數據的讀取和寫入工作,不同存儲引擎中 存放的格式 一般是不同的,甚至有的存儲引擎(Memory)不用磁碟來存儲數據。 頁 (Page) 是磁碟和記憶體之間交互的基本單位 ,也就是說資料庫管理存儲空間的基本單位是頁,資料庫I/O操作的最小單位是頁 (InnoDB頁 ...
存儲引擎
負責對錶中數據的讀取和寫入工作,不同存儲引擎中 存放的格式
-
頁 (Page) 是磁碟和記憶體之間交互的
基本單位
,也就是說資料庫管理存儲空間的基本單位是頁,資料庫I/O操作的最小單位
是頁 (InnoDB頁預設大小16KB)
-
區 (Extent) 是比頁大一級的存儲結構,在InnoDB存儲引擎中 ,一個區會分配64個連續的頁。因為InnoDB中頁的預設大小為16KB,所以一個區的大小是1MB=64*16KB
-
資料庫中的分配單位
,不同類型的資料庫對象以不同的段形式存在,當我們創建數據表、索引時,會創建對應的段。
-
表空間 (Tablespace) 是一個邏輯容器,在一個表空間中可以有一個或多個段,一個段只能屬於一個表空間。資料庫由一個或多個表空間組成,表空間從管理上可以劃分為系統表空間、用戶表空間、撤銷表空間、臨時表空間等。
資料庫中的頁可以 不在物理結構上相連
,只要通過 雙向鏈表
關聯即可。每個數據頁中的記錄會按照列 (主鍵或其他) 的順序 (從小到大或者從大到小) 組成一個 單向鏈表
,每個數據頁都會為存儲在頁內的記錄生成一個 頁目錄
,在通過該列查找某條記錄的時候可以在頁目錄中使用 二分法
快讀定位到對應的槽,然後再遍歷該槽對應的分組中的記錄即可快速找到指定的紀錄。
1.1.File Header
File Header 文件頭部 (38位元組) :描述頁的通用信息。
-
FIL_PAGE_OFFSET(4位元組) : 頁號(唯一)
-
FIL_PAGE_TYPE(2位元組) : 代表當前頁的類型。
-
FIL_PAGE_PREV(4位元組) 和 FIL_PAGE_NEXT(4位元組) : 分別代表本頁的上一個和下一個頁的頁號。
通過建立一個雙向鏈表把許多頁都串聯起來,保證這些頁之間
不需要是物理上的連續,而是邏輯上的連續
。 -
FIL_PAGE_SPACE_OR_CHKSUM(4位元組) : 當前頁面的校驗和(checksum)。 為了檢測一個頁是否完整(也就是在同步的時候有沒有發生只同步一半的尷尬情況)
什麼是校驗和?
就是對於一個很長的位元組串來說,我們會通過某種演算法來計算一個比較短的值來代表這個很長的位元組串,這個比較短的值就稱為校驗和。 在比較兩個很長的位元組串之前,先比較這兩個長位元組串的校驗和,如果校驗和都不一樣,則兩個長位元組串肯定是不同的,所以省去了直接比較兩個比較長的位元組串的時間損耗。
頁的校驗和的作用:
為了檢測一個頁是否完整,這時可以通過
文件尾的校驗和
與文件頭的校驗和
做比對,如果兩個值不相等則證明頁的傳輸有問題,需要重新進行傳輸,否則認為頁的傳輸已經完成。每當一個頁面在記憶體中修改了,在同步之前就要把它的校驗和算出來,因為File Header在頁面的前邊,所以校驗和會被首先同步到磁碟,當完全寫完時,校驗和也會被寫到頁的尾部,如果完全同步成功,則頁的首部和尾部的校驗和應該是一致的。如果寫了一半兒斷電了,那麼在File Header中的校驗和就代表著已經修改過的頁,而在File Trailer中的校驗和代表著原先的頁,二者不同則意味著同步中間出了錯。
-
FIL_PAGE_LSN(8位元組) : 頁面被最後修改時對應的日誌序列位置
1.2.File Trailer
File Trailer 文件尾部(8位元組):檢驗頁是否完整
-
前4個位元組代表頁的校驗和:這個部分是和File Header中的校驗和相對應的。
-
後4個位元組代表頁面被最後修改時對應的日誌序列位置(LSN):這個部分也是為了校驗頁的完整性的,如果首部和尾部的LSN值校驗不成功的話,就說明同步過程出現了問題。
1.3.Free Space
Free Space 空閑記錄:頁中還沒有被使用的記錄
我們自己存儲的記錄會按照指定的行格式存儲到User Records部分。但是在一開始生成頁的時候,其實並沒有User Records這個部分,每當我們插入一條記錄,都會從Free Space部分,也就是尚未使用的存儲空間中申請一個記錄大小的空間劃分到User Records部分
,當Free Space部分的空間全部被User Records部分替代掉之後,也就意味著這個頁使用完了,如果還有新的記錄插入的話,就需要去申請新的頁了。
1.4.User Records
User Records 用戶記錄:存儲行記錄的內容
User Records中的這些記錄按照指定的行格式
一條一條擺在User Records部分,相互之間形成單鏈表
。
1.5.Infimum+Supremum
Infimum+Supremum 最小和最大記錄(26位元組):虛擬的行記錄
記錄可以比大小,對於一條完整的記錄來說,比較記錄的大小就是比較主鍵的大小
。
比方說我們插入的4行記錄的主鍵值分別是:1、2、3、4,這也就意味著這4條記錄是從小到大依次遞增。
InnoDB規定的最小記錄與最大記錄這兩條記錄的構造十分簡單,都是由5位元組大小的記錄頭信息
和8位元組大小的一個固定的部分
組成的
這兩條記錄不是我們自己定義的記錄,所以它們並不存放在頁的User Records部分,他們被單獨放在一個稱為Infimum + Supremum的部分
1.6.Page Directory
Page Directory 頁目錄(56位元組):存儲用戶記錄的相對位置
在頁中,記錄是以單向鏈表的形式進行存儲的。單向鏈表的特點就是插入、刪除非常方便,但是檢索效率不高,最差的情況下需要遍歷鏈表上的所有節點才能完成檢索。因此在頁結構中專門設計了頁目錄這個模塊,專門給記錄做一個目錄
,通過二分查找法
的方式進行檢索,提升效率。
-
將所有的記錄
分成幾個組
,這些記錄包括最小記錄和最大記錄,但不包括標記為“已刪除”的記錄。InnoDB規定:
-
對於最小記錄所在的分組只能有1條記錄,
-
最大記錄所在的分組擁有的記錄條數只能在1~8條之間,
-
剩下的分組中記錄的條數範圍只能在是 4~8 條之間。
-
-
在每個組中最後一條記錄的頭信息中會存儲該組一共有多少條記錄,作為
n_owned
欄位。n_owned欄位存在於行的記錄頭中
n_owned(4bit):頁目錄中每個組中最後一條記錄的頭信息中會存儲該組一共有多少條記錄
-
頁目錄用來存儲
每組最後一條記錄的地址偏移量
,這些地址偏移量會按照先後順序存儲
起來,每組的地址偏移量也被稱之為槽(slot)
,每個槽相當於指針指向了不同組的最後一個記錄。
分組的步驟:
-
初始情況下一個數據頁里只有最小記錄和最大記錄兩條記錄,它們分屬於兩個分組。
-
每插入一條記錄,都會從頁目錄中找到
主鍵值比本記錄的主鍵值大並且差值最小的槽
,然後把該槽對應的記錄的n_owned值加1,表示本組內又添加了一條記錄,直到該組中的記錄數等於8個。 -
在一個組中的記錄數等於8個後再插入一條記錄時,會
將組中的記錄拆分成兩個組
,一個組中4條記錄,另一個5條記錄。這個過程會在頁目錄中新增一個槽來記錄這個新增分組中最大的那條記錄的偏移量。
舉例:
頁目錄結構下查找記錄
在一個數據頁中查找指定主鍵值的記錄的過程分為兩步:
-
通過二分法確定該記錄所在的槽,並找到該槽所在分組中主鍵值最小的那條記錄。
-
通過記錄的next_record屬性遍歷該槽所在的組中的各個記錄。
舉例:
查找記錄中主鍵值為6的記錄:
因為各個槽代表的記錄的主鍵值都是從小到大排序
的,所以我們可以使用二分法
來進行快速查找。
5個槽的編號分別是:0、1、2、3、4,所以初始情況下最低的槽就是low=0,最高的槽就是high=4。
計算中間槽的位置:(0+4)/2=2,所以查看槽2對應記錄的主鍵值為8,又因為8 > 6,所以設置high=2,low保持不變。
重新計算中間槽的位置:(0+2)/2=1,所以查看槽1對應的主鍵值為4,又因為4 < 6,所以設置low=1,high保持不變。
因為high - low的值為1,所以確定主鍵值為6的記錄在槽2對應的組中。此刻我們需要找到槽2中主鍵值最小的那條記錄,然後沿著單向鏈表遍歷槽2中的記錄。
通過二分法確定主鍵值為6的記錄在槽2對應的組中,定位槽2中最小的數據
怎麼定位一個組中最小的記錄呢?
別忘了各個槽都是挨著的,我們可以很輕易的拿到槽1對應的記錄(主鍵值為4),該條記錄的下一條記錄就是槽2中主鍵值最小的記錄 (記錄之間通過單鏈錶鏈接),該記錄的主鍵值為5。
從槽2中最小的數據 (主鍵值為5的記錄) 出發,遍歷槽2中的各條記錄,直到找到主鍵值為6的那條記錄即可。
1.7.Page Header
Page Header 頁面頭部(56位元組):頁的狀態信息
為了能得到一個數據頁中存儲的記錄的狀態信息
,比如本頁中已經存儲了多少條記錄,第一條記錄的地址是什麼,頁目錄中存儲了多少個槽等等,特意在頁中定義了一個叫Page Header的部分,這個部分占用固定的56個位元組,專門存儲各種狀態信息。
2_InnoDB行格式
我們平時的數據以行為單位來向表中插入數據,這些記錄在磁碟上的存放方式也被稱為行格式
或者記錄格式
。InnoDB存儲引擎設計了4種不同類型的行格式,分別是Compact
、Redundant
、Dynamic
和Compressed
行格式。
行格式操作
查看MySQL8的預設行格式:
SELECT @@innodb_default_row_format;
查看具體表的行格式:
SHOW TABLE STATUS like '表名';
指定行格式:
#創建表時指定行格式
CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名稱
#修改表的行格式
ALTER TABLE 表名 ROW_FORMAT=行格式名稱
2.1.compact行格式
在MySQL 5.1版本中,預設設置為Compact行格式。一條完整的記錄其實可以被分為記錄的額外信息
和記錄的真實數據
兩大部分。
1.變長欄位長度列表
變長欄位長度列表:記錄所有變長欄位的真實數據占用的位元組長度
MySQL支持一些變長的數據類型,比如VARCHAR(M)、VARBINARY(M)、TEXT類型,BLOB類型,這些數據類型修飾列稱為
變長欄位
,變長欄位中存儲多少位元組的數據不是固定的,所以我們在存儲真實數據的時候需要順便把這些數據占用的位元組數也存起來。
註意:存儲的變長長度和欄位順序相反
舉例:
各個列都使用的是ASCII
字元集(每個字元只需要1個位元組來進行編碼)。
長度值按照列的逆序存放,所以最後變長欄位長度列表的位元組串用十六進位表示的效果就是:060408
行格式:
2.NULL值列表
NULL值列表:把可以為NULL的列統一管理起來,標記對應列的值是否為NULL
註意:
-
這裡面存儲的NULL值列表和欄位
順序相反
。 -
如果表中沒有允許存儲 NULL 的列,則 NULL值列表也不存在了。
為什麼定義NULL值列表?
-
數據都是需要對齊的
,如果沒有標註出來NULL值的位置,就有可能在查詢數據的時候出現混亂。 -
如果使用一個特定的符號放到相應的數據位表示空置的話,雖然能達到效果,但是這樣很浪費
空間
,所以直接就在行數據得頭部開闢出一塊空間專門用來記錄該行數據哪些是非空數據,哪些是空數據-
二進位位的值為1時,代表該列的值為NULL。
-
二進位位的值為0時,代表該列的值不為NULL。
-
舉例:
行格式:
3.記錄頭信息
-
delete_mask(1bit):標記著當前記錄是否被刪除(1被刪除,0沒有刪除)
被刪除的記錄為什麼還在頁中存儲呢? 你以為它刪除了,可它還在真實的磁碟上。這些被刪除的記錄之所以不立即從磁碟上移除,是因為移除它們之後其他的記錄在磁碟上需要重新排列,導致
性能
消耗。所以只是打一個刪除標記而已,所有被刪除掉的記錄都會組成一個所謂的垃圾鏈表
,在這個鏈表中的記錄占用的空間稱之為可重用空間,之後如果有新記錄插入到表中的話,可能把這些被刪除的記錄占用的存儲空間覆蓋掉。 -
min_rec_mask(1bit):標識是否為最小的目錄項記錄(1最小記錄,0不是最小記錄)
-
n_owned(4bit):頁目錄中每個組中最後一條記錄的頭信息中會存儲該組一共有多少條記錄
-
heap_no(13bit):表示當前記錄在本頁中的位置
怎麼不見heap_no值為0和1的記錄呢? MySQL會自動給每個頁裡加了兩個記錄,由於這兩個記錄並不是我們自己插入的,所以有時候也稱為偽記錄或者虛擬記錄。這兩個偽記錄一個代表
最小記錄
,一個代表最大記錄
。最小記錄和最大記錄的heap_no值分別是0和1,也就是說它們的位置最靠前。 -
record_type(3bit):當前記錄的類型
-
0:表示普通記錄
-
1:表示目錄項記錄
-
2:表示最小記錄
-
3:表示最大記錄
-
-
next_record(16bit):表示從當前記錄的真實數據到下一條記錄的真實數據的地址偏移量。
下一條記錄:
下一條記錄指得並不是按照我們插入順序的下一條記錄,而是
按照主鍵值由小到大的順序
的下一條記錄。而且規定Infimum記錄(也就是最小記錄)的下一條記錄就是本頁中主鍵值最小的用戶記錄,而本頁中主鍵值最大的用戶記錄的下一條記錄就是 Supremum記錄(也就是最大記錄)。
4.記錄的真實數據
記錄的真實數據除了我們自己定義的列的數據以外,還會有三個隱藏列:
-
row_id:一個表沒有手動定義主鍵,則會選取一個Unique鍵作為主鍵,如果連Unique鍵都沒有定義的話,則會為表預設添加一個名為row_id的隱藏列作為主鍵。所以row_id是在沒有自定義主鍵以及Unique鍵的情況下才會存在的。
2.2.dynamic&compressed行格式
在MySQL 8.0中,預設行格式就是Dynamic,Dynamic、Compressed行格式和Compact行格式挺像,只不過在處理行溢出數據時有分歧:
行溢出:
一個頁的大小一般是16KB,也就是16384位元組,而一個VARCHAR(M)類型的列就最多可以存儲65533個位元組,這樣就可能出現一個頁存放不了一條記錄,這種現象稱為行溢出。
-
Compressed和Dynamic兩種記錄格式對於存放在BLOB中的數據採用了
完全的行溢出
的方式。 -
Compact和Redundant兩種格式會在記錄的真實數據處存儲一部分數據
Compressed行記錄格式的另一個功能就是,存儲在其中的行數據會以zlib的演算法進行壓縮
,因此對於BLOB、TEXT、VARCHAR這類大長度類型的數據能夠進行非常有效的存儲。
2.3.redundant行格式
欄位長度偏移列表與變長欄位長度列表有兩處不同:
-
少了“變長”兩個字:Redundant行格式會把該條記錄中所有列(包括隱藏列)的長度信息都按照逆序存儲到欄位長度偏移列表。
-
多了“偏移”兩個字:這意味著計算列值長度的方式不像Compact行格式那麼直觀,它是採用兩個相鄰數值的差值來計算各個列值的長度。
記錄頭信息與Compact行格式的記錄頭信息對比來看,有兩處不同:
-
Redundant行格式多了n_field和1byte_offs_flag這兩個屬性。
-
n_fields:代表一行中列的數量,占用10位
-
1byte_offs_flags:定義了偏移列表占用的大小(1:1個位元組,0:2個位元組)
1byte_offs_flag的值是怎麼選擇的 根據該條Redundant行格式記錄的
真實數據占用的總大小
來判斷的:-
當記錄的真實數據占用的位元組數值不大於127(十六進位0x7F,二進位01111111)時,每個列對應的偏移量占用1個位元組。
-
當記錄的真實數據占用的位元組數大於127,但不大於32767(十六進位0x7FFF,二進位0111111111111111)時,每個列對應的偏移量占用2個位元組。
-
當記錄的真實數據占用的位元組數大於32767時,將部分記錄存放到溢出頁中,在本頁中只保留前768個位元組和20個位元組的溢出頁面地址。
-
-
-
Redundant行格式沒有record_type這個屬性。
Redundant行格式中NULL值的處理:
因為Redundant行格式並沒有NULL值列表,所以Redundant行格式在欄位長度偏移列表中的各個列對應的偏移量處做了一些特殊處理 —— 將列對應的偏移量值的第一個比特位作為是否為NULL的依據
,該比特位也可以被稱之為NULL比特位。也就是說在解析一條記錄的某個列時,首先看一下該列對應的偏移量的NULL比特位是不是為1。如果為1,那麼該列的值就是NULL,否則不是NULL。