MySQL索引详解

本文主要介绍MySQL索引底层原理及优化,理解SQL是如何执行,MySQL如何选择合适的索引以及时间都消耗在哪些地方,再加上一些优化的知识,可以帮助大家更好的理解MySQL,理解常见优化技巧背后的原理。希望本文中的原理、示例能够帮助大家更好的将理论和实践联系起来,更多的将理论知识运用到实践中。

一、MySQL存储引擎


2.1 存储引擎

负责数据的存储和提取,存储引擎是可以多选的,支持InnoDB、MyISAM、Memory等,现在最常用的存储引擎是InnoDB,它从MySQL 5.5.5版本开始成为了默认存储引擎。

2.1.1 InnoDB

InnoDB是事务型数据库的首选引擎,给MySQL提供了具有事务、回滚和崩溃修复能力、多版本并发控制的事务安全型表,支持行级锁定。InnoDB的整个体系架构就是由多个内存块组成的缓冲池及多个后台线程构成。缓冲池缓存磁盘数据(解决cpu读取速度和磁盘读取速度的严重不匹配问题),后台进程保证缓冲池和磁盘数据的一致性(读取、刷新),并保证数据异常宕机时能恢复到正常状态。

InnoDB体系架构图:

MySQL索引详解_第1张图片

「Dirty Page(脏页)」 表示此数据页【已被使用】且【已经被修改】,数据页中数据和磁盘上的数据已经不一致。

后台线程(Thread)

  • • Master Thread
    这是最核心的一个线程,主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括脏页的刷新、合并插入缓冲等。

  • • IO Thread
    在InnoDB存储引擎中大量使用了异步IO来处理IO请求,IO Thread的工作主要是负责这些IO请求的回调处理。

  • • Purge Thread
    事务被提交之后,undo log可能不再需要,因此需要Purge Thread来回收已经使用并分配的undo页。InnoDB支持多个Purge Thread,这样做可以加快undo页的回收。

  • • Page Cleaner Thread
    Page Cleaner Thread是在InnoDB 1.2.x版本新引入的,其作用是将之前脏页的刷新操作都放入单独的线程中来完成,这样减轻了Master Thread的工作及对于用户查询线程的阻塞。

内存(In-Memory Structure)

内存中的结构主要包括Buffer Pool,Change Buffer、Adaptive Hash Index以及Log Buffer四部分。

如果从内存上来看,Change Buffer 和 Adaptive Hash Index占用的内存都属于Buffer Pool,Log Buffer占用的内存与Buffer Pool独立。

  • • Buffer Pool
    缓冲池缓存的数据包括Data page、Index page、Data Dictionary 等,通常MySQL服务器的80%的物理内存会分配给Buffer Pool。基于效率考虑,InnoDB中数据管理的最小单位为页,默认每页大小为16KB,每页包含若干行数据。为了提高缓存管理效率,InnoDB的缓存池通过页链表实现,很少访问的页会通过缓存池的LRU算法淘汰出去,也就是LRU链表。LRU链表分为两部分:young区域和old区域,从磁盘读取数据页会加入到LRU链表的old区域头部,从old区域数据页中读取行记录进行where进行匹配,old区域中的页被访问,就会将数据页加入到young区域头部,由于是全表扫描(因此所有数据都会被逐个加入young区域头部,从而替换淘汰原有的young区域数据)。

MySQL索引详解_第2张图片
  • • Change Buffer
    通常来说,InnoDB辅助索引不同于聚集索引的顺序插入,如果每次修改二级索引都直接写入磁盘,则会有大量频繁的随机IO。Change buffer的主要目的是将对非唯一辅助索引页的操作缓存下来,以此减少辅助索引的随机IO,并达到操作合并的效果。它会占用部分Buffer Pool的内存空间。如果辅助索引页已经在缓冲区了,则直接修改即可;如果不在,则先将修改保存到Change Buffer。Change Buffer的数据在对应辅助索引页读取到缓冲区时合并到真正的辅助索引页中。Change Buffer内部实现也是使用的B+树,可以通过innodb_change_buffering配置是否缓存辅助索引页的修改,默认为all,即缓存insert/delete-mark/purge操作(注:MySQL删除数据通常分为两步,第一步是delete-mark,即只标记,而purge才是真正的删除数据)。

  • • Adaptive Hash Index
    自适应哈希索引查询非常快,一般时间复杂度为O(1),相比B+树通常要查询 3~4次,效率会有很大提升。innodb通过观察索引页上的查询次数,如果发现建立哈希索引可以提升查询效率,则会自动建立哈希索引,称之为自适应哈希索引,不需要人工干预,可以通过innodb_adaptive_hash_index开启,MySQL5.7 默认开启。

  • • Log Buffer
    Log Buffer是重做日志在内存中的缓冲区,大小由innodb_log_buffer_size 定义,默认是16M。一个大的Log Buffer可以让大事务在提交前不必将日志中途刷到磁盘,可以提高效率。如果你的系统有很多修改很多行记录的大事务,可以增大该值。

