深刻浅出mysql优化--一篇博客让你精通mysql优化策略--上

一篇博客和你们一块儿学习mysql优化的通用策略。
内容花了好些时间来整理书写,若是以为有用,还请点个赞,还有就是发现博客园的MD文本格式和在其余软件的不太同,格式调的也很差,还请将就一下
接下来一块儿来学习一下mysql优化的内容吧(注意 本文中使用的字符集是4个长度的)mysql

1. 一条查询sql的执行过程

select * from T where ID=10;  的执行过程详解
  • MySQL 的逻辑架构算法

    MySQL 的逻辑架构图.jpg

    大致来讲,MySQL能够分为 Server层 和 存储引擎层 两部分sql

    Server层:
    包括链接器、查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,
    以及全部的内置函数(如日期、时间、数学和加密函数等),全部跨存储引擎的功能都在这一层实现,
    好比存储过程、触发器、视图等数据库

    存储引擎层:
    负责数据的存储和提取。
    其架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。
    如今最经常使用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始成为了默认存储引擎json

    执行create table建表的时候,若是不指定引擎类型,默认使用的就是InnoDB。
    不过,也能够经过指定存储引擎的类型来选择别的引擎,
    好比在 create table语句中使用 engine=memory, 来指定使用内存引擎建立表。
    不一样存储引擎的表数据存取方式不一样,支持的功能也不一样
    从图中不难看出,不一样的存储引擎共用一个Server层,也就是从链接器到执行器的部分。
  • 第一步:链接器数组

    第一步,先链接到这个数据库上,这时候接待的就是链接器。
    链接器负责跟客户端创建链接、获取权限、维持和管理链接。
    链接命令通常是这么写的: mysql -h$ip -P$port -u$user -p缓存

    输完命令以后,须要在交互对话里面输入密码。
    虽然密码也能够直接跟在 -p 后面写在命令行中,但这样可能会致使密码泄露。
    若是连的是生产服务器,强烈建议不要这么作性能优化

    若是链接命令中的 mysql 是客户端工具,用来跟服务端创建链接。
    在完成经典的 TCP 握手后,链接器就要开始认证身份,这个时候用的就是输入的用户名和密码服务器

    1. 若是用户名或密码不对,就会收到一个"Access denied for user"的错误,而后客户端程序结束执行
     2. 若是用户名密码认证经过,链接器会到权限表里面查出用户拥有的权限。
        以后,这个链接里面的权限判断逻辑,都将依赖于此时读到的权限
     
     这就意味着,一个用户成功创建链接后,即便用管理员帐号对这个用户的权限作了修改,
     也不会影响已经存在链接的权限,修改完成后,只有再新建的链接才会使用新的权限设置

    链接完成后,若是没有后续的动做,这个链接就处于空闲状态,能够在 show processlist 命令中看到它,
    如下图其中的Command列显示为“Sleep”的这一行,就表示如今系统里面有一个空闲链接session

showprocresslist.jpg

客户端若是太长时间没动静,链接器就会自动将它断开。
 这个时间是由参数 wait_timeout控制的,默认值是 8 小时
 
 若是在链接被断开以后,客户端再次发送请求的话,就会收到一个错误提醒:Lost connection to MySQL server during query。
 这时候若是要继续,就须要重连,而后再执行请求了
  • 长链接和短链接

    长链接: 是指链接成功后,若是客户端持续有请求,则一直使用同一个链接。
    短链接: 是指每次执行完不多的几回查询就断开链接,下次查询再从新创建一个。

    创建链接的过程一般是比较复杂的,因此建议在使用中要尽可能减小创建链接的动做,也就是尽可能使用长链接

    可是所有使用长链接后,可能会发现,有些时候 MySQL 占用内存涨得特别快,
    这是由于 MySQL 在执行过程当中临时使用的内存是管理在链接对象里面的。这些资源会在链接断开的时候才释放。
    因此若是长链接累积下来,可能致使内存占用太大,被系统强行杀掉(OOM),从现象看就是 MySQL 异常重启了

    那么怎么解决这个问题呢?能够考虑如下两种方案

    1. 按期断开长链接。使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开链接,以后要查询再重连
     2. 若是你用的是 MySQL 5.7 或更新版本,能够在每次执行一个比较大的操做后,经过执行 mysql_reset_connection 
        来从新初始化链接资源。这个过程不须要重连和从新作权限验证,可是会将链接恢复到刚刚建立完时的状态。
  • 第二步:查询缓存

    链接创建完成后,就能够执行 select 语句了。执行逻辑就会来到第二步:查询缓存

    MySQL 拿到一个查询请求后,会先到 查询缓存 看看以前是否是执行过这条语句。
    以前执行过的语句及其结果可能会以 key-value 对的形式被直接缓存在内存中。key 是查询的语句,value 是查询的结果。
    若是当前的查询可以直接在这个缓存中找到 key,那么这个value 就会被直接返回给客户端

    若是语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。
    能够看到,若是查询命中缓存,MySQL不须要执行后面的复杂操做,就能够直接返回结果,这个效率会很高。

    可是大多数状况下建议不要使用查询缓存,为何呢?

    由于查询缓存每每弊大于利
     查询缓存的失效很是频繁,只要有对一个表的更新,这个表上全部的查询缓存都会被清空。
     所以极可能费劲地把结果存起来,还没使用呢,就被一个更新全清空了。
     对于更新压力大的数据库来讲,查询缓存的命中率会很是低。除非业务就是有一张静态表,很长时间才会更新一次。
     好比,一个系统配置表,那这张表上的查询才适合使用查询缓存

    好在 MySQL 也提供了这种“按需使用”的方式。
    能够将参数 query_cache_type 设置成 DEMAND,这样对于默认的 SQL 语句都不使用查询缓存。
    而对于肯定要使用查询缓存的语句,能够用 SQL_CACHE 显式指定,像下面这个语句同样

    select SQL_CACHE * from T where ID=10
     须要注意的是,MySQL 8.0 版本直接将查询缓存的整块功能删掉了,也就是说 8.0 开始完全没有这个功能了
  • 第三步:分析器

    若是没有命中查询缓存,就要开始真正执行语句了。MySQL 须要知道要作什么,所以须要对 SQL 语句作解析。

    分析器先会作“词法分析”。
    输入的是由多个字符串和空格组成的一条 SQL 语句,MySQL须要识别出里面的字符串分别是什么,表明什么。

    MySQL 从输入的"select"这个关键字识别出来,这是一个查询语句。它也要把字符串“T”识别成“表名 T”,把字符串“ID”识别成“列 ID”

    作完了这些识别之后,就要作“语法分析”。
    根据词法分析的结果,语法分析器会根据语法规则,判断输入的这个 SQL 语句是否知足 MySQL 语法

    若是语句不对,就会收到“You have an error in your SQL syntax”的错误提醒,好比下面这个语句 select 少打了开头的字母“s”

    elect * from t where ID=1
     You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for 
     the right syntax to use near 'elect * from t where ID=1' at line 1
     通常语法错误会提示第一个出现错误的位置,因此你要关注的是紧接“use near”的内容
  • 第四步:优化器

    通过了分析器,MySQL 就知道要作什么了。在开始执行以前,还要先通过优化器的处理

    优化器是在表里面有多个索引的时候,决定使用哪一个索引,或者在一个语句有多表关联(join)的时候,决定各个表的链接顺序。
    好比执行下面这样的语句,这个语句是执行两表的 join

    select * from t1 join t2 using(ID) where t1.c=10 and t2.d=20;
     
     既能够先从表 t1 里面取出 c=10 的记录的 ID 值,再根据 ID 值关联到表 t2,再判断 t2里面 d 的值是否等于 20
     也能够先从表 t2 里面取出 d=20 的记录的 ID 值,再根据 ID 值关联到 t1,再判断 t1 里面 c 的值是否等于 10

    这两种执行方法的逻辑结果是同样的,可是执行的效率会有不一样,而优化器的做用就是决定选择使用哪个方案

    优化器阶段完成后,这个语句的执行方案就肯定下来了,而后进入执行器阶段。

  • 第五步:执行器

    MySQL 经过分析器知道了要作什么,经过优化器知道了该怎么作,因而就进入了执行器阶段,开始执行语句

    开始执行的时候,要先判断一下当前用户对这个表 T 有没有执行查询的权限,若是没有,就会返回没有权限的错误,
    以下所示 (在工程实现上,若是命中查询缓存,会在查询缓存返回结果的时候,作权限验证。查询也会在优化器以前调用 precheck 验证权限)

    select * from T where ID=10;
     ERROR 1142 (42000): SELECT command denied to user 'b'@'localhost' for table 'T'

    若是有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口

    好比这个例子中的表T中,ID 字段没有索引,那么执行器的执行流程是这样的:

    1. 调用 InnoDB 引擎接口取这个表的第一行,判断 ID 值是否是 10,若是不是则跳过,若是是则将这行存在结果集中;
     2. 调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。
     3. 执行器将上述遍历过程当中全部知足条件的行组成的记录集做为结果集返回给客户端

    至此,这个语句就执行完成了

    对于有索引的表,执行的逻辑也差很少。
    第一次调用的是“取知足条件的第一行”这个接口,以后循环取“知足条件的下一行”这个接口,这些接口都是引擎中已经定义好的

    在数据库的慢查询日志中能够看到一个 rows_examined 的字段,表示这个语句执行过程当中扫描了多少行。
    这个值就是在执行器每次调用引擎获取数据行的时候累加的

    在有些场景下,执行器调用一次,在引擎内部则扫描了多行,所以引擎扫描行数跟rows_examined 并非彻底相同的。

  • 问题

    若是表 T 中没有字段 k,而执行了这个语句 select * from T where k=1,
    那确定是会报“不存在这个列”的错误: “Unknown column ‘k’ in ‘where clause’”。
    那么这个错误是在咱们上面提到的哪一个阶段报出来的呢?

    分析器

