當前位置:網站首頁>Mysql之存儲原理(1)

Mysql之存儲原理(1)

2022-01-27 10:32:59 Chen_leilei

首先我們知道,對於Mysql而言,數據是存儲在文件中的,為了能够快速的定比特我們想要的數據,我們就需要一種數據結構,就是索引。一般操作系統以4kb為一個數據頁讀取數據,而mysql是以16kb作為一個數據塊,已經讀取過的數據會放在緩存中,如果多次讀取的數據在同一個數據塊上,只需要一次磁盤IO就可以了,在mysql中我們用N叉樹來代替二叉樹的原因是因為在極端的條件下,二叉樹會變成鏈錶的結構,所以我們使用N叉樹,這個N一般為1200,當樹高為4的時候可以存儲億級別的數據,mysql用到的b+樹,一般非葉子節點構建索引,葉子節點存儲數據。

InnoDB 中,有聚簇索引和普通索引之分,聚簇索引根據主鍵來構建,葉子節點存放的是該主鍵對應的這一行記錄,而普通索引根據聲明這個索引時候的列來構建,葉子節點存放的是這一行記錄對應的主鍵的值,而普通索引中還有唯一索引和聯合索引兩個特例,唯一索引在插入和修改的時候會校驗該索引對應的列的值是否已經存在,而聯合索引將兩個列的值按照申明時候的順序進行拼接後在構建索引。

數據是以行為單比特存儲在聚簇索引裏的,根據主鍵查詢可以直接利用聚簇索引定比特到所在記錄,根據普通索引查詢需要先在普通索引上找到對應的主鍵的值,然後根據主鍵值去聚簇索引上查找記錄,俗稱回錶。

普通索引上存儲的值是主鍵的值,如果主鍵是一個很長的字符串並且建了很多普通索引,將造成普通索引占有很大的物理空間,這也是為什麼建議使用 自增ID 來替代訂單號作為主鍵,另一個原因是 自增ID 在插入的時候可以保證相鄰的兩條記錄可能在同一個數據塊,而訂單號的連續性在設計上可能沒有自增ID好,導致連續插入可能在多個數據塊,增加了磁盤讀寫次數。

如果我們查詢一整行記錄的話,一定要去聚簇索引上查找,而如果我們只需要根據普通索引查詢主鍵的值,由於這些值在普通索引上已經存在,所以並不需要回錶,這個稱為索引覆蓋,在一定程度上可以提高查詢效率,由於聯合索引上通過多個列構建索引,有時候我們可以將需要頻繁查詢的字段加到聯合索引裏面,例如如果經常需要根據 name 查找 age 我們可以建一個 name 和 age 的聯合索引。

查詢的時候如果在索引上用了函數,將導致無法用到根據之前列上的值構建的索引,索引遵循最左匹配原則,所以如果需要查詢某個列的值中間是否包含某個字符串,將無法利用索引,如果有這種需求可以利用全文索引,而如果查詢是否以某個字符串開頭就可以,聯合索引根據第一個列查詢可以用到索引,僅僅根據第二個列將無法用到索引,查詢的時候用 IN 的效率高於 NOT = 。另外建議將索引的列設置為非空,這個和 NULL 字段的存儲有關,下文在分析。

有了以上的索引知識我們在來分析數據是怎麼存儲的,InnoDB 存儲引擎的邏輯存儲結構從大到小依次可以分為:錶空間、段、區、頁、行。

 錶空間作為存儲結構的最高層,所有數據都存放在錶空間中,默認情况下用一個共享錶空間 ibdata1 ,如果開啟了 innodb_file_per_table 則每張錶的數據將存儲在單獨的錶空間中,也就是每張錶都會有一個文件,錶空間由各個段構成,InnoDB存儲引擎由索引組織的,而索引中的葉子節點用來記錄數據,存儲在數據段,而非葉子節點用來構建索引,存儲在索引段,而回滾段我們在後面分析鎖的時候在聊。

區是由連續的頁組成,任何情况下一個區都是 1MB ,一個區中可以有多個頁,每個頁默認為 16KB ,所以默認情况下一個區中可以包含64個連續的頁,頁的大小是可以通過 innodb_page_size 設置,頁中存儲的是具體的行記錄。一行記錄最終以二進制的方式存儲在文件裏,我們要能够解析出一行記錄中每個列的值,存儲的時候就需要有固定的格式,至少需要知道每個列占多少空間,而 MySQL 中定義了一些固定長度的數據類型,例如 int、tinyint、bigint、char數組、float、double、date、datetime、timestamp 等,這些字段我們只需要讀取對應長度的字節,然後根據類型進行解析即可,對於變長字段,例如 varchar、varbinary 等,需要有一個比特置來單獨存儲字段實際用到的長度,當然還需要頭信息來存儲元數據,例如記錄類型,下一條記錄的比特置等。下面我們以 Compact 行格式分析一行數據在 InnoDB 中是怎麼存儲的。

 變長字段長度列錶,該比特置用來存儲所聲明的變長字段中非空字段實際占有的長度列錶,例如有3個非空字段,其中第一個字段長度為3,第二個字段為空,第三個字段長度為1,則將用 01 03 錶示,為空字段將在下一個比特置進行標記。變長字段長度不能超過 2 個字節,所以 varchar 的長度最大為 65535。

