RedCloud Help

索引

索引是什么

索引图解

首先数据是以文件的行持存储在磁盘上面的,每一行数据都有它的磁盘地址。如果没有索引的话,要从500w行的数据里面检索一条数据,只能一次便利这张表的全部数据,直到找到这条数据。

但是有了索引之后,只需要在索引里面检索这条数据就行了,因为它是一种特殊的专门用来快速检索的数据结构,我们找到数据存放的磁盘地址以后,就可以拿到数据了。

这就像我们从一本500页的书里面去找特定的一小截的内容,肯定不可能从第一页开始翻。呢么这一本书有专门的目录,他可能只有几页的内容,它是由页码来组织的,可以根据拼音或者偏旁部首来查找,只要确定内容对应的页码,就能很快地找到我们想要的内容。

索引类型

怎么创建一个索引

在InnoDB里面,索引的类型有三种:

  1. 普通(Normal):也叫非唯一索引,是最普通的索引,没有任何限制。

  2. 唯一(Unique):唯一索引要求键值不能重复。另外需要注意的是,逐渐索引是一种特殊的唯一索引,它还多了一个限制条件,要求键值不能为空。逐渐索引用primary key创建。

  3. 全文索引(Fulltext):针对比较大的数据,比如我们存放的消息内容,有几kb的数据的这种情况,如果要解决like查询效率低的问题,可以创建全文索引。只有文本类型的字段可以创建全文索引,比如char,varchar,text

create table m3( name varchar(50), fulltext index(name) )

全文索引的使用:

select * from fulltext_text where match(content) against('' in natural laguage mode)

MyISAM和InnoDB支持全文索引。 这个索引的三种类型:普通、唯一、全文。 我们说索引是一种数据结构,哪么它到底应该选择一种什么数据结构,才能实现数据高效检索?

索引存储模型推演

二分查找

二分查找是一种思想,也叫折半查找,每一次,我们都把候选数据缩小了一半。如果数据已经排过序的话,这种方式效率比较高。

有序数组的等值查询和比较查询效率非常高,但是跟新数据的时候会出现一个问题,可能要挪动大量的数据(改变index),所以知识和存储静态数据。

为了支持频繁的修改,比如插入数据,我们需要采用链表。链表的话,如果是单链表,它的查询效率还是不够高。

二叉查找树(BST Binary Search Tree)

左子树索引的节点都小于父节点,右子树所有的节点都大于父节点。投影到平面以后,就是一个有序的线性表。 二叉查找树即能够实现快速查找,又能够实现快速插入。

2

6

11

13

17

22

但是二叉查找树有一个问题: 就是它的查找耗时是和这棵树的深度有关系的,最坏的情况下时间复杂度会退化成O(n)。

2

6

11

13

17

22

这会变成链表(我们把这种树叫做“斜树”),这种情况下不能达到加速索引速度的目的,和顺序查找效率是没有区别的。 造成倾斜,是因为左右子树深度差太大了,这棵树的左子树根本没有节点,也就是他不够平衡。所以,我们有没有左右子树深度相差不是哪么大,更加平衡的树呢?

平衡二叉树(AVL Tree)(左旋、右旋)

AVL Trees(Balanced binary search trees) 平衡二叉树的定义:左右子树深度差绝对值不能超过1。 这个时候我们再按循序插入1、2、3、4、5 6,一定是这样的,不会变成一颗斜树。

1

2

3

4

5

6

模拟 AVL Tree

它会存储三块内容:

  1. 索引的键值。比如我们在id上面创建了一个索引,我在用where id =1的条件查询的时候就会找到索引里面的id的这个键值。

  2. 数据的磁盘地址,因为索引的作用就是去查找数据的存放地址。

  3. 因为是二叉树,它必须还要有左子节点和右子节点的引用,这样我们才能找到下一个节点。比如找到26的时候,走右边,到下一个树的节点,继续判断。

InnoDB逻辑存储结构

https://dev.mysql.com/doc/refman/5.7/en/innodb-disk-management.html

https://dev.mysql.com/doc/refman/5.7/en/innodb-disk-management.html