2. 一条更新sql的执行过程

create table T(ID int primary key, c int);
update T set c=c+1 where ID=2;

在第一部分说过,在一个表上有更新的时候,跟这个表有关的查询缓存会失效,
因此这条语句就会把表 T 上全部缓存结果都清空。这也就是咱们通常不建议使用查询缓存的缘由。

接下来,分析器会经过词法和语法解析知道这是一条更新语句。
优化器决定要使用 ID 这个索引。
而后,执行器负责具体执行,找到这一行,而后更新。

与查询流程不同的是,更新流程还涉及两个重要的日志模块:

    redo log(重作日志)和 binlog(归档日志)。
  • 重要的日志模块:redo log

    以《孔乙己》这篇文章做为例子,
    酒店掌柜有一个粉板,专门用来记录客人的赊帐记录。
    若是赊帐的人很少,那么他能够把顾客名和帐目写在板上。
    但若是赊帐的人多了,粉板总会有记不下的时候,这个时候掌柜必定还有一个专门记录赊帐的帐本
    
    若是有人要赊帐或者还帐的话,掌柜通常有两种作法:
    
         1. 直接把帐本翻出来,把此次赊的帐加上去或者扣除
         2. 先在粉板上记下此次的帐,等打烊之后再把帐本翻出来核算
    
    在生意红火柜台很忙时,掌柜必定会选择后者,由于前者操做实在是太麻烦了。
    首先,你得找到这我的的赊帐总额那条记录。可能找要一段实践,找到后再拿出算盘计算,最后再将结果写回到帐本上
    
    这整个过程想一想都麻烦。相比之下,仍是先在粉板上记一下方便。
    想一想,若是掌柜没有粉板的帮助,每次记帐都得翻帐本,效率是否是低得让人难以忍受?
    
    
    一样,在 MySQL 里也有这个问题,若是每一次的更新操做都须要写进磁盘,而后磁盘也要找到对应的那条记录,而后再更新,
    整个过程 IO 成本、查找成本都很高。为了解决这个问题,MySQL 的设计者就用了相似酒店掌柜粉板的思路来提高更新效率。
    
    而粉板和帐本配合的整个过程,其实就是 MySQL 里常常说到的 WAL 技术,
    WAL 的全称是 Write-Ahead Logging,它的关键点就是:先写日志,再写磁盘,也就是先写粉板,等不忙的时候再写帐本
    
    具体来讲,当有一条记录须要更新的时候,InnoDB 引擎就会先把记录写到 redo log里面,并更新内存,
    这个时候更新就算完成了。
    同时,InnoDB 引擎会在适当的时候,将这个操做记录更新到磁盘里面,而这个更新每每是在系统比较空闲的时候作, 
    
    若是今天赊帐的很少,掌柜能够等打烊后再整理。
    但若是某天赊帐的特别多,粉板写满了,
    又怎么办呢?这个时候掌柜只好放下手中的活儿,把粉板中的一部分赊帐记录更新到帐本中,
    而后把这些记录从粉板上擦掉,为记新帐腾出空间         
    
    与此相似,InnoDB 的 redo log 是固定大小的,好比能够配置为一组 4个文件,
    每一个文件的大小是 1GB,那么这块“粉板”总共就能够记录 4GB 的操做。
    从头开始写,写到末尾就又回到开头循环写,
    以下面这个图所示

    write redo log.jpg

    write pos是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。
    checkpoint 是当前要擦除的位置,也是日后推移而且循环的,擦除记录前要把记录更新到数据文件
    
    
    write pos 和 checkpoint 之间的是“粉板”上还空着的部分,能够用来记录新的操做。
    若是 write pos 追上 checkpoint,表示“粉板”满了,这时候不能再执行新的更新,
    得停下来先擦掉一些记录,把 checkpoint 推动一下
    
    有了 redo log,InnoDB 就能够保证即便数据库发生异常重启,
    以前提交的记录都不会丢失,这个能力称为crash-safe
    
    要理解 crash-safe 这个概念,能够想一想前面赊帐记录的例子。
    只要赊帐记录记在了粉板上或写在了帐本上,以后即便掌柜忘记了,好比忽然停业几天,
    恢复生意后依然能够经过帐本和粉板上的数据明确赊帐帐目
  • 重要的日志模块:binlog

    redo log 是 InnoDB 引擎特有的日志,而 Server 层也有本身的日志,称为 binlog

    最开始 MySQL 里并无 InnoDB 引擎。
    MySQL 自带的引擎是 MyISAM,可是MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。
    而 InnoDB 是另外一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,
    因此 InnoDB使用另一套日志系统——也就是 redo log 来实现 crash-safe 能力

    两种日志有如下三点不一样

    1. redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,全部引擎均可以使用。
     2. redo log 是物理日志,记录的是“在某个数据页上作了什么修改”;
        binlog 是逻辑日志,记录的是这个语句的原始逻辑,好比“给 ID=2 这一行的 c 字段加 1 ”。
     3. redo log 是循环写的,空间固定会用完;binlog 是能够追加写入的。
        “追加写”是指 binlog 文件写到必定大小后会切换到下一个,并不会覆盖之前的日志

    update 语句时的内部流程

    1. 执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。若是
        ID=2 这一行所在的数据页原本就在内存中,就直接返回给执行器;不然,须要先从磁盘
        读入内存,而后再返回。
     2. 执行器拿到引擎给的行数据,把这个值加上 1,好比原来是 N,如今就是 N+1,获得新
        的一行数据,再调用引擎接口写入这行新数据。
     3. 引擎将这行新数据更新到内存中,同时将这个更新操做记录到 redo log 里面,此时
        redo log 处于 prepare 状态。而后告知执行器执行完成了,随时能够提交事务。
     4. 执行器生成这个操做的 binlog,并把 binlog 写入磁盘。
     5. 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改为提交(commit)状态,更新完成

    update 语句的执行流程图,图中浅色框表示是在 InnoDB 内部执行的,深色框表示是在执行器中执行的

