索引,性能分析及优化

1. 索引的分类

MySQL的索引包括普通索引、唯一性索引、单列索引、多列索引、全文索引和空间索引等。

  • 从功能逻辑上说,索引主要有 4 种,分别是普通索引、唯一索引、主键索引、全文索引。
  • 按照物理实现方式 ,索引可以分为 2 种:聚簇索引和非聚簇索引。
  • 按照作用字段个数进行划分,分成单列索引和联合索引。

普通索引

在创建普通索引时,不附加任何限制条件,只是用于提高查询效率。这类索引可以创建在任何数据类型中,其值是否唯一和非空,是由字段本身的完整性约束条件决定。

例如,在表student的字段name上建立一个普通索引,查询记录时就可以使用该索引。

唯一性索引

使用UNIQUE参数可以设置索引为唯一性索引,在创建唯一性索引时,限制该索引的值必须是唯一的,但允许有空值。在一张数据表里可以有多个唯一索引。

例如,在表student的字段email中创建唯一性索引,那么字段email的值就必须是唯一的。通过唯一性索引可以更快速地确定某条记录。

主键索引

主键索引就是一种特殊的唯一性索引,在唯一索引的基础上增加了不为空的约束,也就是NOT NULL+UNIQUE,一张表里最多只有一个主键索引。

Why?这是由主键索引的物理实现方式决定的,因为数据存储在文件中只能按照一种顺序进行存储。

多列(组合/联合)索引

多列索引是在表的多个字段组合上创建一个索引。该索引指向创建时对应的多个字段,可以通过这几个字段进行查询,但是只有查询条件中使用了这些字段中的第一个字段时才会被使用

例如,在表中的字段id、name和gender上建立一个多列索引idxid_name_gender,只有在查询条件中使用了字段id时该索引才会被使用。使用组合索引时遵循最左前缀集合。

 全文索引

使用参数FULLTEXT可以设置索引为全文索引。在定义索引的列上支持值的全文查找,允许在这些索引列中插入重复值和空值。全文索引只能创建在CHAR、VARCHAR或TEXT类型及其系列类型的字段上,查询数据量较大的字符串类型的字段时,使用全文索引可以提高查询速度。

例如,表student的字段information是TEXT类型该字段包含了很多文字信息。在字段information上建立全文索引后,可以提高查询字段information的速度。

 全文索引典型的有两种类型:自然语言的全文索引和布尔全文索引

  • 自然语言搜索引擎将计算每一个文档对象和查询的相关度。这里,相关度是基于匹配的关键词的个数,以及关键词在文档中出现的次数。在整个索引中出现次数越少的词语,匹配时的相关度就越高。相反,非常常见的单词将不会被搜索,如果一个词语的在超过50%的记录中都出现了,那么自然语言的搜索将不会搜索这类词语。

MySQL数据库从3.23.23版开始支持全文索引,但MySQL5.6.4以前只有Myisam支持,5.6.4版本以后innodb才支持,但是官方版本不支持中文分词,需要第三方分词插件。在5.7.6版本,MySOL内置了ngram全文解析器,用来支持亚洲语种的分词。测试或使用全文索引时,要先看一下自己的MySQL版本、存储引擎和数据类型是否支持全文索引。此外,随着大数据时代的到来,关系型数据库应对全文索引的需求已力不从心,逐渐被Solr、ElasticSearch等专门的搜索引擎所替代。

空间索引

使用参数SPATIAL可以设置索引为空间索引。空间索引只能建立在空间数据类型上,这样可以提高系统获取空间数据的效率。MvSOL中的空间数据类型包括GEOMETRY、POINT、LINESTRING和POLYGON等。目前只有MvISAM存储引警支持空间检索,而且索引的字段不能为空值

小结:不同的存储引擎支持的索引类型也不一样

  • InnoDB:支持B-tree、Full-text等索引,不支持Hash索引;
  • MylSAM:支持B-tree、Full-text等索引,不支持Hash索引:
  • Memory:支持B-tree、Hash等索引,不支持Full-text索引;
  • NDB:支持Hash索引,不支持B-tree、Full-text等索引;
  • Archive:不支持B-tree、Hash、Full-text等索引;

2.MySQL8.0索引新特性

2.1 支持降序索引

降序索引以降序存储键值。虽然在语法上,从MySQL4版本开始就已经支持降序索引的语法了,但实际上该DESC定义是被忽略的,直到MySQL8x版本才开始真正支持降序索引(仅限于InnoDB存储引擎)。
MySQL在8.0版本之前创建的仍然是升序索引,使用时进行反向扫描,这大大降低了数据库的效率。在某些场景下,降序索引意义重大。例如,如果一个查询,需要对多个列进行排序,且顺序要求不一致,那么使用降序索引将会避免数据库使用额外的文件排序操作,从而提高性能。

 

 2.2 隐藏索引

在MySQL5.7版本及之前,只能通过显式的方式删除索引。此时,如果发现删除索引后出现错误,又只能通过显式创建索引的方式将删除的索引创建回来。如果数据表中的数据量非常大,或者数据表本身比较大,这种操作就会消耗系统过多的资源,操作成本非常高。
从MySQL8x开始支持隐藏索引(invisible indexes),只需要将待删除的索引设置为隐藏索引,使查询优化器不再使用这个索引(即使使用force index(强制使用索引),优化器也不会使用该索引),确认将索引设置为隐藏索引后系统不受任何响应,就可以彻底删除索引。这种通过先将索引设置为隐藏索引,再删除索引的方式就是软删除。同时,如果你想验证某个索引删除之后的查询性能影响,就可以暂时先隐藏该索引。

