谈谈InnoDB辅助索引的几个特征。

阅读目录

0. 初始化测试表、数据1. 问题1:索引列允许为NULL,对性能影响有多少    结论1,存储大量的NULL值,除了计算更复杂之外,数据扫描的代价也会更高一些2. 问题2:辅助索引需要MVCC多版本读的时候,为什么需要依赖聚集索引    结论2,辅助索引中不存储DB_TRX_ID,需要依托聚集索引实现MVCC3. 问题3:为什么查找数据时,一定要读取叶子节点,只读非叶子节点不行吗    结论3,在索引树中查找数据时,最终一定是要读取叶子节点才行4. 问题4:索引列允许为NULL,会额外存储更多字节吗  结论4,定义列值允许为NULL并不会增加物理存储代价,但对索引效率的影响要另外考虑5. 几点总结6. 延伸阅读

本文开始之前,有几篇文章建议先复习一下

  • InnoDB表聚集索引层高什么时候发生变化

  • 浅析InnoDB索引结构

  • Innodb页合并和页分裂

  • innblock | InnoDB page观察利器

接下来,我们一起测试验证关于辅助索引的几个特点。

0. 初始化测试表、数据

测试表结构如下:

[root@yejr.run]> CREATE TABLE `t_sk` (  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,  `c1` int(10) unsigned NOT NULL,  `c2` int(10) unsigned NOT NULL,  `c3` int(10) unsigned NOT NULL,  `c4` int(10) unsigned NOT NULL,  `c5` datetime NOT NULL,  `c6` char(20) NOT NULL,  `c7` varchar(30) NOT NULL,  `c8` varchar(30) NOT NULL,  `c9` varchar(30) NOT NULL,  PRIMARY KEY (`id`),  KEY `k1` (`c1`)) ENGINE=InnoDB;

除了主键索引外,还有个 c1 列上的辅助索引。

用 mysql_random_data_load 灌入50万测试数据。

[root@yejr.run]# mysql_random_data_load -hXX -uXX -pXX test t_sk 500000

1. 问题1:索引列允许为NULL,对性能影响有多少

把辅助索引列 c1 修改为允许NULL,并且随机更新5万条数据,将 c1 列设置为NULL

[root@yejr.run]> alter table t_sk modify c1 int unsigned;

[root@yejr.run]> update t_sk set c1 = NULL order by rand() limit 50000;Query OK, 50000 rows affected (2.83 sec)Rows matched: 50000  Changed: 50000  Warnings: 0#随机1/10为null[root@yejr.run]> select count(*) from t_sk where c1 is null;+----------+| count(*) |+----------+|    50000 |+----------+

好,现在观察辅助索引的索引数据页结构。

[root@yejr.run]# innblock test/t_sk.ibd scan 16...Datafile Total Size:100663296===INDEX_ID:46   --聚集索引(主键索引)level2 total block is (1)  --根节点,层高2(共3层),共1个pageblock_no:         3,level:   2|*|level1 total block is (5)  --中间节点,层高1,共5个pageblock_no:       261,level:   1|*|block_no:       262,level:   1|*|block_no:       263,level:   1|*|block_no:       264,level:   1|*|block_no:       265,level:   1|*|level0 total block is (5020)  --叶子节点,层高0,共5020个pageblock_no:         5,level:   0|*|block_no:         6,level:   0|*|block_no:         7,level:   0|*|...===INDEX_ID:47   --辅助索引level1 total block is (1)  --根节点,层高1(共2层),共1个pageblock_no:         4,level:   1|*|level0 total block is (509)  --叶子节点,层高0,共509个pageblock_no:        18,level:   0|*|block_no:        19,level:   0|*|block_no:        31,level:   0|*|...

观察辅助索引的根节点里的数据