磁盘(On-Disk Structure)

  • • 系统表空间
    分为ibd文件、undo日志等。

  • • Redo日志
    存储的就是Log Buffer刷到磁盘的数据。

2.1.2 MyISAM

在MySQL5.1及之前的版本中,MyISAM是默认的存储引擎。它支持3种不同的存储格式,分别是静态表、动态表、压缩表,因为MyISAM可被压缩,存储空间较小,在筛选大量数据时非常快,但MyISAM只支持表级锁,不支持行级锁,也不支持事务和外键。

  • • 静态表
    表中的字段都是非变长字段,这样每个记录都是固定长度的,优点存储非常迅速,容易缓存,出现故障容易恢复;缺点是占用的空间通常比动态表多(因为存储时会按照列的宽度定义补足空格)注意:在取数据的时候,默认会把字段后面的空格去掉,如果不注意会把数据本身带的空格也会忽略。

  • • 动态表
    记录不是固定长度的,这样存储的优点是占用的空间相对较少;缺点:频繁的更新、删除数据容易产生碎片,需要定期执行optimize table命令重新利用未使用的空间来改善性能。

  • • 压缩表
    因为每个记录是被单独压缩的,所以只有非常小的访问开支。

2.1.3 Memory

Memory基于内存的存储引擎,所有数据置于内存来创建表,每个Memory表实际只对应一个磁盘文件,格式是.frm。它拥有极高的插入,更新和查询效率。但是会占用和数据量成正比的内存空间,并且其内容会在MySQL重新启动时丢失。

二、索引基本介绍


2.1 什么是索引

在数据库中,索引被定义为一种特殊的数据结构,由数据库中的一列或多列组合而成,可以用来快速查询数据表中某一特定值的记录,就像一本书的目录一样。索引是在表的字段的基础上建立的一种数据库对象,它由DBA或者表的拥有者创建或撤销,他是创建表与表之间关联关系的基础。

2.2 索引优缺点

索引的优点

  • • 可以大大加快数据的检索速度,这也是创建索引的最主要的原因。

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

索引的缺点

  • • 时间方面:创建索引和维护索引要耗费时间,具体地,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,会降低增/改/删的执行效率;

  • • 空间方面:索引需要占物理空间。

2.3 索引数据结构

二叉树

每个节点有两个子节点,数据量的增大必然导致高度的快速增加,对那种逐渐增大的数据查询相当于链表查询,效率低下,显然这个不适合作为大量数据存储的基础结构。

MySQL索引详解_第3张图片

MySQL索引详解_第4张图片

Hash

对索引的key进行一次hash计算就可以定位出数据存储的位置,很多时候Hash索引要比B+树索引更高效仅能满足 “=”,“IN”,不支持范围查询,有时候会有hash冲突问题。

MySQL索引详解_第5张图片

B Tree

叶节点具有相同的深度,叶节点的指针为空所有索引元素不重复 ,一块空间存储一个kv结构,节点中的数据从左到右递增排列,非叶子节点可以存储键和data。

MySQL索引详解_第6张图片

B+Tree

B+Tree只在叶子节点存储数据,所以B+Tree的单个节点的数据量更小,只存储索引,可以放更多的索引,在相同的磁盘I/O次数下,就能查询更多的节点。另外,B+Tree叶子节点采用的是双指针连接,适合MySQL中常见的基于范围的顺序查找,一个节点可以放16kb,1170个元素,1170*1170*16=2千多万,高度为3,即使是2千万数据,B+树的高度为3,这样我们查找数据的效率会比较高,高度是由非叶子节点能放多少个索引元素决定的。

SHOW GLOBAL STATUS LIKE ``'Innodb_page_size'

MySQL索引详解_第7张图片

2.3.2 数据结构对比

B+Tree vs B Tree

B+Tree只在叶子节点存储数据,而B树的非叶子节点也要存储数据,所以B+Tree的单个节点的数据量更小,在相同的磁盘 I/O 次数下,就能查询更多的节点。另外,B+Tree叶子节点采用的是双指针连接,适合MySQL中常见的基于范围的顺序查找,而B树无法做到这一点。

B+Tree vs 二叉树

B+Tree即使数据达到千万级别时高度依然维持在3层左右,也就是说一次数据查询操作只需要做3次的磁盘 I/O 操作就能查询到目标数据。而二叉树的每个父节点有两个子节点,数据量的增大必然导致高度的快速增加,对那种逐渐增大的数据查询相当于链表查询,效率低下。

B+Tree vs Hash

Hash在做等值查询的时候效率很快,搜索复杂度为 O(1)。但是Hash表不适合做范围查询,它更适合做等值的查询,这也是B+Tree索引要比Hash索引有着更广泛的适用场景的原因。

2.4 索引分类

你知道索引有哪些吗?大家肯定都能说出聚集索引、主键索引、二级索引、普通索引、唯一索引、hash索引、B+树索引等等。

