1. 索引介绍

索引 : 是排序的快速查找的特殊数据结构 , 定义作为查找条件的字段上 , 又称为键key , 索引通过存储引擎实现

索引的存储原理大致可以概括为一句话 : 以空间换时间

一般来说索引本身也很大 , 不可能全部存储在内存中 , 因此索引往往是存储在磁盘上的文件中的(可能存储在单独的索引文件中 , 也可能和数据一起存储在数据文件中)。

数据库在未添加索引进行查询的时候默认是进行全文搜索 , 也就是说有多少数据就进行多少次查询 , 然后找到相应的数据就把它们放到结果集中 , 直到全文扫描完毕。

建立索引时 , 需要确保该索引应用在SQL查询语句的条件(一般作为WHERE子句的条件)。索引也是一张表 , 该表保存了主键与索引字段 , 并指向实体表的记录。然而 , 过多的使用索引将会造成滥用。虽然索引大大提高了查询速度 , 同时却会降低更新表的速度, 如对表进行INSERTUPDATEDELETE。因为更新表时 , MySQL不仅要保存数据 , 还要保存一下索引文件。建立索引会占用磁盘空间的索引文件。

MySQL查询优化器会优化and连接 , 将组合索引列规则排号。在MySQL建立联合索引时会遵守最左前缀匹配原则,即最左优先(查询条件精确匹配索引的左边连续一列或几列 , 则构建对应列的组合索引树) , 在检索数据时也从联合索引的最左边开始匹配。

MySQL 5.6以前的版本 , 只有MyISAM存储引擎支持全文索引 ; MySQL 5.6及以后的版本 , MyISAMInnoDB存储引擎均支持全文索引 ; 只有字段的数据类型为charvarchartext及其系列才可以建全文索引。InnoDB内部并不支持中文、日文等 , 因为这些语言没有分隔符。可以使用插件辅助实现中文、日文等的全文索引。

优点

  • 索引大大减小了服务器需要扫描的数据量 , 从而大大加快数据的检索速度 , 这也是创建索引的最主要的原因。

  • 通过索引列对数据进行排序 , 降低数据的排序成本降低了CPU的消耗。

  • 被索引的列会自动进行排序 , 包括单例索引组合索引 , 只是组合索引的排序需要复杂一些。

  • 索引可以将随机IO变成顺序IO

  • 索引对于InnoDB(对索引支持行级锁)非常重要 , 因为它可以让查询锁更少的元组 , 提高了表访问并发性

  • 关于InnoDB、索引和锁 : InnoDB在二级索引上使用共享锁(读锁) , 但访问主键索引需要排他锁(写锁)

  • 通过创建唯一性索引 , 可以保证数据库表中每一行数据的唯一性。

  • 可以加速表和表之间的连接 , 特别是在实现数据的参考完整性方面特别有意义。

  • 在使用分组和排序子句进行数据检索时 , 同样可以显著减少查询中分组和排序的时间。

  • 通过使用索引 , 可以在查询的过程中 , 使用优化隐藏器 , 提高系统的性能。

缺点

  • 创建索引和维护索引要耗费时间 , 这种时间随着数据量的增加而增加

  • 索引需要占物理空间 ,除了数据表占用数据空间之外 , 每一个索引还要占用一定的物理空间 , 如果需要建立聚簇索引 , 那么需要占用的空间会更大

  • 对表中的数据进行增、删、改的时候 , 索引也要动态的维护 , 这就降低了整数的维护速度

  • 如果某个数据列包含许多重复的内容 , 为它建立索引就没有太大的实际效果。

  • 对于非常小的表 , 大部分情况下简单的全表扫描更高效;

2. 索引分类

索引类型 :

  • B+ TREEHASHR TREEFULL TEXT

  • 聚簇(集)索引、非聚簇索引 : 数据和索引是否存储在一起

  • 主键索引、二级(辅助)索引

  • 稠密索引、稀疏索引 : 是否索引了每一个数据项

  • 简单索引、组合索引

  • 左前缀索引 : 取前面的字符做索引

  • 覆盖索引 : 从索引中即可取出要查询的数据 , 性能高

MySQL索引是对数据库表中一列或多列的值进行排序的一种结构 , 可以大大提高MySQL的检索速度。索引分为单列索引组合索引 , 其中单列索引只包含单个列 , 一个表可以有多个单列索引, 但这不是组合索引。组合索引是一个索引包含多个列。

MySQL的索引有两种分类方式 : 逻辑分类物理分类。逻辑分类是根据索引的数据结构分类 , 如B-TREEHASH等。物理分类是根据索引的存储方式分类 , 如聚簇索引、非聚簇索引等。聚簇索引是将数据存储与索引一起,它的叶子节点存储的是整行数据 , 因此它只能有一个 , 而且必须是主键索引。非聚簇索引是将数据存储与索引分开 , 索引的叶子节点存储的是指向数据行的指针, 可以有多个。

  • 逻辑分类 : 有多种逻辑划分的方式 , 比如按功能划分 , 按组成索引的列数划分等

    • 按功能划分

      • 主键索引(primary key) : 一张表只能有一个主键索引 , 不允许重复、不允许为NULL ;

        ALTER TABLE TableName ADD PRIMARY KEY(column_list);
      • 唯一索引 : 数据列不允许重复 , 允许为NULL值 , 一张表可有多个唯一索引 , 索引列的值必须唯一 ,但允许有空值。如果是组合索引 , 则列值的组合必须唯一。

        CREATE UNIQUE INDEX IndexName ON `TableName`(`字段名`(length));
        # 或者
        ALTER TABLE TableName ADD UNIQUE (column_list); 
      • 普通索引 : 一张表可以创建多个普通索引 , 一个普通索引可以包含多个字段 , 允许数据重复 , 允许 NULL值插入 ;

        CREATE INDEX IndexName ON `TableName`(`字段名`(length));
        # 或者
        ALTER TABLE TableName ADD INDEX IndexName(`字段名`(length));
      • 全文索引(fulltext) : 它查找的是文本中的关键词 , 主要用于全文检索。

    • 按列个数分类

      • 单列索引 : 一个索引只包含一个列 , 一个表可以有多个单例索引。

      • 组合索引 : 一个组合索引包含两个或两个以上的列。查询的时候遵循mysql组合索引的"最左前缀"原则 , 即使用where时条件要按照建立索引的时候字段的排列方式放置索引才会生效。

    • 其他类型

      • 空间索引 : MySQL5.7之后的版本支持了空间索引 , 而且支持OpenGIS几何数据模型 , MySQL在空间索引这方年遵循OpenGIS几何数据模型规则。

      • 前缀索引 : 在文本类型为charvarchartext类列上创建索引时 , 可以指定索引列的长度 , 但是数值类型不能指定。

  • 物理分类 :分为聚簇索引和非聚簇索引 ( 有时也称辅助索引或二级索引 )

    聚簇是为了提高某个属性(或属性组)的查询速度 , 把这个或这些属性(称为聚簇码)上具有相同值的元组集中存放在连续的物理块。

    • 聚簇索引 : 聚簇索引(clustered index)不是单独的一种索引类型 , 而是一种数据存储方式。这种存储方式是依靠B+树来实现的 , 根据表的主键构造一棵B+树B+树叶子节点存放的都是表的行记录数据时 ,方可称该主键索引为聚簇索引。聚簇索引也可理解为将数据存储与索引放到了一块 , 找到索引也就找到了数据。

    • 非聚簇索引 : 数据和索引是分开的 , B+树叶子节点存放的不是数据表的行记录。

    虽然InnoDBMyISAM存储引擎都默认使用B+树结构存储索引 , 但是只有InnoDB的主键索引才是聚簇索引 , InnoDB中的辅助索引以及MyISAM使用的都是非聚簇索引。每张表最多只能拥有一个聚簇索引。

  • 数据结构分类 :B-Tree索引、B+tree索引、Hash索引、Full-text索引

2.1 Hash索引

Hash索引 : 基于哈希表实现 , 只有精确匹配索引中的所有列的查询才有效 , 索引自身只存储索引列对 应的哈希值和数据指针 , 索引结构紧凑 , 查询性能好

Memory存储引擎支持显式hash索引 , InnoDBMyISAM存储引擎不支持

适用场景 : 只支持等值比较查询 , 包括=,<=>,IN()

不适合使用hash索引的场景

  • 不适用于顺序查询 : 索引存储顺序的不是值的顺序

  • 不支持模糊匹配 不支持范围查询

  • 不支持部分索引列匹配查找 : 如A , B列索引 , 只查询A列索引无效

2.2 空间数据索引

R-Tree(Geospatial indexing ) MyISAM支持地理空间索引 , 可使用任意维度组合查询 , 使用特有的函数访问 ,常用于做地理数据存储 , 使用不多InnoDBMySQL5.7之后也开始支持。

R树在数据库等领域做出的功绩是非常显著的。它很好的解决了在高维空间搜索等问题。举个R树在现实领域中能够解决的例子吧 : 查找20英里以内所有的餐厅。如果没有R树你会怎么解决?一般情况下我们会把餐厅的坐标(x,y)分为两个字段存放在数据库中 , 一个字段记录经度 , 另一个字段记录纬度。这样的话我们就需要遍历所有的餐厅获取其位置信息 , 然后计算是否满足要求。如果一个地区有100家餐厅的话 , 我们就要进行100次位置计算操作了 , 如果应用到谷歌地图这种超大数据库中 , 我想这种方法肯定不可行吧。