[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,    --min_rec表示最小记录   :deleted=>false,   :nulls=>["c1"],   :lengths=>{},   :externs=>[],   :length=>6}, :next=>428, :type=>:secondary, :key=>[{:name=>"c1", :type=>"INT UNSIGNED", :value=>:NULL}],    --对应c1列值为NULL :row=>[{:name=>"id", :type=>"INT UNSIGNED", :value=>9}],    --对应id=9 :sys=>[], :child_page_number=>18,    --指向叶子节点 pageno = 18 :length=>8}...{:format=>:compact, :offset=>6246,    --最后一条记录(next=>112,指向supremum) :header=>  {:next=>112,   :type=>:node_pointer,   :heap_number=>346,   :n_owned=>0,   :min_rec=>false,   :deleted=>false,   :nulls=>[],   :lengths=>{},   :externs=>[],   :length=>6}, :next=>112, :type=>:secondary, :key=>[{:name=>"c1", :type=>"INT UNSIGNED", :value=>2142714688}],    --对应c1=2142714688 :row=>[{:name=>"id", :type=>"INT UNSIGNED", :value=>73652}],    --对应id=73652 :sys=>[], :child_page_number=>2935,    --指向叶子节点2935 :length=>12}

经过统计,根节点中c1列值为NULL的记录共有33条,其余476条是c1列值为非NULL,共509条记录。

叶子节点中,每个page大约可以存储1547条记录,共有5万条记录值为NULL,因此需要至少33个page来保存(ceiling(50000/1547) = 33)。

看下这个SQL的查询计划

[root@yejr.run]> desc select count(*) from t_sk where c1 is null\G*************************** 1. row ***************************           id: 1  select_type: SIMPLE        table: t_sk   partitions: NULL         type: refpossible_keys: k1          key: k1      key_len: 5ref: const         rows: 99112     filtered: 100.00        Extra: Using where; Using index

从上面的输出中,我们能看到,当索引列设置允许为NULL时,是会对其纳入索引统计信息,并且值为NULL的记录,都是存储在索引树的最左边。

接下来,跑几个SQL查询。

SQL1,统计所有NULL值数量

[root@yejr.run]> select count(*) from t_sk where c1 is null;+----------+| count(*) |+----------+|    50000 |+----------+

查看slow log

InnoDB_pages_distinct: 34...select count(*) from t_sk where c1 is null;

共需要扫描34个page,根节点(1)+叶子节点(33),正好34个page。

备注:需要用Percona版本才能在slow query log中有InnoDB_pages_distinct信息。

SQL2, 查询 c1 is null

[root@yejr.run]> select id,c1 from t_sk where c1 is null limit 1;+------+------+| id   | c1   |+------+------+| 9607 | NULL |+------+------+

查看slow log

InnoDB_pages_distinct: 12...select id,c1 from t_sk where c1 is null limit 1;

这次的查询需要扫描12个page,除去1个根节点外,还需要扫描12个叶子节点,只是为了返回一条数据而已,这代价有点大。

如果把SQL微调改成下面这样

[root@yejr.run]> select id,c1 from t_sk where c1 is null limit 10000,1;+-------+------+| id    | c1   |+-------+------+| 99671 | NULL |+-------+------+

可以看到还是需要扫描12个page。

InnoDB_pages_distinct: 12...select id,c1 from t_sk where c1 is null limit 10000,1;

SQL3, 查询 c1 任意非NULL值
如果把 c1列条件改成正常的int值,结果就不太一样了

[root@yejr.run]> select id, c1 from t_sk where c1  = 907299016;+--------+-----------+| id     | c1        |+--------+-----------+| 365115 | 907299016 |+--------+-----------+1 row in set (0.00 sec)

slow log是这样的

InnoDB_pages_distinct: 2...select id, c1 from t_sk where c1  = 907299016;

可以看到,只需要扫描2个page,这个看起来就正常了。

结论1,存储大量的NULL值,除了计算更复杂之外,数据扫描的代价也会更高一些

另外,如果要查询的c1值正好介于两个page的临界位置,那么需要多读取一个page。

扫描第31号page,确认该数据页中的最小和最大物理记录