注:主键不能被设置为隐藏索引。当表中没有显示主键时,表中第一个唯一非空索引会成为隐式主键,也不能设置为隐藏索引。

索引默认是可见的,在使用CREATETABLE,CREATEINDEX或者ALTERTABLE等语句时可以通过VISIBLE或者 INVISIBLE关键词设置索引的可见性。切换索引可见状态 已存在的索引可通过如下语句切换可见状态:

ALTER TABLE tablename ALTER INDEX index_name INVISIBLE; #切换成隐藏索引
ALTER TABLE tablename ALTER INDEX index_name VISIBLE; #切换成非隐藏索引

 注:当索引被隐藏时,它的内容仍然是和正常索引一样实时更新的。如果一个索引需要长期被隐藏,那么可以将其删除,因为索引的存在会影响插入、更新和删除的性能。通过设置隐藏索引的可见性可以查看索引对调优的帮助。

3. 索引的设计原则

3.1 适合创建索引的情况

1. 字段的数值有唯一性的限制

业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。(来源:Alibaba)
说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的。

2. 频繁作为 WHERE 查询条件的字段(UPDATE、DELETE 的 WHERE 条件列)

某个字段在SELECT语句的 WHERE 条件中经常被使用到,那么就需要给这个字段创建索引了。尤其是在数据量大的情况下,创建普通索引就可以大幅提升数据查询的效率。

3. 经常 GROUP BY 和 ORDER BY 的列

索引就是让数据按照某种顺序进行存储或检索,因此当我们使用 GROUP BY 对数据进行分组查询,或者使用 ORDER BY 对数据进行排序的时候,就需要 对分组或者排序的字段进行索引 。如果待排序的列有多个,那么可以在这些列上建立 组合索引 。

4. DISTINCT 字段需要创建索引 

使用 DISTINCT,那么对这个字段创建索引,也会提升查询效率。是因为索引会对数据按照某种顺序进行排序,所以在去重的时候也会快很多。

5. 多表 JOIN 连接操作时,创建索引注意事项

  • 首先, 连接表的数量尽量不要超过 3 张 ,因为每增加一张表就相当于增加了一次嵌套的循环,数量级增长会非常快,严重影响查询的效率。
  • 其次, 对 WHERE 条件创建索引 ,因为 WHERE 才是对数据条件的过滤。如果在数据量非常大的情况下,没有 WHERE 条件过滤是非常可怕的。
  • 最后, 对用于连接的字段创建索引 ,并且该字段在多张表中的 类型必须一致 。比如 course_id 在student_info 表和 course 表中都为 int(11) 类型,而不能一个为 int 另一个为 varchar 类型。

6. 区分度高(散列性高)的列适合作为索引

 7. 使用最频繁的列放到联合索引的左侧

8. 在多个字段都要创建索引的情况下,联合索引优于单值索引 

 9. 使用列的类型小的创建索引(一个数据页中可以存放更多的列值)

10. 使用字符串前缀创建索引 

3.2 不适合创建索引的情况

  1. 在where中使用不到的字段,不要设置索引
  2. 数据量小的表最好不要使用索引
  3. 有大量重复数据的列上不要建立索引
  4. 避免对经常更新的表创建过多的索引
  5. 不建议用无序的值作为索引(例如身份证、UUID、MD5、HASH、无序长字符串等。)
  6. 删除不再使用或者很少使用的索引
  7. 不要定义冗余或重复的索引

4. 性能分析

4.1 数据库服务器的优化步骤

在遇到数据库调优问题时,可以参考如下流程图。整个流程划分为观察(show status)和行动(action)两部分。字母S的部分代表观察(使用相应的分析工具),字母A代表的部分是行动(对应分析可以采取的行动)。

详细解释一下这张图:

  1. 首先在S1部分,我们需要观察服务器的状态是否存在周期性的波动。如果存在周期性波动,有可能是周期性节点的原因,比如双十一、促销活动等。这样的话,我们可以通过A1这一步骤解决,也就是加缓存,或者更改缓存失效策略。
  2. 如果缓存策略没有解决,或者不是周期性波动的原因,我们就需要进一步分析查询延迟和卡顿的原因。接下来进入 S2这一步,我们需要开启慢查询。慢查询可以帮我们定位执行慢的SQL语句。我们可以通过设置long_query_time参数定义“慢”的阈值,如果SQL执行时间超过long_query_time,则会认为是慢查询。当收集上来这些慢查询之后,我们就可以通过分析工具对慢查询日志进行分析。在S3这一步骤中,我们就知道了执行慢的SQL,这样就可以针对性地用EXPLAIN 查看对应SQL语句的执行计划,或者使用show profile查看SQL中每一个步骤的时间成本。这样我们就可以了解SQL查询慢是因为执行时间长,还是等待时间长。
  3. 如果是SOL等待时间长,我们进入A2步骤。在这一步骤中,我们可以调优服务器的参数,比如适当增加数据库缓冲池等。如果是SOL执行时间长,就进入A3步骤,这一步中我们需要考虑是索引设计的问题?还是查询关联的数据表过多?还是因为数据表的字段设计问题导致了这一现象。然后在这些维度上进行对应的调整。
  4. 如果A2和A3都不能解决问题,我们需要考虑数据库自身的SOL查询性能是否已经达到了瓶颈,如果确认没有达到性能瓶颈,就需要重新检查,重复以上的步骤。如果已经达到了性能瓶颈,进入A4阶段,需要考虑增加服务器,采用读写分离的架构,或者考虑对数据库进行分库分表,比如垂直分库、垂直分表和水平分表等。