NULL 標志比特,占 1 個字節,如果對應的列為空則在對應的比特上置為 1 ,否則為 0 ,由於該標志比特占一個字節,所以列的數量不能超過 255。如果某字段為空,在後面具體的列數據中將不會在記錄。這種方式也導致了在處理索引字段為空的時候需要進行額外的操作。

記錄頭信息,固定占 5 字節,包含下一條記錄的比特置,該行記錄總長度,記錄類型,是否被删除,對應的 slot 信息等

列數據 包含具體的列對應的值,加上兩個隱藏列,事務 ID 列和回滾指針列。如果沒有申明主鍵,還會增加一列記錄內部 ID。

舉個例子

CREATE TABLE mytest(
t1 varchar(10),
t2 varchar(10),
t3 char(10),
t4 varchar(10)
) engine = innodb;

insert into mytest VALUES('a','bb','bb','ccc');
insert into mytest VALUES('d',NULL,NULL,'fff');

該錶定義了 3 個變長字段和 1 個定長字段,然後插入兩行記錄,第二行記錄包含空值,我們打開錶空間 mytest.ibd 文件,轉換為 16 進制,並定比特到如下內容:

//第一行記錄
03 02 01 為變長字段長度列錶,這裏是倒序存放的,分別對應 ccc、bb、a 的長度。
00 錶示沒有為空的字段
00 00 10 00 2c 為記錄頭
00 00 00 2b 68 00 沒有申明主鍵,維護內部 ID
00 00 00 00 06 05 事務ID
80 00 00 00 32 01 10 回滾指針
61 第一列 a 的值
62 62 第二列 bb 的值
62 62 20 20 20 20 20 20 20 20 第三列 bb 的值,固定長度 char(10) 以20進行填充
63 63 63 第四列 ccc 的值

//第二行記錄
03 01 為變長字段長度列錶,這裏是倒序存放的,分別對應 fff、a 的長度,第二列比特空。
06 轉換為二進制為 00000110 錶示第二列和第三列為空
00 00 20 ff 98 為記錄頭
00 00 00 2b 68 01 沒有申明主鍵,維護內部 ID
00 00 00 00 06 06 事務ID
80 00 00 00 32 01 10 回滾指針
64 第一列 d 的值
65 65 65 第四列 fff 的值

到此,我們了解了一個數據行是怎麼存儲的,然而數據行並不是存儲引擎管理的最小存儲單比特,索引只能够幫助我們定比特到某個數據頁,每一次磁盤讀寫的最小單比特為也是數據頁,而一個數據頁內存儲了多個數據行,我們需要了解數據頁的內部結構才能知道存儲引擎怎麼定比特到某一個數據行。InnoDB 的數據頁由以下 7 個部分組成:

  • 文件頭(File Header) 固定 38 個字節 (頁的比特置,上一頁下一頁比特置,checksum , LSN)

  • 數據頁頭( Page Header)固定 56 個字節 包含slot數目,可重用空間起始地址,第一個記錄地址,記錄數,最大事務ID等

  • 虛擬的最大最小記錄 (Infimum + Supremum Record)

  • 用戶記錄 (User Records) 包含已經删除的記錄以鏈錶的形式構成可重用空間

  • 待分配空間 (Free spaces) 未分配的空間

  • 頁目錄 (Page Directory) slot 信息,下面單獨介紹

  • 文件尾 (File Trailer) 固定8個字節,用來保證頁的完整性

 

頁目錄裏維護多個 slot ,一個 slot 包含多個行記錄。每個 slot 占 2 個字節,記錄這個 slot 裏的行記錄相對頁初始比特置的偏移量。由於索引只能定比特到數據頁,而定比特到數據頁內的行記錄還需要在內存中進行二分查找,而這個二分查找就需要借助 slot 信息,先找到對應的 slot ,然後在 slot 內部通過數據行中記錄頭裏的下一個記錄地址進行遍曆。每一個 slot 可以包含 4 到 8 個數據行。如果沒有 slot 輔助,鏈錶本身是無法進行二分查找的。

 

 

版權聲明
本文為[Chen_leilei]所創,轉載請帶上原文鏈接,感謝
https://cht.chowdera.com/2022/01/202201271032590101.html

隨機推薦