[root@yejr.run]# innodb_space -s ibdata1 -T test/t_sk -p 31 page-dump...records:{:format=>:compact, :offset=>126, :header=>  {:next=>9996,   :type=>:conventional,   :heap_number=>2,   :n_owned=>0,   :min_rec=>false,   :deleted=>false,   :nulls=>[],   :lengths=>{},   :externs=>[],   :length=>6}, :next=>9996, :type=>:secondary, :key=>[{:name=>"c1", :type=>"INT UNSIGNED", :value=>1531865685}], :row=>[{:name=>"id", :type=>"INT UNSIGNED", :value=>1507}], :sys=>[], :length=>8} ...{:format=>:compact, :offset=>5810, :header=>  {:next=>112,   :type=>:conventional,   :heap_number=>408,   :n_owned=>0,   :min_rec=>false,   :deleted=>false,   :nulls=>[],   :lengths=>{},   :externs=>[],   :length=>6}, :next=>112, :type=>:secondary, :key=>[{:name=>"c1", :type=>"INT UNSIGNED", :value=>1536700825}], :row=>[{:name=>"id", :type=>"INT UNSIGNED", :value=>361382}], :sys=>[], :length=>8} 

指定c1的值为 1531865685、1536700825 执行查询,查看slow log,确认都需要扫描3个page,而如果换成介于这两个值之间的数据,则只需要扫描2个page。

InnoDB_pages_distinct: 3...select id, c1 from t_sk where c1  = 1531865685;

InnoDB_pages_distinct: 3...select id, c1 from t_sk where c1  = 1536700825;

InnoDB_pages_distinct: 2...select id, c1 from t_sk where c1  = 1536630003;

InnoDB_pages_distinct: 2...select id, c1 from t_sk where c1  = 1536575377;

这是因为辅助索引是非唯一的,即便是在等值查询时,也需要再读取下一条记录,以确认已获取所有符合条件的数据。

还有,当利用辅助索引读取数据时,如果要读取整行数据,则需要回表。

也就是说,除了扫描辅助索引数据页之外,还需要扫描聚集索引数据页。

来个例子看看就知道了。

#无需回表时InnoDB_pages_distinct: 2...select id, c1 from tnull where c1  = 1536630003;

#需要回表时InnoDB_pages_distinct: 5...select * from t_sk where c1  = 1536630003;

需要回表时,除了扫描辅助索引页2个page外,还需要回表扫描聚集索引页,而聚集索引是个3层树,因此总共需要扫描5个page。

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 :)

全文完。


由叶老师主讲的「MySQL优化」课已升级到MySQL 8.0版本,扫码开启MySQL 8.0的修行之旅吧