以上就是数据库调优的流程思路。如果我们发现执行SQL时存在不规则延迟或卡顿的时候,就可以采用分析工具帮我们定位有问题的SOL,这三种分析工具你可以理解是SOL调优的三个步骤:慢查询、EXPLAIN 和 SHOW PROFILING。
 

 4.2 查看系统性能参数

在MySQL中,可以使用 SHOW STATUS 查询一些MySQL服务器的性能参数 、 执行频率等 。SHOW STATUS语句语法如下:

SHOW [GLOBAL|SESSION] STATUS LIKE '参数';

一些常用的性能参数如下:

  • Connetions:连接MySQL服务器的次数。
  • Uptime:MySQL服务器的上线时间。
  • Slow_queries:慢查询的次数。
  • Innodb_rows_read:Select查询返回的行数
  • Innodb_rows_inserted:执行INSERT操作插入的行数
  • Innodb_rows_updated:执行UPDATE操作更新的行数
  • Innodb_rows_deleted:执行DELETE操作删除的行数
  • Com_select:查询操作的次数。
  • Com_insert:插入操作的次数。对于批量插入的 INSERT 操作,只累加一次。
  • Com_update:更新操作的次数。
  • Com_delete:删除操作的次数。
  • last_query_cost:查询成本对应的是SQL语句所需要读取的页的数量。

SOL查询是一个动态的过程,从页加载的角度来看,我们可以得到以下两点结论:

  • 位决定效率。如果页就在数据库缓冲池中,那么效率是最高的,否则还需要从内存或者磁盘中进行读取,当然针对单个页的读取来说,如果页存在于内存中,会比在磁盘中读取效率高很多。
  • 决定效率。如果我们从磁盘中对单一页进行随机读,那么效率是很低的(差不多10ms),而采用顺序读取的方式,批量对页进行读取,平均一页的读取效率就会提升很多,甚至要快于单个页面在内存中的随机读取。

所以说,遇到 I/O 并不用担心,方法找对了,效率还是很高的。我们首先要考虑数据存放的位置,如果是经常使用的数据就要尽量放到缓冲池中,其次我们可以充分利用磁盘的吞吐能力,一次性批量读取数据,这样单个页的读取效率也就得到了提升。

4.3 定位执行慢的SQL

1. 开启slow_query_log(默认关闭,因为影响性能)

mysql > set global slow_query_log='ON';
mysql > show varibales like '%slow_query_log%';

2. 修改long_query_time阈值

#测试发现:设置global的方式对当前session的long_query_time失效。对新连接的客户端有效。所以可以一并
执行下述语句
mysql > set global long_query_time = 1;
mysql > show global variables like '%long_query_time%';
mysql > set long_query_time = 1;
mysql > show variables like '%long_query_time%';

3. 查看慢查询数码

mysql > SHOW GLOBAL STATUS LIKE '%Slow_queries%';

除了上述变量,控制慢查询日志的还有一个系统变量:min_examined_row_limit。这个变量的意思是,查询扫描过的最少记录数。这个变量和查询执行时间,共同组成了判别一个查询是否是慢查询的条件。如果查询扫描过的记录数大于等于这个变量的值,并且查询执行时间超过long_query_time的值,那么,这个查询就被记录到慢查询日志中;反之,则不被记录到慢查询日志中。

这个值默认是0。与long_query_time=10合在一起,表示只要查询的执行时间超过10秒钟,哪怕一个记录也没有扫描过,都要被记录到慢查询日志中。你也可以根据需要,通过修改“my.ini”文件,来修改查询时长,或者通过SET指令,用SQL语句修改“min_examined row_limit”的值。

4.  查看 SQL 执行成本:SHOW PROFILE

mysql > show variables like 'profiling'; 
mysql > set profiling = 'ON'; #通过设置 profiling='ON’ 来开启 show profile

#然后执行相关的查询语句


mysql > show profiles;  #查看统计的所有执行成本
mysql > show profile; #查看最近一次查询的开销
mysql > show profile cpu,block io for query 2; #查看执行查询语句的执行开销

show profile的常用查询参数:

  • ALL:显示所有的开销信息。
  • BLOCK IO:显示块IO开销。
  • CONTEXT SWITCHES:上下文切换开销。
  • CPU:显示CPU开销信息。
  • IPC:显示发送和接收开销信息。
  • MEMORY:显示内存开销信息。
  • PAGE FAULTS:显示页面错误开销信息。
  • SOURCE:显示和Source_function,Source_file,Source_line相关的开销信息。
  • SWAPS:显示交换次数开销信息。

 executing:执行耗时严重,需要优化sql或者索引