R树就很好的解决了这种高维空间搜索问题。它把B树的思想很好的扩展到了多维空间 , 采用了B树分割空间的思想 , 并在添加、删除操作时采用合并、分解结点的方法 , 保证树的平衡性。因此 , R树就是一棵用来存储高维数据的平衡树。

8394323_130749300836fQ

2.3 全文索引

全文索引(FULLTEXT) 在文本中查找关键词 , 而不是直接比较索引中的值 , 类似搜索引擎InnoDBMySQL 5.6之后也开始支持。

在当我们进行模糊查询的时候, 一般都是使用 like 关键字来实现 , 在数据量小的情况下效率确实可以。有的人会说了 ,数据量大的时候我加索引啊。确实可以加索引 , 而且 like name% 是可以使用到索引的, 但是当你要 like %name% 这样使用就会导致索引无效。在某些情况下就造成了局限性。

在数据量大的情况下 , 全文索引的效率会比like高 , 在MySql5.6版本以前 , 只有MyISAM索引支持全文索引 , 在这之后Innodb也支持。

2.3.1 全文索引创建和使用

//创建全文索引
CREATE FULLTEXT INDEX <index_name> on tableName(字段名)  
​
ALTER TABLE tableName ADD FULLTEXT[index_name](字段名);
​
CREATE TABLE tableName([....],FULLTEXT KEY[index_name](字段名))

和常用的like不同 , 全文索引有自己的格式 , 使用matchagainst关键字 , 如下 :

select * from user where match(name) against('aaa');

2.3.2 全文索引实战

创建一张用户表people , 主键ID和用户名name

//插入四条记录
insert into people (`name`) VALUES ('a');
insert into people (`name`) VALUES ('aa');
insert into people (`name`) VALUES ('aaa');
insert into people (`name`) VALUES ('aaaa');
​
​
//在字段name上建立全文索引
create fulltext index name_index_ft on people(name);
​
//开始查询
// 分别执行下面这4条SQL
select * from people where match(name) against('a');  // 没记录
select * from people where match(name) against('aa'); // 没记录
select * from people where match(name) against('aaa'); // 1条记录
select * from people where match(name) against('aaaa'); // 1条记录

很多人可能会问 , 为什么我数据库有name = a name = aa的记录 , 为什么会找不到 , 而且全文索引不是跟like一样可以模糊查询的吗??当我执行 select * from people where match(name) against('aaa'); 应该有二条记录啊 , 为什么只有一条??,接下来就要去了解全文的原理了

2.3.3 全文索引原理

MySql客户端执行 show variables like '%ft%'; 查看全文索引的信息

mysql-index-fulltext

为什么在查询 a 和 aa的时候会没有记录

先解释下其中几个参数

  • ft_min_word_len : 针对MyISAM引擎的 , 也就是在你创建的全文索引的字段的内容最小长度

  • ft_max_word_len : 针对MyISAM引擎的 , 也就是在你创建的全文索引的字段的内容最大长度 也就是说 , 只有在内容长度为4 ~ 84(图中数值)的时候 , 你的全文索引才会有效,但是我们使用的Innodb存储引擎 , 所以你要看的是这二项。

  • innodb_ft_min_token_size : 针对Innodb引擎的 , 也就是在你创建的全文索引的字段的内容最小长度

  • innodb_ft_max_token_size : 针对Innodb引擎的 , 也就是在你创建的全文索引的字段的内容最大长度

也就是说你的全文索引字段的内容长度必须在3 ~ 84之间才会有效 , 现在能明白为什么在查 aaa的时候找不到记录 , 但是在查aaa 的时候就会有记录了吧 , 因为aaa长度刚好等于3

为什么在查询aaa的时候 , name = aaaa的记录找不到

在全文索引底层是有一个切词的概念的 , 比如 祝中国越来越强大 全文索引按照一个规则切词 , 有可能会被切成 中国越来越强大。那么切词的依据是什么呢? 全文索引又是怎么切词的呢??

mysql-index-fulltext

在这里有个很重要的参数 : ft_boolean_syntax , 就是第一项 , 改变IN BOOLEAN MODE的查询字符 , 不用重新启动MySQL也不用重建索引。 后面有很多内容 , 全文索引就是按照这些来切词的。现在表的记录如下:

arzuq-e8emd

将记录更新如下:

a7qv9-mfaul

现在我们再来执行
select * from people where match(name) against('aaa');

a3j6p-youev

现在就能把所有的记录都找出来了吧,但是细心的你会发现 , id = 6的这条记录还是没找出来 , 这是为什么??

全文索引为什么默认不支持模糊查询

为什么是默认呢 , 因为至少到现在我们发现全文索引都是 等值查询 的 , 这是因为全文索引默认支持的就是等值查询 , 如果要支持模糊查询需要改写SQL如下 :

select * from people where match(name) against('aaa*' in boolean mode);

adjzu-sred3

现在就可以把所有符合的数据都查出来了 , *表示一个或多个字符

2.4 聚簇索引, 非聚簇索引 , 主键和二级索引

  • 聚簇索引 :

    • 聚簇索引就是按照每张表的主键构造一颗B+树 , 同时叶子节点中存放的就是整张表的行记录数据 , 也将聚簇索引的叶子节点称为数据页。这个特性决定了索引组织表中数据也是索引的一部分 , 每张表只能拥有一个聚簇索引

    • InnoDB通过主键聚集数据 , 如果没有定义主键 , InnoDB会选择非空的唯一索引代替。如果没有这样的索引 , InnoDB会隐式定义一个主键来作为聚簇索引。

    • 将数据存储和索引放到了一块 , 找到了索引也就找到了数据

    • 一般情况下主键会默认创建聚簇索引。

  • 辅助索引(非聚簇索引)

    • 聚集索引之上创建的索引叫做辅助索引

    • 辅助索引访问数据总是需要二次查找第一次找到主键值 , 第二次根据主键值找到行数据。

      • 辅助索引叶子节点存储的不再是行的物理位置 , 而是主键值。

      • 通过辅助索引首先找到的是主键值 , 再通过主键值找到数据行的数据页 , 再通过数据页中的Page Directory找到数据行。

    • 辅助索引的存储不影响数据在聚簇索引中的组织 , 所以一张表可以由多个辅助索引。在innodb中有时也称辅助索引为二级索引。

    • 将数据存储于索引分开结构 , 索引结构的叶子节点指向了数据的对应行

    • MyISAM通过key_buffer把索引先缓存到了内存中 , 当需要访问数据时(通过索引访问数据) , 在内存中直接查找索引 , 然后通过索引找到磁盘相应数据。这也就是为什么索引不在key buffer命中时 , 速度慢的原因。

聚集索引⼀个表只能有⼀个 , 而非聚集索引⼀个表可以存在多个。聚集索引存储记录是物理上连续存在 ,而非聚集索引是逻辑上的连续 , 物理存储并不连续。

澄清一个概念 :

  • InnoDB中 , 在聚集索引上创建的索引叫做辅助索引

  • 辅助索引访问数据总是需要二次查找 , 非聚簇索引都是辅助索引 , 像复合索引、前缀索引、唯一索引、辅助引擎叶子节点存储的不再是行的物理位置 , 而是主键值。

12eb99478e80b3d54deaa649ab74c5c8

75c476fe91ab1391766cfafbd89d9d89

聚簇索引默认是主键 , 如果表中没有定义主键 , InnoDB会选择一个唯一的非空索引代替。如果没有这样的索引 , InnoDB隐式定义一个主键来作为聚簇索引。InnoDB只聚集在同一个页面中的记录。包含相邻键值的页面可能相距甚远。如果你已经设置了主键为聚簇索引 , 必须先删除主键 , 然后添加我们想要的聚簇索引 , 最后恢复设置主键即可

此时其他索引只能被定义为非聚簇索引。这个是最大的误区。有的主键还是无意义的自动增量字段 , 那样的话Clustered index对效率的帮助 , 完全被浪费了。

刚才说到了 , 聚簇索引性能最好而且具有唯一性 , 所以非常珍贵 , 必须慎重设置。一般要根据这个表最常用的SQL查询方式来进行选择 , 某个字段作为聚簇索引 , 或组合聚簇索引 , 这个要看实际情况。

记住我们的最终目的就是在相同结果集情况下 , 尽可能减少逻辑IO

结合图再仔细点看

2q05hsflfa

  1. InnoDB使用的是聚簇索引 , 将主键组织到一棵B+树中 , 而行数据就储存在叶子节点上 , 若使用"where id = 14"这样的条件查找主键 , 则按照B+树的检索算法即可查找到对应的叶节点 , 之后获得行数据

  2. 若对Name列进行条件搜索 , 则需要两个步骤 :

    1. 第一步在辅助索引B+树中检索Name , 到达其叶子节点获取对应的主键。

    2. 第二步使用主键在主索引B+树种再执行一次B+树检索操作 , 最终到达叶子节点即可获取整行数据。(重点在于通过其他键需要建立辅助索引)

MyISM使用的是非聚簇索引 , 非聚簇索引的两棵B+树看上去没什么不同 , 节点的结构完全一致只是存储的内容不同而已 , 主键索引B+树的节点存储了主键 , 辅助键索引B+树存储了辅助键。表数据存储在独立的地方 , 这两颗B+树的叶子节点都使用一个地址指向真正的表数据 , 对于表数据来说 , 这两个键没有任何差别。由于索引树是独立的 , 通过辅助键检索无需访问主键的索引树