distinct作用于后面所有的列吗_InnoDB索引允许NULL对性能有影响吗相关推荐

  1. distinct作用于后面所有的列吗_所有的鱼缸都适合放底砂吗?有的沙子让观赏鱼变美,有的起反作用...

    对不起,我是哗仔! 欢迎朋友们再一次回到这个有点不一样的观赏鱼专栏,我就是那个鱼和熊掌已经兼得的哗仔. 对于大部分淡水观赏鱼玩家来说,底砂并非必需品. 对底砂比较看重的淡水玩家有鼠鱼异形鱼玩家.三湖慈 ...

  2. mysql 去除重复 Select中DISTINCT关键字的用法(查询两列,只去掉重复的一列)

    在使用mysql时,有时需要查询出某个字段不重复的记录,虽然mysql提供 有distinct这个关键字来过滤掉多余的重复记录只保留一条,但往往只用它来返回不重复记录的条数,而不是用它来返回不重记录的 ...

  3. mysql引用表无效列_Mysql使用索引可能失效的场景

    1.WHERE字句的查询条件里有不等于号(WHERE column!=-),MYSQL将无法使用索引 2.类似地,如果WHERE字句的查询条件里使用了函数(如:WHERE DAY(column)=-) ...

  4. mysql中索引的作用是什么_详解mysql中索引的作用

    1. 索引是什么,首先我们可以举个例子,字典大家应该都使用过,我们可以使用目录快速定位到所要查找的内容,那么索引跟目录的作用类似,在数据库表记录中,利用索引,可以快速过滤查找到数据记录. 2. 索引类 ...

  5. Pandas-高级处理(七):透视表(pivot_table)【以指定列作为行索引对另一指定列的值进行分组聚合操作】、交叉表(crosstab)【统计频率】

    交叉表与透视表的作用 交叉表:计算一列数据对于另外一列数据的分组个数 透视表:指定某一列对另一列的关系 一.透视表 透视表是一种可以对数据动态排布并且分类汇总的表格格式. 透视表:透视表是将原有的Da ...

  6. oracle复合索引列顺序,复合索引顺序选择性问题(一)

    索引是我们经常选择的数据表检索优化方案之一.其中,复合索引是我们经常选择的策略.那么,构建索引列的顺序上,有何种差异和需要注意的方面呢?下面我们通过实验来进行说明. 实验环境说明 准备数据表和实验环境 ...

  7. pandas使用date_range函数按照指定的频率(freq)和指定的个数(periods)生成dataframe的时间格式数据列、基于dataframe的日期数据列生成日期索引

    pandas使用date_range函数按照指定的频率(freq)和指定的个数(periods)生成dataframe的时间格式数据列.基于dataframe的日期数据列生成日期索引(dates in ...

  8. Pandas把dataframe的索引、复合索引变换为数据列:包含单索引到单列(重命名)、复合索引到多数据列、复合索引的其中一个水平变换为数据列、

    Pandas把dataframe的索引.复合索引变换为数据列:包含单索引到单列(重命名).复合索引到多数据列.复合索引的其中一个水平变换为数据列 目录

  9. pandas把dataframe的数据列转化为索引列实战:单列转化为索引、多列转化为复合索引

    pandas把dataframe的数据列转化为索引列实战:单列转化为索引.多列转化为复合索引 目录

最新文章

  1. 微信公众号开发用书php,php微信公众号开发(3)php实现简单微信文本通讯
  2. 洛谷 - P1829 - Crash的数字表格 - 莫比乌斯反演
  3. OpenCASCADE:读取和写入 STEP
  4. WdOS源码编译安装MySQL 5.5.25a
  5. 如果给你机会,阿里巴巴的中层职位和马云的专属司机,你怎么选?
  6. 好玩的Scratch
  7. julia在mac环境变量_在Julia中找到值/变量的类型
  8. python r转义_Python快速入门系列之二:还学不会我直播跪搓衣板
  9. 作为程序员的你,除了撸代码,还能干什么?
  10. 如何不做老板手中一次性筷子?
  11. html5手机的注册页面,H5页面结合vue实现登录注册组件
  12. Verilog基础语法(一)
  13. Matlab绘图模板
  14. HDU 2243(AC自动机+矩阵快速幂)
  15. helm charts 入门指南
  16. 计算机月考分析报告,月考分析总结500字(通用7篇)
  17. 微信小程序懒加载测试
  18. 最新系统MacOs13 Ventura(M1/M2芯片) + Parallels Desktop 18(PD18史上最强虚拟机)永久使用攻略
  19. springboot毕业设计 基于springboot多用户商城(淘宝京东)系统毕业设计设计与实现参考
  20. 1药网母公司路演PPT曝光:发行区间14到16美元 中旬上市

热门文章

  1. 4.11 一维到三维推广-深度学习第四课《卷积神经网络》-Stanford吴恩达教授
  2. STM32 电机教程 1 - 用ST Motor Profiler 测量无刷电机参数
  3. 【Android工具】好软件推荐,安卓手机免费好用的SSH客户端——JuiceSSH
  4. 云端计算模型的MATLAB仿真与分析
  5. uboot中添加新型号步骤以及编译方法
  6. java框架之Spring 核心框架体系结构
  7. python-3.x-基本数据类型
  8. 非聚集索引和聚集索引
  9. 网络设备主备配置系列3:华为防火墙(路由模式)
  10. 使用Arduino模块实施无线信号的重放攻击