update 语句的执行流程图.jpg

将 redo log 的写入拆成了两个步骤:
 
    prepare 和 commit,这就是"两阶段提交"。
  • 两阶段提交

    存在两阶段提交为了让两份日志之间的逻辑一致,怎样让数据库恢复到半个月内任意一秒的状态?
     
         binlog 会记录全部的逻辑操做,而且是采用“追加写”的形式。若是你的 DBA 承诺说半个月内能够恢复,
         那么备份系统中必定会保存最近半个月的全部binlog,同时系统会按期作整库备份。
         这里的“按期”取决于系统的重要性,能够是一天一备,也能够是一周一备。
         当须要恢复到指定的某一秒时,好比某天下午两点发现中午十二点有一次误删表,须要找回数据,那你能够这么作:
         
             首先,找到最近的一次全量备份,若是你运气好,可能就是昨天晚上的一个备份,从这个备份恢复到临时库;
             而后,从备份的时间点开始,将备份的 binlog 依次取出来,重放到中午误删表以前的那个时刻。
         
         这样你的临时库就跟误删以前的线上库同样了,而后你能够把表数据从临时库取出来,按须要恢复到线上库去。

    为何日志须要“两阶段提交”

    因为 redo log 和 binlog 是两个独立的逻辑,若是不用两阶段提交,要么就是先写完 redolog 再写 binlog,
     或者采用反过来的顺序。看看这两种方式会有什么问题
     
     仍然用前面的 update 语句来作例子。
     假设当前 ID=2 的行,字段 c 的值是 0,再假设执行update 语句过程当中在写完第一个日志后,
     第二个日志尚未写完期间发生了 crash,会出现什么状况呢?
     
       1.先写 redo log 后写 binlog。
         假设在 redo log 写完,binlog 尚未写完的时候,MySQL 进程异常重启。
         因为前面说过的,redo log 写完以后,系统即便崩溃,仍然可以把数据恢复回来,因此恢复后这一行 c 的值是 1。
         可是因为 binlog 没写完就 crash 了,这时候 binlog 里面就没有记录这个语句。
         所以,以后备份日志的时候,存起来的 binlog 里面就没有这条语句。
         而后会发现,若是须要用这个 binlog 来恢复临时库的话,因为这个语句的 binlog 丢失,
         这个临时库就会少了这一次更新,恢复出来的这一行 c 的值就是 0,与原库的值不一样。
         
       2.先写 binlog 后写 redo log。
         若是在 binlog 写完以后 crash,因为 redo log 还没写,
         崩溃恢复之后这个事务无效,因此这一行 c 的值是 0。可是 binlog 里面已经记录了“把c 从 0 改为 1”这个日志。
         因此,在以后用 binlog 来恢复的时候就多了一个事务出来,恢复出来的这一行 c 的值就是 1,与原库的值不一样
         
     能够看到,若是不使用“两阶段提交”,那么数据库的状态就有可能和用它的日志恢复出来的库的状态不一致。
     这个几率是否是很低,平时也没有什么动不动就须要恢复临时库的场景呀?
     其实不是的,不仅是误操做后须要用这个过程来恢复数据。当须要扩容的时候,也就是须要再多搭建一些备库来增长系统的读能力的时候,
     如今常见的作法也是用全量备份加上应用binlog 来实现的,这个“不一致”就会致使你的线上出现主从数据库不一致的状况。
     简单说,redo log 和 binlog 均可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。
  • tip

    redo log 用于保证 crash-safe 能力。
     innodb_flush_log_at_trx_commit 这个参数设置成1 的时候,表示每次事务的 redo log 都直接持久化到磁盘。
     这个参数建议设置成 1,这样能够保证 MySQL 异常重启以后数据不丢失。
     sync_binlog 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。
     这个参数也建议你设置成 1,这样能够保证 MySQL 异常重启以后 binlog 不丢失。
     
     在什么场景下,一天一备会比一周一备更有优点呢?或者说,它影响了这个数据库系统的哪一个指标?
     
         一天一备跟一周一备的对比。
         好处是“最长恢复时间”更短。
         在一天一备的模式里,最坏状况下须要应用一天的 binlog。
         好比,天天 0 点作一次全量备份,而要恢复出一个到昨天晚上 23 点的备份。
         一周一备最坏状况就要应用一周的 binlog 了。
         系统的对应指标就是恢复目标时间
         频繁全量备份须要消耗更多存储空间,因此这个 RTO 是成本换来的,须要根据业务重要性来评估

3. 深刻浅出mysql索引

  • 使用hash索引存储

若是要维护一个身份证信息和姓名的表,须要根据身份证号查找对应的名字,这时 对应的哈希索引的示意图以下所示

图中,User2 和 User3 根据身份证号算出来的值都是 n,后面还跟了一个链表。
若是这时候要查 card-2 对应的名字是什么,处理步骤就是:
    首先,将 card-2 经过哈希函数算出n,而后,按顺序遍历,找到 User2。
须要注意的是,图中四个 card-n 的值并非递增的,这样作的好处是增长新的 User 时速度会很快,只须要日后追加。
但缺点是,由于不是有序的,因此哈希索引作 区间查询 的速度是很慢的。
若是如今要找身份证号在 [card_X, card_Y] 这个区间的全部用户,就必须所有扫描一遍了。
因此,哈希表这种结构适用于只有等值查询的场景,好比 Memcached 及其余一些 NoSQL 引擎,这一点上一边索引类型介绍中已经说得很清楚了
  • 有序数组

有序数组 在等值查询和范围查询场景中的性能就都很是优秀,如下是其索示意图

假设身份证号没有重复,这个数组就是按照身份证号递增的顺序保存的。
这时候若是要查 card_n2 对应的名字,用二分法就能够快速获得,这个时间复杂度是 O(log(N))。
同时很显然,这个索引结构支持范围查询。你要查身份证号在 [card_X, card_Y] 区间的user,
    能够先用二分法找到 card_X(若是不存在card_X,就找到大于card_X 的第一个user),而后向右遍历,直到查到第一个大于card_Y 的身份证号,退出循环。
若是仅仅看查询效率,有序数组就是最好的数据结构了。
可是,在须要更新数据的时候却很差,你往中间插入一个记录就必须得挪动后面全部的记录,成本过高。
因此,有序数组索引只适用于静态存储引擎,好比你要保存的是2020年某个城市的全部人口信息,这类不会再修改的数据
  • 二叉搜索树示意图

二叉搜索树的特色是:

每一个节点的左儿子小于父节点,父节点又小于右儿子。这样若是你要查card_n2 的话,按照图中的搜索顺序就是按照 UserA -> UserC -> UserF -> User2 这个路径获得。这个时间复杂度是 O(log(N))。
固然为了维持 O(log(N)) 的查询复杂度,你就须要保持这棵树是平衡二叉树。为了作这个 保证,更新的时间复杂度也是 O(log(N))。
树能够有二叉,也能够有多叉。多叉树就是每一个节点有多个儿子,儿子之间的大小保证从左 到右递增。
二叉树是搜索效率最高的,可是实际上大多数的数据库存储却并不使用二叉树。 其缘由是,索引不止存在内存中,还要写到磁盘上。
你能够想象一下一棵 100 万节点的平衡二叉树,树高20。一次查询可能须要访问 20 个数据块。
在机械硬盘时代,从磁盘随机读一个数据块须要 10 ms 左右的寻址时间。也就是说,对于一个100万行的表,若是使用二叉树来存储,单独访问一个行可能须要 20 个10 ms 的时间