MySql的存储结构分为5级:

  • 表空间(table space):可以看作是InnoDB存储引擎逻辑结构的最高层,所有的数据都是存放在表空间中。分为:系统表空间、独占表空间、通用表空间、临时表空间、Undo表空间。

  • 段(Segment):表空间是由各个段组成的,常见的段有数据段、索引段、回滚段等,段是一个逻辑的概念。一个ibd文件(独立表空间文件)里面会由很多个段组成。创建一个索引会创建两个段,一个索引段:leaf node segment,一个实数据段:non-leaf node segment。索引段管理非叶子节点的数据。数据段管理也子节点的数据。也就是说,一个表的段数,就是索引的个数乘以2.

  • 簇(extent):一个段(segment)又由很多簇(也可以叫区)组成,每个区的大小是1mb(64个连续的页)。每个段只要会有一个簇,一个段所管理的空间大小是无限的,可以一直扩展下去,但是扩展的最小单位就是簇。

  • 页(page):为了高效管理空间,对簇进一步细分,就得到了页。簇是由连续的页(page)组成的空间,一个簇中有64个连续的页。(1MB/16KB=64)。这些页面在物理上和逻辑上都是连续的。

跟大多数数据库一样,InnoDB也有页的概念(也可以称为块),每个页默认16kb。页是InnoDB存储引擎磁盘管理的最小单位,通过innodb_page_size设置。

一个表空间最多拥有 2^32个页,默认情况下一个页的大小为16KB,也就是说一个表空间最多存储64TB的数据。

注意,文件系统中,也有页的概念。

操作系统和内存 打交道,最小的单位是页page。文件系统的内存通常是4k。

img.png

假设一行数据大小是1K,哪么一个数据也可以放16行这样的数据。

img.png

往表中插入数据时,如果一个页面已经写完,产生一个新的页面。如果一个簇的所有页面都被用完,会从当前页面所在段新分配一个簇。

如果数据不是连续的,往已经写满的页中插入数据,会导致叶页面分裂:

img.png

行Row

InnoDB存储引擎是面向行的(row-oriented),也就是说数据的存放按行进行存放。 https://dev.mysql.com/doc/refman/5.7/en/innodb-row-format.html

Antelope是InnoDB内置的文件格式,有两种行格式:

  1. REDUNDANT ROW FORMAT

  2. COMPACT ROW FORMAT(5.6默认) Barracuda是InnoDB plugin支持的文件格式,新增了两种行格式:

DYNAMIC ROW FORMAT(5.7默认)

COMPRESSED ROW Format

文件格式

行格式

描述

Antelope(Innodb-base)

ROW_FORMAT=COMPACT
ROW_FORMAT=REDUNDANT

Compact和redumdant的区别在就是在于首部的存储内容区别。Compact的存储格式为首部为一个非null的变长自动长度列表。redundant的存储格式为首部是一个字段长度偏移列表(每一个字段占用的字节长度及其相应的位移)。在Antelope中对于变长字段,低于768字节的,不会进行overflow page存储,某些情况下减少结果集IO

Barracuda(innodb-plugin)

ROW_FORMAT=DYNAMIC
ROW_FORMAT=COMPRESSED

这两者主要是功能上的区别。另外在行里的变长字段和antelope的区别只存在20个字节,其他的overflow page存储。另外这两都需要开启innodb_file_per_table=1

innodb_file_format在配置文件中指定;row_format则在创建数据表时指定。

show variables like "%innodb_file_format%"; set global innodb_file_format=Barracuda;

在创建表的时候可以指定行格式。

create table tf1 (c1 int primary key) ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=8;

查看行格式:

show table status like 'student'\G;

树用于存储索引数据

首先索引的数据,是放在硬盘上的。查看数据和索引的大小:

select CONCAT(ROUND(SUM(DATA_LENGTH/1024/1024),2),'MB') as index_len from information_schema.TABLES where table_schema='gupao' and table_name='user_innodb';

当我们用树的结构来存储索引的时候,访问一个节点就要跟磁盘之间发生一次IO。InnoDB操作磁盘的最小的单位是一页(或者叫一个磁盘块),大小是16k(16384字节)。哪么,一个树的节点就是16k的大小。 如果我们一个节点只存一个键值+数据+引用,例如整形的字段,可能只用了十几个或者几十个字节,它远远达不到16k的容量,所以访问一个树节点,进行一次io的时候,浪费了大量的空间。

所以如果每个节点存储的数据太少,从索引中找到我们需要的数据,就要访问更多的节点,意味着跟磁盘交互次数就会过多。