5. 使用EXPLAIN分析

5.1 使用语法

  • MySQL 5.6.3以前只能 EXPLAIN SELECT ;MYSQL 5.6.3以后就可以 EXPLAIN SELECT,UPDATE,DELETE
  • 在5.7以前的版本中,想要显示 partitions 需要使用 explain partitions 命令;想要显示filtered 需要使用 explain extended 命令。在5.7版本后,默认explain直接显示partitions和filtered中的信息。

EXPLAIN 或 DESCRIBE语句的语法形式如下:

EXPLAIN SELECT select_options
或者
DESCRIBE SELECT select_options
  • id 在一个大的查询语句中每个SELECT关键字都对应一个 唯一的id
  • select_type SELECT关键字对应的那个查询的类型
  • table 表名
  • partitions 匹配的分区信息
  • type 针对单表的访问方法(访问类型)
  • possible_keys 可能用到的索引
  • key 实际上使用的索引
  • key_len 实际使用到的索引长度
  • ref 当使用索引列等值查询时,与索引列进行等值匹配的对象信息
  • rows 预估的需要读取的记录条数
  • filtered 某个表经过搜索条件过滤后剩余记录条数的百分比
  • Extra 一些额外的信息(如using index, using index condition)

5.2 EXPLAIN各列作用

table

不论我们的查询语句有多复杂,里边儿 包含了多少个表 ,到最后也是需要对每个表进行 单表访问 的,所以MySQL规定EXPLAIN语句输出的每条记录都对应着一个单表,该条记录的table列代表着该表的表名(有时不是真实的表名字,可能是简称)。

 id

在一个大的查询语句中每个select关键字都对应一个唯一的id。

  • id如果相同,可以认为是一组,从上往下顺序执行
  • 在所有组中,id值越大,优先级越高,越先执行
  • 关注点:id号每个号码,表示一趟独立的查询, 一个sql的查询趟数越少越好

 select_type

每一个select关键字代表的小查询都定义了一个称之为select_type的属性,表示当前的小查询在大查询中扮演的角色。

  • SIMPLE: Simple SELECT (not using UNION or subqueries),查询语句中不包含‘union’或者子查询的查询都算作‘simple’,包括连接查询
  • PRIMARY: Outermost SELECT,包含‘union’或者子查询的最外层select
  • UNION: Second or later SELECT statement in a UNION,union语句中靠后的select
  • UNION RESULT: Result of a UNION,union的结果集
  • SUBQUERY: First SELECT in subquery,子查询中的第一个select(不相关子查询)
  • DEPENDENT SUBQUERY:First SELECT in subquery, dependent on outer query(相关子查询)
  • DEPENDENT UNION:Second or later SELECT statement in a UNION, dependent on outer query(相关union)
  • DERIVED: Derived table,派生表(从每个表中select结果当作外层的from数据源)
  • MATERIALIZED: Materialized subquery,物化子查询
  • UNCACHEABLE SUBQUERY:A subquery for which the result cannot be cached and must be re-evaluated for each row of the outer query
  • UNCACHEABLE UNION:The second or later select in a UNION that belongs to an uncacheable subquery (see UNCACHEABLE SUBQUERY)

 partitions

代表分区表中的命中情况,非分区表,该项为null。一般情况下我们的查询语句的执行计划的partitions列的值都是null。

 type⭐

代表着mysql对某个表,在执行查询时的访问类型。完整的访问类型如下: system , const , eq_ref , ref , fulltext , ref_or_null ,index_merge , unique_subquery , index_subquery , range , index , ALL 。

system: 当表中`只有一条记录`并且该表使用的存储引擎(MyISAM、Memory(Innodb不可以))的统计数据是精确的,那么对该表的访问方法就是`system`。

const:当我们根据主键或者唯一二级索引列与常数进行等值匹配时,对单表的访问方法就是`const`。

eq_ref: 在连接查询时,如果被驱动表是通过主键或者唯一二级索引列等值匹配的方式进行访问的(如果该主键或者唯一二级索引是联合索引的话,所有的索引列都必须进行等值比较),则对该被驱动表的访问方法就是`eq_ref`。

ref:当通过普通的二级索引列与常量进行等值匹配时来查询某个表,那么对该表的访问方法就可能是`ref`。

  • EXPLAIN SELECT * FROM s1 WHERE key1 = 'a';

ref_or_null:当对普通二级索引进行等值匹配查询,该索引列的值也可以是`NULL`值时,那么对该表的访问方法就可能是`ref_or_null`

  • EXPLAIN SELECT * FROM s1 WHERE key1 = 'a' OR key1 IS NULL;

index_merge:单表访问方法时在某些场景下可以使用`Intersection`、`Union`、`Sort-Union`这三种索引合并的方式来执行查询

  • EXPLAIN SELECT * FROM s1 WHERE key1 = 'a' OR key3 = 'a'; #key1和key3分别为单列索引,在执行时索引将合并;如果是and,只会使用到key1

unique_subquery:针对在一些包含`IN`子查询的查询语句中,如果查询优化器决定将`IN`子查询转换为`EXISTS`子查询,而且子查询可以使用到主键进行等值匹配的话,那么该子查询执行计划的`type`列的值就是`unique_subquery`