然后再问你,你能将这些索引分一下类吗?可能大家就有点模糊了。其实,要对这些索引进行分类,要清楚这些索引的使用和实现方式,然后再针对有相同特点的索引归为一类。

我们可以按照四个角度来分类索引。

分类

索引名称

数据结构

B+Tree索引

Hash索引

物理存储

聚集索引

二级索引

字段特性

主键索引

唯一索引

普通索引

前缀索引

字段个数

单列索引

联合索引

接下来,按照这些角度来说说各类索引的特点。

2.4.1 按数据结构来分类

从数据结构的角度来看,MySQL常见索引有B+Tree索引、Hash索引。

B+Tree索引

B+Tree只在叶子节点存储数据,所以B+Tree的单个节点的数据量更小,只存储索引,可以放更多的索引,在相同的磁盘I/O次数下,就能查询更多的节点。另外,B+Tree叶子节点采用的是双指针连接,适合MySQL中常见的基于范围的顺序查找,一个节点可以放16kb,1170个元素,1170*1170*16=2千万,高度为3,即使是2千万数据,B+树的高度为3,这样我们查找数据的效率会比较高,高度是由非叶子节点能放多少个索引元素决定的。

Hash 索引

对索引的key进行一次hash计算就可以定位出数据存储的位置,很多时候Hash索引要比B+树索引更高效,仅能满足 “=”,“IN”,不支持范围查询,还会有hash冲突问题,它更适合做等值的查询。

2.4.2 按物理存储分类

从物理存储的角度来看,索引分为聚集索引(主键索引)、二级索引(辅助索引)。

主键索引

主键索引的B+Tree的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的B+Tree的叶子节点里。

二级索引

二级索引的B+Tree的叶子节点存放的是主键值,而不是实际数据。所以,在查询时使用了二级索引,如果查询的数据能在二级索引里查询的到,那么就不需要回表,这个过程就是覆盖索引。如果查询的数据不在二级索引里,就会先检索二级索引,找到对应的叶子节点,获取到主键值后,然后再检索主键索引,就能查询到数据了,这个过程就是回表。

2.4.3 按字段特性分类

从字段特性的角度来看,索引分为主键索引、唯一索引、普通索引、前缀索引。

主键索引

主键索引就是建立在主键字段上的索引,通常在创建表的时候一起创建,一张表最多只有一个主键索引,索引列的值不允许有空值。

在创建表时,创建主键索引的方式如下:

CREATETABLE table_name (....PRIMARY KEY (index_column_1) USING BTREE);

唯一索引

唯一索引建立在唯一字段上的索引,一张表可以有多个唯一索引,索引列的值必须唯一,但是允许有空值。

在创建表时,创建唯一索引的方式如下:

CREATETABLE table_name (....UNIQUEKEY(index_column_1,index_column_2,...));

建表后,如果要创建唯一索引,可以使用这面这条命令:

CREATE UNIQUE INDEX index_name ON table_name(index_column_1,index_column_2,...);

普通索引

普通索引就是建立在普通字段上的索引,既不要求字段为主键,也不要求字段唯一。

在创建表时,创建普通索引的方式如下:

CREATETABLE table_name (....INDEX(index_column_1,index_column_2,...));

建表后,如果要创建普通索引,可以使用这面这条命令:

CREATEINDEX index_name ONtable_name(index_column_1,index_column_2,...);

前缀索引

前缀索引是指对字符类型字段的前几个字符建立的索引,而不是在整个字段上建立的索引,前缀索引可以建立在字段类型为 char、 varchar、binary、varbinary 的列上。使用前缀索引的目的是为了减少索引占用的存储空间,提升查询效率。

在创建表时,创建前缀索引的方式如下:

CREATETABLE table_name(column_list,INDEX(column_name(length)));

建表后,如果要创建前缀索引,可以使用这面这条命令:

CREATEINDEX index_name ON table_name(column_name(length));

2.4.4 按字段个数分类

从字段个数的角度来看,索引分为单列索引、联合索引(复合索引)。

单列索引

建立在单列上的索引称为单列索引,比如主键索引。

联合索引

通过将多个字段组合成一个索引,该索引就被称为联合索引。比如将employees表中的name、age、position字段组合成联合索引(name,age,position),创建联合索引的方式如下:

CREATEINDEX idx_name_age_position ON employees(name,age,position);
MySQL索引详解_第8张图片

可以看到,联合索引的非叶子节点保持了三个字段的值作为B+Tree的key值。当在联合索引查询数据时,是先按name进行排序,如果name相同的情况再按age字段排序,如果age还相同就在按position字段排序,如果全部相等的话就根据主键去回表查询数据。因此,使用联合索引时,存在最左匹配原则,也就是按照最左优先的方式进行索引的匹配。