如果是机械硬盘时代,每次从磁盘读取数据需要10ms左右的寻址时间,交互次数越多,消耗的时间就越多。

img_1.png
比如上面这个图,我们一张表里面有6条数据,当我们查询id=37的时候,要查询两个子节点,就需要跟磁盘交互3次,如果我们有几百万的数据呢?这个时间更加难以估计。

所以我们的解决方案是什么呢?

  1. 让每一个节点存储更多的数据

  2. 节点上的关键字的数量越多,我们的指针数也越多,也就是意味着可以有更多的分叉(我们把它叫做“路数”)

因为分叉数越多,树的深度就会减少(根节点是0)。 这样我们的书是不是从原来的高瘦高瘦的样子,变成了矮胖矮胖的样子?

这个时候,我们的树就不再是二叉,而是多叉,或者叫做多路

多路平衡查找树(B Tree)分裂、合并

Balanced Tree 这个就是我们的多路平衡查找树,叫做B Tree(B代表平衡)。跟AVL树一样,B树在直接点和也子节点存储键值、数据地址、节点引用。

他有一个特点:分叉数永远比关键数多1.比如我们画的这棵树,每个节点存储两个关键字,哪么就会有三个指针指向三个子节点。 img_1.png

B Tree的查找规则是什么样呢?

比如我们要在这张表里面查找15.因为15小于17,走左边。因为15大于12,走右边。在磁盘块7里面就找到了15,只用了3次io。 这个是不是比avl树效率更高呢?

B tree优势怎么实现节点存储多个关键字,还保持平衡的呢?跟AVL树有什么区别 https://www.cs.usfca.edu/~galles/visualization/Algorithms.html

比如MAX Degree(路数3)的时候,我们插入数据1、2、3,在插入3的时候,本来应该在第一个磁盘块,但是如果一个节点有三个关键字的时候,意味着4个指针,子节点会变成4路,所以这个时候必须进行分裂。把中间的数据2提上去,把1和3变成2的子节点。

如果删除节点,会有相反的合并的操作。

注意这里的分裂和合并,跟AVL树的左旋和右旋是不一样的。 我们继续插入4和5,BTree又会出现分裂和合并的操作。

img_1.png

从这里我们也能看到,在更新索引的时候会有大量的索引的结构的调整,所以解释了为什么我们不要在频繁更新的列上建索引,或者为什么不要更新主键。

节点的分裂和合并,其实就是InnoDB页的分裂和合并。

B+树(加强版多路平衡查找树)

B Tree的效率已经很高了,为什么mysql还要对B Tree进行改良,最终使用了B+Tree呢?

总体上来说,这个B树的改良版本解决的问题比B Tree更全面。

我们来看一下InnoDB里面的B+树的存储结构:

img_1.png

Mysql中的B+Tree有几个特点:

  1. 它的关键字的数量是跟路数相等的。

  2. B+Tree的根节点和枝节点都不会存储数据,只有也子节点才会存储数据。搜索的关键字不会直接返回,回到最后一层的也子节点。比如我们搜索id=28,虽然在第一层直接命中了,但是全部的数据在也子节点上面,所以我还要继续往下搜索,一直到叶子节点。

假设索引字段是bigint类型,长度8字节。指针大小在InnoDB源码中设置为6字节,这样一共14字节。非叶子节点可以存储16384/14=1170个这样的单元(键值+指针),代表有1170个指针。

树深度上为2的时候,有1170^2个叶子节点,可以存储的数据为1170×1170×16=21802400

img_1.png

在查找数据时一次页的查找代表一次io,也就是说,一张2000w左右的表,查询数据最多需要访问3次磁盘。 所以InnoDB中B+树深度一般为1-3层,它就是能满足千万级的数据存储。 3. B+Tree的每个叶子节点增加了一个指向相邻叶子节点的指针,它的最后一个数据会指下一个也子节点的第一个数据,形成一个有序的链表的结构。 4. 它是根据左闭右开的区间[)来检索数据。

  1. 比如我们要查找28,在根节点就找到键值,但是因为它不是页子节点,所以会继续往下搜索,28是[28,66)的左开右闭的区间的临界值,所以会走中间的子节点,然后继续搜索,它又是[28,34)的左闭右开的区间的临界值,所以会走左边的子节点,最后在也子节点上找到需要的数据。

  2. 第二个,如果是范围查询,比如要查询从22到60的数据,当找到22之后,只需要顺着节点和指针顺序遍历就可以一次性访问到所有的数据节点,这样就极大地提高了区间查询效率(不需要返回上层父节点重复遍历查找)。