聚簇索引的优势

看上去聚簇索引的效率明显要低于非聚簇索引 , 因为每次使用辅助索引检索都要经过两次B+树查找 , 这不是多此一举吗 ? 聚簇索引的优势在哪?

  1. 由于行数据和叶子节点存储在一起 , 同一页中会有多条行数据 , 访问同一数据页不同行记录时 , 已经把页加载到了Buffer中 , 再次访问的时候 , 会在内存中完成访问 , 不必访问磁盘。这样主键和行数据是一起被载入内存的 , 找到叶子节点就可以立刻将行数据返回了, 如果按照主键Id来组织数据 , 获得数据更快。

  2. 辅助索引使用主键作为"指针"而不是使用地址值作为指针的好处是 , 减少了当出现行移动或者数据页分裂时辅助索引的维护工作 , 使用主键值当作指针会让辅助索引占用更多的空间 , 换来的好处是InnoDB在移动行时无须更新辅助索引中的这个"指针"。也就是说行的位置 ( 实现中通过16K的Page来定位 ) 会随着数据库里数据的修改而发生变化 ( 前面的B+树节点分裂以及Page的分裂 ) , 使用聚簇索引就可以保证不管这个主键B+树的节点如何变化 , 辅助索引树都不受影响。

  3. 聚簇索引适合用在排序的场合, 非聚簇索引不适合

  4. 取出一定范围数据的时候 , 使用聚簇索引

  5. 二级索引需要两次索引查找 , 而不是一次才能取到数据 , 因为存储引擎第一次需要通过二级索引找到索引的叶子节点 , 从而找到数据的主键 , 然后在聚簇索引中用主键再次查找索引 , 再找到数据

  6. 可以把相关数据保存在一起。例如实现电子邮箱时 , 可以根据用户 ID 来聚集数据 , 这样只需要从磁盘读取少数的数据页就能获取某个用户的全部邮件。如果没有使用聚簇索引 , 则每封邮件都可能导致一次磁盘 I/O。

聚簇索引的劣势

  1. 维护索引很昂贵 , 特别是插入新行或者主键被更新导至要分页(page split)的时候。建议在大量插入新行后 , 选在负载较低的时间段 , 通过OPTIMIZE TABLE优化表 , 因为必须被移动的行数据可能造成碎片。使用独享表空间可以弱化碎片

  2. 表因为使用UUID(随机ID)作为主键 , 使数据存储稀疏 , 这就会出现聚簇索引有可能有比全表扫面更慢。

    iywj5q0imm

    所以建议使用intauto_increment作为主键

    td2fso5cth

    主键的值是顺序的 , 所以InnoDB把每一条记录都存储在上一条记录的后面。当达到页的最大填充因子时(InnoDB默认的最大填充因子是页大小的15/16, 留出部分空间用于以后修改) , 下一条记录就会写入新的页中。一旦数据按照这种顺序的方式加载 , 主键页就会近似于被顺序的记录填满(二级索引页可能是不一样的)。

  3. 如果主键比较大的话 , 那辅助索引将会变的更大 , 因为辅助索引的叶子存储的是主键值;过长的主键值 , 会导致非叶子节点占用占用更多的物理空间

为什么主键通常建议使用自增id

聚簇索引的数据的物理存放顺序与索引顺序是一致的 , 即 : 只要索引是相邻的 , 那么对应的数据一定也是相邻地存放在磁盘上的。如果主键不是自增id , 那么可以想象它会干些什么 , 不断地调整数据的物理地址、分页 , 当然也有其他一些措施来减少这些操作 , 但却无法彻底避免。但如果是自增的那就简单了 , 它只需要一页一页地写 , 索引结构相对紧凑 , 磁盘碎片少 , 效率也高。

因为MyISAM的主索引并非聚簇索引 , 那么他的数据的物理地址必然是凌乱的 , 拿到这些物理地址 , 按照合适的算法进行I/O读取 , 于是开始不停的寻道不停的旋转聚簇索引则只需一次I/O。 ( 强烈的对比 )

不过 , 如果涉及到大数据量的排序、全表扫描、count之类的操作的话 , 还是MyISAM占优势些 , 因为索引所占空间小 , 这些操作是需要在内存中完成的。

mysql中聚簇索引的设定

聚簇索引默认是主键 , 如果表中没有定义主键 , InnoDB会选择一个唯一的非空索引代替。如果没有这样的索引 , InnoDB隐式定义一个主键来作为聚簇索引。InnoDB只聚集在同一个页面中的记录。包含相邻健值的页面可能相距甚远。

2.5 冗余索引,重复索引

MySQL是允许在相同列上创建多个索引 , 无论你创建的时候是有意的还是无意的。MySQL需要单独维护重复的索引 , 并且优化器在优化查询的时候也需要进行逐个考虑 , 这回影响性能。

重复索引 : 就是指在相同的列上按照相同的顺序创建的相同类型的索引。应该避免这样的创建 , 发现之后也应该立即移除。已经有索引 , 再次建立索引

冗余索引重复索引其实又有一些不同。如果创建了索引(A,B) , 再创建索引(A)那就是冗余索引了 , 因为这只是前一个索引的前缀索引。但是你要是创建的是(B,A)那可就不是冗余索引了。当然不同的索引类型肯定也不会涉及到冗余索引的事情。

还有一种情况 , 将索引(A)扩展为了索引(A , ID) , 其中ID是主键 , 这在innodb中来说 , 主键列已经包含到二级索引中去了 , 所以这也是冗余索引。

大多数情况下 , 我们不应该是创建新索引 , 而是扩展已有的索引。但你话也不能说死 , 有时候出于性能的考虑 , 也会考虑冗余索引 , 因为你扩展索引会导致其变得太大 , 从而影响其他使用该索引的查询的性能。

3 索引结构

3.1 二叉树

二叉查找树(BST , Binary Search Tree) , 也叫二叉排序树。在二叉树的基础上需要满足 :任意节点的左子树上所有节点值不大于根节点的值 , 任意节点的右子树上所有节点值不小于根节点的值。

如果先查二叉树的话 , 由于我们的数据量庞大 , 二叉树的深度会变得非常大 , 我们的索引树会变成参天大树 , 每次查询会导致很多磁盘IO

对于二叉树而言 , 每个节点只能有两个子节点 , 如果是一颗单边二叉树 , 查询某个节点的次数与节点所处的高度相同 , 时间复杂度为O(n) ; 如果是一颗平衡二叉树 , 查找效率高出一半 , 时间复杂度为O(Log2n)

并且二叉树还有另一个坏处 , 二叉树上的每一个节点都是数据节点 , 那么对于一个比较高的数如果要获取最下面的数据遍历的节点数将会很消耗性能。

a1kgf-t5ze2

3.2 红黑树

红黑树 : 当单边的节点大于3时候 , 就会自动调整 , 这样可以解决二叉树的弊端 ;

平衡二叉查找树 : 简称平衡二叉树。由前苏联的数学家 Adelse-VelskilLandis在 1962 年提出的高度平衡的二叉树 , 根据科学家的英文名也称为 AVL 树。它具有如下几个性质 :

  1. 可以是空树。

  2. 假如不是空树 , 任何一个结点的左子树与右子树都是平衡二叉树 , 并且高度之差的绝对值不超过 1。

平衡之意 , 如天平 , 即两边的分量大约相同。

平衡因子 : 某节点的左子树与右子树的高度(深度)差即为该节点的平衡因子(BF,Balance Factor) , ,平衡二叉树中不存在平衡因子大于 1 的节点。

当然 , 红黑树也有弊端的 , 当数据量特别大的时候 , 红黑树的高度特别大 ; 假如有500W条数据 ,则红黑树高度为 23 , 若我们要查找的刚好是红黑树的叶子节点 , 则需要查找23次才可以 , 即要发生23次的磁盘I/O操作 , 性能就太差了 ;

红黑树首先肯定是一个排序二叉树 , 它在每个节点上增加了一个存储位来表示节点的颜色 , 可以是REDBLACK

  • 性质1 : 每个节点要么是红色 , 要么是黑色。

  • 性质2 : 根节点永远是黑色的。

  • 性质3 : 所有的叶子节点都是空节点(即null) , 并且是黑色的。

  • 性质4 : 每个红色节点的两个子节点都是黑色。(从每个叶子到根的路径上不会有两个连续的红色节点。)

  • 性质5 : 从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点。

hongheitree-first

3.3 B-Tree索引

B树是一种自平衡树数据结构 , 它维护有序数据并允许以O(lgn)时间进行搜索 , 顺序访问、插入和删除。B树是二叉搜索树的一般化 , 因为节点可以有两个以上的子节点 , 并且每个节点可以有多个键值 , 这些属性减少了定位记录时所经历的中间过程 , 加快了存取速度。与其他自平衡二进制搜索树不同 , B树非常适合读取和写入相对较大的数据块(如光盘)的存储系统。它通常用于数据库文件系统

