2. 问题2:辅助索引需要MVCC多版本读的时候,为什么需要依赖聚集索引
InnoDB的MVCC是通过在聚集索引页中同时存储了DB_TRX_ID和DB_ROLL_PTR来实现的。
但是我们从上面page dump出来的结果也很明显能看到,附注索引页是不存储DB_TRX_ID信息的。
所以说,辅助索引上如果想要实现MVCC,需要通过回表读聚集索引来实现。
结论2,辅助索引中不存储DB_TRX_ID,需要依托聚集索引实现MVCC
3. 问题3:为什么查找数据时,一定要读取叶子节点,只读非叶子节点不行吗
在辅助索引的根节点这个页面中(pageno=4),我们注意到它记录的最小记录(min_rec)对应的是(c1=NULL, id=9)这条记录。
在它指向的叶子节点页面中(pageno=18)也确认了这个情况。
现在把id=9的记录删掉,看看辅助索引数据页会发生什么变化。
[root@yejr.run]> delete from t_sk where id = 9 and c1 is null; Query OK, 1 row affected (0.01 sec)
先检查第4号数据页。
[root@yejr.run]# innodb_space -s ibdata1 -T test/t_sk -p 4 page-dump ... records: {:format=>:compact, :offset=>126, :header=> {:next=>428, :type=>:node_pointer, :heap_number=>2, :n_owned=>0, :min_rec=>true, :deleted=>false, :nulls=>["c1"], :lengths=>{}, :externs=>[], :length=>6}, :next=>428, :type=>:secondary, :key=>[{:name=>"c1", :type=>"INT UNSIGNED", :value=>:NULL}], :row=>[{:name=>"id", :type=>"INT UNSIGNED", :value=>9}], :sys=>[], :child_page_number=>18, :length=>8} ...
看到第四号数据页中,最小记录还是 id=9,没有更新。
再查看第18号数据页。
[root@yejr.run]# innodb_space -s ibdata1 -T test/t_sk -p 18 page-dump ... records: {:format=>:compact, :offset=>136, :header=> {:next=>146, :type=>:conventional, :heap_number=>3, :n_owned=>0, :min_rec=>false, :deleted=>false, :nulls=>["c1"], :lengths=>{}, :externs=>[], :length=>6}, :next=>146, :type=>:secondary, :key=>[{:name=>"c1", :type=>"INT UNSIGNED", :value=>:NULL}], :row=>[{:name=>"id", :type=>"INT UNSIGNED", :value=>30}], :sys=>[], :length=>4} ...
在这个数据页(叶子节点)中,最小记录已经被更新成 id=30 这条数据了。
可见,索引树中的非叶子节点数据不是实时更新的,只有叶子节点的数据才是最准确的。
结论3,在索引树中查找数据时,最终一定是要读取叶子节点才行
4. 问题4:索引列允许为NULL,会额外存储更多字节吗
之前流传有一种说法,不允许设置列值允许NULL,是因为会额外多存储一个字节,事实是这样吗?
我们先把c1列改成NOT NULL DEFAULT 0,当然了,改之前要先把所有NULL值更新成0。
[root@yejr.run]> update t_sk set c1=0 where c1 is null; [root@yejr.run]> alter table t_sk modify c1 int unsigned not null default 0;
在修改之前,每条索引记录长度都是10字节,更新之后却变成了13个字节。
直接对比索引页中的数据,发现不同之处
#允许为NULL,且默认值为NULL时 {:format=>:compact, :offset=>136, :header=> {:next=>146, :type=>:conventional, :heap_number=>3, :n_owned=>0, :min_rec=>false, :deleted=>false, :nulls=>["c1"], :lengths=>{}, :externs=>[], :length=>6}, :next=>146, :type=>:secondary, :key=>[{:name=>"c1", :type=>"INT UNSIGNED", :value=>:NULL}], :row=>[{:name=>"id", :type=>"INT UNSIGNED", :value=>48}], :sys=>[], :length=>4} #不允许为NULL,默认值为0时 {:format=>:compact, :offset=>138, :header=> {:next=>151, :type=>:conventional, :heap_number=>3, :n_owned=>0, :min_rec=>false, :deleted=>false, :nulls=>[], :lengths=>{}, :externs=>[], :length=>5}, :next=>151, :type=>:secondary, :key=>[{:name=>"c1", :type=>"INT UNSIGNED", :value=>0}], :row=>[{:name=>"id", :type=>"INT UNSIGNED", :value=>48}], :sys=>[], :length=>8}
可以看到,原先允许为NULL时,record header需要多一个字节(共6字节),但实际物理存储中无需存储NULL值。
而当设置为NOT NULL DEFAULT 0时,record header只需要5字节,但实际物理存储却多了4字节,总共多了3字节,所以索引记录以前是10字节,更新后变成了13字节,实际上代价反倒变大了。
列值允许为NULL更多的是计算代价变大了,以及索引对索引效率的影响,反倒可以说是节省了物理存储开销。
结论4,定义列值允许为NULL并不会增加物理存储代价,但对索引效率的影响要另外考虑
最后,本文使用的MySQL版本Percona-Server-5.7.22,下载源码后自编译的。
Server version: 5.7.22-22-log Source distribution
5. 几点总结
最后针对InnoDB辅助索引,总结几条建议吧。
a) 索引列最好不要设置允许NULL。
b) 如果是非索引列,设置允许为NULL基本上无所谓。
c) 辅助索引需要依托聚集索引实现MVCC。
d) 叶子节点总是存储最新数据,而非叶子节点则不一定。
e) 尽可能不SELECT *,尽量利用覆盖索引完成查询,能不回表就不回表。
6. 延伸阅读
- InnoDB表聚集索引层高什么时候发生变化
- 浅析InnoDB索引结构
- Innodb页合并和页分裂
- innblock | InnoDB page观察利器
- jcole.us:The physical structure of InnoDB index pages
- jcole.us:B+Tree index structures in InnoDB
Enjoy MySQL :)
全文完。