InnoDB 是 mysql 的預設引擎,也是我們最常用的,所以基於 InnoDB,學習頁結構。而學習頁結構,是為了更好的學習索引。
一、頁的簡介
頁是 InnoDB 管理儲存空間的基本單位,一個頁的大小一般是 16kb。
為了達成不同的目的,作者設計了多種型別的頁,比如:
- 存放表空間頭部資訊的頁
- 存放 change buffer 資訊的頁
- 存放 inode 資訊的頁
- 存放 undo 日誌資訊的頁
- ... ...
然而我們最關心的,還是那些存放進表中那些資料記錄是在哪種頁上,官方稱這種存放記錄的頁為索引(INDEX)頁,但是為了便於理解,本篇暫把它稱為資料頁。
二、資料頁的結構
這資料頁也有 16kb 的儲存空間,可以大致劃分為 7 個部分。
從結構圖中可以看到,有些部分的佔用位元組數是確定的,有的是不確定的。我們最關心的使用者儲存的記錄,在 User Records
部分。
不過,在一開始生成頁的時候,並沒有 User Records 部分。當有新的記錄插入時,就會從 Free Space
部分申請一個記錄大小的空間,然後劃分到 User Records 部分,直到 Free Space 全部被 User Records 替代,表示這個頁已經用完。如果還有新的記錄插入,需要申請新的頁。
我覺得這裡可以把這個資料頁當作是書本的頁,書頁上的內容通常是一行行的呈現,當整個頁都用完了,就得翻到下一頁(新頁)去繼續寫了。
三、記錄在頁中的儲存結構
那麼,User Records 部分裡的這些記錄,是如何管理的呢?
先來建一張表:
CREATE TABLE pingguo_demo(
c1 INT,
c2 INT,
c3 VARCHAR(10000),
PRIMARY KEY (c1)
) CHARSET = ASCII ROW_FORMAT = COMPACT;
這裡的指定使用行格式為 COMPACT(引擎中還存在其他的行格式),暫且知道 COMPACT 即可。
當我們在資料庫的插入了一條記錄後,其實背後的行格式是這樣的:
注意這裡橙色標識的記錄頭資訊,它又包含了很多重要資訊:
- 預留位1:佔用 1 位元,沒有使用。
- 預留位2:佔用 1 位元,沒有使用。
- deleted_flag:佔用 1 位元,標記該記錄是否被刪除。
- min_rec_flag:佔用 1 位元,在 B+ 樹(後面索引會講到)中每層非 葉子節點中的最小的目錄項,都會新增此標記。
- n_owned:一個頁面中的記錄被分為若干個組,每個組裡有一個記錄是“大哥”,其他記錄都是“小弟”。而這位“大哥”記錄的 n_owned 就是所在組的所有記錄條數,而小弟們的 n_owned 都是 0
- heap_no:佔用 13 位元,表示當前記錄在頁面堆中的相對位置。
- record_type:佔用 3 位元,表示當前記錄的型別,0是普通記錄,1是 B+樹非葉節點的目錄項記錄,2是 Infimum 記錄,3是 Suprememum 記錄。
- next_record:佔用 16 位元,表示下一條記錄的相對位置。
四、記錄頭資訊
現在,向上面新建的表中插入 4 條記錄:
INSERT INTO pingguo_demo VALUES
(1, 100, 'aaaa'),
(2, 200, 'bbbb'),
(3, 300, 'cccc'),
(4, 400, 'dddd');
那麼,對應這4條記錄的行格式應該為:
注意,這裡為了便於記憶,作了簡化。另外,記錄中的資訊實際是二進位制位資料,這裡為了理解寫的是十進位制。而且,各條記錄在 User Records 中儲存是沒有空隙的,這裡抽象表示。
1. deleted_flag
這個屬性用來標記當前記錄是否被刪除,1 表示被刪除,0 表示沒有被刪除。
嗯?我表裡刪除了資料居然還在頁裡。
是的,你以為被刪除了,其實還在磁碟上。為什麼呢?
因為如果在磁碟上移除這些記錄,還要再重新排列其他記錄,會帶來效能消耗,所以只打了一個刪除的標記。
然後,所有的刪除的記錄會組成一個垃圾連結串列。而記錄在這個連結串列中所佔用的空間稱為可重用空間,當後面有新記錄插入到表中,它們就可能覆蓋掉這些空間。
2. min_rec_flag
在 B+ 樹中每層非葉子節點中的最小的目錄項,都會新增此標記。這裡說的目錄項,要後續講解。
這裡4條記錄的 min_rec_flag 都是 0,表示都不是 B+ 樹非葉子節點中的最小的目錄項記錄。
3. n_owned
要下一章講解。
4. heap_no
表示當前記錄在頁面堆中的相對位置。
上面的4條記錄是抽象的描述,實際上這些記錄都是一條一條緊密無縫排列在一起的,這就是堆(heap)。
為了方便管理,把一條記錄在堆中的相對位置稱為 heap_no。
- 在頁面前面的記錄 heap_no 相對較小
- 在頁面後面的記錄 heap_no 相對較大
- 每申請一條記錄的儲存空間時,該記錄比物理位置在它之前的那條記錄的 heap_no 值大 1
上述 4 條記錄的 heap_no 分別為 2、3、4、5,嗯?怎麼沒有 0 和 1?
虛擬記錄-Infimum 和 Supremum
這個在本文第二部分有提到過。其實這2條記錄是頁裡自動新增的:
- Infimum:代表頁面中的最小記錄
- Supremum:代表頁面中的最大記錄
作者規定,無論向頁中插入了多少條記錄,任何使用者記錄都比 Infimum 記錄大,都比 Supremum 記錄小。
這 2 條虛擬記錄的結構也很簡單。
所以,對於上面插入的 4 條使用者記錄,還應該加上這2個預設記錄,而且位置最靠前。
另外,還需要注意,當堆中記錄的 heap_no 值分配後,就不會發生改動。即使刪除了堆中的某條記錄,這條被刪記錄的 heap_no 值也仍然不變。
5. record_type
這個屬性表示當前記錄的型別,共 4 種:
- 0:表示普通記錄
- 1:表示 B+ 樹非葉節點的目錄項記錄
- 2:表示 Infimum 記錄
- 3:表示 Supremum 記錄
6. next_record
這個屬性很重要,表示從當前記錄的真實資料到下一條記錄的真實資料之間的距離。
- 屬性值為正數:說明當前記錄的下一條記錄在當前記錄的後面。
- 屬性值為負數:說明當前記錄的下一條記錄在當前記錄的前面。
比如,第 1 條記錄的 next_record 值為 32,那麼從此記錄的真實資料地址向後找 32 位元組就是下一條記錄的真實資料。再比如,當值為 -111,那麼就代表從此記錄向前找 111 位元組。
很熟悉?沒錯,就是連結串列。
- 下一條記錄,是指按照主鍵從小到大排列的下一條。
- Infrimum 記錄的下一條記錄,就是本頁中主鍵值最小的使用者記錄。
- 本頁主鍵值最大的使用者記錄的下一條記錄,就是 Supremum 記錄。
所以,現在再來重新看下記錄之間的示意圖,可以用單向連結串列來描述了:
如果這時候,刪掉其中的某條記錄,改變的是指標。
本文參考書籍: 小孩子4919 《mysql是怎樣執行的》