一棵mB树(balanced tree of order m)是一棵平衡的m路搜索树。它或者是空树 , 或者是满足下列性质的树 :

  1. 根结点至少有两个子女;

  2. 根结点的儿子数为[2, M]

  3. 除根结点以外的其他非叶子结点的儿子数为[M/2, M]

  4. 每个结点中的关键字都按照从小到大的顺序排列 , 每个关键字的左子树中的所有关键字都小于它 , 而右子树中的所有关键字都大于它。

  5. j个孩子的非叶子节点恰好有j-1个关键字,关键字按递增次序排序

  6. 每个非根节点所包含的关键字个数 j 满足 : Math.ceil(m/2) <= j <= m-1;

  7. 所有叶子结点都位于同一层 , 或者说根结点到每个叶子结点的长度都相同。B树的叶子节点可以看成一种外部节点, 不包含任何信息。

: Math.ceil(m/2) :表示向上取整 , Math.ceil(5/2)=3

如 : ( M=3 )

1139419-20190618221747255-309293709

B-树的搜索 , 从根结点开始 , 对结点内的关键字(有序)序列进行二分查找 , 如果命中则结束 , 否则进入查询关键字所属范围的儿子结点;重复 , 直到所对应的儿子指针为空 , 或已经是叶子结点 ;

B-树的特性 :

  1. 关键字集合分布在整颗树中 ;

  2. 任何一个关键字出现且只出现在一个结点中 ;

  3. 搜索有可能在非叶子结点结束;

  4. 其搜索性能等价于在关键字全集内做一次二分查找

  5. 自动层次控制;

由于限制了除根结点以外的非叶子结点 , 至少含有M/2个儿子 , 确保了结点的至少利用率 , 其最底搜索性能为 : O(logN)

所以B-树的性能总是等价于二分查找(与M值无关) , 也就没有B树平衡的问题;

由于M/2的限制 , 在插入结点时 , 如果结点已满 , 需要将结点分裂为两个各占M/2的结点 ; 删除结点时 , 需将两个不足M/2的兄弟结点合并 ;

B树数据结构大致如下 :

f1dbeadce2c14cef9210a119d6f195f5

举个简单的例子 , 在B树中查询数据的情况 :

假如我们要查询key等于10对应的数据data , 根据上图我们可知在磁盘中的查询路径是 : 磁盘块1->磁盘块2->磁盘块6

  • 第一次磁盘IO : 将磁盘块1加载到内存中 , 在内存中从头遍历比较 , 10<15 , 走左子树 , 到磁盘中寻址到磁盘块2。

  • 第二次磁盘IO : 将磁盘块2加载到内存中 , 在内存中从头遍历比较 , 10>7 , 走右子树 , 到磁盘中寻址到磁盘块6

  • 第三次磁盘IO : 讲磁盘块6加载到内存中 , 在内存中从头遍历比较 , 10=10 , 找到key=10的位置 , 取出对应的数据data , 如果data存储的是行记录 , 直接取出数据 , 查询结束 ; 如果data存储的是行磁盘地址 , 还需要根据磁盘地址到对应的磁盘中取出数据 , 查询结束。

相比较二叉平衡查找树 , 在整个查找过程中 , 虽然数据的比较次数并没有明显减少 , 但是对于磁盘IO的次数会大大减少。同时由于我们是在内存中进行的数据比较 , 所以比较数据所消耗的时间可以忽略不计。B树的高度一般23层就能满足大部分的应用场景 , 所以使用B树构建索引可以很好的提升查询的效率。

过程如下图所示:

df8512d3bfde44b99c17b9c0d90b1252

看到上面的情况 , 觉得B树已经很理想了 , 但是其中还是存在可以优化的地方 :

  • B树不支持范围查询的快速查找 , 例如 : 仍然根据上图 , 我们想要查询1035之间的数据 , 查找到10之后 , 需要回到根节点重新遍历查找 , 需要从根节点进行多次遍历 , 查询效率有待提高。

  • 如果data存储的是行记录 , 行的大小随着列数的增加 , 所占空间会变大 , 这时一页中可存储的数据量就会减少 , 树相应就会变高 , 磁盘IO次数就会随之增加 , 有待优化。

3.4 B+Tree索引

为了实现动态多层索引 , 通常采用 B-树B+树。但是 , 用于索引的 B-树存在缺陷 , 它的所有中间结点均存储的是数据指针(指向包含键值的磁盘文件块的指针) , 与该键值一起存储在B-树的结点中。这就会导致可以存储在 B-树中的结点目数极大地减少了 , 从而增加 B-树的层数 , 进而增加了记录的搜索时间。

B+Tree是在B-Tree基础上的一种优化 , 使其更适合实现外存储索引结构 , InnoDB存储引擎就是用B+Tree实现其索引结构。

B+树通过仅在树的叶子结点中存储数据指针而消除了上述缺陷。因此 , B+树的叶结点的结构与 B-树的内部结点的结构完全不同。在这里应该注意 , 由于数据指针仅存在于叶子结点中 , 因此叶子结点必须将所有键值及其对应的数据指针存储到磁盘文件块以便访问。此外 , 叶子结点被链接磁盘的某个位置 , 以提供对记录的有序访问。因此 , 叶子结点形成第一级索引 , 而内部结点形成多层索引的其他层。叶子结点的某些关键字key也出现在内部结点中 , 充当控制搜索记录的媒介。

B+树是应文件系统所需而出的一种B-树的变型树。一棵m阶的B+树和m阶的B-树的差异在于:

  1. n棵子树的结点中含有n个关键字 , 每个关键字不保存数据 , 只用来索引 , 所有数据都保存在叶子节点。

  2. 所有的叶子结点中包含了全部关键字的信息 , 及指向含这些关键字记录的指针 , 且叶子结点本身依关键字的大小自小而大顺序链接。

  3. 所有的非终端结点可以看成是索引部分 , 结点中仅含其子树(根结点)中的最大(或最小)关键字。

一棵m阶的B+树主要有这些特点:

  • 每个结点至多有m个子女;

  • 非根节点关键值个数范围 : Math.ceil(m/2) <= j <= m-1

  • 相邻叶子节点是通过指针连起来的 , 并且是关键字大小排序的。

997909-20190728114240297-169990922

可以使用B+Tree索引的查询类型 :

  • 全值匹配 : 精确所有索引列 , 如 : 姓wang , 名xiaochun , 年龄30

  • 匹配最左前缀 : 即只使用索引的第一列 , 如 : 姓wang

  • 匹配列前缀 : 只匹配一列值开头部分 , 如 : 姓以w开头的

  • 匹配范围值 : 如 , 姓ma和姓wang之间

  • 精确匹配某一列并范围匹配另一列 : 如 , 姓wang,名以x开头的, 只访问索引的查询

B+Tree索引的限制 :

  • 如不从最左列开始 , 则无法使用索引 , 如 : 查找名为xiaochun , 或姓为g结尾

  • 不能跳过索引中的列 , 如 : 查找姓wang , 年龄30的 , 只能使用索引第一列

特别提示 : 索引列的顺序和查询语句的写法应相匹配 ,才能更好的利用索引 为优化性能 , 可能需要针对相同的列但顺序不同创建不同的索引来满足不同类型的查询需求。

B+树的大致数据结构:

e85b73936ca14d75b3652cf256cbfa17

B+树的最底层叶子节点包含了所有的索引项。从上图可以看出 , B+树在查找数据的时候 , 由于数据都存放在最底层的叶子节点上 , 所以每次查找都需要检索到叶子节点才能查询到数据。所以在查询数据的情况下每次的磁盘IO次数跟树的高度有直接的关系 ; 但是从另一方面来说 , 由于数据都被存放到了叶子节点 , 所以存放索引的磁盘块 , 所存放的的索引数量会随之增加 , 所以相对于B树来说 , B+树的树高理论情况下是比B树树高要矮的。 但是也存在索引覆盖查询的情况 , 在索引中数据满足了查询语句所需要的全部数据, 此时只需要找到索引即可立刻返回 , 不需要检索到最底层的叶子节点。

等值查询实例 :

假如我们要查询key9对应的数据data , 查询路径为 : 磁盘块1->磁盘块2->磁盘块6

  • 第一次磁盘IO : 将磁盘块1加载到内存中 , 在内存中从头遍历比较 , 9<15 , 走左子树 , 到磁盘寻址定位到磁盘块2

  • 第二次磁盘IO : 将磁盘块2加载到内存中 , 在内存中从头遍历比较 , 7<9<12 , 到磁盘中寻址定位到磁盘块6

  • 第三次磁盘IO : 将磁盘块6加载到内存中 , 在内存中从头遍历比较 , 在第三个索引中找到9 , 取出对应的数据data , 如果data存储的是行记录 , 直接取出data , 查询结束 ; 如果存储的是磁盘地址 , 还需要根据磁盘地址再次寻址定位到指定磁盘取出数据 , 查询终止。

0cf6f390478e4f83a72c82325c329926

范围查询实例 :

假如我们想要查找926之间的数据 , 查找路径为 : 磁盘块1->磁盘块2->磁盘块6->磁盘块7

  • 前三次磁盘IO : 首先查找到键值为9对应的数据(定位到磁盘块6) , 然后缓存大结果集中。这一步和前面等值查询流程一样 , 发生了三次磁盘IO

  • 继续查询 , 查找到节点15之后 , 底层的所有叶子节点是一个有序列表 , 我们从磁盘块6中的键值9开始向后遍历筛选出所有符合条件的数据。

  • 第四次磁盘IO : 根据磁盘块6的后继指针到磁盘中寻址定位到磁盘块7 , 将磁盘块7加载到内存中 , 在内存中从头遍历比较, 9<25<26 , 9<26<=26 , 将数据data缓存到结果集中。

  • 逐渐具备唯一性(后面不会再有<=26的数据) , 不需要再向后查找 , 查询结束 , 将结果集返回给用户。