range:如果使用索引获取某些`范围区间`的记录,那么就可能使用到`range`访问方法

  • EXPLAIN SELECT * FROM s1 WHERE key1 IN ('a', 'b', 'c');
  • EXPLAIN SELECT * FROM s1 WHERE key1 > 'a' AND key1 < 'b';

index:当我们可以使用索引覆盖(不用回表),但需要扫描全部的索引记录时,该表的访问方法就是`index`,select和where后都有联合索引的部分列

  •  EXPLAIN SELECT key_part2 FROM s1 WHERE key_part3 = 'a';

all:全表扫描

 结果值从最好到最坏依次是: system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL 其中比较重要的几个提取出来(见上图中的蓝色)。SQL 性能优化的目标:至少要达到 range 级别,要求是 ref 级别,最好是 consts级别。(阿里巴巴开发手册要求)

 possible_keys和key

表示在某个查询语句中,对某个表执行单表查询时可能用到的索引有哪些。一般查询涉及到的字段上所存在索引,则该索引将被列出,但不一定被查询使用。key列表示实际用到的索引(选择成本比较低的,并非时间最短的),如果为null,则没有使用到索引。key并非是possible_keys的子集,possible_keys有可能为null,而key实际使用到了索引。

 key_len

实际使用到的索引长度(即:字节数),帮你检查`是否充分的利用上了索引`,`值越大越好`。主要针对于联合索引,有一定的参考意义,值越大用到的索引越多。

  • varchar(10)变长字段且允许NULL = 10 * ( character set:utf8=3, gbk=2, latin1=1) + 1(NULL) + 2(变长字段)
  • varchar(10)变长字段且不允许NULL = 10 * ( character set:utf8=3,gbk=2,latin1=1) + 2(变长字段)
  • char(10)固定字段且允许NULL = 10 * ( character set:utf8=3,gbk=2,latin1=1)+1(NULL)
  • char(10)固定字段且不允许NULL = 10 * ( character set:utf8=3,gbk=2,latin1=1)

ref

当使用索引列等值查询时,与索引列进行等值匹配的对象信息。比如只是一个常数或者是某个列。

  rows

预估的需要读取的记录条数,`值越小越好`。

  filtered

某个表经过搜索条件过滤后剩余记录条数的百分比,值越大越好。

  • 如果使用的是索引执行的单表扫描,那么计算时需要估计出满足除使用到对应索引的搜索条件外的其他搜索条件的记录有多少条。
  • 对于单表查询来说,这个filtered列的值没什么意义,我们`更关注在连接查询中驱动表对应的执行计划记录的 filtered 值`,它决定了被驱动表要执行的次数(即:rows * filtered)
  • EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.key1 = s2.key1 WHERE s1.common_field = 'a';

 Extra

一些额外的信息,更准确的理解MySQL到底将如何执行给定的查询语句。