为了让一个查询尽可能少地读磁盘,就必须让查询过程访问尽可能少的数据块。
那么,咱们就不该该使用二叉树,而是要使用“N 叉”树。这里,“N 叉”树中的“N”取决于数据块的大小。
以 InnoDB 的一个整数字段索引为例,这个N差很少是 1200。这棵树高是 4 的时候,就能够存 1200 的 3 次方个值,这已经 17 亿了。
考虑到树根的数据块老是在内存中的,一个 10 亿行的表上一个整数字段的索引,查找一个值最多只须要访问3次磁盘。
其实,树的第二层也有很大几率在内存中,那么访问磁盘的平均次数就更少了。
N叉树因为在读写上的性能优势,以及适配磁盘的访问模式,已经被普遍应用在数据库引擎中了。

在 MySQL 中,索引是在存储引擎层实现的,因此并无统一的索引标准,即不一样存储引 擎的索引的工做方式并不同。而即便多个存储引擎支持同一种类型的索引,其底层的实现 也可能不一样。因为 InnoDB 存储引擎在 MySQL 数据库中使用最为普遍,下面以 InnoDB为例子

  • InnoDB 的索引模型

在 InnoDB 中,表都是根据主键顺序以索引的形式存放的,这种存储方式的表称为索引组织表。
InnoDB 使用了 B+ 树索引模型,因此数据都是存储在 B+ 树中的,每个索引在 InnoDB 里面对应一棵 B+ 树。
假设,有一个主键列为 ID 的表,表中有字段 k,而且在 k 上有索引

CREATE TABLE T ( id INT PRIMARY KEY, k INT NOT NULL, NAME VARCHAR ( 16 ), INDEX ( k ) ) ENGINE = INNODB;

表中 R1~R5 的 (ID,k) 值分别为 (100,1)、(200,2)、(300,3)、(500,5) 和 (600,6),两棵树 的示例示意图以下

从图中不难看出,根据叶子节点的内容,索引类型分为 主键索引 和 非主键索引 。
主键索引的叶子节点存的是整行数据。在 InnoDB 里,主键索引也被称为聚簇索引 (clustered index)。
非主键索引的叶子节点内容是主键的值。在 InnoDB 里,非主键索引也被称为二级索引 (secondary index)。
根据上面的索引结构说明,来讨论一个问题:基于主键索引和普通索引的查询有什么区别?

若是语句是 select * from T where ID=500,即主键查询方式,则只须要搜索 ID 这棵 B+ 树;
若是语句是 select * from T where k=5,即普通索引查询方式,则须要先搜索 k 索引 树,获得 ID 的值为 500,再到 ID 索引树搜索一次。
这个过程称为回表。

也就是说,基于非主键索引的查询须要多扫描一棵索引树。所以,在应用中应该尽可能使用主键查询。.

  • 索引维护

    B+树为了维护索引有序性,在插入新值的时候须要作必要的维护。
    以上面这个图为例,
    若是插入新的行ID值为 700,则只须要在 R5 的记录后面插入一个新记录。
    若是新插入的ID值为400,就相对麻烦了,须要逻辑上挪动后面的数据,空出位置。
    而更糟的状况是,若是 R5 所在的数据页已经满了,根据 B+ 树的算法,这时候须要申请一个新的数据页,而后挪动部分数据过去。
    这个过程称为页分裂。在这种状况下,性能天然会受影响。
    除了性能外,页分裂操做还影响数据页的利用率。本来放在一个页的数据,如今分到两个页中,总体空间利用率下降大约50%。

  • 基于上面的索引维护过程说明,讨论一个案例:

    在一些建表规范里面见到过相似的描述,要求建表语句里必定要有自增主键。上一个文章中也提到了这点,这里再次描述。
    分析一下哪些场景下应该使用自增主键,而哪些场景下不该该。

    自增主键是指自增列上定义的主键,在建表语句中通常是这么定义的: NOT NULL PRIMARY KEY AUTO_INCREMENT。
    插入新记录的时候能够不指定 ID 的值,系统会获取当前 ID 最大值加 1 做为下一条记录的 ID 值。

    也就是说,自增主键的插入数据模式,正符合了咱们前面提到的递增插入的场景。
    每次插入一条新记录,都是追加操做,都不涉及到挪动其余记录,也不会触发叶子节点的分裂。
    而有业务逻辑的字段作主键,则每每不容易保证有序插入,这样写数据成本相对较高。

除了考虑性能外,还能够从存储空间的角度来看。
假表中确实有一个惟一字段, 好比字符串类型的身份证号,那应该用身份证号作主键,仍是用自增字段作主键呢?

因为每一个非主键索引的叶子节点上都是主键的值(由于要根据非主键索引找到主键索引位置而后再找到数据,可看上图)。
若是用身份证号作主键,那么每一个二级索引的叶子节点占用约 20 个字节,
而若是用整型作主键,则只要 4 个字节,若是是长整型 (bigint)则是 8 个字节。
显然,主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。
因此,从性能和存储空间方面考量,自增主键每每是更合理的选择。

什么场景适合用业务字段直接作主键的呢?有些业务的场景需求是以下:

1. 只有一个索引
2. 该索引必须是惟一索引 这就是典型的 KV 场景。

因为没有其余索引,因此也就不用考虑其余索引的叶子节点大小的问题。
这时候就要优先考虑上一段提到的“尽可能使用主键查询”原则,直接将这个索引设置为主键,能够避免每次查询须要搜索两棵树。

对于上面例子中的 InnoDB 表 T,若是要重建索引k,能够写:

alter table T drop index k;
alter table T add index(k);

要重建主键索引,能够写

alter table T drop primary key;
alter table T add primary key(id);

这样写是否合理?

重建索引 k的作法是合理的,能够达到省空间的目的。
可是,重建主键的过程不合理。
不管是删除主键仍是建立主键,都会将整个表重建。
因此连着执行这两个语句的话,第一个语句就白作了。
这两个语句,能够用这个语句代替 :alter table T engine=InnoDB
  • sql扫描行数的探讨

    CREATE TABLE T (
     ID INT PRIMARY KEY,
     k INT NOT NULL DEFAULT 0,
     s VARCHAR ( 16 ) NOT NULL DEFAULT '',
     INDEX k( k ) 
     ) ENGINE = INNODB;
     INSERT INTO T
     VALUES
      ( 100, 1, 'aa' ),
      ( 200, 2, 'bb' ),
      ( 300, 3, 'cc' ),
      ( 500, 5, 'ee' ),
      ( 600, 6, 'ff' ),
      ( 700, 7, 'gg' );

这个表 T 中,若是我执行如下sql 须要执行几回树的搜索操做,会扫描多少行?

select * from T where k between 3 and 5;

先来看看这条 SQL 查询语句的执行流程:

1. 在 k 索引树上找到 k=3 的记录,取得 ID = 300 
 2. 再到 ID 索引树查到 ID=300 对应的 R3 
 3. 在 k 索引树取下一个值 k=5,取得 ID=500 
 4. 再回到 ID 索引树查到 ID=500 对应的 R4 
 5. 在 k 索引树取下一个值 k=6,不知足条件,循环结束 
     
 在这个过程当中,回到主键索引树搜索的过程,称为回表。
 能够看到,这个查询过程读了k索引树的3条记录(步骤 一、3 和 5),回表了两次(步骤 2 和 4)
 在这个例子中,因为查询结果所须要的数据只在主键索引上有,因此不得不回表。
 那么,有没有可能通过索引优化,避免回表过程呢?
 
    答案是覆盖索引
        
        若是执行的语句是 select ID from T where k between 3 and 5,这时只须要查 ID 的值, 
        而 ID 的值已经在 k 索引树上了,所以能够直接提供查询结果,不须要回表。
        也就是说,在这个查询里面,索引k已经“覆盖了”查询需求,称为覆盖索引。

        因为覆盖索引能够减小树的搜索次数,显著提高查询性能,因此使用覆盖索引是一个经常使用的性能优化手段。
        
        须要注意的是,在引擎内部使用覆盖索引在索引 k上其实读了三个记录,R3~R5(对应的索引 k 上的记录项)
        可是对于 MySQL 的 Server 层来讲,它就是找引擎拿到了两条记录,所以 MySQL 认为扫描行数是 2。(这个行数扫描后面须要注意)

