本文讨论Secondary Key Page的解析,也就是表非主键索引的记录存储。与Clustered Key Page有相同的基本记录结构,也细分为Leaf Page和Non-Leaf Page,我们先看结构:
### Contents (Secondary Key - Leaf Page) ###
------------------------+------------------------+
Secondary Key Fields (k)| Cluster Key Fields (j) |
------------------------+------------------------+
### Contents (Secondary Key - Non-Leaf Page) ###
-----------------------------------------+----------------------------------------+-----------------------+
Secondary Key Min. Key on Child Page (k) | Cluster Key Min. Key on Child Page (j) | Child Page Number (4) |
-----------------------------------------+----------------------------------------+-----------------------+
先看Leaf Page, 记录的基本结构都是一样的,有Extra Bytes, null-bitmap, variable-field-lengths部分,但是内容不同。InnoDB中,非主键索引(组合索引)字段是允许包含空值的。null-bitmap用于标记Secondary Key Fields中的空值。variable-field-lengths包含Secondary和Cluster Key的可变长字段的记录存储的长度。
记录内容中,最前面部分存放索引的字段,后面接着索引字段值对应的主键值,记录内容不包含DB_TRX_ID和DB_ROLL_PTR伪字段。以sakila.film表的idx_title(index_id = 597)索引为例,根据《InnoDB文件物理结构解析2》的IdxPage1案例的输出,idx_title索引的相关页为:
PAGE PAGE_TYPE LEVEL INDEX_ID PAGE_PREV PAGE_NEXT
----- --------------- ----- -------- ----------- -----------
5 FIL_PAGE_INDEX 1 597 4294967295 4294967295
16 FIL_PAGE_INDEX 0 597 4294967295 17
17 FIL_PAGE_INDEX 0 597 16 4294967295
所以,page(5)是Secondary Key Non-Leaf Page,page(16)和page(17)为Secondary Key Leaf Page,我们写个案例分析page(16)的内容:
public class IdxPage6 {
public static void main(String[] args) throws Exception {
String fileName = "D:\\Data\\mysql\\8.0.18\\data\\sakila\\film.ibd";
try (IbdFileParser parser = new IbdFileParser(fileName)) {
IndexPage page = (IndexPage) parser.getPage(16);
long indexId = page.getIndexHeader().getIndexId().longValueExact();
TableMeta tableMeta = IdxPage3.getFilmTableMeta();
定义索引的元数据(索引包含哪些列)
tableMeta.setSecondaryKey(indexId, 1, "title");
SecondaryKeyLeafPage leafPage = new SecondaryKeyLeafPage(page.getPageRaw(), page.getPageSize());
List<SecondaryKeyLeafRecord> records = leafPage.getUserRecords(tableMeta);
StringBuilder buff = new StringBuilder();
buff.append("Secondary Key Fields Cluster Key Fields \n")
.append("-------------------------------- ------------------------\n");
for (SecondaryKeyLeafRecord record : records) {
List<RecordField> skFields = record.getSecondaryKeyFields();
List<RecordField> ckFields = record.getClusterKeyFields();
StringBuilder skValue = new StringBuilder();
StringBuilder ckValue = new StringBuilder();
for(RecordField field: skFields) {
skValue.append(field.getName()).append(" = ").append(field.getContent()).append(" ");
}
buff.append(String.format("%-32s ", skValue));
for(RecordField field: ckFields) {
ckValue.append(field.getName()).append(" = ").append(field.getContent()).append(" ");
}
buff.append(String.format("%-24s\n", ckValue));
}
System.out.println(buff);
}
}
}
/*
程序输出:
Secondary Key Fields Cluster Key Fields
-------------------------------- ------------------------
title = ACADEMY DINOSAUR film_id = 1
title = ACE GOLDFINGER film_id = 2
title = ADAPTATION HOLES film_id = 3
title = AFFAIR PREJUDICE film_id = 4
title = AFRICAN EGG film_id = 5
title = AGENT TRUMAN film_id = 6
title = AIRPLANE SIERRA film_id = 7
title = AIRPORT POLLOCK film_id = 8
title = ALABAMA DEVIL film_id = 9
title = ALADDIN CALENDAR film_id = 10
title = ALAMO VIDEOTAPE film_id = 11
...
*/
由输出可以看到,InnoDB索引,记录的是索引值对应的主键值,而不是行记录的地址,这与Oracle数据库的索引是不同的,或许与InnoDB是索引组织表有关,行记录存储在主键中。找条输出验证一下解析结果:
root@localhost [sakila]> select title from film where film_id=10;
+------------------+
| title |
+------------------+
| ALADDIN CALENDAR | // film_id =10 <--> title = ALADDIN CALENDAR与预期的一样;
+------------------+
1 row in set (0.00 sec)
我们接下来通过案例解析page(5), Non-Leaf页:
public class IdxPage7 {
public static void main(String[] args) throws Exception {
String fileName = "D:\\Data\\mysql\\8.0.18\\data\\sakila\\film.ibd";
try (IbdFileParser parser = new IbdFileParser(fileName)) {
IndexPage page = (IndexPage) parser.getPage(5);
long indexId = page.getIndexHeader().getIndexId().longValueExact();
TableMeta tableMeta = IdxPage3.getFilmTableMeta();
定义索引的元数据(索引包含哪些列)
tableMeta.setSecondaryKey(indexId, 1, "title");
SecondaryKeyNonLeafPage rootPage = new SecondaryKeyNonLeafPage(page.getPageRaw(), page.getPageSize());
List<SecondaryKeyNonLeafRecord> records = rootPage.getUserRecords(tableMeta);
StringBuilder buff = new StringBuilder();
buff.append("Secondary Key Min. Key on Child Page Cluster Key Min. Key on Child Page Child Page Number \n")
.append("------------------------------------ ----------------------------------- ------------------\n");
for(SecondaryKeyNonLeafRecord record : records) {
List<RecordField> minSkFields = record.getMinSecondaryKeyOnChild();
List<RecordField> minCkFields = record.getMinClusterKeyOnChild();
long childPageNo = record.getChildPageNumber();
StringBuilder skValue = new StringBuilder();
StringBuilder ckValue = new StringBuilder();
for(RecordField field: minSkFields) {
skValue.append(field.getName()).append(" = ").append(field.getContent()).append(" ");
}
buff.append(String.format("%-36s ", skValue));
for(RecordField field: minCkFields) {
ckValue.append(field.getName()).append(" = ").append(field.getContent()).append(" ");
}
buff.append(String.format("%-35s ", ckValue));
buff.append(String.format("%-15d\n", childPageNo));
}
System.out.println(buff);
}
}
}
/*
程序输出:
Secondary Key Min. Key on Child Page Cluster Key Min. Key on Child Page Child Page Number
------------------------------------ ----------------------------------- ------------------
title = ACADEMY DINOSAUR film_id = 1 16
title = GILMORE BOILED film_id = 358 17
*/
索引深度level>0时,根据非叶子节点内的值范围找叶子节点,再根据所在叶节点中找到对应的主键值,然后根据主键"回表",找到对应行记录。
到这里,FIL_PAGE_INDEX的解析介绍完成,下文将介绍一下8.0新引入的FIL_PAGE_SDI。