#No tables used:当查询语句的没有`FROM`子句时将会提示该额外信息
 EXPLAIN SELECT 1;
 
 
 #Impossible WHERE:查询语句的`WHERE`子句永远为`FALSE`时将会提示该额外信息
 EXPLAIN SELECT * FROM s1 WHERE 1 != 1;
 
 
 #Using where:该语句的`WHERE`子句中的搜索条件有存在不使用索引的列时,在`Extra`列中会提示上述额外信息。
 EXPLAIN SELECT * FROM s1 WHERE common_field = 'a';
 EXPLAIN SELECT * FROM s1 WHERE key1 = 'a' AND common_field = 'a';
 
 
 #No matching min/max row:当查询列表处有`MIN`或者`MAX`聚合函数,但是并没有符合`WHERE`子句中
 #的搜索条件的记录时,将会提示该额外信息
 EXPLAIN SELECT MIN(key1) FROM s1 WHERE key1 = 'abcdefg';
 
 EXPLAIN SELECT MIN(key1) FROM s1 WHERE key1 = 'NlPros'; #NlPros 是 s1表中key1字段真实存在的数据
 
 #select * from s1 limit 10;
 
 #Using index(覆盖索引):当我们的查询列表以及搜索条件中只包含属于某个索引的列,也就是在可以
 #使用覆盖索引的情况下,在`Extra`列将会提示该额外信息。比方说下边这个查询中只
 #需要用到`idx_key1`而不需要回表操作:
 EXPLAIN SELECT key1,id FROM s1 WHERE key1 = 'a';
 
 
 #Using index condition(索引条件下推):有些搜索条件中虽然出现了索引列,但却不能使用到索引
 EXPLAIN SELECT * FROM s1 WHERE key1 > 'z' AND key1 LIKE '%a';
 
 
 #Using join buffer (Block Nested Loop):在连接查询执行过程中,当被驱动表不能有效的利用索引加快访问速度,MySQL一般会为
 #其分配一块名叫`join buffer`的内存块来加快查询速度,也就是我们所讲的`基于块的嵌套循环算法`
 EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.common_field = s2.common_field;
 
 
 #Not exists:当我们使用左(外)连接时,如果`WHERE`子句中包含要求被驱动表的某个列等于`NULL`值的搜索条件,
 #而且那个列又是不允许存储`NULL`值的,那么在该表的执行计划的Extra列就会提示`Not exists`额外信息
 EXPLAIN SELECT * FROM s1 LEFT JOIN s2 ON s1.key1 = s2.key1 WHERE s2.id IS NULL;
 
 
 #Using intersect(...) 、 Using union(...) 和 Using sort_union(...):
 #如果执行计划的`Extra`列出现了`Using intersect(...)`提示,说明准备使用`Intersect`索引
 #合并的方式执行查询,括号中的`...`表示需要进行索引合并的索引名称;
 #如果出现了`Using union(...)`提示,说明准备使用`Union`索引合并的方式执行查询;
 #出现了`Using sort_union(...)`提示,说明准备使用`Sort-Union`索引合并的方式执行查询。
 EXPLAIN SELECT * FROM s1 WHERE key1 = 'a' OR key3 = 'a';
 
 
 #Zero limit:当我们的`LIMIT`子句的参数为`0`时,表示压根儿不打算从表中读出任何记录,将会提示该额外信息
 EXPLAIN SELECT * FROM s1 LIMIT 0;
 
 
 #Using filesort:有一些情况下对结果集中的记录进行排序是可以使用到索引的。
 #比如:
 EXPLAIN SELECT * FROM s1 ORDER BY key1 LIMIT 10;
 
 #很多情况下排序操作无法使用到索引,只能在内存中(记录较少的时候)或者磁盘中(记录较多的时候)
 #进行排序,MySQL把这种在内存中或者磁盘上进行排序的方式统称为文件排序(英文名:`filesort`)。
 #如果某个查询需要使用文件排序的方式执行查询,就会在执行计划的`Extra`列中显示`Using filesort`提示
 EXPLAIN SELECT * FROM s1 ORDER BY common_field LIMIT 10;
 
 
 #Using temporary:在许多查询的执行过程中,MySQL可能会借助临时表来完成一些功能,比如去重、排序之类的,比如我们
 #在执行许多包含`DISTINCT`、`GROUP BY`、`UNION`等子句的查询过程中,如果不能有效利用索引来完成
 #查询,MySQL很有可能寻求通过建立内部的临时表来执行查询。如果查询中使用到了内部的临时表,在执行
 #计划的`Extra`列将会显示`Using temporary`提示
 EXPLAIN SELECT DISTINCT common_field FROM s1;
 
 #EXPLAIN SELECT DISTINCT key1 FROM s1;
 
 #同上。
 EXPLAIN SELECT common_field, COUNT(*) AS amount FROM s1 GROUP BY common_field;
 
 #执行计划中出现`Using temporary`并不是一个好的征兆,因为建立与维护临时表要付出很大成本的,所以
 #我们`最好能使用索引来替代掉使用临时表`。比如:扫描指定的索引idx_key1即可
 EXPLAIN SELECT key1, COUNT(*) AS amount FROM s1 GROUP BY key1;
 
#json格式的explain
EXPLAIN FORMAT=JSON SELECT * FROM s1 INNER JOIN s2 ON s1.key1 = s2.key2 
WHERE s1.common_field = 'a';
 

小结

  • EXPLAIN不考虑各种Cache
  • EXPLAIN不能显示MySQL在执行查询时所作的优化工作
  • EXPLAIN不会告诉你关于触发器、存储过程的信息或用户自定义函数对查询的影响情况
  • EXPLAIN的部分统计信息是估算的,并非精确值

6. optimizer_trace 和 sys.schema

6.1 optimizer_trace

OPTIMIZER_TRACE是MySQL5.6引入的一项跟踪功能,它可以跟踪优化器做出的各种决策(比如访问表的方法各种开销计算、各种转换等),并将跟踪结果记录到INFORMATION_SCHEMA.OPTIMIZER_TRACE 表中此功能默认关闭。开启trace,并设置格式为JSON,同时设置trace最大能够使用的内存大小,避免解析过程中因为默认内存过小而不能够完整展示。

SET optimizer_trace="enabled=on",end_markers_in_json=on;
set optimizer_trace_max_mem_size=1000000;

#测试
select * from student where id < 10;
select * from information_schema.optimizer_trace\G;

6.2 sys.schema

关于MySQL的性能监控和问题诊断,我们一般都从performanceschema中去获取想要的数据,在MySQL5.7.7版本中新增sysschema,它将performanceschema和informationschema中的数据以更容易理解的方式总结归纳为”视图”其目的就是为了降低查询performanceschema的复杂度,让DBA能够快速的定位问题。

  • 主机相关:以host_summary开头,主要汇总了IO延迟的信息。
  • Innodb相关:以innodb开头,汇总了innodb buffer信息和事务等待innodb锁的信息。
  • I/o相关:以io开头,汇总了等待I/O、I/O使用量情况。
  • 内存使用情况:以memory开头,从主机、线程、事件等角度展示内存的使用情况
  • 连接与会话信息:processlist和session相关视图,总结了会话相关信息。
  • 表相关:以schema_table开头的视图,展示了表的统计信息。
  • 索引信息:统计了索引的使用情况,包含冗余索引和未使用的索引情况。
  • 语句相关:以statement开头,包含执行全表扫描、使用临时表、排序等的语句信息。
  • 用户相关:以user开头的视图,统计了用户使用的文件I/O、执行语句统计信息。
  • 等待事件相关信息:以wait开头,展示等待事件的延迟情况。