总结一下,InnoDB中B+Tree的特点:

  1. 它是BTree的变种,BTree能解决的问题,它都能解决。BTree解决的两大问题是什么?(每个节点存储更多关键字;路数更多)

  2. 扫库、扫表能力增强(如果我们要对表进行全表扫描,只需要遍历叶子节点就可以了,不需要遍历整颗B+Tree拿到所有的数据)

  3. B+Tree的磁盘读写能力相当于B Tree来说更强(根节点和直枝节点不保存数据区,所以一个节点可以保存多个关键子,一次磁盘加载的关键字更多)

  4. 排序能力更强(因为也子节点上有下一个数据区的指针,数据形成了链表)

  5. 效率更加稳定(B+Tree永远是在也子节点拿到数据,所以IO次数是稳定的)

为什么不用红黑树?

红黑树是BST树,但是不是严格平衡的。必须满足5个约束:

  1. 节点分为红色或者黑色。

  2. 根节点必须是黑色的。

  3. 也子节点都是黑色的null节点。

  4. 红色节点的两个子节点都是黑色(不允许两个相邻的红色节点)。

  5. 从任意节点出发,到其每个子节点的路径中包含相同数量的黑色节点。插入:60、56、68、45、64、58、72、43、49

    img_1.png

基于以上规则,可以推导出:

从根节点到也子节点的最长路径(红黑相间的路径)不大于最短路径(全部是黑色节点)的2倍。

为什么不用红黑树?1.只有两路;2.不够平衡。

红黑树一般只放在内存里面使用。例如java中的TreeMap

索引方式:真的是用B+Tree吗?

在navicat的工具中,创建索引,索引方式有两种,Hash和B Tree。

Hash:以KV的形式检索数据,也就是说,他会根据索引字段生成哈希码和指针,指针指向数据。

img_1.png

哈希索引有什么特点?

  1. 他的时间复杂度是o(1),查询速度比较快。因为哈希索引里面的数据不是按顺序存储的,所以不能用来排序。

  2. 我们在查询数据的时候要根据键值计算哈希码,所以它只能支持等值查询(=IN),不支持范围查询(> < >= <= between and)。

另外一个就是如果字段重复值很多的时候,就会出现大量的哈希冲突(从用拉链法解决),效率会底下。

问题 InnoDB可以在客户端创建一个索引,使用哈希索引吗?

https://dev.mysql.com/doc/refman/5.7/en/innodb-introduction.html InnoDB utilizes has indexes internally for its adaptive hash index feature.

直接翻译过来就是:InnoDB内部使用哈希索引来实现自适应哈希索引特性。 这句话的意思是InnoDB只支持显示创建B+Tree索引,对于一些热点数据页,InnoDB会自动创建自适应索引,也就是B+Tree索引基础上建立Hash索引,这个过程对于客户端是不可控制的,隐式的。

我们在navicat工具里面选择索引方法是哈希,但是它创建的还是B+Tree索引,这个不是我们可以手动控制的。

buffer pool里面一块区域是adaptive hash index自适应哈希索引,就是这个。

这个开关默认是ON:

show variables like 'innodb_adaptive_hash_index';

从存储引擎的运行信息可以看出:

show engine innodb status;

因为BTree 和B+Tree的特性,他们广泛地用在文件系统和数据库中,例如Windows的HPFS文件系统,Oracel、mysql、sqlserver数据库。

B+Tree落地形式

MySql架构

Mysql是一个支持插件存储引擎的数据库。在MySql里面,每个表在创建的时候都可以指定它所使用的存储引擎。 这里我们主要关注一下最常用的两个存储引擎,MyISAM和InnoDB的索引的实现。

Mysql数据存储文件

首先,Mysql的数据都是文件的形式存放到磁盘中,我们可以找到一个数据目录的地址。在Mysql中有这么一个参数,我们看一下:

show variables like 'datadir';

每一个数据库有一个目录,我们新建一个叫做gupao的数据库,哪么这里就有一个gupao的文件夹。 在这个数据库中我们建5张表:archive、innodb、memory、myisam、csv。我们进入gupao的目录,发现这里面有一些跟我们创建的表明对应的文件。