基于上面覆盖索引的说明,讨论另外一个问题:

在一个市民信息表上,是否有必要将身份证号和名字创建联合索引?

假设这个市民表的定义是这样的:
    CREATE TABLE `tuser` (
    `id` INT ( 11 ) NOT NULL,
    `id_card` VARCHAR ( 32 ) DEFAULT NULL,
    `name` VARCHAR ( 32 ) DEFAULT NULL,
    `age` INT ( 11 ) DEFAULT NULL,
    `ismale` TINYINT ( 1 ) DEFAULT NULL,
    PRIMARY KEY ( `id` ),
    KEY `id_card` ( `id_card` ),
    KEY `name_age` ( `name`, `age` ) 
    ) ENGINE = INNODB;
    
身份证号是市民的惟一标识。
也就是说,若是有根据身份证号查询市民信息的需求,只要在身份证号字段上创建索引就够了。
而再创建一个(身份证号、姓名)的联合 索引,是否是浪费空间?
    
若是如今有一个高频请求,要根据市民的身份证号查询他的姓名,这个联合索引就有意义了。
它能够在这个高频请求上用到覆盖索引,再也不须要回表查整行记录,减小语句的执行时间。
固然,索引字段的维护老是有代价的。所以,在创建冗余索引来支持覆盖索引时就须要权衡考虑了。
这些是业务 DBA,或者称为业务数据架构师的工做。
  • 最左前缀原则 (后面会详细介绍)

    看到这里你必定有一个疑问,若是为每一种查询都设计一个索引,索引是否是太多了。
    若是我如今要按照市民的身份证号去查他的家庭地址呢?
    虽然这个查询需求在业务中出现的几率不高,但总不能让它走全表扫描吧?
    反过来讲,单独为一个不频繁的请求建立一个(身份证号,地址)的索引又感受有点浪费。应该怎么作呢?

    这里即可以利用b+tree索引的“最左前缀原则”

    为了直观地说明这个概念,这里用(name,age)这个联合索引来分析。

    能够看到,索引项是按照索引定义里面出现的字段顺序排序的。
     
     当你的逻辑需求是查到全部名字是“张三”的人时,能够快速定位到ID4,而后向后遍历获得全部须要的结果
     
     若是你要查的是全部名字第一个字是“张”的人,你的SQL语句的条件是"where namelike ‘张 %’"。
     这时,你也可以用上这个索引,查找到第一个符合条件的记录是 ID3,而后向后遍历,直到不知足条件为止。
     
     能够看到,不仅是索引的所有定义,只要知足最左前缀,就能够利用索引来加速检索。
     这个最左前缀能够是联合索引的最左N个字段,也能够是字符串索引的最左M个字符

    基于上面对最左前缀索引的说明,那么在创建联合索引的时候,如何安排索引内的字段顺序?

    这里大多的评估标准是,索引的复用能力。
     由于能够支持最左前缀,因此当已经有了 (a,b)这个联合索引后,通常就不须要单独在 a上创建索引了。
     所以,第一原则是,若是经过调整顺序,能够少维护一个索引,那么这个顺序每每就是须要优先考虑采用
     
     因此如今能够知道了,这段开头的问题里,要为高频请求建立 (身份证号,姓名)这个联合索引,并用这个索引支持“根据身份证号查询地址”的需求
     
     那么,若是既有联合查询,又有基于 a、b 各自的查询呢?查询条件里面只有 b 的语句,是没法使用 (a,b) 这个联合索引的,
     这时候你不得不维护另一个索引,也就是说须要同时维护 (a,b)、(b) 这两个索引。
     这时候,要考虑的原则就是空间了。
     好比上面这个市民表的状况,name 字段是比age 字段大的 ,那建议建立一个(name,age) 的联合索引和一个 (age) 的单字段索引。
  • 索引下推

    上一段说到知足最左前缀原则的时候,最左前缀能够用于在索引中定位记录。那么那些不符合最左前缀的部分,会怎么样呢?

    仍是以市民表的联合索引(name, age)为例。若是如今有一个需求:
      检索出表中“名字第一个字是张,并且年龄是 10岁的全部男孩”。那么,SQL 语句是这么写的
          select * from tuser where name like '张 %' and age=10 and ismale=1;
      
      已经知道了前缀索引规则,因此这个语句在搜索索引树的时候,只能用 “张”,找到第一个知足条件的记录 ID3。
      这还不错,总比全表扫描要好。而后判断其余条件是否知足。
      
      在 MySQL 5.6 以前,只能从 ID3 开始一个个回表。到主键索引上找出数据行,再对比字段值
      
      而 MySQL 5.6 引入的索引下推优化(index condition pushdown),能够在索引遍历过程当中,
      对索引中包含的字段先作判断,直接过滤掉不知足条件的记录,减小回表次数
      
      看下图分析

    索引下推执行流程.jpg

  • tip

    实际上主键索引也是可使用多个字段的。
    假如DBA小吕在入职新公司的时候,就发现本身接手维护的库里面,有这么一个表,表结构定义相似这样的

    CREATE TABLE `geek` (
      `a` INT ( 11 ) NOT NULL,
      `b` INT ( 11 ) NOT NULL,
      `c` INT ( 11 ) NOT NULL,
      `d` INT ( 11 ) NOT NULL,
      PRIMARY KEY ( `a`, `b` ),
      KEY `c` ( `c` ),
      KEY `ca` ( `c`, `a` ),
      KEY `cb` ( `c`, `b` ) 
      ) ENGINE = INNODB;
    
       公司的同事告诉他说,因为历史缘由,这个表须要 a、b 作联合主键,这个小吕理解了
       但是根据上面提到的内容,主键包含了 a、b 这两个字段,那意味着单独在字段 c 上建立一个索引,
       就已经包含了三个字段了,为何要建立“ca”“cb”这两个索引?
       
       同事告诉他,是由于他们的业务里面有这样的两种语句:
          select * from geek where c=N order by a limit 1;
          select * from geek where c=N order by b limit 1;
       
     为了这两个查询模式,这两个索引是否都是必须的?为何呢?
     
       假如表记录
       –a--|–b--|–c--|–d--
       1 2 3 d
       1 3 2 d
       1 4 3 d
       2 1 3 d
       2 2 2 d
       2 3 4 d
       主键 a,b的聚簇索引组织顺序至关于 order by a,b ,也就是先按 a 排序,再按 b 排序,c 无序。
       
       索引 ca 的组织是先按 c排序,再按 a 排序,同时记录主键
       –c--|–a--|–主键部分b-- 
       2 1            3
       2 2            2
       3 1            2
       3 1            4
       3 2            1
       4 2            3
       这个跟索引 c 的数据是如出一辙的。
       
       索引 cb 的组织是先按 c 排序,再按 b 排序,同时记录主键
       –c--|–b--|–主键部分a--
       2 2            2
       2 3            1
       3 1            2
       3 2            1
       3 4            1
       4 3            2
       
       ca索引能够去掉,cb索引能够保留。
       ca索引,经过索引对数据进行筛选,回表的时候,a自己就是主键索引,因此能够保证有序;
       cb索引,b上并无索引,ab索引也没法知足最左匹配原则,能够保留加快排序速度。
       包含主键后应该是cab,根据最左匹配原则,cb是有必要的,ca没有必要
       因此,结论是 ca 能够去掉,cb 须要保留。