###索引情况
#1. 查询冗余索引
select * from sys.schema_redundant_indexes;
#2. 查询未使用过的索引
select * from sys.schema_unused_indexes;
#3. 查询索引的使用情况
select index_name,rows_selected,rows_inserted,rows_updated,rows_deleted
from sys.schema_index_statistics where table_schema='dbname';


###表相关
# 1. 查询表的访问量
select table_schema,table_name,sum(io_read_requests+io_write_requests) as io from
sys.schema_table_statistics group by table_schema,table_name order by io desc;
# 2. 查询占用bufferpool较多的表
select object_schema,object_name,allocated,data
from sys.innodb_buffer_stats_by_table order by allocated limit 10;
# 3. 查看表的全表扫描情况
select * from sys.statements_with_full_table_scans where db='dbname';


###语句相关
#1. 监控SQL执行的频率
select db,exec_count,query from sys.statement_analysis
order by exec_count desc;
#2. 监控使用了排序的SQL
select db,exec_count,first_seen,last_seen,query
from sys.statements_with_sorting limit 1;
#3. 监控使用了临时表或者磁盘临时表的SQL
select db,exec_count,tmp_tables,tmp_disk_tables,query
from sys.statement_analysis where tmp_tables>0 or tmp_disk_tables >0
order by (tmp_tables+tmp_disk_tables) desc;

###IO相关
#1. 查看消耗磁盘IO的文件
select file,avg_read,avg_write,avg_read+avg_write as avg_io
from sys.io_global_by_file_by_bytes order by avg_read limit 10;


###Innodb 相关
#1. 行锁阻塞情况
select * from sys.innodb_lock_waits;



7. 索引优化与查询优化

可以进行数据库调优的维度:

  • 索引失效、没有充分利用索引 -- 建立索引
  • 关联查询太多join,设计缺陷或不得已的需求 -- SQL优化
  • 服务器调优及各个参数设置(缓冲、线程数等) -- 调整my.cnf
  • 数据过多 -- 分库分表

索引优化:大多数情况下采用B+tree来构建索引,空间列类型的索引使用R-tree,并且memory存储引擎还支持hash索引。其实,最终用不用索引还是优化器决定,优化器时基于cost开销(costBaseOptimizer),它不是基于规则(rule-basedoptimizer),也不是基于语义。另外,SQL语句是否使用索引,和数据库版本、数据量、数据选择度都有关系。

  • 全值匹配我最爱(where条件中的字段都有建立索引)
  • 最佳左前缀法则:主要针对联合索引来说,where条件中要先使用上靠左的索引列。MySQL可以为多个字段创建索引,一个索引可以包括16个字段。对于多列索引,过滤条件要使用索引必须按照索引建立时的顺序,依次满足,一旦跳过某个字段,索引后面的字段都无法被使用。如果查询条件中没有使用这些字段中第1个字段时,多列(或联合)索引不会被使用。 I
  • 计算、函数、类型转换(自动或手动)导致索引失效
  • 范围条件右边的列索引失效
  • 不等于(!= 或者<>)索引失效
  • is null可以使用索引,is not null无法使用索引
  • like以通配符%开头索引失效
  • OR 前后存在非索引的列,索引失效
  • 数据库和表的字符集统一使用utf8mb4

对于内连接来讲,如果表的连接条件中只能有一个字段有索引,则有索引的字段所在的表会被作为被驱动表出现。对于内连接来说,在两个表的连接条件都存在索引的情况下,会选择小表作为驱动表。“小表驱动大表”。“大小”的度量标准是,where过滤后的行数*每行大小。

7.1 Join语句原理

join方式连接多个表,本质就是各个表之间数据的循环匹配。MySQL5.5版本之前,MySQL只支持一种表间关联方式,就是嵌套循环(Nested Loop Join)。如果关联表的数据量很大,则ioin关联的执行时间会非常长。在MySOL5.5以后的版本中,MySQL通过引入BNLJ算法来优化嵌套执行。

Block Nested-LoopJoin(块嵌套循环连接)

如果存在索引,那么会使用index的方式进行join,如果join的列没有索引,被驱动表要扫描的次数太多了。每次访问被驱动表,其表中的记录都会被加载到内存中,然后再从驱动表中取一条与其匹配,匹配结束后清除内存,然后再从驱动表中加载一条记录,然后把被驱动表的记录在加载到内存匹配,这样周而复始,大大增加了10的次数。为了减少被驱动表的10次数,就出现了Block Nested-Loop Join的方式。不再是逐条获取驱动表的数据,而是一块一块的获取,引入了ioin buffer缓冲区,将驱动表join相关的部分数据列(大小受join buffer的限制)缓存到ioin buffer中,然后全表扫描被驱动表,被驱动表的每一条记录一次性和join buffer中的所有驱动表记录进行匹配(内存中操作),将简单嵌套循环中的多次比较合并成一次,降低了被驱动表的访问频率

注意:
这里缓存的不只是关联表的列,select后面的列也会缓存起来。在一个有N个join关联的sql中会分配N-1个join buffer。所以查询的时候尽量减少不必要的字段,可以让join buffer中可以存放更多的列。

 

  •  保证被驱动表的JOIN字段已经创建了索引
  • 需要JOIN 的字段,数据类型保持绝对一致。
  • LEFT JOIN 时,选择小表作为驱动表, 大表作为被驱动表 。减少外层循环的次数。
  • INNER JOIN 时,MySQL会自动将 小结果集的表选为驱动表 。选择相信MySQL优化策略。
  • 能够直接多表关联的尽量直接关联,不用子查询。(减少查询的趟数)
  • 不建议使用子查询,建议将子查询SQL拆开结合程序多次查询,或使用 JOIN 来代替子查询。
  • 衍生表建不了索引