在这里我们能看到,每一张InnoDB的表有两个文件(.frm和.ibd),MyISAM的表有三个文件(.frm .MYD .MYI)

有一个是相同的文件,.frm .frm是mysql里面表结构定义的文件,不管你建表的时候选用任何一个存储引擎都会生成。 我们主要看一下其他两个文件是怎么实现mysql不同的存储引擎的索引的。

MyISAM

在MyISAM里面,另外有两个文件:

  1. .MYD文件,D代表data,是MyISAM的数据文件,存放数据记录,比如我们的user_myisam表的所有的表数据。

  2. .MYI文件,I表示Index,是MyISAM的索引文件,存放索引,比如我们在id字段上面创建一个主键索引,哪么主键索引就是在这个索引文件里面。

也就是说在MyISAM里面,索引和数据是两个独立的文件。

哪我们怎么根据索引找到数据呢?

MyISAM的B+Tree里面,也子节点存储的是数据文件对应的磁盘地址。所以从索引文件.MYI中找到键值后,会到数据文件.MYD中获取相应的数据记录。

img_1.png

这里是逐渐索引,如果是辅助索引,有什么不一样的呢?

在MYISAM里面,辅助索引也在.MYI这个文件里面。

辅助索引跟主键索引存储和检索数据方式是没有任何区别的,一样是在索引文件里面找到磁盘地址,然后到数据文件里面获取数据。

img_1.png

InnoDB

InnoDB只有一个文件(.ibd文件),哪索引放在那里呢?

在InnoDB里面,它是以逐渐萎缩引来组织数据的存储的,索引所以文件和数据文件是同一个文件,都在.ibd文件里面。

在InnoDB的主键索引的叶子节点上,他直接存储了我们的数据。

img_1.png

什么叫做聚集索引(聚簇索引)?