由上述实例可知 : B+树可以保证等值和范围查询的快速查找 , MySQL的索引采用的就是B+树的结构。

4 索引优化

  • 独立地使用列 : 尽量避免其参与运算 , 独立的列指索引列不能是表达式的一部分 , 也不能是函数的参数 , 在where条件中 , 始终将索引列单独放在比较符号的一侧 , 尽量不要在列上进行运算(函数操作和表达式操作)

  • 左前缀索引 : 构建指定索引字段的左侧的字符数 , 要通过索引选择性( 不重复的索引值和数据表的记录总数的比值 )来评估 , 尽量使用短索引 , 如果可以 , 应该制定一个前缀长度

  • 多列索引 : AND操作时更适合使用多列索引 , 而非为每个列创建单独的索引

  • 选择合适的索引列顺序 : 无排序和分组时 ,将选择性最高放左侧

  • 只要列中含有NULL值 , 就最好不要在此列设置索引 , 复合索引如果有NULL值 , 此列在使用时也 不会使用索引

  • 对于经常在where子句使用的列 , 最好设置索引

  • 对于有多个列where或者order by子句 , 应该建立复合索引

  • 对于like语句 , 以%或者_开头的不会使用索引 , 以%结尾会使用索引

  • 尽量不要使用not in<>操作, 虽然可能使用索引 , 但性能不高

  • 不要使用RLIKE正则表达式会导致索引失效

  • 查询时 , 能不要*就不用* ,尽量写全字段名 , 比如:select id,name,age from students;

  • 大部分情况连接效率远大于子查询

  • 在有大量记录的表分页时使用limit

  • 对于经常使用的查询 , 可以开启查询缓存

  • 多使用explainprofile分析查询语句

  • 查看慢查询日志 , 找出执行时间长的sql语句优化

5 管理索引

5.1 基础用法

  • 创建索引 :

    • 创建数据库时创建索引

      CREATE TABLE table_name[col_name data type] [unique|fulltext][index|key][index_name](col_name[length])[asc|desc]
      
      create table 表名(
      	字段名1  字段类型1,
      	字段名2  字段类型2,
      	.....,
      	spatial [index|key] [索引名] [索引类型] (字段名[(长度)][asc | desc]) 
      	);

      1568723-20190807173450973-1964194997

    • 直接创建索引

      CREATE [UNIQUE] INDEX index_name ON tbl_name (index_col_name[(length)],...);
      
      create [unique|fulltext|spatial] index 索引名称  [索引的类型]
              on  表名(字段名1[(长度)][asc|desc],字段名2[(长度)][asc|desc]....);

      1568723-20190807173520255-378663767

      • UNIQUE : 可选参数 , 表示索引为唯一索引。

      • FULLTEXT : 可选参数 , 表示索引为全文索引。

      • SPATIAL : 可选参数 , 表示索引为空间索引。

      • INDEX | KEY : 用于指定字段为索引的 , 用户在选择时 , 只需要二选一即可。

      • [索引名] : 可选参数 , 其作用是给创建的索引取新名称。(起到方便使用的目的)

      • 被选定的字段名 : 必选参数 , 被用作索引的对应的字段名称 , 该字段必须被预先定义。

      • 长度 : 可选参数 , 其指索引的长度 , 必须是字符串类型才可以使用。(比如 : 电话号码)

      • [ASC | DESC] : 可选参数 , ASC表示升序排列 , DESC表示降序排列。

    • 通过修改表结构创建索引

      ALTER TABLE tbl_name ADD INDEX index_name(index_col_name[(length)]);
      alter table 表名 add index|key [索引名] [索引类型] (字段名[长度][asc|desc]);
    • 查看创建索引帮助

      MariaDB [(none)]> help create index
      Name: 'CREATE INDEX'
      Description:
      Syntax:
      CREATE [ONLINE|OFFLINE] [UNIQUE|FULLTEXT|SPATIAL] INDEX index_name
          [index_type]
          ON tbl_name (index_col_name,...)
          [index_option] ...
      
      index_col_name:
          col_name [(length)] [ASC | DESC]
      
      index_type:
          USING {BTREE | HASH}
      
      index_option:
          KEY_BLOCK_SIZE [=] value
        | index_type
        | WITH PARSER parser_name
        | COMMENT 'string'
  • 删除索引

    • 使用alter table删除

      alter table 表名 drop index|key 索引名称;
      ALTER TABLE tbl_name DROP INDEX index_name(index_col_name);
    • 使用drop index删除

      drop index 索引名称 on 表名;
      DROP INDEX index_name ON tbl_name;
  • 查看索引

    SHOW INDEXES FROM [db_name.]tbl_name;

    含义

    table

    表名称

    Non_unique

    如果该列索引中不包括重复的值则为0否则为1

    Key_name

    索引名称 , 如果是主键的话则为PRIMARY

    Seq_in_index

    索引中序列的序列号,从1开始。如果是组合索引那么按照字段在建立索引时的顺序排列如('c1','c2','c3')那么分别为1,2,3

    Column_name

    列名称

    collation

    列以什么方式存储在索引中。在MySQL中 , 有值‘A’ ( 升序 ) 或NULL ( 无分序 )

    Cardinality

    索引中唯一值的数目的估计值 , 通过运行ANALYZE TABLE or myisamchk-a来更新 , 数根据被存储为整数的统计数据来计数 , 所以对于小表该值没必要太过于精确 ,而对于大数据量的表来说 , 该值越大当进行联合时 , MySQL使用该索引的机会就越大。

    Sub_part

    索引的长度 , 如果是部分被编入索引则该值表示索引的长度 。如果是整列被编入索引则为null。例如name_Indexschool_Index两个索引 , 比较一下上面两个索引创建时候的区别

    Packed

    指示关键字如何被压缩。如果没有被压缩 , 则为NULL

    Null

    如果该列的值有NULL , 则是YES否则为NO

    Index_type

    所用索引方法(BTREE,FULLTEXT,HASH,RTREE)

    Comment

    关于在其列中没有描述的索引的信息

    Index_comment

    为索引创建时提供了一个注释属性的索引的任何评论

  • 优化表结构

    OPTIMIZE TABLE tb_name;
  • 查看索引统计信息

    SET GLOBAL userstat=1;
    SHOW INDEX_STATISTICS;

范例

#创建表
CREATE TABLE  user  (
   id int(11) NOT NULL AUTO_INCREMENT COMMENT 'id 自增,主键',
   name  varchar(12) NOT NULL DEFAULT '' COMMENT '用户姓名',
   score  int(3) NOT NULL COMMENT '分数',
   class  varchar(12) NOT NULL COMMENT '班级',
   school_id  int(11) NOT NULL COMMENT '学校id',
   PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=192904 DEFAULT CHARSET=utf8;

MariaDB [db]> CREATE TABLE  user  (
    ->    id int(11) NOT NULL AUTO_INCREMENT COMMENT 'id 自增,主键',
    ->    name  varchar(12) NOT NULL DEFAULT '' COMMENT '用户姓名',
    ->    score  int(3) NOT NULL COMMENT '分数',
    ->    class  varchar(12) NOT NULL COMMENT '班级',
    ->    school_id  int(11) NOT NULL COMMENT '学校id',
    ->    PRIMARY KEY (id)
    -> ) ENGINE=InnoDB AUTO_INCREMENT=192904 DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.050 sec)

MariaDB [db]> desc user;
+-----------+-------------+------+-----+---------+----------------+
| Field     | Type        | Null | Key | Default | Extra          |
+-----------+-------------+------+-----+---------+----------------+
| id        | int(11)     | NO   | PRI | NULL    | auto_increment |
| name      | varchar(12) | NO   |     |         |                |
| score     | int(3)      | NO   |     | NULL    |                |
| class     | varchar(12) | NO   |     | NULL    |                |
| school_id | int(11)     | NO   |     | NULL    |                |
+-----------+-------------+------+-----+---------+----------------+
5 rows in set (0.002 sec)

MariaDB [db]> CREATE INDEX name_Index ON `user`(`name`(5));
Query OK, 0 rows affected (0.039 sec)
Records: 0  Duplicates: 0  Warnings: 0

MariaDB [db]> CREATE INDEX school_Index ON `user`(`school_id`);
Query OK, 0 rows affected (0.037 sec)
Records: 0  Duplicates: 0  Warnings: 0

MariaDB [db]> CREATE INDEX class_score_Index ON `user`(`class`, `score`);
Query OK, 0 rows affected (0.066 sec)
Records: 0  Duplicates: 0  Warnings: 0

MariaDB [db]> desc user;
+-----------+-------------+------+-----+---------+----------------+
| Field     | Type        | Null | Key | Default | Extra          |
+-----------+-------------+------+-----+---------+----------------+
| id        | int(11)     | NO   | PRI | NULL    | auto_increment |
| name      | varchar(12) | NO   | MUL |         |                |
| score     | int(3)      | NO   |     | NULL    |                |
| class     | varchar(12) | NO   | MUL | NULL    |                |
| school_id | int(11)     | NO   | MUL | NULL    |                |
+-----------+-------------+------+-----+---------+----------------+
5 rows in set (0.001 sec)