7.2 子查询优化

使用子查询可以进行SELECT语句的嵌套查询,即一个SELECT查询的结果作为另一个SELECT语句的条件。 子查询可以一次性完成很多逻辑上需要多个步骤才能完成的SQL操作 。但是,子查询的执行效率不高:

  • 执行子查询时,MySQL需要为内层查询语句的查询结果 建立一个临时表 ,然后外层查询语句从临时表中查询记录。查询完毕后,再 撤销这些临时表 。这样会消耗过多的CPU和IO资源,产生大量的慢查询。
  • 子查询的结果集存储的临时表,不论是内存临时表还是磁盘临时表都 不会存在索引 ,所以查询性能会受到一定的影响。
  • 对于返回结果集比较大的子查询,其对查询性能的影响也就越大。

在MySQL中,可以使用连接(JOIN)查询来替代子查询。连接查询 不需要建立临时表 ,其 速度比子查询要快 ,如果查询中使用索引的话,性能就会更好。

7.3 排序优化

1. SQL 中,可以在 WHERE 子句和 ORDER BY 子句中使用索引,目的是在 WHERE 子句中 避免全表扫描 ,在 ORDER BY 子句 避免使用 FileSort 排序 。当然,某些情况下全表扫描,或者 FileSort 排序不一定比索引慢。但总的来说,我们还是要避免,以提高查询效率。
2. 尽量使用 Index 完成 ORDER BY 排序。如果 WHERE 和 ORDER BY 后面是相同的列就使用单索引列;如果不同就使用联合索引。
3. 无法使用 Index 时,需要对 FileSort 方式进行调优。

-- 优化分页查询
explain select * from student limit 400000,10;

EXPLAIN SELECT * FROM student t,(SELECT id FROM student ORDER BY id LIMIT 40000,10) s WHERE t.id = s.id; #自建不自增主键

EXPLAIN SELECT * FROM student t join (SELECT id FROM student ORDER BY id LIMIT 40000,10) s on t.id = s.id; #自建不自增主键

EXPLAIN SELECT * FROM student WHERE id > 2000000 LIMIT 10; #主键自增

select count(*) FROM student;


-- filesort特例
CREATE INDEX idx_age_stuno_name ON student (age,stuno,NAME);

EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno <101000 ORDER BY
NAME ;

#当【范围条件】和【group by 或者 order by】的字段出现二选一时,优先观察条件字段的过
#滤数量,如果过滤的数据足够多,而需要排序的数据并不多时,优先把索引放在范围字段
#上。反之,亦然。

7.4 覆盖索引

理解方式一:索引是高效找到行的一个方法,但是一般数据库也能使用索引找到一个列的数据,因此它不必读取整个行。毕竟索引叶子节点存储了它们索引的数据;当能通过读取索引就可以得到想要的数据,那就不需要读取行了。一个索引包含了满足查询结果的数据就叫做覆盖索引
理解方式二:非聚簇复合索引的一种形式,它包括在查询里的SELECT、JOIN和WHERE子句用到的所有列(即建索引的字段正好是覆盖查询条件中所涉及的字段)。
简单说就是, 索引列+主键 包含 SELECT 到 FROM之间查询的列

好处:
1. 避免Innodb表进行索引的二次查询(回表)
2. 可以把随机IO变成顺序IO加快查询效率
弊端:
索引字段的维护 总是有代价的。因此,在建立冗余索引来支持覆盖索引时就需要权衡考虑了。这是业务DBA,或者称为业务数据架构师的工作。

7.5 索引下推(ICP)

 Index Condition Pushdown(ICP)是MySQL 5.6中新特性,是一种在存储引擎层使用索引过滤数据的一种优化方式。ICP可以减少存储引擎访问基表的次数以及MySQL服务器访问存储引擎的次数。

在不使用ICP索引扫描的过程:
storage层:只将满足index key条件的索引记录对应的整行记录取出,返回给server层
server 层:对返回的数据,使用后面的where条件过滤,直至返回最后一行。

 使用ICP扫描的过程:
storage层:首先将index key条件满足的索引记录区间确定,然后在索引上使用index filter进行过滤。将满足的indexfilter条件的索引记录才去回表取出整行记录返回server层。不满足index filter条件的索引记录丢弃,不回表、也不会返回server层。
server 层:对返回的数据,使用table filter条件做最后的过滤。

 ICP的使用条件:
① 只能用于二级索引(secondary index)
②explain显示的执行计划中type值(join 类型)为 range 、 ref 、 eq_ref 或者 ref_or_null 。
③ 并非全部where条件都可以用ICP筛选,如果where条件的字段不在索引列中,还是要读取整表的记录到server端做where过滤。
④ ICP可以用于MyISAM和InnnoDB存储引擎
⑤ MySQL 5.6版本的不支持分区表的ICP功能,5.7版本的开始支持。
⑥ 当SQL使用覆盖索引时,不支持ICP优化方法。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值