一文搞懂MySQL索引頁(yè)結(jié)構(gòu)
1. 前言
「頁(yè)」是InnoDB管理存儲(chǔ)空間的基本單位,也是內(nèi)存和磁盤交互的基本單位。也就是說(shuō),哪怕你需要1字節(jié)的數(shù)據(jù),InnoDB也會(huì)讀取整個(gè)頁(yè)的數(shù)據(jù),下次讀取的數(shù)據(jù)如果恰巧也在這個(gè)頁(yè)里,就能命中緩存了。寫也是一樣的,寫數(shù)據(jù)前要先把頁(yè)加載到內(nèi)存,然后在內(nèi)存中修改,該頁(yè)被記為「臟頁(yè)」,臟頁(yè)淘汰之前必須刷盤。
InnoDB有很多類型的頁(yè),它們的用處也各不相同。比如:有存放undo日志的頁(yè)、有存放INODE信息的頁(yè)、有存放Change Buffer信息的頁(yè)、存放用戶記錄數(shù)據(jù)的頁(yè)等等。今天我們要聊的,就是最基礎(chǔ)也是最重要的,存放用戶記錄數(shù)據(jù)的「索引頁(yè)」。
2. 索引頁(yè)結(jié)構(gòu)
InnoDB默認(rèn)的頁(yè)大小是16KB,在初始化表空間之前可以在配置文件中進(jìn)行配置,一旦初始化完成就不可再變更了。查看頁(yè)大小的命令如下,顯示的是字節(jié)數(shù)。
SHOW VARIABLES LIKE 'innodb_page_size';
索引頁(yè)結(jié)構(gòu)如下圖所示:
索引頁(yè)由七部分組成,其中Infimum和Supremum也屬于記錄,只不過(guò)是虛擬記錄,這里為了與用戶記錄區(qū)分開(kāi),還是決定將兩者拆開(kāi)。
名稱 | 大小 | 描述 |
---|---|---|
File Header | 38字節(jié) | 所有頁(yè)的通用文件頭信息 |
Page Header | 56字節(jié) | 索引頁(yè)特有的頁(yè)頭信息 |
Infimum+Supremum | 26字節(jié) | 頁(yè)中虛擬的最小、最大記錄 |
User Records | 變長(zhǎng) | 用戶記錄數(shù)據(jù) |
Free Space | 變長(zhǎng) | 空閑空間 |
Page Directory | 變長(zhǎng) | 頁(yè)目錄,加速頁(yè)內(nèi)數(shù)據(jù)檢索效率 |
File Trailer | 8字節(jié) | 所有頁(yè)的通用文件尾信息,校驗(yàn)頁(yè)是否完整 |
2.1 File Header
File Header是所有頁(yè)都有的一個(gè)通用的結(jié)構(gòu),占用固定的38字節(jié),它記錄了頁(yè)的一些通用的狀態(tài)信息,例如:頁(yè)的頁(yè)號(hào)、Checksum、把頁(yè)串聯(lián)成雙向鏈表的指針、頁(yè)的類型等等。
名稱 | 大小 | 描述 |
---|---|---|
FIL_PAGE_SPACE_OR_CHECKSUM | 4字節(jié) | 新版本中代表頁(yè)的校驗(yàn)和Checksum |
FIL_PAGE_OFFSET | 4字節(jié) | 頁(yè)號(hào) |
FIL_PAGE_PREV | 4字節(jié) | 上一個(gè)頁(yè)的頁(yè)號(hào) |
FIL_PAGE_NEXT | 4字節(jié) | 下一個(gè)頁(yè)的頁(yè)號(hào) |
FIL_PAGE_LSN | 8字節(jié) | 頁(yè)面最后被修改時(shí)的LSN值 |
FIL_PAGE_TYPE | 2字節(jié) | 頁(yè)的類型 |
FIL_PAGE_FILE_FLUSH_LSN | 8字節(jié) | 僅在系統(tǒng)表空間的第1個(gè)頁(yè)中使用,代表文件至少被刷新到了對(duì)應(yīng)的LSN值 |
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID | 4字節(jié) | 頁(yè)數(shù)據(jù)哪個(gè)表空間 |
FIL_PAGE_SPACE_OR_CHECKSUM
基于當(dāng)前頁(yè)計(jì)算出的校驗(yàn)和(Checksum),可以把它看作是哈希值,校驗(yàn)和不同,則兩個(gè)頁(yè)數(shù)據(jù)肯定不同。它的作用是InnoDB在臟頁(yè)刷盤時(shí),有可能會(huì)遇到頁(yè)刷到一半斷電的情況,頁(yè)的頭和尾部分分別記錄校驗(yàn)和,只有當(dāng)頭尾的校驗(yàn)和一致的時(shí)候,才代表磁盤上的頁(yè)是完整的,否則就是一個(gè)損壞的頁(yè)。
FIL_PAGE_OFFSET
頁(yè)號(hào),頁(yè)的唯一標(biāo)識(shí),全局遞增的數(shù)字,InnoDB通過(guò)頁(yè)號(hào)來(lái)定位唯一的一個(gè)頁(yè)。4字節(jié)存儲(chǔ),意味著一個(gè)表空間最多可以有232個(gè)頁(yè),按照一個(gè)頁(yè)16KB計(jì)算,則一個(gè)表空間最多支持64TB的數(shù)據(jù)。
FIL_PAGE_PREV & FIL_PAGE_NEXT
一個(gè)頁(yè)大小才16KB,一張表數(shù)據(jù)其實(shí)是由N多個(gè)頁(yè)構(gòu)成的,頁(yè)與頁(yè)之間在物理上可以是不連續(xù)的,但是邏輯上要連續(xù),F(xiàn)IL_PAGE_PREV和FIL_PAGE_NEXT分別指向當(dāng)前頁(yè)的上一個(gè)頁(yè)和下一個(gè)頁(yè)的頁(yè)號(hào),通過(guò)這兩個(gè)指針將索引頁(yè)串聯(lián)成了一個(gè)雙向鏈表。記錄與記錄之間是單向的,頁(yè)與頁(yè)之間是雙向的!
FIL_PAGE_LSN
頁(yè)面最后被修改時(shí),對(duì)應(yīng)的LSN值。LSN的全稱是Log Sequence Number,日志序列號(hào)。它是一個(gè)遞增的數(shù)字,和事務(wù)相關(guān),這里不作贅述。
FIL_PAGE_TYPE
當(dāng)前頁(yè)的類型,InnoDB為了不同的目的設(shè)計(jì)了很多不同類型的頁(yè),索引頁(yè)的固定值是0x45BF
。
FIL_PAGE_FILE_FLUSH_LSN
僅在第1個(gè)頁(yè)中使用,用來(lái)判斷數(shù)據(jù)庫(kù)是正常關(guān)閉還是異常宕機(jī)。
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID
僅記錄當(dāng)前頁(yè)數(shù)據(jù)哪個(gè)表空間。
2.2 Page Header
Page Header是索引頁(yè)特有的結(jié)構(gòu),占用固定的56字節(jié),它記錄了索引頁(yè)中記錄相關(guān)的狀態(tài)信息。
名稱 | 大小 | 描述 |
---|---|---|
PAGE_N_DlR_SLOTS | 2字節(jié) | 頁(yè)目錄中的槽數(shù)量 |
PAGE_HEAP_TOP | 2字節(jié) | 未使用的空間最小地址,User Records和Free Space分界點(diǎn) |
PAGE_N_HEAP | 2字節(jié) | 本頁(yè)中的記錄的數(shù)量(包括虛擬記錄和刪除記錄) |
PAGE_FREE | 2字節(jié) | 第一個(gè)刪除的記錄地址,后續(xù)刪除的記錄會(huì)形成鏈表。 |
PAGE_GARBAGE | 2字節(jié) | 已刪除記錄占用的字節(jié)數(shù) |
PAGE_LAST_INSERT | 2字節(jié) | 最后插入記錄的位置 |
PAGE_DIRECTION | 2字節(jié) | 記錄插入的方向 |
PAGE_N_DIRECTION | 2字節(jié) | 同一個(gè)方向連續(xù)插入的記錄數(shù)量 |
PAGE_N_RECS | 2字節(jié) | 該頁(yè)中記錄的數(shù)量(不包括虛擬記錄和刪除記錄) |
PAGE_MAX_TRX_ID | 8字節(jié) | 修改當(dāng)前頁(yè)的最大事務(wù)ID,僅在二級(jí)索引中使用 |
PAGE_LEVEL | 2字節(jié) | 當(dāng)前頁(yè)在B+樹(shù)中所處的層級(jí) |
PAGE_INDEX_ID | 8字節(jié) | 索引ID,表示當(dāng)前頁(yè)屬于哪個(gè)索引 |
PAGE_BTR_SEG_LEAF | 10字節(jié) | B+樹(shù)葉子段的頭部信息,僅在B+樹(shù)的Root頁(yè)定義 |
PAGE_BTR_SEG_TOP | 10字節(jié) | B+樹(shù)非葉子段的頭部信息,僅在B+樹(shù)的Root頁(yè)定義 |
不用每個(gè)屬性都了解,我們挑幾個(gè)比較重要的看看。
PAGE_N_DlR_SLOTS
一個(gè)頁(yè)內(nèi)可能有上千條記錄,挨個(gè)遍歷的話效率太慢了。為了提高頁(yè)內(nèi)記錄的檢索效率,InnoDB將頁(yè)內(nèi)的記錄劃分為多個(gè)組,組里最大的那條記錄相較于頁(yè)的地址偏移量會(huì)記錄到「Page Directory」部分,每個(gè)組都對(duì)應(yīng)一個(gè)槽,槽的大小是固定的2字節(jié)。該屬性記錄的就是頁(yè)內(nèi)槽的數(shù)量。
PAGE_HEAP_TOP
Free Space的起始位置,它是User Records和Free Space分界點(diǎn)。一個(gè)全新的頁(yè)一開(kāi)始是沒(méi)有User Records部分的,每插入一條記錄,都要向Free Space申請(qǐng)空間,F(xiàn)ree Space耗盡就代表頁(yè)滿了。
PAGE_FREE
DELETE命令刪除記錄時(shí),InnoDB并不會(huì)真的將記錄從磁盤中刪除,而是在記錄的頭信息里打個(gè)標(biāo)記,然后將其加入到「垃圾鏈表」中。PAGE_FREE指向的就是垃圾鏈表的表頭記錄。后面刪除的記錄,也會(huì)自動(dòng)加入到鏈表里。
PAGE_DIRECTION & PAGE_N_DIRECTION
PAGE_DIRECTION表示最后一條記錄插入的方向,比上一條記錄值大則記為右邊,反之則是左邊。PAGE_N_DIRECTION表示同一方向連續(xù)插入的記錄數(shù),方向變了該值就會(huì)重置。
PAGE_LEVEL
InnoDB組織數(shù)據(jù)的形式就是B+樹(shù),樹(shù)中的節(jié)點(diǎn)就是索引頁(yè),PAGE_LEVEL代表當(dāng)前頁(yè)在B+樹(shù)中所處的層級(jí)。InnoDB規(guī)定,葉子節(jié)點(diǎn)層級(jí)為0,然后向上遞增。
2.3 User Records
Infimum和Supremum也屬于記錄,只是為了與用戶記錄區(qū)分開(kāi)才劃分成了兩部分,我們先看User Records。
用戶記錄存放在User Records部分,一個(gè)全新的頁(yè)一開(kāi)始全是Free Space,是沒(méi)有User Records部分的。每插入一條記錄都需要到Free Space申請(qǐng)一塊空間,并將其劃分到User Records用來(lái)存放用戶記錄。當(dāng)Free Space耗盡也就代表當(dāng)前頁(yè)已經(jīng)用完了,再有新記錄需要插入,就需要申請(qǐng)一個(gè)新的頁(yè)了。
還記得MySQL的行格式嗎?它決定了記錄在磁盤里的存儲(chǔ)格式。以COMPACT為例,存儲(chǔ)格式如下圖:
記錄頭信息里的字段比較關(guān)鍵,以防大家忘記,我這里再貼一下:
名稱 | 大小(Bit) | 說(shuō)明 |
---|---|---|
預(yù)留位1 | 1 | 沒(méi)有使用 |
預(yù)留位2 | 1 | 沒(méi)有使用 |
deleted_flag | 1 | 記錄刪除標(biāo)記 |
min_rec_flag | 1 | B+樹(shù)非葉子節(jié)點(diǎn)的最小目錄項(xiàng)標(biāo)記 |
n_owned | 4 | 同一頁(yè)內(nèi)同一組里最大的記錄會(huì)記錄組里的記錄數(shù)量,其余記錄該值為0 |
heap_no | 13 | 當(dāng)前記錄在頁(yè)面堆里的相對(duì)位置 |
record_type | 3 | 記錄類型。0:普通記錄,1:B+樹(shù)非葉子節(jié)點(diǎn)目錄項(xiàng)記錄,2:Infimum記錄,3:Supremum記錄. |
next_record | 16 | 下一條記錄的相對(duì)位置 |
記錄頭信息的最后2字節(jié)用來(lái)連接下一條記錄,將頁(yè)內(nèi)所有記錄串聯(lián)成一個(gè)單向鏈表。所以我們隱藏變長(zhǎng)字段長(zhǎng)度列表和NULL值列表,記錄的格式應(yīng)該是這樣的:
記錄是怎么排序的?
我們已經(jīng)知道,頁(yè)內(nèi)的記錄會(huì)自動(dòng)串聯(lián)成一個(gè)單向鏈表。那這個(gè)鏈表的編排順序是什么呢?是按照記錄的插入時(shí)間排序的嗎?其實(shí)不是的,如果表有主鍵,會(huì)根據(jù)主鍵排序;沒(méi)主鍵有唯一非空索引,會(huì)根據(jù)該索引排序;兩者都沒(méi)有,InnoDB會(huì)自動(dòng)生成一個(gè)row_id
列并根據(jù)該列進(jìn)行排序。
若無(wú)特殊說(shuō)明,本文均假定表有主鍵。
2.4 Infimum & Supremum
Infimum和Supremum是索引頁(yè)內(nèi)的兩條虛擬記錄,InnoDB規(guī)定所有索引頁(yè)都會(huì)有這兩條記錄,而且所有的用戶記錄都比Infimum大,都比Supremum小。
記錄頭信息里的heap_no代表記錄在堆里的相對(duì)位置,該值越小代表記錄越靠前。細(xì)心的同學(xué)會(huì)發(fā)現(xiàn),上圖中的用戶記錄heap_no值是從2開(kāi)始的,那0和1呢?不說(shuō)你也肯定猜到了,就是被Infimum和Supremum占用了。Infimum和Supremum的heap_no值分別是0和1,它倆在所有用戶記錄的最前面。
Infimum和Supremum結(jié)構(gòu)非常的簡(jiǎn)單,和用戶記錄一樣也有頭信息,真實(shí)數(shù)據(jù)部分是固定的字符串,如下圖所示:
我們把這兩條虛擬記錄也加入到記錄里面,完整的結(jié)構(gòu)就是下面這樣的:
Supremum記錄的next_record屬性為0,代表它已經(jīng)沒(méi)有下一條記錄了。
2.5 Page Directory
Free Space沒(méi)什么好說(shuō)的,就是一塊未被使用的空閑空間。
Page Directory也叫作「頁(yè)目錄」,它的目的是提高頁(yè)內(nèi)記錄的檢索效率。相較于一張表幾千萬(wàn)的記錄來(lái)說(shuō),一個(gè)頁(yè)內(nèi)幾百上千條記錄已經(jīng)是很少很少了??杉幢闳绱耍灿袔装偕锨l啊,如果頁(yè)內(nèi)檢索記錄只能挨個(gè)遍歷的話,那也太低效了。別忘了,頁(yè)內(nèi)的記錄是根據(jù)索引值排好序的,我們可以巧用「二分法」來(lái)快速查找。
具體做法是:將頁(yè)內(nèi)所有非刪除的記錄劃分為N個(gè)組,每個(gè)組里最后一條記錄(即主鍵最大的記錄)稱作“大哥”,其余記錄是“小弟”,“大哥”的n_owned
屬性記錄了組內(nèi)的記錄數(shù)量。將“大哥”在頁(yè)內(nèi)的地址偏移量提取出來(lái),按順序依次從File Trailer部分往前寫,每個(gè)地址偏移量占用2字節(jié),稱作一個(gè)「槽」,Page Directory就是由這些槽構(gòu)成的。
InnoDB對(duì)于分組內(nèi)的記錄數(shù)量有一些規(guī)定:
- Infimum記錄所在分組,只能有一條記錄。
- Supremum記錄所在分組,允許有1~8條記錄。
- 其余分組,允許有4~8條記錄。
由此可見(jiàn),一個(gè)組里最多有8條記錄,只要通過(guò)二分法快速定位到組,InnoDB也只需要遍歷這8條記錄,相較于遍歷頁(yè)內(nèi)所有記錄,效率要高的多。
2.6 File Trailer
File Trailer是所有頁(yè)都有的通用結(jié)構(gòu),占用固定的8字節(jié),它的主要作用就是為了校驗(yàn)頁(yè)的完整性。磁盤的速度實(shí)在是太慢了,InnoDB不會(huì)每次寫點(diǎn)數(shù)據(jù)都直接刷新到磁盤上,那樣MySQL會(huì)慢死。而是將頁(yè)作為刷盤的基本單位,數(shù)據(jù)修改時(shí),先改內(nèi)存里的頁(yè),稍后再將整個(gè)頁(yè)的數(shù)據(jù)一次性刷新到磁盤里。但是這會(huì)帶來(lái)一個(gè)問(wèn)題,一個(gè)頁(yè)16KB,刷到第10KB的時(shí)候磁盤斷電了怎么辦?重啟后InnoDB如何判斷磁盤里的頁(yè)數(shù)據(jù)是完整的?
?
InnoDB是這么處理的,刷盤前根據(jù)頁(yè)數(shù)據(jù)計(jì)算出一個(gè)Checksum,在頁(yè)頭和頁(yè)尾都寫一份。頁(yè)刷盤的時(shí)候,先刷頁(yè)頭再刷頁(yè)尾,當(dāng)頭尾兩個(gè)Checksum值一致的時(shí)候,代表磁盤里的頁(yè)是完整的,否則就表示頁(yè)頭刷了頁(yè)尾沒(méi)刷,那肯定是刷到一半出錯(cuò)了。
大小 | 說(shuō)明 |
---|---|
4字節(jié) | 頁(yè)的校驗(yàn)和Checksum |
4字節(jié) | 頁(yè)最后被修改時(shí)對(duì)應(yīng)的LSN的后4個(gè)字節(jié),正常情況下應(yīng)該與File Header里的FIL_PAGE_LSN的后4個(gè)字節(jié)相同。 |
3. 總結(jié)
頁(yè)是InnoDB存取數(shù)據(jù)的基本單位,默認(rèn)頁(yè)大小是16KB,InnoDB為了不同的目的設(shè)計(jì)了很多不同類型的頁(yè),本文重點(diǎn)分析了存放用戶記錄的索引頁(yè)。頁(yè)的頭尾部分File Header和File Trailer是所有頁(yè)都有的一個(gè)通用結(jié)構(gòu),它們記錄了頁(yè)的一些通用狀態(tài)信息,和Checksum用來(lái)驗(yàn)證頁(yè)的完整性。Page Header是索引頁(yè)特有的結(jié)構(gòu),它記錄了頁(yè)內(nèi)用戶記錄相關(guān)的狀態(tài)信息。User Records部分用來(lái)存放用戶記錄。另外,由于頁(yè)內(nèi)的記錄數(shù)量也不少,為了提高頁(yè)內(nèi)記錄的檢索效率,InnoDB在索引頁(yè)中加入了Page Directory,它通過(guò)將記錄分組,將組里最大的記錄的地址偏移量形成一個(gè)個(gè)槽,Page Directory就是由這些槽構(gòu)成的。檢索數(shù)據(jù)時(shí),使用二分法快速定位到槽所在的組,就可以避免遍歷所有組的記錄了。
到此這篇關(guān)于MySQL索引頁(yè)結(jié)構(gòu)的文章就介紹到這了,更多相關(guān)MySQL索引頁(yè)結(jié)構(gòu)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
MySQL 存儲(chǔ)過(guò)程的優(yōu)缺點(diǎn)分析
存儲(chǔ)過(guò)程(Stored Procedure)是一種在數(shù)據(jù)庫(kù)中存儲(chǔ)復(fù)雜程序,以便外部程序調(diào)用的一種數(shù)據(jù)庫(kù)對(duì)象。本文將分析存儲(chǔ)過(guò)程的優(yōu)缺點(diǎn)2021-05-05MYSQL數(shù)據(jù)庫(kù)如何設(shè)置主從同步
大家好,本篇文章主要講的是MYSQL數(shù)據(jù)庫(kù)如何設(shè)置主從同步,感興趣的同學(xué)趕快來(lái)看一看吧,對(duì)你有幫助的話記得收藏一下2022-01-01SELECT INTO 和 INSERT INTO SELECT 兩種表復(fù)制語(yǔ)句簡(jiǎn)單介紹
Insert是T-sql中常用語(yǔ)句,Insert INTO table(field1,field2,...) values(value1,value2,...)這種形式的在應(yīng)用程序開(kāi)發(fā)中必不可少2012-11-11解決啟動(dòng)MySQL服務(wù)時(shí)出現(xiàn)"mysql本地計(jì)算機(jī)上的MySQL服務(wù)啟動(dòng)后停止"的問(wèn)題
某一天我的MySQL啟動(dòng)突然出現(xiàn)了異常:“mysql本地計(jì)算機(jī)上的MySQL服務(wù)啟動(dòng)后停止,某些在未由其他服務(wù)或程序使用時(shí)將自動(dòng)停止,”?,小編在網(wǎng)絡(luò)上面找了很多方法,MySQL啟動(dòng)成功了,但是第二天開(kāi)啟MySQL時(shí)還是出現(xiàn)了這個(gè)問(wèn)題,現(xiàn)把兩種方法總結(jié)一下,需要的朋友可以參考下2023-11-11MySQL窗口函數(shù)OVER()用法及說(shuō)明
這篇文章主要介紹了MySQL窗口函數(shù)OVER()用法及說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08