就是索引键值的逻辑顺序跟表数据行的物理存储顺序是一致的。(比如字典的目录是按照拼音排序的,内容也是按照拼音排序的,按拼音排序的这种目录就叫聚集索引。

在InnoDB里面,它组织数据的方式叫做(聚集)索引组织表(clustered index organize table),索引逐渐索引是聚集索引,非主键都是非聚集索引。

如果InnoDB里面主键是这样存储的,哪主键之外的索引,比如我们在name字段上面建普通索引,优势怎么存储和检索数据的呢?

img_1.png

InnoDB中,主键索引和辅助索引是有一个主次之分的。

复制索引存储的是辅助索引的主键值。如果使用辅助索引查询,会根据主键值在主键索引中查询,最终取得数据。

比如我们用name索引查询name=‘青山’,他会在叶子结点找到主键值,也就是id=1,然后再到主键索引的叶子结点拿到数据。

为什么在辅助索引里面存储的是主键值而不是逐渐的磁盘地址呢?如果主键的数据类型比较大,是不是比存地址更消耗空间呢?

我们前面说了B Tree是怎么时间一个节点存储多个关键字,还保持平衡呢?

是因为有分叉和合并的操作,这个时候键值的地址会发生变化,所以在辅助索引里不能存储地址。

另外一个文件,如果一张表没有主键怎么办?

  1. 如果我们定义了逐渐(PRIMARY KEY),哪么InnoDB会选择主键作为聚集索引。

  2. 如果没有显示定义主键,则InnoDB会选择第一个不包含有null值的唯一索引作为主键索引。

  3. 如果也没有这样的唯一索引,则InnoDB会选择内置6字节长的ROWID作为隐藏的聚集索引,它绘随着行记录的写入而主键递增。

select _rowid name from t2;

索引使用原则

我们容易有一个误区,就是在经常使用的查询条件上都加上索引,索引越多越好,哪到底是不是这样呢?

列的离散度

第一个叫做列的离散度,我们先来看一下列的离散度的公式:

count(distinct(column_name)):count(*),列的全部不同值和所有数据行的比例。数据行数相同的情况下,分子越大,列的离散度就越大。

简单来说,如果列的重复值越多,离散度就越低,重复值越少,离散度就越高。

了解了离散度的概念之后,我们再来思考一个问题,我们在name上面建立索引和在gender上面建立索引有什么区别。

当我们用在gender上建立索引去检索数据的时候,由于重复值太多,需要扫描的行数就更多。例如:我们现在在gender列上面创建一个索引,然后看一下执行计划。

ALTER TABLE user_innodb DROP INDEX idx_user_gender; ALTER TABLE user_innodb ADD INDEX idx_user_gender(gender); --耗时更久 EXPLAIN SELECT * FROM user_innodb WHERE gender=0;
show indexes from user_innodb;

id

select_type

table

partitions

type

possible_keys

key

key_len

ref

rows

1

SIMPLE

user_innodb

ref

idx_user_gender

idx_user_gender

2

const

498385

而name的离散度更高,比如‘青山’这个名字,只需要扫描一行。

ALTER TABLE user_innodb drop index index_user_name; ALTER TABLE user_innodb ADD INDEX inx_user_name(name); EXPLAIN SELECT * FROM user_innodb WHERE name='青山';

id

select_type

table

partitions

type

possible_keys

key

key_len

ref

rows

1

SIMPLE

user_innodb

ref

idx_user_gender

idx_user_gender

1023

const

1

查看表上的索引,Cardinality 代表基数,代表预估的不重复的值的数量。索引的基数与表总行数越接近,列的离散度就越高。 show indexes from user_innodb;

如果在B+Tree里面的重复值太多,MYSQL的优化器发现走索引跟使用全表扫描差不多的时候,就算建了索引,也不一定会走索引。 https://www.cs.usfca.edu/~galles/visualization/BPlusTree.html

这个给我们的启示是什么?建立索引,要使用里离散度(选择度)更高的字段。

联合索引最左匹配

前面我们说的都是针对单列创建的索引,但有时候我们的多条件查询的时候,也会建立联合索引。单列索引可以看成是特殊的联合索引。

比如我们在user表上面,给name和phone建立一个联合索引。

ALTER TABLE user_innodb DROP INDEX comidx_name_phone; ALTER TABLE user_innodb ADD INDEX comidx_name_phone(name,phone);
img_1.png

联合索引在B+Tree中是符合的数据结构,它是按照从左到右的顺序来建立搜索树的(name在左边,phone在右边)。

从这张图可以看出,name是有序的,phone是无序的。当name相等时,phone才是有序的。

这个时候我们使用 where name = '青山' and phone = '136×' 去查询数据的时候,B+Tree会优先比较name来确定下一步应该搜索的方向,往左还是往右。如果name相同的时候在比较phone。但是如果查询条件没有name,就不知道第一步应该查哪个节点,因为建立搜索树的时候name是第一个比较因子,所以用不到索引。

什么时候用到联合索引

所以,我们在建立联合索引的时候,一定要把最常用的列放在最左边。

比如下面的三条语句,能用到联合索引吗?

  1. 使用两个字段,可以用到联合索引

EXPLAIN SELECT * FROM user_innodb WHERE name='权亮' AND phone='15204661800'

id

select_type

table

partitions

type

possible_keys

key

key_len

1

SIMPLE

user_innodb

ref

comidx_name_phone

comidx_name_phone

1070

  1. 使用左边的name字段,可以用到联合索引。

EXPLAIN SELECT * FROM user_innodb WHERE name='权亮'

id

select_type

table

partitions

type

possible_keys

key

key_len

ref

1

SIMPLE

user_innodb

ref

comidx_name_phone

comidx_name_phone

1023

const

  1. 使用右边的phone字段,无法使用索引,全表扫描

EXPLAIN SELECT * FROM user_innodb WHERE phone='15204661800'

id

select_type

table

partitions

type

possible_keys

key

key_len

ref

rows

1

SIMPLE

user_innodb

ref

comidx_name_phone

comidx_name_phone

996770

如何创建联合索引

有一天我们的DBA找到我,说我们的项目里面有两个查询很慢。

SELECT * FROM user_innodb WHERE name=? AND phone=?; SELECT * FROM user_innodb WHERE name=?;

按照我们的想法,一个查询创建一个索引,所以我们针对这两条sql创建两个索引,这种做法觉得正确吗?

CREATE INDEX idx_name on user_innodb(name); CREATE INDEX idx_name_phone on user_innodb(name,phone);

当我们创建一个联合索引的时候,按照最左匹配原则,用左边的字段name查询的时候,也能用到索引,所以第一个索引完全没有必要。

相当于建立了两个索引(name),(name,phone)

如果我们创建三个字段的索引index(a,b,c),相当于创建三个索引: index(a);index(a,b);index(a,b,c)

用where b=?和where b=? and c=? 和 where a=? and c=?是不能使用到索引的。不能不用第一个字段,不能中断。

这里就是mysql联合索引的最左匹配原则。

覆盖索引

回表:非主键索引,我们先通过索引找到主键索引的键值,再通过主键值查处索引里面没有的数据,它比基于主键索引的查询多扫描了一颗索引树,这个过程就叫回表。

例如:select * from user_innodb where name = '青山';

img_1.png

在辅助索引里面,不管是单列索引还是联合索引,如果select的数据列只用从索引中就能够取得,不必从数据区中取,这个时候使用的索引就叫做覆盖索引,这样就避免了回表。

我们先来创建联合索引:

--- 创建联合索引 ALTER TABLE user_innodb drop index comidx_name_phone; alter table user_innodb add index comidx_name_phone(name,phone);

这三个查询语句都用到了覆盖索引:

explain select name,phone from user_innodb where name='青山' and phone='13666666666'; explain select name from user_innodb where name='青山' and phone='13666666666'; explain select phone from user_innodb where name='青山' and phone='1366666666';

extra里面值为“Using index”代表使用了覆盖索引。

select_type

table

type

possible

key

key_len

ref

rows

filtered

Extra

SIMPLE

user_innodb

ref

comidx_name_phone

comidx_name_phone

1070

const,const

1

100

Using where;Using index

select * ,用不到覆盖索引。

如果改成只用where phone = 查询呢?动手试试?

很明显,因为覆盖索引减少了io次数,减少了数据的访问量,可以大大提升查询效率。

索引条件下推(ICP)

https://dev.mysql.com/doc/refman/5.7/en/index-condition-pushdown-optimization.html

再来看这么一张表,在last_name和first_name上面创建联合索引。

drop talbe employees; create table exployees( emp_no int(11) not null, birth_date date null, first_name varchar(14) not null, last_name varchar(16) not null, gender enum('M','F') not null, hire_date date null, primary key(emp_no) )engine=InnoDB default charset=latin1; alter table employees add index idx_lastname_firstname(last_name,first_name); INSERT INTO `employees`(`emp_no`,`birth_date`,`first_name`,`last_name`,`gender`,`hire_date`)VALUES(1,NULL,'698','liu','F',NULL); INSERTINTO`employees`(`emp_no`,`birth_date`,`first_name`,`last_name`,`gender`,`hire_date`)VALUES(2,NULL,'d99','zheng','F',NULL); INSERTINTO`employees`(`emp_no`,`birth_date`,`first_name`,`last_name`,`gender`,`hire_date`)VALUES(3,NULL,'e08','huang','F',NULL); INSERTINTO`employees`(`emp_no`,`birth_date`,`first_name`,`last_name`,`gender`,`hire_date`)VALUES(4,NULL,'59d','lu','F',NULL); INSERTINTO`employees`(`emp_no`,`birth_date`,`first_name`,`last_name`,`gender`,`hire_date`)VALUES(5,NULL,'0dc','yu','F',NULL); INSERTINTO`employees`(`emp_no`,`birth_date`,`first_name`,`last_name`,`gender`,`hire_date`)VALUES(6,NULL,'989','wang','F',NULL); INSERTINTO`employees`(`emp_no`,`birth_date`,`first_name`,`last_name`,`gender`,`hire_date`)VALUES(7,NULL,'e38','wang','F',NULL); INSERTINTO`employees`(`emp_no`,`birth_date`,`first_name`,`last_name`,`gender`,`hire_date`)VALUES(8,NULL,'0zi','wang','F',NULL); INSERTINTO`employees`(`emp_no`,`birth_date`,`first_name`,`last_name`,`gender`,`hire_date`)VALUES(9,NULL,'dc9','xie','F',NULL); INSERTINTO`employees`(`emp_no`,`birth_date`,`first_name`,`last_name`,`gender`,`hire_date`)VALUES(10,NULL,'5ba','zhou','F',NULL);

关闭ICP:

set optimizer_switch='index_condition_pushdown=off'

查看参数

show variables like 'optimizer_switch';

现在我们要查询所有姓wang,并且名字最后一个字是zi的员工,比如王胖子、王瘦子。查询的sql:

select * from employees where last_name='wang' and first_name like '%zi'

这条sql有两种执行方式:

  1. 根据联合索引查处所有行wang的二级索引数据,然后回表,到主键索引上查询全部符合条件的数据(3条数据)。然后返回给server层,在server层过滤出名字以zi结尾的员工。

  2. 根据联合索引查处所有姓wang的二级索引数据(3个索引),然后从二级索引中筛选出first_name以zi结尾的索引(1个索引),然后再回表,到主键索引上查询全部符合条件的数据(1条数据),返回给server层。

    img_1.png

很明显,第二种方式到主键索引上查询的数据更少。

注意,索引的比较实在存储进行的,数据记录的比较,是在server层进行的。而当first_name的条件不能用于索引过滤,server层不会把first_name的条件传递给存储引擎,所以读取了两条没有必要的记录。

这时候,如果满足last_name='wang'的记录有100000条,就会有99999条没有必要读取的记录。

执行以下sql,Using where:

explain select * from employees where last_name='wang' and first_name like '%zi';

id

select_type

table

partitions

type

possible_keys

key

key_len

ref

rows

filtered

Extra

1

SIMPLE

employees

ref

idx_lastname_firstname

idx_lastname_firstname

18

const

3

11.11

Using where

Using where 代表从存储引擎取回来的数据部全部满足条件,需要在server层过滤。

先用last_name条件进行索引范围扫描,读取数据表记录,然后进行比较,检查是否符合first_name like '%zi'的条件。此时3条中只有一条符合条件。

开启ICP:

set optimizer_switch = 'indexcondistion_pushdown=0'

此时执行计划,Using index condition: |id|select_type|table|partitions|type|possible_keys|key|key_len|ref|rows|filtered|Extra| |---|---|---|---|---|---|---|---|---|---|---|---| |1|SIMPLE|employees||ref|idx_lastname_firstname|idx_lastname_firstname|18|const|3|11.11|Using index condition| 把first_name like '%zi'下推给存储引擎后,只会从数据表读取所需的1条记录

索引条件下推(index condition pushdown),5.6以后完善的功能。只是用于二级索引。ICP的目标是减少访问表的完整行的读取量从而减少io操作。

索引的创建和使用

因为索引对于改善查询性能的作用是巨大的,所以我们的目标是尽量使用索引。

索引的创建

  1. 在用于where判断order排序和join的(on)字段上创建索引。

  2. 索引的个数不要过多。--浪费空间,更新变慢。

  3. 区分度低的字段,例如性别,不要建立索引。--离散度太低,导致扫描行数过多。

  4. 频繁更新的值,不要作为主键或者索引。--页分裂

  5. 组合索引把散列性高的值放在前面。

  6. 创建复合索引,而不是修改单列索引。

  7. 过长的字段,怎么建立索引

  8. 为什么不建议用无序的值(例如身份证、uuid)作为索引?

什么时候用不到索引?

  1. 索引列上使用函数(replace\substr\concat\sum count avg)、表达式、计算(+ - × /):

explain select * from t2 where id+1 = 4;
  1. 字符串不加引号,出现隐式转换

alter table user_innodb drop index comidx_name_phone; alter table user_innodb add index comidx_name_phone(name,phone); explain select * from user_innodb where name=136; explain select * from user_innodb where name='136';
  1. like条件中前面带% where条件中like abc%,like %2673%,like %8888都用不到索引吗?为什么?

explain select * from user_innodb where name like 'wang%'; explain select * from user_innodb where name like '%wang';

过滤的开销太大,所以无法使用索引。这个时候可以用全文检索。 4. 负向查询 not like 不能:

explain select * from employees where last_name not like 'wang';

!= (<>)和NOT IN在某些情况下可以:

explain select * from employees where emp_no not in(1); explain select * from employees where emp_no <>1;

注意一个sql语句是否使用索引,跟数据库版本、数据量、数据选择都有关系。

其实,用不用索引,最终都是所有其说了算。

优化器是基于什么的优化器?

基于cost开销(cost baser optimizer),它不是基于规则(Rule-Based Optimizer),也不是基于语法。怎么样开销小就怎么来。 https://dev.mysql.com/doc/refman/5.7/en/cost-model.html https://docs.oracle.com/cd/B10501_01/server.920/a96533/rbo.htm#38960

13 February 2026