对于InnoDB索引的一些见解

索引,大家非常熟悉也是用的非常多的东西,在数据库中正确有效使用索引能使查询效率大大提高。那么索引到底是怎么工作的呢,今天从innodb存储引擎说说索引的工作原理。

索引

索引,有时候也叫key,是存储引擎用于快速找到记录的一种数据结构。

索引对于良好的性能非常关键。尤其当表的数据越来越大时,索引对性能的影响愈发重要。索引优化应该是查询性能优化最有效的手段了,索引能够轻易将查询性能提高几个数量级。
总的来说,索引有如下优点:

  1. 索引大大减少服务器需要扫描的数据量。

  2. 索引可以帮助服务器避免排序和临时表。

  3. 索引可以将随机I/O变为顺序I/O。

B-Tree 索引

当讨论到索引的时候,如果没有特别指明类型,多半说的是B-Tree索引,本篇文章主要讲的也是这种类型。

B-Tree意味着所有的值都是按顺序存储的,并且每一个叶子页到根的距离相同,并且保存了指向数据的指针(依赖于不同的存储引擎)。

MyISAM的叶子节点存储的是指向数据的指针(行号);而InnoDB则存储的是数据所有的列值。

下面这幅图展示了B-Tree索引的抽象表示,大致反映了InnoDB索引是如何工作的。MyISAM使用的结构有所不同,但基本思想是类似的。

B-Tree结构

B-Tree索引能够加快访问数据的速度,因为存储引擎不再需要进行全表扫描来获取需要的数据,取而代之的是从索引节点开始进行搜索。
根节点的槽中存放了指向子节点的指针,存储引擎根据这些指针向下层查找。通过比较节点页的值和要查找的值可以找到合适的指针进入下层子节点。

聚簇索引

聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。具体的细节依赖于其实现方式,但InnoDB的聚簇索引实际上在同一结构中保存了B-Tree索引和数据行(MyISAM则是保留了数据的指针)。

当表有聚簇索引时,它的数据行实际上存放在索引的叶子页中。
因为无法同时把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引,不过,覆盖索引可以模拟多个聚簇索引的情况,后面会有介绍。

我们知道,MyISAM的数据和索引文件是分开的,分别是.myd和.myi;InnoDB则都是存储在.ibd中的。这种区别决定了InnoDB索引就是表,换一句话说,在InnoDB里,所有的东西都是索引。

来看看聚簇索引的结构:

聚簇索引结构

InnoDB将通过主键聚集数据,也就是说上图中被索引的列就是主键列。

聚集的数据有一些重要的优点:

  1. 可以把相关数据保存在一起,减少磁盘的I/O。

  2. 数据访问更快。聚簇索引将索引和数据保存到同一个B-Tree中,因此从聚簇索引中获取数据通常比在非聚簇索引中查找更快。

  3. 使用覆盖索引扫描的查询可以直接使用叶节点中的主键值。

从InnoDB角度上说,聚簇索引就是主键索引。
我们现在了解了聚簇索引的概念,但都是比较抽象的东西,现在根据一个具体的例子来加深了解。

首先我们建立一个数据表:

CREATE TABLE `test_user` (
  `id` int(9) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '用户名',
  `password` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '密码',
  `email` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '邮箱',
  `age` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '年龄',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

此用户表非常简单,除了自增主键id,我们没有建立任何索引,现在执行一个sql:

EXPLAIN SELECT id FROM test_user\G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test_user
         type: index
possible_keys: NULL
          key: PRIMARY
      key_len: 4
          ref: NULL
         rows: 9588
        Extra: Using index
1 row in set (0.00 sec)

这条sql的select字段只包含主键id,也就是一个用到的索引查询。

通过mysql的查询计划,我们看到type字段用的是index,说明是扫描数据行的类型是索引扫描。。
Extra字段显示的是Using index,这说明mysql是通过索引获取的数据,避免访问了数据行,效率不错。

也就是说,此sql扫描表的方式是按照聚簇索引的结构从上至下扫描的。

下面再来看一个sql:

EXPLAIN SELECT name,password FROM test_user\G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test_user
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 9588
        Extra: 
1 row in set (0.00 sec)

这条sql查询了一个没有索引列的字段,type字段为ALL,说明扫描方式是全表扫描,效率不高。

对于InnoDB,这种方式就是扫描聚簇索引的所有叶子页(因为数据列全部都在叶子页)。

其实第一种查询方式就是覆盖索引查询,只不过是最简单的一种。

在通常业务中,不可能存在只需查询主键id的场景,就像第二条sql一样,我们很有可能需要查询多个字段,还会有where条件,此时我们会在这些字段上建立索引,那么如果才能建立最优化的索引呢?

覆盖索引

通常大家都会根据查询where条件来创建合适的索引,不过这只是索引优化的一个方面。
优秀设计的索引应该考虑到整个查询,而不单单是where条件部分。

索引确实是一种查找数据的高效方式,但是MySQL也可以使用索引来直接获取列的数据,这样就不再需要读取数据行。

如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为“覆盖索引”

如果在InnoDB中建立一个二级索引(非聚簇索引),InnoDB会在二级索引叶子结点保存主键值,而不是指向行的物理指针。

让我们来看看InnoDB中的二级索引是怎样分布的:

InnoDB二级索引结构

很明显,InnoDB二级索引的存储情况和MyISAM很不一样(MyISAM叶子结点保存的是数据行指针)。

下张图展示了这两种存储引擎二级索引结构:

聚簇与非聚簇索引结构

现在我们在刚刚建立的表上建立一个二级索引,看看InnoDB是如何工作的。

我们在namepassword字段上建立一个组合索引:

create index index1 on test_user(name,password);

下面用实例从几种情况分析InnoDB的查询计划。

查询全表

执行上面第2条sql查询全表的sql:

EXPLAIN SELECT name,password FROM test_user\G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test_user
         type: index
possible_keys: NULL
          key: index1
      key_len: 919
          ref: NULL
         rows: 9588
        Extra: Using index
1 row in set (0.00 sec)

此时我们看到,Extra字段显示Using index,证明我们这条查询使用到了覆盖索引。

因为在这张表的二级索引中的key节点保存了namepassword的值,InnoDB直接就可以从索引中读出数据,而不需要通过主键值回到聚簇索引查找主键对应的叶子结点的值

这种查询效率往往是非常高的,如果我们select的字段里有不包含建立的索引,查询计划会是什么情况呢?执行如下sql:

EXPLAIN SELECT * FROM test_user\G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test_user
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 9588
        Extra: 
1 row in set (0.00 sec)

很显然,查询没有用到任何索引,这也是为什么很多时候不要用select *查询的原因。

带where查询

让我们再来看看带上where语句会是什么样子,下面从多种情况分析:

EXPLAIN SELECT id FROM test_user where name = 'abc'\G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test_user
         type: ref
possible_keys: index1
          key: index1
      key_len: 152
          ref: const
         rows: 1
        Extra: Using where; Using index
1 row in set (0.00 sec)

说明:type为ref,说明使用索引扫描;Extra为Using where; Using index,表明查找使用了索引并且要查找的数据都在索引列中查到,不需要回表查询;rows为1,说明只扫描了一行就把需要的数据查出来,效率很高。
此查询就是用到了覆盖索引,是一个很优秀的查询。

EXPLAIN SELECT * FROM test_user where name = 'abc'\G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test_user
         type: ref
possible_keys: index1
          key: index1
      key_len: 152
          ref: const
         rows: 1
        Extra: Using index condition
1 row in set (0.00 sec)

说明:type为ref,说明使用索引扫描;Extra为Using index condition,表明查找使用了索引,但是需要回表查询;rows为1,说明只扫描了一行数据。
此查询虽然用到了索引,但是相对于上条sql来说,有回表的动作,效率相对来说要低一点。

EXPLAIN SELECT * FROM test_user where email = 'abc'\G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test_user
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 9588
        Extra: Using where
1 row in set (0.00 sec)

说明:type为all,说明为全表扫描;Extra为Using index,表明用全表扫描后用到了where条件过滤;rows为9588,说明几乎扫描了全表才把数据找到。
此查询效率非常差,没有命中索引,如果数据量很大,需要花费很多时间查询,在实际应用中应该尽量避免。

带排序查询

我们的表结构中有一个年龄字段,我们想要对用户表里的年龄进行排序,先来看看不加索引的情况:

explain select id from test_user order by age\G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test_user
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 9588
        Extra: Using filesort
1 row in set (0.00 sec)

可以看到,type为ALL,ExtraUsing filesort,说明此查询进行了全表扫描,并且使用了效率低下的文件排序。

下面在age列加上索引:

create index index2 on test_user(age);

再次执行上面的sql:

explain select id from test_user order by age\G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test_user
         type: index
possible_keys: NULL
          key: index2
      key_len: 1
          ref: NULL
         rows: 9588
        Extra: Using index
1 row in set (0.00 sec)

此查询中,type为index,ExtraUsing index,说明MySQL使用了索引扫描来做排序。
此sql的select字段为主键值,就存储在二级索引的叶子结点上,没有回表操作,这也是一种覆盖索引的情况

扫描索引本身是很快的,因为只需要从一条索引记录移动到紧接着的下一条记录。但如果索引不能覆盖查询所需的全部列,那就不得不每扫描一条索引记录就回表查询一次对应的行。

对于这种没有覆盖的情况,就算有索引,MySQL也可能会直接扫描全表,因为这样的效率也许更高,让我们来证明一下。

explain select * from test_user order by age\G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test_user
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 9588
        Extra: Using filesort
1 row in set (0.00 sec)

我把select的字段从id改为*了,这样需要查询出所有列。
可以看到,MySQL果然用了全表扫描文件排序,与其每次从索引(二级索引)回表获取数据列造成大量随机I/O,还不如直接扫描数据表(聚簇索引)。

通过以上几个sql可以看到,覆盖索引的效率是非常高的,在实际项目中,我们需要多多运用,有时候能达到事半功倍的效果。

总结

索引优化应该是对查询性能优化最有效的手段了,索引能够轻易查询性能提高几个数量级,而InnoDB中的覆盖索引又是非常高的方式,所以这篇文章大概说明了一下这个概念。
举得例子都比较简单,如果需要深入了解,这里推荐一本书:《高性能MySQL》,这本书对理解MySQL的工作原理有非常大的帮助,希望大家有时间可以阅读。

本篇文章结束!

本文章系博主原创,如需转载,请注明出处。

你可能感兴趣的:(对于InnoDB索引的一些见解)