數據的具體存儲是交由具體的存儲引擎實現的,所以同樣的數據,在不同的存儲引擎中的存儲方式也是不同的,現在只討論InnoDB引擎的數據結構。
CPU發出讀取數據的指令后,Mysql進程需要把存儲在硬盤中的數據讀取到內存中,CPU真正處理數據的地方是在內存。如果每次讀取或寫入都要去操作內存和磁盤的話,那樣太慢了。
Mysql的策略是:將真實數據划分為若干個頁,內存與磁盤交互的最小單位是頁,頁的大小一般為16KB,也就是說,一次最少從磁盤中讀取16KB的內容到內存中,一次最少把內存中的16KB內容刷新到磁盤中。
1.行結構
平時操作Mysql是以一行一行的數據為單位,表中的行也有他自己的行格式,每張表中所有行共享一種行格式,目前共有四種行格式供我們選擇,他們大同小異,我們先介紹Compact:
- Dynamic (5.7.0默認)
- Compact
- Compressed
- Redundant (5.5.0廢棄)
/**創建時指定行格式**/
CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名稱
/**修改行格式**/
ALTER TABLE 表名 ROW_FORMAT=行格式名稱
/**查看表的行格式**/
show table status like '表名'
Compact行格式包括:
-
一條記錄的額外信息
- 變長字段長度
- NULL值列表
- 頭信息
-
一條記錄的真實數據
-
列的真實值
-
隱藏列
-
變長字段長度列表(不必須)
如果行中有變長類型的字段(比如VARCHAR(M、各種TEXT類型,各種BLOB類型),並且這些字段的數據值不為null;或者這個表的字符集是變長字符集(比如utf8每個字符占用1-3個字節,而ascii固定每個字符固定占用1個字節),存儲這些變長字段的時候,要把他們的真實數據的字節長度一並存入變長字段長度列表。變長字段長度列表最多占用2個字節。
每個變長字段的長度都占據一個或兩個字節,他們逆序排列,組成了變長字段長度列表。
值得注意的有兩點,
- 當變長數據類型的列數據為null時,不會存入變長字段長度列表
- 變長字段長度列表不是必須存在的,如果所有字段都是固定長度的,並且字符集也是定長字符集,則不存在變長字段長度列表
這里說一下定長和變長字段的區別,比如char(10)和varchar(10)都存"zz",表編碼為ascii,varchar列占用2個字節;而char列占用10個字節,沒有使用的8個字節全部用0補齊。
括號內的10代表最多占用的字符數,嘗試插入11個字符會報錯。
NULL值列表(不必須)
如果字段中有允許為NULL的字段,則將這些字段是否為null的信息存儲在NULL值列表中。NULL值列表占用1個字節。
值得注意的是,NULL值列表頁不是必須的,如果表中沒有允許存儲 NULL 的列,則不存在NULL值列表。
記錄頭信息(必須)
記錄頭信息固定占用5個字節也就是40位,不同位代表不同信息,主要有:
-
delete_mask 標記該記錄是否被刪除
-
record_type 表示當前記錄的類型
0表示普通記錄,1表示B+樹非葉子節點記錄,2表示最小記錄,3表示最大記錄
-
next_record 表示下一條記錄的相對位置
隱藏列(必須)
隱藏列中的信息因為與事務和主鍵有關,所以很重要,總共占用19個字節,有三列:
- row_id (不必須) 替補主鍵id
- trx_id 事務id
- roll_pointer 回滾指針
這里需要提一下
InnoDB
表對主鍵的生成策略:優先使用用戶自定義主鍵作為主鍵,如果用戶沒有定義主鍵,則選取一個
Unique
鍵作為主鍵,如果表中連Unique
鍵都沒有定義的話,則InnoDB
會為表默認添加一個名為row_id
的隱藏列作為主鍵。
真實數據
Mysql存儲數據的規定限制:
- 每行最多65535個字節
- 每頁最少存儲兩行數據
//TODO 待整理
varchar(M)的最大M
mysql表中的一條記錄占用的最大存儲空間是有限的,除了BLOB
和text
類型的字段意外,其他所有的列占用的字節長度加起來不能超過65536個字節,這65535個字節除了這條記錄本身的真實數據之外,還包括一些其他數據。
比如我們存儲一個VARCHAR(M)類型的字段,總共可能需要占用3部分存儲空間:
- 真實數據占用的字節長度(1-2個字節)
- Null值標識(1個字節)
- 真實數據
所以正常來說每行真實數據最多占用65532
個字節。
假設一張表中只有一個字段varchar,這張表的行格式是ascii字符集(ascii字符集每個字符占用一個字節),而一行真實數據最多最多占用65532個字節
,所以M最大可以是65532;
如果VARCHAR(M)
類型的列使用的不是ascii字符集,那M的最大取值取決於該字符集表示一個字符最多需要的字節數。在列的值允許為NULL的情況下,gbk字符集表示一個字符最多需要2
個字節,那在該字符集下,M的最大取值就是32766
(也就是:65532/2);utf8字符集表示一個字符最多需要3個字節,那在該字符集下,M的最大取值就是21844
,就是說最多能存儲21844(65532/3)個字符。
值得注意的是:上述所言都是在表中只有一個字段的情況下說的,要記住一個行中的所有列(不包括隱藏列和記錄頭信息)占用的字節長度加起來不能超過65535個字節!
行數據溢出
上一節我們知道,一行數據最多可以有65535個字節,而一個頁只有16kb,也就是16384個字節,很有可能出現一頁裝不下一行數據的尷尬情況,這就是行數據溢出。
在Compact
和Reduntant
行格式中,對於占用存儲空間非常大的列,在記錄的真實數據
處只會存儲該列的一部分數據,把剩余的數據分散存儲在幾個其他的頁中,然后記錄的真實數據
處用20個字節存儲指向這些頁的地址(當然這20個字節中還包括這些分散在其他頁面中的數據的占用的字節數),從而可以找到剩余數據所在的頁,如圖所示:
從圖中可以看出來,對於Compact
和Reduntant
行格式來說,如果某一列中的數據非常多的話,在本記錄的真實數據處只會存儲該列的前768
個字節的數據和一個指向其他頁的地址,然后把剩下的數據存放到其他頁中。如下圖:
最后需要注意的是,不只是 VARCHAR(M) 類型的列,其他的 TEXT、BLOB 類型的列在存儲數據非常多的時候也會發生行溢出
。
溢出臨界點
每行存儲多少數據的時候會發生行溢出呢?
mysql規定每個數據頁至少要存儲兩行數據,每個頁除了記錄數據以外,還會有132
個字節存儲頁的信息,每行記錄額外需要的存儲空間是27
字節:
- 2個字節用於存儲真實數據的長度
- 1個字節用於存儲列是否是NULL值
- 5個字節大小的頭信息
- 6個字節的
row_id
列 - 6個字節的
transaction_id
列 - 7個字節的
roll_pointer
列
所以計算公式是:
132 + 2×(27 + n) < 16×1024
算出x<8099,也就是說當一行總數據量>=8099
字節時,會發生行溢出。
Dynamic行格式
默認行格式Dynamic
與Compact很像,只不過在處理行溢出
數據時有點兒分歧,它不會在記錄的真實數據處存儲字段真實數據的前768
個字節,而是把所有的字節都存儲到其他頁面中,只在記錄的真實數據處存儲其他頁面的地址,就像這樣:
參考:《MySQL 是怎樣運行的:從根兒上理解 MySQL》