: 查看表结构(desc user)中 ,key列对应三个类型: PRI , 主键约束 ; UNI, 唯一约束 ; MUL , 可以重复

范例: 查看index内容

MariaDB [db]> SHOW INDEX FROM user;
+-------+------------+-------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name          | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-------+------------+-------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| user  |          0 | PRIMARY           |            1 | id          | A         |           0 |     NULL | NULL   |      | BTREE      |         |               |
| user  |          1 | name_Index        |            1 | name        | A         |           0 |        5 | NULL   |      | BTREE      |         |               |
| user  |          1 | class_score_Index |            1 | class       | A         |           0 |     NULL | NULL   |      | BTREE      |         |               |
| user  |          1 | class_score_Index |            2 | score       | A         |           0 |     NULL | NULL   |      | BTREE      |         |               |
| user  |          1 | school_Index      |            1 | school_id   | A         |           0 |     NULL | NULL   |      | BTREE      |         |               |
+-------+------------+-------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
5 rows in set (0.001 sec)

20200325110326964

范例: 查看索引统计信息

MariaDB [db]> SET GLOBAL userstat=1;
Query OK, 0 rows affected (0.000 sec)
MariaDB [hellodb]> SHOW INDEX_STATISTICS;
Empty set (0.000 sec)
MariaDB [hellodb]> select * from students where stuid=10;
+-------+--------------+-----+--------+---------+-----------+
| StuID | Name         | Age | Gender | ClassID | TeacherID |
+-------+--------------+-----+--------+---------+-----------+
|    10 | Yue Lingshan |  19 | F     |       3 |      NULL |
+-------+--------------+-----+--------+---------+-----------+
1 row in set (0.000 sec)
MariaDB [hellodb]> SHOW INDEX_STATISTICS;
+--------------+------------+------------+-----------+
| Table_schema | Table_name | Index_name | Rows_read |
+--------------+------------+------------+-----------+
| hellodb     | students   | PRIMARY   |         1 |
+--------------+------------+------------+-----------+
MariaDB [hellodb]> select * from students where stuid=10;
+-------+--------------+-----+--------+---------+-----------+
| StuID | Name         | Age | Gender | ClassID | TeacherID |
+-------+--------------+-----+--------+---------+-----------+
|    10 | Yue Lingshan |  19 | F     |       3 |      NULL |
+-------+--------------+-----+--------+---------+-----------+
1 row in set (0.000 sec)
MariaDB [hellodb]> SHOW INDEX_STATISTICS;
+--------------+------------+------------+-----------+
| Table_schema | Table_name | Index_name | Rows_read |
+--------------+------------+------------+-----------+
| hellodb     | students   | PRIMARY   |         2 |
+--------------+------------+------------+-----------+
1 row in set (0.000 sec)

5.2 ANALYZE TABLE

  • 语法 : ANALYZE TABLE table;

  • 作用 : 执行ANALYZE TABLE , MySQL会分析指定表的键的值(主键、唯一键、外键等 , 也可以看成就是索引列的值)分布情况 , 并会记录分布情况。

  • 限制 : 执行此语句需要具有SELECTDELETE权限 , 且只对存储引擎为InnoDBMyISAMNDB的表有作用 , 不能用于视图。如下图所示 , actor_info是视图 , 执行失败。

20201104204841256

执行输出结果 :

解释

Table

表名

Op

总是analyze

Msg_type

status, error, info, note, 或者warning

Msg_text

信息

在对表的键分布进行分析时 , ANALYZE TABLE操作会将指定的表从表定义缓存中移除 , 并且会对于InnoDBMyISAM表会加上读锁 , 即其他会话只能对表数据进行查询 , 无法对表数据进行修改 , 直到执行分析的会话释放了锁。

默认的 , MySQL服务会将ANALYZE TABLE语句写到binlog中 , 以便在主从架构中 , 从服务能够同步数据。(从服务通过binlog与主服务完成数据同步) 。可以添加参数取消将语句写到binlog中:

ANALYZE NO_WRITE_TO_BINLOG TABLE 表名

或者

ANALYZE LOCAL TABLE 表名

20201104204953504

ANALYZE TABLE分析后的统计结果会反应到cardinality的值 , 该值统计了表中某一键所在的列 , 不重复的值的个数。该值越接近表中的总行数 , 则在表连接查询或者索引查询时 , 就越优先被优化器选择使用。也就是索引列的cardinality的值与表中数据的总条数差距越大 , 即使查询的时候使用了该索引作为查询条件 , 实际存储引擎实际查询的时候使用的概率就越小。我们都知道 , 索引尽量建立在重复值很少的列上就是基于这个原因。下面通过例子来验证下。cardinality可以通过SHOW INDEX FROM 表名查看 : 20201104205055330

film表中的数据总条数是1000 。由上图可知 , film表建立了四个索引 , 前两个的索引的cardinality就等于表的数据总条数 , 表示很优秀。下面两个的值才是1 , 就很差了。查看select * from film where film_id = 1;的执行计划 , 其中film_id是索引列 , cardinality=100020201104205142556

从执行计划的结果可以看出 , 上面的语句是使用了索引的。再来查看select * from film where language_id = 1;的执行计划 , 其中language_id也是索引列 , 但是cardinality=1

20201104205217624

由上面执行计划的结果可以看出 , 虽然语句中使用了索引 , 但是存储引擎在实际执行查询的时候并没有使用索引。因为cardinality的值与表中的数据总条数差距太大了。

  • analyze table的作用

    • analyze table会统计索引分布信息。

    • 对于MyISAM表 , 相当于执行了一次myisamchk --analyze

    • 支持InnoDBNDBMyISAM等存储引擎 , 但不支持视图(view)

    • 执行analyze table时 , 会对表加上读锁(read lock)

    • 该操作会记录binlog

  • 生产上操作的风险

    • analyze table的需要扫描的page代价粗略估算公式 : sample_pages * 索引数 * 表分区数

    • 因此 , 索引数量较多 , 或者表分区数量较多时 , 执行analyze table可能会比较费时 , 要自己评估代价 , 并默认只在负载低谷时执行。

    • 特别提醒 , 如果某个表上当前有慢SQL , 此时该表又执行analyze table 。则该表后续的查询均会处于waiting for table flush的状态 , 严重的话会影响业务 , 因此执行前必须先检查有无慢查询。

: 上面多次提到MyISAM , 仅是本次总结所需 , 并不推荐大家使用MyISAM引擎 , 使用InnoDB才是正道。

6. MySQL的索引实现

分析MySQL的两种存储引擎的索引实现 : MyISAM引擎和InnoDB引擎

6.1 MyISAM索存储引擎索引

以一个简单的user表为例 , user表存在两个索引 , id列为主键索引 , age列为普通索引。

CREATE TABLE `user`
(
   id       int(11) NOT NULL AUTO_INCREMENT,
   username varchar(20) DEFAULT NULL,
   age      int(11)     DEFAULT NULL,
   PRIMARY KEY (id) USING BTREE,
   KEY idx_age (age) USING BTREE
) ENGINE = MyISAM
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8;

MyISAM的数据文件和索引文件是分开存储的 , MyISAM使用B+树构建索引树时 , 叶子节点中键值key存储的是索引列的值 , 数据data存储的是索引所在行的磁盘地址。

主键ID列索引 :

e91e5091172346568d93c9bc3814e5f9

user的索引存储在索引文件user.MYI中 , 数据存储在数据文件user.MYD中。

6.1.1 根据主键等值查询数据

select * from user where id = 28
  1. 第一次磁盘IO : 先在主键索引树中从根节点开始检索 , 将根节点加载到内存中 , 比较28<75 , 所以走左子树。

  2. 第二次磁盘IO : 将左子树节点加载到内存中 , 比较16<28<47 , 向下检索。

  3. 第三次磁盘IO : 检索到叶子节点 , 将节点加载到内存中遍历 , 从16<2818<2828=28 , 查找到键值等于28的索引项。

  4. 第四次磁盘IO : 从索引项中获取磁盘地址 , 然后到数据文件user.MYD中获取对应整行记录。

  5. 将记录返回给客户端。

磁盘IO次数 : 3次索引检索+1次记录数据检索 :

7c1104d6f12845478fbb7e8cdf9de622

6.1.2 根据主键范围查询数据

select * from user where id between 28 and 47;
  1. 第一次磁盘IO : 先在主键索引树中从根节点开始检索 , 将根节点加载到内存中 , 比较28<75 , 所以走左子树。

  2. 第二次磁盘IO : 将左子树节点加载到内存中 , 比较16<28<47 , 向下检索。

  3. 第三次磁盘IO : 检索到叶子节点 , 将节点加载到内存中遍历 , 从16<2818<2828=28 , 查找到键值等于28的索引项 , 根据次磁盘地址从数据文件中获取行记录缓存到结果集中。

  4. 第四次磁盘IO :我们在查询时时根据范围查找 , 将下一个节点加载到内存中 , 遍历比较 , 28<47=47 , 根据磁盘地址从数据文件中获取地址缓存到结果集中。

  5. 根据上图可知 , 最后得到两条符合筛选条件的结果集 , 将结果返回给客户端。

磁盘IO次数 : 4次索引检索+1次记录数据检索。