4. Explain详解

  • Explain

    使用EXPLAIN关键字能够模拟优化器执行SQL语句,分析你的查询语句或是结构的性能瓶颈在 select 语句以前增长 explain 关键字,
     MySQL 会在查询上设置一个标记,执行查询会返回执行计划的信息,而不是执行这条SQL
     注意:若是 from 中包含子查询,仍会执行该子查询,将结果放入临时表中
     
     
     drop table if exists actor;
     CREATE TABLE `actor` (
       `id` int(11) NOT NULL AUTO_INCREMENT,
       `name` varchar(45)  NOT NULL,
       `update_time` datetime(6) DEFAULT NULL,
       PRIMARY KEY (`id`)
     ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
     INSERT INTO `actor`(`id`, `name`, `update_time`) VALUES (1, 'a', '2020-12-29 22:23:44.000000');
     INSERT INTO `actor`(`id`, `name`, `update_time`) VALUES (2, 'b', '2020-12-29 22:23:44.000000');
     INSERT INTO `actor`(`id`, `name`, `update_time`) VALUES (3, 'c', '2020-12-29 22:23:44.000000');
     
     drop table if exists film;
      CREATE TABLE film (
       id int(11) NOT NULL AUTO_INCREMENT,
       name varchar(10)  NOT NULL,
       PRIMARY KEY (id),
         key (name)
     ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
     INSERT INTO film(id,name) values(1,'film1');
     INSERT INTO film(id,name) values(2,'film2');
     INSERT INTO film(id,name) values(3,'film0');
     
     drop table if exists film_actor;
     CREATE TABLE film_actor (
     id int(11) not null,
     film_id int(11) not null,
     actor_id int(11) not null,
     remark VARCHAR(255) null,
     PRIMARY key(id),
     KEY idx_film_actor_id(film_id,actor_id) 
     )ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
     INSERT INTO film_actor(id,film_id,actor_id) values(1,1,1);
     INSERT INTO film_actor(id,film_id,actor_id) values(2,1,2);
     INSERT INTO film_actor(id,film_id,actor_id) values(3,2,1);
     
     explain select * from actor;

图1

在查询中的每一个表会输出一行,若是有两个表经过 join 链接查询,那么会输出两行,输出数字越高,执行越先
  • explain 两个变种

    explain extended
     
         会在 explain 的基础上额外提供一些查询优化的信息。
         紧随其后经过 show warnings 命令能够获得优化后的查询语句,从而看出优化器优化了什么。
         额外还有filtered列,是一个半分比的值,
         rows*filtered/100 
             能够估算出将要和explain中前一个表进行链接的行数(前一个表指explain中的id值比当前表id值小的表)
         
         explain extended select * from film where id = 1;

    图2

    show warnings;

    图3

    explain partitions
         
         相比explain多了个partitions字段,若是查询是基于分区表的话,会显示查询将访问的分区。
  • explain中的列介绍

  • id

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

  • select_type

    select_type 表示对应行是简单仍是复杂的查询,其中还分为五种类型

    1. simple:简单查询。查询不包含子查询和union
         explain select * from film where id = 2
     2. primary:复杂查询中最外层的 select
     3. subquery:包含在select中的子查询(不在from子句中)
     4. derived:包含在from子句中的子查询。
        MySQL会将结果存放在一个临时表中,也称为派生表(derived的英文含义)
        
        #在执行前 关闭mysql5.7新特性对衍生表的合并优化 以后关闭
        set session optimizer_switch='derived_merge=off'
        explain select (select 1 from actor where id = 1) from (select * from film where id = 1)der;
        set session optimizer_switch='derived_merge=on'
        
     5. union:在union中的第二个和随后的select
         explain select 1 union all select 1 ;

    图4

  • table

    这一列表示explain的一行正在访问哪一个表。
     当from子句中有子查询时,table列是 <derivenN> 格式,表示当前查询依赖 id=N的查询,因而先执行id=N的查询。
     当有union时,UNION RESULT的table列的值为<union1,2>,1和2表示参与union的select行id。
  • type

    这一列表示关联类型或访问类型,即MySQL决定如何查找表中的行,查找数据行记录的大概范围。
     依次从最优到最差分别为:system > const > eq_ref > ref > range > index > ALL
     通常来讲,得保证查询达到range级别,最好达到ref
     NULL: mysql可以在优化阶段分解查询语句,在执行阶段用不着再访问表或索引。
     例如:在索引列中选取最小值,能够单独查找索引来完成,不须要在执行时访问表
     
     explain select min(id) from film

图5

const,system
        mysql能对查询的某部分进行优化并将其转化成一个常量(能够看showwarnings 的结果)。
        用于primary key或 unique key的全部列与常数比较时,因此表最多有一个匹配行,读取1次,速度比较快。
        system是const的特例,表里只有一条元组匹配时为system
        
    explain extended select * from (select * from film where id= 1) tmp;
    使用show warnings;能够看到Message 已是直接select常量了

图6

eq_ref
        primary key或unique key索引的全部部分被链接使用,最多只会返回一条符合件的记录。
        这多是在const以外最好的联接类型了,简单的select查询不会出现这种type。
   
     explain select * from film_actor left join film on film_actor.film_id = film.id;

图7

ref
        相比eq_ref,不使用惟一索引,而是使用普通索引或者惟一性索引的部分前缀,索引要和某个值相比较,可能会找到多个符合条件的行。
        
        1. 简单select查询,name是普通索引(非惟一索引)
           explain select * from film where name = 'film1';
            
        2.关联表查询,idx_film_actor_id是film_id和actor_id的联合索引,
          这里使用到了film_actor的左边前缀film_id部分
          explain select film_id from film left join film_actor on film.id = film_actor.film_id;

图8

range
        范围扫描一般出如今in(), between ,> ,<, >= 等操做中。使用一个索引来检索给定范围的行。
        explain select * from actor where id > 1;
        
    index
        扫描全表索引,这一般比ALL快一些
    
    ALL
        即全表扫描,意味着mysql须要从头至尾去查找所须要的行。一般状况下这须要增长索引来进行优化了

图9

  • possible_keys

    这一列显示查询可能使用哪些索引来查找。
     explain时可能出现possible_keys有列,而key显示NULL的状况,
     这种状况是由于表中数据很少,mysql认为索引对此查询帮助不大,选择了全表查询。
     若是该列是NULL,则没有相关的索引。
     在这种状况下,能够经过检查where子句看是否能够创造一个适当的索引来提升查询性能,而后用explain查看效果
  • key

    这一列显示mysql实际采用哪一个索引来优化对该表的访问。若是没有使用索引,则该列是NULL。
     若是想强制mysql使用或忽视possible_keys列中的索引,在查询中使用 force index、ignore index
  • key

    这一列显示了mysql在索引里使用的字节数,经过这个值能够算出具体使用了索引中的哪些列。
     举例来讲,film_actor的联合索引 idx_film_actor_id 由 film_id 和 actor_id 两个int列组成,
     而且每一个int是4字节。经过结果中的key_len=4可推断出查询使用了第一个列:film_id列来执行索引查找。
     
     key_len计算规则以下:
         字符串
             char(n):n字节长度
             varchar(n):2字节存储字符串长度,若是是utf-8,则长度: 3n+2
         数值类型
             tinyint:1字节
             smallint:2字节
             int:4字节
             bigint:8字节 
         时间类型
             date:3字节
             timestamp:4字节
             datetime:8字节
     若是字段容许为 NULL,须要1字节记录是否为 NULL
     索引最大长度是768字节,当字符串过长时,mysql会作一个相似左前缀索引的处理,将前半
     部分的字符提取出来作索引
     
     explain select * from film_actor where film_id = 2

图10

  • ref

    这一列显示了在key列记录的索引中,表查找值所用到的列或常量,常见的有:const(常量),字段名(例:film.id)
  • rows

    这一列是mysql估计要读取并检测的行数,注意这个不是结果集里的行数(这个是包括索引扫描、回表等加起来的)
  • Extra列

    1. Using index 
             使用覆盖索引
             explain select film_id from film_actor where film_id = 1;
             
         2. Using where
             使用 where 语句来处理结果,查询的列未被索引覆盖
             explain select * from actor where name = 'a';
             
         3. Using index condition
            查询的列不彻底被索引覆盖,where条件中是一个前导列的范围;
            explain select * from film_actor where film_id > 1;

图11

4. Using temporary
       mysql须要建立一张临时表来处理查询。出现这种状况通常是要进行优化的,首先是想到用索引来优化。
       
       1. actor.name没有索引,此时建立了张临时表来distinct
          explain select distinct name from actor;
        
       2. film.name创建了idx_name索引,此时查询时extra是 using index,没有用临时表
          explain select distinct name from film;
          
     5. Using filesort
        将用外部排序而不是索引排序,数据较小时从内存排序,不然须要在磁盘完成排序。
        这种状况下通常也是要考虑使用索引来优化的。
        
        1. actor.name未建立索引,会浏览actor整个表,保存排序关键字name和对应的id,而后排序name并检索行记录
            explain select * from actor order by name;
            
        2. film.name创建了idx_name索引,此时查询时 extra是 using index
             explain select * from film order by name;

图12

6. Select tables optimized away 
        使用某些聚合函数(好比 max、min)来访问存在索引的某个字段
        
        explain select min(id) from film;

图13

5. mysql索引最佳实践

  • CREATE TABLE employees (
         id INT ( 11 ) NOT NULL AUTO_INCREMENT,
         NAME VARCHAR ( 24 ) NOT NULL DEFAULT '',
         age INT ( 11 ) NOT NULL DEFAULT 0,
         position VARCHAR ( 20 ) NOT NULL DEFAULT '',
         hire_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
         PRIMARY KEY ( id ),
         KEY idx_name_age_position ( NAME, age, position ) USING BTREE 
     )ENGINE = INNODB AUTO_INCREMENT = 3 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
     insert into employees(name,age,position) values('LiLei',22,'manager');
     insert into employees(name,age,position) values('HanMeimei',23,'dev');
     insert into employees(name,age,position) values('Lucy',23,'dev');
  • 1.全值匹配

    -- idx_name_age_position ( name, age, position ) 简历索引的顺序
     -- 若是索引了多列,要遵照最左前缀法则。指的是查询从索引的最左前列开始而且不跳过索引中的列
     -- 根据建立索引的顺序 name, age, position 如下6条都会走索引,其中第三条顺序变了,可是mysql会优化调整顺序
     -- 因此也是会走索引 可是前提是其中必须出现最左的索引(name) 否则的话是不会走索引的,好比第八条sql
     explain select * from employees where name = 'LiLei';
     explain select * from employees where name = 'LiLei' and age = 22;
     explain select * from employees where age = 22 and name = 'LiLei';
     explain select * from employees where name = 'LiLei' and age = 22 and position = 'manager';

    图14

    -- 这条sql第一个しname,符合了原则,可是position是在第三个的,中间漏了age,因此不是所有走了索引,使用的是  Using index condition
     explain select * from employees where name = 'LiLei' and position = 'manager';
     explain select * from employees where position = 'manager' and name = 'LiLei';
  • 2.最左前缀

    若是索引了多列,要遵照最左前缀法则。指的是查询从索引的最左前列开始而且不跳过索引中的列
     
     -- 如下2条sql没有遵照最左匹配原则 使用的是 Using where
     explain select * from employees where age = 22;
     explain select * from employees where age = 22 and position = 'manager';

    图15

  • 3.索引列不要使用函数

    不要在索引列上作任何操做(计算、函数、(自动or手动)类型转换),会致使索引失效而转向全表扫描
     
     explain select * from employees where name = 'LiLei';
     explain select * from employees where left(name,3) = 'LiLei';
     
     
     给hire_time增长一个普通索引,而后使用函数包装查询
     alter table `employees` add index `idx_hire_time` (`hire_time`) using btree;
     explain select * from employees where date(hire_time) = '2018-09-30';
     
     优化为日期范围查询,走索引
     explain select * from employees where hire_time >= '2018-09-30 00:00:00' and hire_time <= '2018-09-30 23:59:59';
     
     删除索引
     alter table `employees` drop index `idx_hire_time`;

图16

  • 4.索引中范围条件右边的列没法使用索引

    存储引擎不能使用索引中范围条件右边的列
     
     explain select * from employees where name = 'LiLei' and age = 22 AND position = 'manager';
     explain select * from employees where name = 'LiLei' and age > 22 AND position = 'manager';

图17

  • 5.使用覆盖索引

    尽可能使用覆盖索引(只访问索引的查询(索引列包含查询列)),减小select *语句
     
     explain select name,age from employees where name = 'LiLei' AND age = 23 AND position = 'manager';
     explain select * from employees where name = 'LiLei' AND age = 23 AND position = 'manager';

-6.使用不等于(!=或者<>),is nul is not null 的时候没法使用索引

mysql在使用不等于(!=或者<>)的时候没法使用索引会致使全表扫描
    is null,is not null 也没法使用索引
    
    explain select * from employees where name != 'LiLei';
    explain select * from employees where name is null;

图19

-7.like以通配符开头('$abc...')mysql索引失效会变成全表扫描操做

explain select * from employees where name like '%Lei';
    
    如何解决like'%字符串%'索引不被使用的方法?
        1.使用覆盖索引,查询字段必须是创建覆盖索引字段
            explain select name,age,position from employees where name like '%Lei%';
        2.若是不能使用覆盖索引则可能须要借助搜索引擎
            easysearch等
            
     like KK% 至关于=常量,%KK和%KK% 至关于范围

图20

  • 8.字符串不加单引号索引失效

    底层加了函数进行转换,使用了函数,没法使用索引
     
    
     explain select * from employees where name = '1000';
     explain select * from employees where name = 1000;

图21

-9.少用or或in,用它查询时,mysql不必定使用索引,

mysql内部优化器会根据检索比例、表大小等多个因素总体评估是否使用索引,详见范围查询优化
    
    explain select * from employees where name = 'LiLei' or name = 'HanMeimei';

图22

-10.范围查询优化

给年龄添加单值索引
    alter table `employees` add index `idx_age` (`age`) using btree;
    
    explain select * from employees where age >=1 and age <=2000;
    
    没走索引缘由:(不必定)
        mysql内部优化器会根据检索比例、表大小等多个因素总体评估是否使用索引。
        好比这个例子,多是因为单次数据量查询过大致使优化器最终选择不走索引
    优化方法:能够讲大的范围拆分红多个小范围
    
    -- 数据库中一共就三条数据22,23,23
    -- 这条查询不会走索引 idx_age 由于全部数据都会返回
    explain select * from employees where age >=22 and age <=1000;
    -- 这条会走索引 idx_age
    explain select * from employees where age >=23 and age <=1000;
    -- 这条会走索引 idx_age
    explain select * from employees where age >= 1001 and age <= 2000;
    
    alter table `employees` drop index `idx_age`;

图23

6. Mysql如何选择合适的索引

  • 索引覆盖的实践优化

    explain select * from employees where name > 'a';
      
      以上sql,若是用name索引须要遍历name字段联合索引树,而后还须要根据遍历出来的主键值去主键索引树里再去查出最终数据,
      成本比全表扫描还高,能够用覆盖索引优化,这样只须要遍历name字段的联合索引树就能拿到全部结果,以下:
      
      explain select * from employees where name > 'a';
      
      explain select name,age,position from employees where name > 'a';
       
      explain select * from employees where name > 'zzz';
      
      
      对于上面这两种 name>'a'和name>'zzz' 的执行结果,mysql最终是否选择走索引或者一张表涉及多个索引,
      mysql最终如何选择索引,咱们能够用trace工具来一查究竟,开启trace工具会影响mysql性能,
      因此只能临时分析sql使用,用完以后当即关闭
         
        set session optimizer_trace ="enabled=on",end_markers_in_json=on; ‐‐开启trace
        select * from employees where name > 'a' order by position;
        select * from information_schema.OPTIMIZER_TRACE;
        
        
       "steps": [
         {
           /* ‐‐第一阶段:SQL准备阶段 */
           "join_preparation": {
             "select#": 1,
             "steps": [
               {
                 "expanded_query": "/* select#1 */ select `employees`.`id` AS `id`,`employees`.`NAME` AS `NAME`,`employees`.`age` AS `age`,`employees`.`position` AS `position`,`employees`.`hire_time` AS `hire_time` from `employees` where (`employees`.`NAME` > 'a') order by `employees`.`position`"
               }
             ] /* steps */
           } /* join_preparation */
         },
         {
           /* 第二阶段:SQL优化阶段 */
           "join_optimization": {
             "select#": 1,
             "steps": [
               {
                 /* ‐‐条件处理 */
                 "condition_processing": {
                   "condition": "WHERE",
                   "original_condition": "(`employees`.`NAME` > 'a')",
                   "steps": [
                     {
                       "transformation": "equality_propagation",
                       "resulting_condition": "(`employees`.`NAME` > 'a')"
                     },
                     {
                       "transformation": "constant_propagation",
                       "resulting_condition": "(`employees`.`NAME` > 'a')"
                     },
                     {
                       "transformation": "trivial_condition_removal",
                       "resulting_condition": "(`employees`.`NAME` > 'a')"
                     }
                   ] /* steps */
                 } /* condition_processing */
               },
               {
                 "substitute_generated_columns": {
                 } /* substitute_generated_columns */
               },
               {
                 /* 表依赖详情 */
                 "table_dependencies": [
                   {
                     "table": "`employees`",
                     "row_may_be_null": false,
                     "map_bit": 0,
                     "depends_on_map_bits": [
                     ] /* depends_on_map_bits */
                   }
                 ] /* table_dependencies */
               },
               {
                 "ref_optimizer_key_uses": [
                 ] /* ref_optimizer_key_uses */
               },
               {
                 /* 预估表的访问成本 */
                 "rows_estimation": [
                   {
                     "table": "`employees`",
                     "range_analysis": {
                       /* 全表扫描状况 */
                       "table_scan": {
                         "rows": 3,   ‐‐扫描行数
                         "cost": 3.7  ‐‐查询成本
                       } /* table_scan */,
                       "potential_range_indexes": [ ‐‐查询可能使用的索引
                         {
                           "index": "PRIMARY",      ‐‐主键索引
                           "usable": false,
                           "cause": "not_applicable"
                         },
                         {
                           "index": "idx_name_age_position", ‐‐辅助索引
                           "usable": true,
                           "key_parts": [
                             "NAME",
                             "age",
                             "position",
                             "id"
                           ] /* key_parts */
                         },
                         {
                           "index": "idx_age",
                           "usable": false,
                           "cause": "not_applicable"
                         }
                       ] /* potential_range_indexes */,
                       "setup_range_conditions": [
                       ] /* setup_range_conditions */,
                       "group_index_range": {
                         "chosen": false,
                         "cause": "not_group_by_or_distinct"
                       } /* group_index_range */,
                       "analyzing_range_alternatives": { ‐‐分析各个索引使用成本
                         "range_scan_alternatives": [
                           {
                             "index": "idx_name_age_position",
                             "ranges": [    ‐‐索引使用范围
                               "a < NAME"
                             ] /* ranges */,
                             "index_dives_for_eq_ranges": true,
                             "rowid_ordered": false,  ‐‐使用该索引获取的记录是否按照主键排序
                             "using_mrr": false,
                             "index_only": false,     ‐‐是否使用覆盖索引
                             "rows": 3,               ‐‐索引扫描行数
                             "cost": 4.61,            ‐‐索引使用成本
                             "chosen": false,         ‐‐是否选择该索引
                             "cause": "cost"
                           }
                         ] /* range_scan_alternatives */,
                         "analyzing_roworder_intersect": {
                           "usable": false,
                           "cause": "too_few_roworder_scans"
                         } /* analyzing_roworder_intersect */
                       } /* analyzing_range_alternatives */
                     } /* range_analysis */
                   }
                 ] /* rows_estimation */
               },
               {
                 "considered_execution_plans": [
                   {
                     "plan_prefix": [
                     ] /* plan_prefix */,
                     "table": "`employees`",
                     "best_access_path": {     ‐‐最优访问路径
                       "considered_access_paths": [   ‐‐最终选择的访问路径
                         {
                           "rows_to_scan": 3,
                           "access_type": "scan",    ‐‐访问类型:为scan,全表扫描
                           "resulting_rows": 3,
                           "cost": 1.6,
                           "chosen": true,           ‐‐肯定选择
                           "use_tmp_table": true
                         }
                       ] /* considered_access_paths */
                     } /* best_access_path */,
                     "condition_filtering_pct": 100,
                     "rows_for_plan": 3,
                     "cost_for_plan": 1.6,
                     "sort_cost": 3,
                     "new_cost_for_plan": 4.6,
                     "chosen": true
                   }
                 ] /* considered_execution_plans */
               },
               {
                 "attaching_conditions_to_tables": {
                   "original_condition": "(`employees`.`NAME` > 'a')",
                   "attached_conditions_computation": [
                   ] /* attached_conditions_computation */,
                   "attached_conditions_summary": [
                     {
                       "table": "`employees`",
                       "attached": "(`employees`.`NAME` > 'a')"
                     }
                   ] /* attached_conditions_summary */
                 } /* attaching_conditions_to_tables */
               },
               {
                 "clause_processing": {
                   "clause": "ORDER BY",
                   "original_clause": "`employees`.`position`",
                   "items": [
                     {
                       "item": "`employees`.`position`"
                     }
                   ] /* items */,
                   "resulting_clause_is_simple": true,
                   "resulting_clause": "`employees`.`position`"
                 } /* clause_processing */
               },
               {
                 "reconsidering_access_paths_for_index_ordering": {
                   "clause": "ORDER BY",
                   "steps": [
                   ] /* steps */,
                   "index_order_summary": {
                     "table": "`employees`",
                     "index_provides_order": false,
                     "order_direction": "undefined",
                     "index": "unknown",
                     "plan_changed": false
                   } /* index_order_summary */
                 } /* reconsidering_access_paths_for_index_ordering */
               },
               {
                 "refine_plan": [
                   {
                     "table": "`employees`"
                   }
                 ] /* refine_plan */
               }
             ] /* steps */
           } /* join_optimization */
         },
         {
           "join_execution": {  ‐‐第三阶段:SQL执行阶段
             "select#": 1,
             "steps": [
               {
                 "filesort_information": [
                   {
                     "direction": "asc",
                     "table": "`employees`",
                     "field": "position"
                   }
                 ] /* filesort_information */,
                 "filesort_priority_queue_optimization": {
                   "usable": false,
                   "cause": "not applicable (no LIMIT)"
                 } /* filesort_priority_queue_optimization */,
                 "filesort_execution": [
                 ] /* filesort_execution */,
                 "filesort_summary": {
                   "rows": 3,
                   "examined_rows": 3,
                   "number_of_tmp_files": 0,
                   "sort_buffer_size": 262080,
                   "sort_mode": "<sort_key, packed_additional_fields>"
                 } /* filesort_summary */
               }
             ] /* steps */
           } /* join_execution */
         }
       ] /* steps */
     }
    
     结论:全表扫描的成本低于索引扫描,因此mysql最终选择全表扫描
     
     select * from employees where name > 'zzz' order by position;
     select * from information_schema.OPTIMIZER_TRACE;
     查看trace字段可知索引扫描的成本低于全表扫描,因此mysql最终选择索引扫描
     
     set session optimizer_trace ="enabled=on",end_markers_in_json=off; ‐‐关闭trace

图24

相关文章
相关标签/搜索