另外,建立联合索引时的字段顺序,对索引效率也有很大影响。越靠前的字段被用于索引过滤的概率越高,实际开发工作中建立联合索引时,要把区分度大的字段排在前面,这样区分度大的字段越有可能被更多的SQL使用到。

我们来看一个示例,针对下面这条SQL,怎么通过索引来提高查询效率呢?

SELECT * FROM employees WHERE age=``22orderby create_time;

有的同学会认为,单独给age建立一个索引就可以了。

但是更好的方式给age和create_time列建立一个联合索引,因为这样可以避免MySQL数据库发生文件排序。因为在查询时,如果只用到age的索引,但是这条语句还要对create_time排序,这时就要用文件排序,也就是在SQL执行计划中,Extra列会出现Using filesort。所以,要利用索引的有序性,在age和create_time列建立联合索引,这样根据age筛选后的数据就是按照create_time排好序的,避免在文件排序,提高了查询效率。

三、MySQL存储引擎索引实现


3.1 InnoDB索引实现

表数据文件本身就是按B+Tree组织的一个索引结构文件,聚集索引-叶节点包含了完整的数据记录,也就是把索引所在列的数据都放在了一起。

使用InnoDB引擎创建数据表,将产生2个文件,文件的名字以表名字开始,扩展名之处文件类型:frm文件存储表定义,数据文件的扩展名为.ibd。

InnoDB采用的聚集索引,聚集索引默认由主键实现(用主键作为B+树的key,并且把数据行绑定在叶子节点)

如果表中没有定义主键,InnoDB会选择一个唯一且非空的列代替,如果没有这样的列,InnoDB会隐式定义一个主键(类似oracle中的RowId)来作为聚集索引。

ibd文件结构:

读写操作:

将内存中的数据刷到磁盘,或者将磁盘中的数据加载到内存,都是以批次为单位,这个批次就是我们常说的:数据页

数据页:

主要是用来存储表中记录的,它在磁盘中是用双向链表相连的,方便查找,能够非常快速得从一个数据页,定位到另一个数据页,数据页主要包含如下几个部分:

  • 文件头部

  • 页头部

  • 最大和最小记录

  • 用户记录

  • 空闲空间

  • 页目录

  • 文件尾部

InnoDB数据页结构图:

总结:

多个数据页之间通过页号构成了双向链表。而每一个数据页的行数据之间,又通过下一条记录的位置构成了单项链表。

整体结构图:

由于记录不断增多,一层目录也就不能满足我们的需求,在原来目录页的基础之上我们可以生成再高一级的目录页,生成多层级的目录,这样的结构就是B+树,ibd文件就是这样存储的。

  • 为什么建议InnoDB表必须建主键,并且推荐使用整型的自增主键?

  • 为什么非主键索引结构叶子节点存储的是主键值?(一致性和节省存储空间)

MySQL索引详解_第9张图片
MySQL索引详解_第10张图片

3.2 MyISAM索引实现

MyISAM把索引和数据分成了两个文件,这样我们查询的时候需要先去索引文件拿到数据文件的地址,然后根据这个地址在去数据文件里获取具体数据。

使用MyISAM引擎创建数据库,将产生3个文件,文件的名字以表名字开始,扩展名之处文件类型:.frm文件存储表定义,.MYD(MYData)数据文件,.MYI(MYIndex)索引文件。

MyISAM是采用的非聚集索引,非聚集索引将数据和索引分开存储的,索引结构的叶子节点存储了这条记录的磁盘的地址,MyISAM通过key_buffer把索引先缓存到内存中,当需要访问数据时(通过索引访问数据),在内存中直接搜索索引,然后通过索引找到磁盘相应数据,这也就是为什么索引不在key buffer命中时,速度慢的原因。

四、Explain


4.1 Explain使用场景

使用Explain关键字可以模拟优化器执行SQL语句,分析你的查询语句的性能瓶颈 ,在SELECT语句之前增加Explain关键字,MySQL会在查询上设置一个标记,执行查询会返回执行计划的信息,而不是执行这条SQL。Explain适用于SELECT、DELETE、INSERT、REPLACE和UPDATE语句。

注意:如果FROM中包含子查询,仍会执行该子查询,将结果放入临时表中。

monitor_main表结构:

CREATE TABLE `monitor_main` (
  `id` bigint(30) NOT NULL AUTO_INCREMENT,
  `index_id` bigint(30) DEFAULT NULL,
  `code` varchar(100) DEFAULT NULL,
  `name` varchar(100) DEFAULT NULL,
  `function` varchar(100) DEFAULT NULL,
  `start_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  `end_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  `value` bigint(30) DEFAULT NULL,
  `created_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  `updated_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_value` (`value`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2711 DEFAULT CHARSET=utf8mb4;

monitor_map表结构:

CREATE TABLE `monitor_map` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `main_id` bigint(20) DEFAULT NULL,
  `map` varchar(255) DEFAULT NULL,
  `mapkey` varchar(255) DEFAULT NULL,
  `mapvalue` varchar(255) DEFAULT NULL,
  `created_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  `updated_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_main_id` (`main_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2914 DEFAULT CHARSET=utf8mb4;

4.2 Explain字段含义

id列

id列的编号是select的序列号,有几个select就有几个id,并且id的顺序是按select出现的顺序增长的,id列越大执行优先级越高,id相同则从上往下执行,id为NULL最后执行。

select_type列

select_type表示对应行是简单还是复杂的查询。

  • simple:简单查询,查询不包含子查询和union。

  • primary:复杂查询中最外层的select。

  • subquery:包含在select 中的子查询(不在from子句中)。

  • derived:包含在from子句中的子查询,MySQL会将结果存放在一个临时表中,也称为派生表。

table列

这一列表示Explain的一行正在访问哪个表,当from子句中有子查询时,table列是格式,表示当前查询依赖id=N的查询,于是先执行id=N的查询。

type列

这一列表示关联类型或访问类型 ,即MySQL决定如何查找表中的行,查找数据行记录的大概范围,依次从最优到最差分别为:system > const > eq_ref > ref > range > index > ALL,一般来说,得保证查询达到range级别,最好达到ref。

  • NULL:MySQL能够在优化阶段分解查询语句,在执行阶段用不着再访问表或索引。例如:在索引列中选取最小值,可以单独查找索引来完成,不需要在执行时访问表。

EXPLAIN SELECT min(monitor_main.id) FROM monitor_main;

  • const, system:MySQL能对查询的某部分进行优化并将其转化成一个常量。用于primary key或unique key的所有列与常数比较时,所以表最多有一个匹配行,读取1次,速度比较快。system是const的特例 ,表里只有一条数据匹配时为system。

EXPLAIN SELECT * FROM monitor_main WHERE monitor_main.id=``2400``;

  • eq_ref:primary key 或unique key索引的所有部分被连接使用 ,最多只会返回一条符合条件的记录,这可能是在const之外最好的连接类型了,简单的select查询不会出现这type。

EXPLAIN SELECT * FROM monitor_main LEFT JOIN monitor_map on monitor_main.id=monitor_map.id;
  • ref:相比eq_ref,不使用唯一索引,而是使用普通索引或者唯一性索引的部分前缀,索引要和某个值相比较,可能会找到多个符合条件的行,简单select查询,monitor_main.value是普通索引(非唯一索引)。

EXPLAIN SELECT * FROM monitor_main WHERE monitor_main.value=``1400``;

  • range:范围扫描通常出现在in(),between ,> ,<,>=等操作中,使用一个索引来检索给定范围的行。

EXPLAIN SELECT * FROM monitor_main WHERE monitor_main.id>``2520``;

  • index:扫描全索引就能拿到结果,一般是扫描某个二级索引,这种扫描不会从索引树根节点开始快速查找,而是直接对二级索引的叶子节点遍历和扫描,速度还是比较慢的,这种查询一般为使用覆盖索引,二级索引一般比较小,所以这种通常比ALL快一些。

EXPLAIN SELECT monitor_main.value FROM monitor_main;

  • ALL:即全表扫描,扫描你的聚集索引的所有叶子节点,通常情况下这需要增加索引来进行优化了。

EXPLAIN SELECT * FROM monitor_main;

possible_keys列

这一列显示查询可能使用哪些索引来查找。Explain时可能出现possible_keys有列,而key显示NULL的情况,这种情况是因为表中数据不多,MySQL认为索引对此查询帮助不大,选择了全表查询,如果该列是NULL,则没有相关的索引,在这种情况下,可以通过检查where子句看是否可以创造一个适当的索引来提高查询性能,然后用Explain查看效果。

key列

这一列显示MySQL实际采用哪个索引来优化对该表的访问,如果没有使用索引,则该列是NULL,如果想强制MySQL使用possible_keys列中的索引,在查询中使用force index。

key_len列

这一列显示了MySQL在索引里使用的字节数,通过这个值可以算出具体使用了索引中的哪些列。

key_len计算规则如下:

  • 字符串:char(n):如果存汉字长度就是3n字节 ;varchar(n):如果存汉字则长度是3n+2字节,加的2字节用来存储字符串长度,因为varchar是变长字符串。

  • 数值类型:tinyint: 1字节 smallint:2字节 int:4字节 bigint:8字节

  • 时间类型:date: 3字节 timestamp:4字节 datetime:8字节

ref列

这一列显示了在key列记录的索引中,表查找值所用到的列或常量,常见的有:const(常量)。

rows列

这一列是MySQL估计要读取并检测的行数,注意这个不是结果集里的行数。

Extra列

这一列展示的是额外信息。

  • Using index:使用覆盖索引,MySQL执行计划Explain结果里的key有使用索引,如果select后面查询的字段都可以从这个索引的树中获取,这种情况一般可以说是用到了覆盖索引,extra里一般都有Using index;覆盖索引一般针对的是辅助索引,整个查询结果只通过辅助索引就能拿到结果,不需要通过辅助索引树找到主键,再通过主键去主键索引树里获取其它字段值。

  • Using where:使用where语句来处理结果,并且查询的列未被索引覆盖。

  • Using filesort:将用外部排序而不是索引排序,数据较小时从内存排序,否则需要在磁盘完成排序。这种情况下一般也是要考虑使用索引来优化的。

五、索引优化


5.1 代码先行,索引后上

不知大家一般是怎么给数据表建立索引的,是建完表马上就建立索引吗?这其实是不对的,一般应该等到主体业务功能开发完毕,把涉及到该表相关sql都要拿出来分析之后再建立索引。

5.2 联合索引尽量覆盖条件

比如可以设计一个或者两三个联合索引(尽量少建单值索引),让每一个联合索引都尽量去包含sql语句里的where、order by、group by的字段,还要确保这些联合索引的字段顺序尽量满足sql查询的最左前缀原则。

5.3 不要在小基数字段上建立索引

索引基数是指这个字段在表里总共有多少个不同的值,比如一张表总共100万行记录,其中有个性别字段,其值不是男就是女,那么该字段的基数就是2。如果对这种小基数字段建立索引的话,还不如全表扫描了,因为你的索引树里就包含男和女两种值,根本没法进行快速的二分查找,那用索引就没有太大的意义了。一般建立索引,尽量使用那些基数比较大的字段,就是值比较多的字段,那么才能发挥出B+树快速二分查找的优势来。

5.4 长字符串采用前缀索引

尽量对字段类型较小的列设计索引,比如说什么tinyint之类的,因为字段类型较小的话,占用磁盘空间也会比较小,此时你在搜索的时候性能也会比较好一点。当然,这个所谓的字段类型小一点的列,也不是绝对的,很多时候你就是要针对varchar(255)这种字段建立索引,哪怕多占用一些磁盘空间也是有必要的。对于这种varchar(255)的大字段可能会比较占用磁盘空间,可以稍微优化下,比如针对这个字段的前20个字符建立索引,就是说,对这个字段里的每个值的前20个字符放在索引树里,类似于 KEY index(name(20),age,position)。此时你在where条件里搜索的时候,如果是根据name字段来搜索,那么此时就会先到索引树里根据name字段的前20个字符去搜索,定位到之后前20个字符的前缀匹配的部分数据之后,再回到聚簇索引提取出来完整的name字段值进行比对。但是假如你要是order by name,那么此时你的name因为在索引树里仅仅包含了前20个字符,所以这个排序是没法用上索引的,group by也是同理。

5.5 where与order by冲突时优先where

在where和order by出现索引设计冲突时,到底是针对where去设计索引,还是针对order by设计索引?到底是让where去用上索引,还是让order by用上索引?一般这种时候往往都是让where条件去使用索引来快速筛选出来一部分指定的数据,接着再进行排序。因为大多数情况基于索引进行where筛选往往可以最快速度筛选出你要的少部分数据,然后做排序的成本可能会小很多。

5.6 常见索引优化

全值匹配

分析:全值匹配 ,就是对索引中的所有列指定具体值,这种SQL一般效率都比较高,访问类型是ref级别,如果是联合索引就尽量把索引列都指定具体值,这样效率更高一些,因为区分度会更高。

EXPLAIN SELECT * FROM employees WHEREname='LiLei';
EXPLAIN SELECT * FROM employees WHEREname='LiLei'AND age =22;
EXPLAIN SELECT * FROM employees WHERE name='LiLei'AND age = 22 
AND position ='manage r';

最左前缀法则

分析:如果索引了多列,要遵守最左前缀法则,指的是查询从索引的最左前列开始并且不跳过索引中的列。最左前缀原则是针对联合索引的,它的底层是一个B+树,但键值数是大于1的,而构建一个B+树就只能根据一个键值来进行,所以数据库依据联合索引最左的字段来构建B+树,是按照联合索引从左到右排好序的,如果跳过前面的,这样后面的就不是有序的了,查询就需要全表扫描。

EXPLAIN SELECT * FROM employees WHERE name ='Bill' and age =31;
EXPLAIN SELECT * FROM employees WHERE age =30 AND position ='dev';
EXPLAIN SELECT * FROM employees WHERE position ='manager';

不在索引列上做操作

分析:计算、函数等操作可能会导致索引失效而转向全表扫描,比如下面这个SQL截取name左边的三位,这样就不是有序了,走不了索引。

EXPLAIN SELECT * FROM employees WHERE name = 'LiLei';
EXPLAIN SELECT * FROM employees WHERE left(name,3)  = 'LiLei';

不能使用索引中范围条件右边的列

分析:在联合索引中,如果前面索引列使用了范围,那么后面的索引列就走不了索引,这是因为在索引树中前面的是范围,后面的就不一定是有序的。

EXPLAIN SELECT * FROM employees WHERE name= 'LiLei'AND age =22AND position ='manage r';
EXPLAIN SELECT * FROM employees WHERE name='LiLei' AND age >22 AND position ='manage r';

尽量使用覆盖索引,减少 select * 语句

分析:查询的时候指明具体的字段,尽量被联合索引覆盖掉,如果要查的是全部列的话,数据量又特别大,可以考虑使用搜索引擎。

select * 的坏处:

  • 使用 * 号查询,会查询出多个我们不需要的字段,增加sql执行的时间,同时大量的多余字段,会增加网络开销。

  • 失去MySQL优化器“覆盖索引”策略优化的可能性。

  • 对于无用的大字段,如 varchar、blob、text,会增加 io 操作。

EXPLAIN SELECT name``,age ``FROM employees ``WHERE name``=``'LiLei'AND age =  23``AND position =``'manager'``;

尽量不使用!=,not in ,not exists

分析:因为这些操作无法使用索引可能会导致全表扫描 ,!=的结果集可能会很大,走索引也和全表扫描差不多,这样MySQL优化器会根据检索比例、表大小等多个因素整体评估是否使用索引。

EXPLAIN SELECT * FROM employees WHERE name !=``'LiLei'``;

is null,is not null 一般也无法使用索引

分析:虽然这个字段是索引字段,但是也可能不会走索引,对于null的字段,在索引树中会集中起来处理,统一在左端或者右端。

EXPLAIN SELECT * FROM employees WHERE name is``null``;

like不要以%开头

分析:like以通配符开头(’%Lei’)MySQL索引失效会变成全表扫描操作,%号在前就意味着前面还有很多其他的字符串,跳过这些字符串在整个索引树里面就不是有序的了,定位不到,没办法用索引;%号在后等于是用了%号前面的字符串,这些字符串在整个索引树里面是有序的,所以能走索引。

EXPLAIN SELECT * FROM employees WHERE name like'%Lei';
EXPLAIN SELECT * FROM employees WHERE name like'Lei%';

问题:解决like’%字符串%’索引不被使用的方法?

  • 使用覆盖索引,查询字段必须是建立覆盖索引字段。

EXPLAIN SELECT name,age,position FROM employees WHERE name like '%Lei%'``;

  • • 如果不能使用覆盖索引则可能需要借助搜索引擎

字符串不加单引号索引失效

分析:字段类型和值的类型要一致,如果不一致,MySQL有可能会做一个转换,但是有时候也不一定会转换,这样的话就会导致索引失效。

EXPLAIN SELECT * FROM employees WHERE name ='1000';
EXPLAIN SELECT * FROM employees WHERE name =  1000;

少用or或in

因为用它查询时,MySQL不一定使用索引,MySQL内部优化器会根据检索比例、表大小等多个因素整体评估是否使用索引。

EXPLAIN SELECT * FROM employees WHERE name = 'LiLei' or name = 'HanMeimei';

5.7 特殊例子
  • 联合索引第一个字段用范围不会走索引

EXPLAIN SELECT * FROM employees WHERE name >``'LiLei'AND age =``22 AND position =``'manager'``;

分析:联合索引第一个字段就用范围查找不会走索引,MySQL内部可能觉得第一个字段就用范围,结果集应该很大,回表效率不高,还不如就全表扫描。

  • 强制走索引

EXPLAIN SELECT * FROM employees forceindex(idx_name_age_position) WHERE name >``'LiLei'AND age =``22 AND position =``'manager'``;

分析:虽然使用了强制走索引让联合索引第一个字段范围查找也走索引,扫描的行rows看上去也少了点,但是最终查找效率不一定比全表扫描高,因为回表效率不高。

测试:

--  执行时间:0.351
SELECT * FROM employees WHEREname >'LiLei';
--  执行时间:0.7333s
SELECT * FROM employees forceindex(idx_name_age_position) WHERE name >'LiLei';

  • in和or在表数据量比较大的情况会走索引,在表记录不多的情况下会选择全表扫描

EXPLAIN SELECT * FROM employees 
WHERE name in  ('LiLei','HanMeimei','Lucy') 
AND age =22 AND position = 'manager';

EXPLAIN SELECT * FROM employees 
WHERE (name ='LiLei'orname ='HanMeimei') 
AND age =22 AND position = 'manager';
--  做一个小测试,将employees表复制一张employees_copy的表,里面保留两三条记录。
EXPLAIN SELECT * FROM employees_copy WHERE name in  ('LiLei','HanMeimei','Lucy') AND age =22AND position ='manager';


EXPLAINS ELECT * FROM employees_copy WHERE (name ='LiLei'orname ='HanMeimei') AND age =22 AND position ='manager';

分析:在这种情况下数据量小的时候全表扫描比走索引可能还要快一点,所以不会走索引;当数据大的时候,走索引比全表扫描肯定会快一点,所以会走索引。

5.8 深入优化

order by优化

示例一:

EXPLAIN SELECT * FROM employees 
WHERE name=``'LiLei' and position=``'dev' ORDER BY age;

分析:利用最左前缀法则:中间字段不能断,因此查询用到了name索引,从key_len=74也能看出,age索引列用在排序过程中,因为Extra字段里没有Using filesort。

示例二:

EXPLAIN SELECT * FROM employees WHERE age=``22 ORDER BY name,position;

分析:从Explain的执行结果来看:key=null,没有走索引,出现了Using filesort,这是因为不符合最左前缀的原则。

优化总结:

order by满足两种情况会使用Using index,order by语句使用索引最左前列;使用where子句与order by子句条件列组合满足索引最左前列,如果order by的条件不在索引列上,就会产生Using filesort。

count()查询优化

示例:

EXPLAIN selectcount(1)  from employees;
EXPLAIN selectcount(id)  from employees;
EXPLAIN selectcount(name)  from employees;
EXPLAIN selectcount(*)  from employees;

注意:以上4条sql只有根据某个字段count不会统计字段为null值的数据行。

四个sql的执行计划一样,说明这四个sql执行效率应该差不多

字段有索引:count(*)≈count(1)>count(字段)>count(主键 id) //字段有索引,count(字段)统计走二级索引,二级索引存储数据比主键索引少,所以count(字段)>count(主键 id)

字段无索引:count(*)≈count(1)>count(主键 id)>count(字段) //字段没有索引,count(字段)统计走不了索引,count(主键 id)还可以走主键索引,所以count(主键 id)>count(字段)

count(1)跟count(字段)执行过程类似,不过count(1)不需要取出字段统计,就用常量1做统计,count(字段)还需要取出字段,所以理论上count(1)比count(字段)会快一点。

count() 是例外,MySQL并不会把全部字段取出来,而是专门做了优化,不取值,按行累加,效率很高,所以不需要用count(列名)或count(常量)来替代count()。

为什么对于count(id),MySQL最终选择辅助索引而不是主键聚集索引?因为二级索引相对主键索引存储数据更少,检索性能应该更高,mysql内部做了优化。

优化:

  • 查询MySQL自己维护的总行数

对于MyISAM存储引擎的表做不带where条件的count查询性能是很高的,因为MyISAM存储引擎的表的总行数会被MySQL存储在磁盘上,查询不需要计算。

EXPLAIN SELECT COUNT(*) FROM test_myisam;

对于InnoDB存储引擎的表MySQL不会存储表的总记录行数,查询count需要实时计算。

EXPLAIN select count(*)  from employees;

  • show table status

如果只需要知道表总行数的估计值可以用如下sql查询,性能很高。

SHOW TABLE STATUS like``'employees'``;

优化总结:

通过对比发现count()在有索引和无索引的情况都是最优的,推荐使用count()。

分页查询优化

示例:

SELECT * FROM employees LIMIT``9000``,``5``;

表示从表employees中取出从9001行开始的5行记录,看似只查询了5条记录,实际这条SQL是先读取9005条记录,然后抛弃前9000条记录,然后读到后面5条想要的数据。因此要查询一张大表比较靠后的数据,执行效率会很低。

优化:

  • 根据自增且连续的主键排序的分页查询

在employees中 ,因为主键是自增并且连续的,所以可以改写成按照主键去查询从第9001开始的五行数据,如下:

SELECT * FROM employees WHERE id >``9000 LIMIT``5``;

通过对比执行计划,显然改写后SQL走了索引,而且扫描的行数大大减少,执行效率更高。

注意:如果主键不连续或者不是按照主键排序则不能使用上面描述的优化方法。

  • 根据非主键字段排序的分页查询

EXPLAIN SELECT * FROM employees ORDER BY name LIMIT ``9000``,``5``;

发现并没有使用name字段的索引(key字段对应的值为null),这个是因为:扫描整个索引并查找到没索引的行(可能要遍历多个索引树)的成本比扫描全表的成本更高,所以优化器放弃使用索引。

知道不走索引的原因,那么怎么优化呢?

其实关键是让排序时返回的字段尽可能少,所以可以让排序和分页操作先查出主键,然后根据主键查到对应的记录,SQL改写如下:

SELECT * FROM employees e1 INNER JOIN (SELECT id FROM employees ORDER BY name LIMIT``9000``,``5``) e2 on e1.id = e2.id;

原SQL使用的是filesort排序,而优化后的SQL使用的是索引排序。

优化总结:

如果主键是自增连续的主键就用主键排序进行分页查询,如果不是就用非主键字段排序分页查询。

六、结语


理解SQL是如何执行,MySQL如何选择合适的索引以及时间都消耗在哪些地方,再加上一些优化的知识,可以帮助大家更好的理解MySQL,理解常见优化技巧背后的原理。希望本文中的原理、示例能够帮助大家更好的将理论和实践联系起来,更多的将理论知识运用到实践中。

你可能感兴趣的:(Mysql,数据库,mysql)