6e6567bdd8e34a77b5d4d246b9a6c054

  • 提示 :

    以上分析仅供参考 , 如果使用MyISAM存储引擎在查询时 , 会将索引节点缓存在MySQL缓存中 , 而数据缓存依赖于操作系统自身的缓存 , 这里只是为了演示分析索引的使用过程。

6.1.3 辅助索引

  • MyISAM存储引擎中 , 辅助索引和主键索引的结构是一样的 , 没有任何区别 , 叶子节点中data阈存储的都是行记录的磁盘地址。

  • 主键列索引的键值是唯一的 , 而辅助索引的键值是可以重复的。

  • 查询数据时 , 由于辅助索引的键值不唯一 , 可能存在多个拥有相同的记录 , 所以即使是等值查询 , 也需要按照范围查询的方式在辅助索引树中检索数据。

6.2 InnoDB存储引擎索引

6.2.1 主键索引(聚簇索引)

每个InnoDB表都有一个聚簇索引 , 聚簇索引使用B+树构建 , 叶子节点的data阈存储的是整行记录。一般情况下 , 聚簇索引等同于主键索引 , 当一个表没有创建主键索引时 , InnoDB会自动创建一个ROWID字段来构建聚簇索引。InnoDB创建索引的具体规则如下:

  • 在创建表时 , 定义主键PRIMARY KEY , InnoDB会自动将主键索引用作聚簇索引。

  • 如果表没有定义主键 , InnoDB会选择第一个不为NULL的唯一索引列用作聚簇索引。

  • 如果以上两个都没有 , InnoDB会自动使用一个长度为6字节的ROWID字段来构建聚簇索引 , 该ROWID字段会在插入新的行记录时自动递增。

除聚簇索引之外的所有索引都被称为辅助索引。在InnoDB中 , 辅助索引中的叶子节点键值存储的是该行的主键值。在检索时 , InnoDB使用此主键在聚餐索引中搜索行记录。

这里以user_innodb表为例 , user_innodbid列为主键 , age列为普通索引。

CREATE TABLE `user_innodb`
(
   id       int(11) NOT NULL AUTO_INCREMENT,
   username varchar(20) DEFAULT NULL,
   age      int(11)     DEFAULT NULL,
   PRIMARY KEY (id) USING BTREE,
   KEY idx_age (age) USING BTREE
) ENGINE = InnoDB;
  • InnoDB的数据和索引存储在t_user_innodb.ibd文件中 , InnoDB的数据组织方式 , 是聚簇索引。

  • 主键索引的叶子节点会存储数据行 , 辅助索引的叶子节点只会存储主键值。

29a27130b5a843b59c5c560cd5c731b2

等值查询数据 :

select * from user_innodb where id = 28;

发生三次磁盘IO :

43285bda9e754299bc0a614282bdd4a6

6.2.2 辅助索引

  • 除聚簇索引之外的所有索引都被称为辅助索引 , InnoDB的辅助索引只会存储值而不存储磁盘地址。

  • user_innodbage列为例 , age列的辅助索引结构如下图:

    23bdd860117243ff909bcff49b6b1e09

  • 辅助索引的底层叶子节点是按照(age , id) 的顺序排序 , 先按照age列从小到大排序 , age相同时按照id列从小到大排序。

  • 使用辅助索引需要检索两遍索引 : 首先检索辅助索引获得主键 , 然后根据主键到主键索引中检索获得数据记录。

辅助索引等值查询的情况 :

select * from t_user_innodb where age=19;

175b62fdfe7c48d6a017e3506272f953

在辅助索引树中获取到主键id , 再根据主键id到主键索引数中检索数据的的过程称为回表查询

磁盘IO数(从根节点开始) : 辅助索引3次 + 回表过程3次。

6.2.3 组合索引

  • 以表abc_innodb为例 , id列为主键索引 , 创建一个联合索引idx_abc(a , b , c)

CREATE TABLE `abc_innodb`
(
   id int(11) NOT NULL AUTO_INCREMENT,
   a  int(11)     DEFAULT NULL,
   b  int(11)     DEFAULT NULL,
   c  varchar(10) DEFAULT NULL,
   d  varchar(10) DEFAULT NULL,
   PRIMARY KEY (id) USING BTREE,
   KEY idx_abc (a, b, c)
) ENGINE = InnoDB;

组合索引的数据结构 :

1c72e1da60724e9d9f9a73e6ab625079

组合索引的查询过程 :

select * from abc_innodb where a = 13 and b = 16 and c = 4;

d7c8ff7d92b347738c3adceba17d8dee

6.2.4 最左匹配原则

最左前缀匹配原则和联合索引的索引存储结构和检索方式是有关系的。

在组合索引树中 , 最底层的叶子节点按照第一列a列从左到右递增排序 , 但是b列和c列是无序的 , b列只有在a列值相等的情况下小范围内有序递增 ; 而c列只能在ab两列值相等的情况下小范围内有序递增。

就像上面的查询 , B+树会先比较a列来确定下一步应该检索的方向 , 往左还是往右。如果a列相同再比较b列 , 但是如果查询条件中没有a列 , B+树就不知道第一步应该从那个节点开始查起。

可以说创建的idx_(a , b , c)索引 , 相当于创建了(a)(a , b)(a , b , c)三个索引。

组合索引的最左前缀匹配原则 : 使用组合索引查询时 , mysql会一直向右匹配直至遇到范围查询(><betweenlike)等就会停止匹配。

6.2.5 覆盖索引

覆盖索引并不是一种索引结构 , 覆盖索引是一种很常用的优化手段。因为在使用辅助索引的时候 , 我们只可以拿到相应的主键值 , 想要获取最终的数据记录 , 还需要根据主键通过主键索引再去检索 , 最终获取到符合条件的数据记录。

在上面的abc_innodb表中的组合索引查询时 , 如果我们查询的结果只需要abc这三个字段 , 那我们使用这个idx_index(a , b , c)组合索引查询到叶子节点时就可以直接返回了 , 而不需要再次回表查询 , 这种情况就是覆盖索引

7 回表和联合索引的应用

7.1 回表查询

InnoDB的存储引擎中 , 使用辅助索引查询的时候, 因为辅助索引叶子节点保存的数据不是当前数据记录, 而是当前数据记录的主键索引。如果需要获取当前记录完整的数据 , 就必须要再次根据主键从主键索引中继续检索查询 , 这个过程我们称之为回表查询

由此可见 , 在数据量比较大的时候 , 回表必然会消耗很多的时间影响性能 , 所以我们要尽量避免回表的发生。

7.2 如何避免回表

使用覆盖索引 , 举个例子: 现有User

CREATE TABLE user
(
   id int(11) NOT NULL AUTO_INCREMENT,
   name  int(11)     DEFAULT NULL,
   sex  char(3)     DEFAULT NULL,
   address  varchar(10) DEFAULT NULL,
   hobby  varchar(10) DEFAULT NULL,
   PRIMARY KEY (id) USING BTREE,
  KEY i_name (name)
) ENGINE = InnoDB;

如果有一个场景:

select id,name,sex from user where name = 'zhangsan';

这个语句在业务上频繁使用到 , 而user表中的其他字段使用频率远低于这几个字段 。在这个情况下 , 如果我们在建立name字段的索引时 , 不是使用单一索引 , 而是使用联合索引(name , sex) , 这样的话再执行这个查询语句 , 根据这个辅助索引(name , sex)查询到的结果就包括了我们所需要的查询结果的所有字段的完整数据 , 这样就不需要再次回表查询去检索sex字段的数据了。

以上就是一个典型的使用覆盖索引的优化策略减少了回表查询的情况。

7.3 联合索引的使用

联合索引 : 在建立索引的时候 , 尽量在多个单列索引上判断是否可以使用联合索引。联合索引的使用不仅可以节省空间 , 还可以更容易的使用到覆盖索引。

节省空间 :

试想一下 , 索引的字段越多 , 是不是更容易满足查询需要返回的数据呢?比如联合索引(a , b , c)是不是等于有了 : a(a , b)(a , b , c)三个索引 , 这样是不是节省了空间 , 当然节省的空间并不是三倍于a(a , b)(a , b , c)三个索引所占用的空间 , 但是联合索引中data字段数据所占用的空间确实节省了不少。

联合索引的创建原则 :

在创建联合索引的时候应该把频繁使用的列、区分度高的列放在前面。频繁使用代表索引利用率高 , 区分度高代表筛选力度大 ,这些都是在创建索引的时候需要考虑到的优化场景 , 也可以将常需要作为查询返回的字段增加到联合索引中。

如果在联合索引上增加了一个字段 , 而恰好满足了使用覆盖索引的情况 , 这种情况建议使用联合索引。

联合索引的使用:

  • 考虑到当前是否已经存在多个可以合并的单列索引。如果有 , 那么推荐将当前的多个单列索引创建为一个联合索引。

  • 当前索引存在频繁使用 , 在结果中有一个列也被频繁查询 , 而这个列不在当前频繁使用的索引中 , 那么这个时候就可以考虑这个列是否可以加入到当前频繁使用的索引中 , 使其查询语句可以使用到覆盖索引。

8 EXPLAIN 工具

可以通过EXPLAIN来分析索引的有效性,获取查询执行计划信息 , 用来查看查询优化器如何执行查询

参考资料 : https://dev.mysql.com/doc/refman/5.7/en/explain-output.html

  • 语法 :

    EXPLAIN SELECT clause

EXPLAIN输出信息说明:

列名

说明

id

执行编号 , 标识select所属的行。如果在语句中没子查询或关联查询 , 只有唯一的select , 每行都将显示1。否则 , 内层的select语句一般会顺序编号 , 对应于其在原始语句中的位置。 id相同 , 执行顺序由上至下 id不同 , 如果是子查询 , id的序号会递增 , id值越大优先级越高 , 越先被执行

select_type

简单查询 : SIMPLE 复杂查询 : PRIMARY(最外面的SELECT)、DERIVED(用于FROM中的子查询)、UNION(UNION语句的第一个之后的SELECT语句)、UNION RESUlT(匿名临时表)、SUBQUERY(简单子查询) SIMPLE : 简单的select查询 , 查询中不包含子查询或者UNION PRIMARY : 查询中若包含任何复杂的子部分 , 最外层查询则被标记为PRIMARY SUBQUERY : 在SELECTWHERE列表中包含了子查询 DERIVED : 在FROM列表中包含的子查询被标记为DERIVED(衍生) , MySQL会递归执行这些子查询 , 把结果放在临时表中 UNION : 若第二个SELECT出现在UNION之后 , 则被标记为UNION 。若UNION包含在FROM子句的子查询中 , 外层SELECT将被标记为 : DERIVED UNION RESULT : 从UNION表获取结果的SELECT

table

访问引用哪个表(引用某个查询 , 如"derived3")

type

关联类型或访问类型 , 即MySQL决定的如何去查询表中的行的方式

possible_keys

查询可能会用到的索引。查询涉及到的字段上若存在索引 , 则该索引将被列出 , 但不一定被查询实际使用

key

显示mysql决定采用哪个索引来优化查询。 实际使用的索引 , 如果为NULL ,则没有使用索引。 查询中若使用了覆盖索引( select后要查询的字段刚好和创建的索引字段完全相同) , 则该索引仅出现在key列表中 , possible_keys会直接显示NULL

key_len

显示mysql在索引里使用的字节数 , 可通过该列计算查询中使用的索引的长度 , 在不损失精确性的情况下 , 长度越短越好key_len显示的值为索引字段的最大可能长度 , 并非实际使用长度 , 即key_len是根据表定义计算而得 , 不是通过表内检索出的。

ref

根据索引返回表中匹配某单个值的所有行

rows

为了找到所需的行而需要读取的行数 、估算值、不精确。通过把所有rows列值相乘 , 可粗略估算整个查询会检查的行数。也就是说 , 用的越少越好

Extra

额外信息 Using index : MySQL将会使用覆盖索引 , 以避免访问表 Using where : MySQL服务器将在存储引擎检索后 , 再进行一次过滤 Using temporary : MySQL对结果排序时会使用临时表 , 常见于排序order by和分组查询group by Using filesort : 对结果使用一个外部索引排序 , 而不是按照表内的索引顺序进行读取。MySQL中无法利用索引完成的排序操作称为"文件排序"。

说明 : type显示的是访问类型 , 是较为重要的一个指标 , 结果值从好到坏依次是 : system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL 。一般来说得保证查询至少达到range级别 , 最好能达到ref

类型

说明

All

最坏的情况,全表扫描

index

和全表扫描一样。只是扫描表的时候按照索引次序进行而不是行。主要优点就是避免了排序, 但是开销仍然非常大。如在Extra列看到Using index , 说明正在使用覆盖索 引 , 只扫描索引的数据 , 它比按索引次序全表扫描的开销要小很多

range

范围扫描 , 一个有限制的索引扫描。key列显示使用了哪个索引。当使用=<>>>=<<=IS NULL<=>BETWEEN或者IN操作符,用常量比较关键字列时,可以 使用range

ref

一种索引访问 , 它返回所有匹配某个单个值的行。此类索引访问只有当使用非唯一性索引或唯一性索引非唯一性前缀时才会发生。这个类型跟eq_ref不同的是 , 它用在关联操作只使用了索引的最左前缀 , 或者索引不是UNIQUEPRIMARY KEYref可以用 于使用=<=>操作符的带索引的列。

eq_ref

最多只返回一条符合条件的记录。使用唯一性索引或主键查找时会发生 (高效)

const

当确定最多只会有一行匹配的时候 , MySQL优化器会在查询前读取它而且只读取一次 , 因此非常快。当主键放入where子句时 , mysql把这个查询转为一个常量(高效)

system

这是const连接类型的一种特例 , 表仅有一行满足条件。

Null

意味着mysql能在优化阶段分解查询语句 , 在执行阶段甚至用不到访问表或索引(高效)

  • system : 表只有一行记录(等于系统表) ,这是const类型的特列 , 平时不会出现 , 这个也可以忽略不计

  • const : 表示通过索引一次就找到了 , const用于比较primary key或者unique索引。因为只匹配一行数据 , 所以很快。如将主键置于where列表中 , MySQL就能将该查询转换为一个常量。

    2018052018171447

    首先进行子查询得到一个结果的d1临时表 , 子查询条件为id = 1是常量 , 所以typeconst , id1的相当于只查询一条记录 , 所以typesystem

  • eq_ref : 唯一性索引扫描 , 对于每个索引键 , 表中只有一条记录与之匹配。常见于主键或唯一索引扫描

  • ref : 非唯一性索引扫描 , 返回匹配某个单独值的所有行 。 本质上也是一种索引访问 , 它返回所有匹配某个单独值的行 。然而 , 它可能会找到多个符合条件的行 , 所以他应该属于查找和扫描的混合体。

    2018052018313393

  • range : 只检索给定范围的行 , 使用一个索引来选择行 , key列显示使用了哪个索引 。一般就是在你的where语句中出现between< >in等的查询 。这种范围扫描索引比全表扫描要好 。因为它只需要开始于索引的某一点 , 而结束于另一点 , 不用扫描全部索引。

    2018052020065932

  • index : Full Index Scan , IndexAll区别为index类型只遍历索引树。这通常比ALL快 , 因为索引文件通常比数据文件小。(也就是说虽然allIndex都是读全表 , 但index是从索引中读取的 , 而all是从硬盘读取的)

    20180520201112960

    id是主键 ,所以存在主键索引

  • all : Full Table Scan将遍历全表以找到匹配的行

    20180520201432800

范例:

MariaDB [hellodb]> explain select * from students where stuid not in (5,10,20);

image-20230506211007170

MariaDB [hellodb]> explain select * from students where stuid <> 10 ;

image-20230506211137440

MariaDB [hellodb]> explain select * from students where age > (select avg(age)  from teachers);

image-20230506211436761

范例 : 创建索引和使用索引

MariaDB [hellodb]> create index idx_name on students(name(10));
Query OK, 0 rows affected (0.009 sec)
Records: 0 Duplicates: 0  Warnings: 0
MariaDB [hellodb]> show indexes from students\G;
*************************** 1. row ***************************
        Table: students
   Non_unique: 0
     Key_name: PRIMARY
 Seq_in_index: 1
  Column_name: StuID
    Collation: A
  Cardinality: 25
     Sub_part: NULL
       Packed: NULL
         Null: 
   Index_type: BTREE
      Comment: 
Index_comment: 
*************************** 2. row ***************************
        Table: students
   Non_unique: 1
     Key_name: idx_name
 Seq_in_index: 1
  Column_name: Name
    Collation: A
  Cardinality: 25
     Sub_part: 10
       Packed: NULL
         Null: 
   Index_type: BTREE
      Comment: 
Index_comment: 
2 rows in set (0.001 sec)
MariaDB [hellodb]> explain select * from students where name like 'w%'; 

image-20230506212108345

MariaDB [hellodb]> explain select * from students where name like 'x%';

image-20230506212148737

MariaDB [hellodb]> SHOW INDEX_STATISTICS;
+--------------+------------+------------+-----------+
| Table_schema | Table_name | Index_name | Rows_read |
+--------------+------------+------------+-----------+
| hellodb      | students   | idx_name   |         2 |
| mysql        | help_topic | name       |         1 |
+--------------+------------+------------+-----------+
2 rows in set (0.000 sec)

范例 : 查询中若使用覆盖索引 (select后要查询的字段刚好和创建的索引字段完全相同) , possible_keys会直接显示NULL 。则该索引仅出现在key列表中

20180520203824108

2018052020384550

范例 : key_len显示的值为索引字段的最大可能长度 , 并非实际使用长度 。即key_len是根据表定义计算而得 , 不是通过表内检索出的。

20180520205651544

范例 :

20180521091813930

  • 执行顺序1 : select_typeUNION , 说明第四个selectUNION里的第二个select , 最先执行【select name,id from t2

  • 执行顺序2 : id3 , 是整个查询中第三个select的一部分。因查询包含在from中 , 所以为DERIVED , 【select id,name from t1 where other_column=’’

  • 执行顺序3 : select列表中的子查询select_typesubquery,为整个查询中的第二个select, 【select id from t3

  • 执行顺序4 : id列为1 , 表示是UNION里的第一个select , select_type列的primary表示该查询为外层查询 , table列被标记为<derived3>,表示查询结果来自一个衍生表 , 其中derived3中的3代表该查询衍生自第三个select查询 , 即id3select。【select d1.name …

  • 执行顺序5 : 代表从UNION的临时表中读取行的阶段 , table列的<union1,4>表示用第一个和第四个select的结果进行UNION操作。【两个结果union操作】

熊熊