MySQL之5---索引及执行计划

MySQL之5---索引及执行计划

介绍

索引是特殊数据结构,定义在查找时作为查找条件的字段,在MySQL又称为键key,索引通过存储引擎实现。作用类似于书中的目录,可以明显的提高查询效率(select, update, delete),是帮助MySQL高效获取数据的数据结构。

优点:

  • 索引可以降低服务需要扫描的数据量,减少了IO次数
  • 索引可以帮助服务器避免排序和使用临时表
  • 索引可以帮助将随机I/O转为顺序I/O

缺点:

  • 占用额外空间,影响插入速度
  • 索引越多需要的磁盘空间就越大,对索引的重构和更新会很麻烦

索引的分类(算法)

  1. 从存储结构(索引存储时保存的形式)上来划分:

    1. BTree(平衡多路查找树)索引(B-Tree或B+Tree或B*Tree索引)
    2. Hash索引:优点:适合随机的等值查询
    3. full-index全文索引
    4. R-Tree索引。
  2. 从应用层次来分:

    1. 普通索引:即一个索引只包含单个列,一个表可以有多个单列索引
    2. 唯一索引:索引列的值必须唯一,但允许有空值
    3. 复合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并
  3. 根据数据的物理顺序与键值的逻辑(索引)顺序关系:

    1. 聚集索引(聚簇索引):并不是一种单独的索引类型,而是一种数据存储方式。具体细节取决于不同的实现,InnoDB的聚簇索引其实就是在同一个结构中保存了B-Tree索引(技术上来说是B+Tree)和数据行。
    2. 非聚集索引:不是聚簇索引,就是非聚簇索引

索引的命令操作

查询索引

mysql> use world;
mysql> desc city;
Key   ==> 键
PRI   ==> 主键索引
MUL   ==> 辅助索引(单列,联合,前缀)
UNI   ==> 唯一索引
mysql> SHOW INDEXES FROM city\G
Table          表名
Key_name       索引名
Column_name    列名
Cardinality    基数(选择度),越大越好

Cardinality 建立索引之前,基数如何计算?

-- 统计所有行
SELECT COUNT(district) FROM city;
-- 统计所有非重复行,基数
SELECT COUNT(DISTINCT district) FROM city;

建议选择重复值在20%以内,也就是所有非重复行/所有行>0.8的行建立索引

SELECT COUNT(DISTINCT district)/COUNT(district) FROM city;

创建索引

-- 单列的辅助索引
ALTER TABLE 表名 ADD INDEX 索引名(列名);
-- 多列的联合索引
ALTER TABLE 表名 ADD INDEX idx_c_p(countrycode,population);
-- 唯一索引(列必须没有重复值)
ALTER TABLE 表名 ADD UNIQUE INDEX uidx_dis(district);
-- 前缀索引
ALTER TABLE 表名 ADD INDEX idx_dis(district(5));
-- 注意:数字列不能用作前缀索引。

删除索引

DROP INDEX 索引名 ON 表名;
ALTER TABLE 表名 DROP INDEX 索引名;

8.0新特性

invisible inxde 不可见索引,隐藏索引而不是删除

不可见的索引可以测试删除索引对查询性能的影响,而无需进行破坏性的更改,如果需要该索引,则必须撤消该更改。对于大型表,删除和重新添加索引可能会很昂贵,而使其不可见和可见则是快速的就地操作。

-- 查看索引是可见还是不可见的信息
mysql> SELECT INDEX_NAME, IS_VISIBLE
-> FROM INFORMATION_SCHEMA.STATISTICS
-> WHERE TABLE_SCHEMA = 'world' AND TABLE_NAME = 'city';
+-------------+------------+
| INDEX_NAME  | IS_VISIBLE |
+-------------+------------+
| CountryCode | YES        |
| idx_c_p     | YES        |
| idx_c_p     | YES        |
| idx_name    | YES        |
| PRIMARY     | YES        |
+-------------+------------+
5 rows in set (0.00 sec)
-- 设置索引不可见
mysql> ALTER TABLE city ALTER INDEX idx_c_p invisible;
Query OK, 0 rows affected (0.43 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> SELECT INDEX_NAME, IS_VISIBLE FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = 'world' AND TABLE_NAME = 'city';
+-------------+------------+
| INDEX_NAME  | IS_VISIBLE |
+-------------+------------+
| CountryCode | YES        |
| idx_c_p     | NO         |
| idx_c_p     | NO         |
| idx_name    | YES        |
| PRIMARY     | YES        |
+-------------+------------+
5 rows in set (0.01 sec)
-- 设置索引可见
mysql> ALTER TABLE city ALTER INDEX idx_c_p VISIBLE;
Query OK, 0 rows affected (0.06 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> SELECT INDEX_NAME, IS_VISIBLE FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = 'world' AND TABLE_NAME = 'city';
+-------------+------------+
| INDEX_NAME  | IS_VISIBLE |
+-------------+------------+
| CountryCode | YES        |
| idx_c_p     | YES        |
| idx_c_p     | YES        |
| idx_name    | YES        |
| PRIMARY     | YES        |
+-------------+------------+
5 rows in set (0.00 sec)

压力测试

  1. 准备:
mysql> CREATE DATABASE test;
mysql> USE test;
mysql> source /tmp/t100w.sql
  1. 未做索引优化之前测试:
[root@db01 ~]# mysqlslap --defaults-file=/etc/my.cnf \
--concurrency=100 --iterations=1 --create-schema='test' \
--query="select * from test.t100w where k2='MN89'" engine=innodb \
--number-of-queries=2000 -uroot -p123 -verbose
  1. 索引优化后测试:
[root@db01 ~]# mysqlslap --defaults-file=/etc/my.cnf --concurrency=100 --iterations=1 --create-schema='test' --query="select * from test.t100w where k2='MN89'" engine=innodb --number-of-queries=2000 -uroot -p123 -verbose

优化索引

仅用于删除或更改大量数据或索引时,立即释放未使用的空间,并整理数据文件的碎片

OPTIMIZE TABLE 表名;

注意:OPTIMIZE TABLE只对MyISAM, BDB和InnoDB表起作用,在运行过程中,MySQL会锁定表。

索引的底层实现

InnoDB索引的物理结构

https://dev.mysql.com/doc/refman/8.0/en/innodb-physical-structure.html

  • 除空间索引外,InnoDB 索引是B树数据结构。
  • 空间索引使用 R树,R树是用于索引多维数据(例如地理坐标,矩形或多边形)的专用数据结构。

索引记录存储在其B树或R树数据结构的叶页中。索引页的默认大小为16KB。

InnoDB架构


change buffer

与聚簇索引不同,二级索引通常是不唯一的,并且二级索引中的插入,删除和更新(DML操作)以相对随机的顺序发生,还可能会影响索引树中不相邻的二级索引页,产生大量随机访问I/O。

为了减少这个情况对性能的影响,暂存这些由INSERTUPDATEDELETE(DML操作)导致的二级索引更改至change buffer中,在系统大部分处于空闲状态或缓慢关闭的期间,运行的清除操作会定期将更新的索引页写入磁盘。

在内存中,change buffer 占用了缓冲池的一部分。在磁盘上,change buffer是系统表空间的一部分,当数据库服务器关闭时,二级索引的更改将存储在其中。


AHI

AHI:自适应HASH索引

索引的索引,为内存中的热点索引页,建立HASH索引表,能够快速找到需要的索引页地址

MySQL 8.0 默认使用的 InnoDB 存储引擎只显式支持B-Tree( 从技术上来说是B+Tree)索引,对于频繁访问的表,innodb会透明建立自适应hash索引,即在B树索引基础上建立hash索引,可以显著提高查找效率,对于客户端是透明的,不可控制的,隐式的。


Hash索引

基于哈希表实现,只有精确匹配索引所有列的查询才有效,对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码(hash code),并且Hash索引将所有的哈希码存储在索引中,同时在索引表中保存指向每个数据行的指针。

img


B+Tree索引

二叉树

img

是n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树组成。

原理:

  1. 将数据存放在一个一个节点上;

  2. 节点又分为三种节点:最上面的叫根节点,中间的叫分支节点,最下面的到底叫叶子节点。

  3. 每个分支节点有一个分支,或者两个分支。

缺点:

二叉树存在不平衡的问题。


红黑树(Red-Black Tree)

img

原理: 就是平衡的二叉树。

红黑树是二叉树的一种改进。我们知道二叉搜索树在最坏的情况下可能会变成一个链表(当所有节点按从小到大的顺序依次插入后)。而红黑树在每一次插入或删除节点之后都会花 O(log N)的时间来对树的结构作修改,以保持树的平衡。也就是说,红黑树的查找方法与二叉树完全一样;插入和删除节点的的方法前半部分节与二叉树完全一样,而后半部分添加了一些修改树的结构的操作。

缺点: 查询数字的速度慢。


B-Tree(平衡多路查找树)

B-Tree数据结构始终保持排序,从而可以快速查找精确匹配(等于运算符)和范围(例如,大于,小于和BETWEEN 运算符),加快数据的访问速度,因为存储引擎不再需要进行全表扫描来获取数据,数据分布在各个节点之中。

img

详解:

B-Tree是为磁盘等外存储设备设计的一种平衡查找树。因此在讲B-Tree之前先了解下磁盘的相关知识。

系统从磁盘读取数据到内存时是以磁盘块(block)为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么。

InnoDB存储引擎中有页(Page)的概念,页是其磁盘管理的最小单位。InnoDB存储引擎中默认每个页的大小为16KB,可通过参数innodb_page_size将页的大小设置为4K、8K、16K,在MySQL中可通过如下命令查看页的大小:

mysql> show variables like 'innodb_page_size';

而系统一个磁盘块的存储空间往往没有这么大,因此InnoDB每次申请磁盘空间时都会是若干地址连续磁盘块来达到页的大小16KB。InnoDB在把磁盘数据读入到磁盘时会以页为基本单位,在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘I/O次数,提高查询效率。

B-Tree结构的数据可以让系统高效的找到数据所在的磁盘块。为了描述B-Tree,首先定义一条记录为一个二元组[key, data] ,key为记录的键值,对应表中的主键值,data为一行记录中除主键外的数据。对于不同的记录,key值互不相同。

一棵m阶的B-Tree有如下特性:
1.每个节点最多有m个孩子。
2.除了根节点和叶子节点外,其它每个节点至少有Ceil(m/2)个孩子。
3.若根节点不是叶子节点,则至少有2个孩子
4.所有叶子节点都在同一层,且不包含其它关键字信息
5.每个非终端节点包含n个关键字信息(P0,P1,…Pn, k1,…kn)
6.关键字的个数n满足:ceil(m/2)-1 <= n <= m-1
7.ki(i=1,…n)为关键字,且关键字升序排序。
8.Pi(i=1,…n)为指向子树根节点的指针。P(i-1)指向的子树的所有节点关键字均小于ki,但都大于k(i-1)

B-Tree中的每个节点根据实际情况可以包含大量的关键字信息和分支,如下图所示为一个3阶的B-Tree:

img

每个节点占用一个盘块的磁盘空间,一个节点上有两个升序排序的关键字和三个指向子树根节点的指针,指针存储的是子节点所在磁盘块的地址。两个关键词划分成的三个范围域对应三个指针指向的子树的数据的范围域。以根节点为例,关键字为17和35,P1指针指向的子树的数据范围为小于17,P2指针指向的子树的数据范围为17~35,P3指针指向的子树的数据范围为大于35。

模拟查找关键字29的过程:

  1. 根据根节点找到磁盘块1,读入内存。【磁盘I/O操作第1次】
  2. 比较关键字29在区间(17,35),找到磁盘块1的指针P2。
  3. 根据P2指针找到磁盘块3,读入内存。【磁盘I/O操作第2次】
  4. 比较关键字29在区间(26,30),找到磁盘块3的指针P2。
  5. 根据P2指针找到磁盘块8,读入内存。【磁盘I/O操作第3次】
  6. 在磁盘块8中的关键字列表中找到关键字29。

分析上面过程,发现需要3次磁盘I/O操作,和3次内存查找操作。由于内存中的关键字是一个有序表结构,可以利用二分法查找提高效率。而3次磁盘I/O操作是影响整个B-Tree查找效率的决定因素。B-Tree相对于AVLTree缩减了节点个数,使每次磁盘I/O取到内存的数据都发挥了作用,从而提高了查询效率。


B+Tree索引

B+Tree是B-Tree的改进版本,同时也是MySQL数据库的innodb引擎索引所采用的存储结构。数据都在叶子节点上,并且增加了顺序访问指针,每个叶子节点都指向相邻的叶子节点的地址。进行范围查找时,B-Tree需要获取所有节点,而B+Tree只需要查找两个节点,进行遍历即可。相比之下B+Tree效率更高。
B+Tree 优点:

  1. 平衡:不管查找那个数,需要查找的次数理论上是相同的,例如:三层B树,查找每个值都是3次IO
  2. 擅长范围查找(> < >= <= like):快速锁定范围

img

详解:

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

从上一节中的B-Tree结构图中可以看到每个节点中不仅包含数据的key值,还有data值。而每一个页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,当存储的数据量很大时同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率。在B+Tree中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度。

B+Tree相对于B-Tree有几点不同:

  1. 非叶子节点只存储键值信息。
  2. 所有叶子节点之间都有一个链指针。
  3. 数据记录都存放在叶子节点中。

将上一节中的B-Tree优化,由于B+Tree的非叶子节点只存储键值信息,假设每个磁盘块能存储4个键值及指针信息,则变成B+Tree后其结构如下图所示:

img

通常在B+Tree上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。因此可以对B+Tree进行两种查找运算:一种是对于主键的范围查找和分页查找,另一种是从根节点开始,进行随机查找。

可能上面例子中只有22条数据记录,看不出B+Tree的优点,下面做一个推算:

InnoDB存储引擎中页的大小为16KB,一般表的主键类型为INT(占用4个字节)或BIGINT(占用8个字节),指针类型也一般为4或8个字节,也就是说一个页(B+Tree中的一个节点)中大概存储16KB/(8B+8B)=1K个键值(因为是估值,为方便计算,这里的K取值为〖10〗3)。也就是说一个深度为3的B+Tree索引可以维护103 * 10^3 * 10^3 = 10亿 条记录。

实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree的高度一般都在24层。[mysql](https://links.jianshu.com/go?to=http%3A%2F%2Flib.csdn.net%2Fbase%2Fmysql)的InnoDB存储引擎在设计时是将根节点常驻内存的,也就是说查找某一键值的行记录时最多只需要13次磁盘I/O操作。

数据库中的B+Tree索引可以分为聚集索引(clustered index)和辅助索引(secondary index)。上面的B+Tree示例图在数据库中的实现即为聚集索引,聚集索引的B+Tree中的叶子节点存放的是整张表的行记录数据。辅助索引与聚集索引的区别在于辅助索引的叶子节点并不包含行记录的全部数据,而是存储相应行数据的聚集索引键,即主键。当通过辅助索引来查询数据时,InnoDB存储引擎会遍历辅助索引找到主键,然后再通过主键在聚集索引中找到完整的行记录数据。


选择

  1. 为什么索引结构默认使用B+Tree,而不是Hash,二叉树,红黑树?

B-tree:因为B树不管叶子节点还是非叶子节点,都会保存数据,这样导致在非叶子节点中能保存的指针数量变少(有些资料也称为扇出),指针少的情况下要保存大量数据,只能增加树的高度,导致IO操作变多,查询性能变低;

Hash:虽然可以快速定位,但是没有顺序,IO复杂度高。

二叉树:树的高度不均匀,不能自平衡,查找效率跟数据有关(树的高度),并且IO代价高。

红黑树:树的高度随着数据量增加而增加,IO代价高。

  1. 为什么官方建议使用自增长主键作为索引?

结合B+Tree的特点,自增主键是连续的,在插入过程中尽量减少页分裂,即使要进行页分裂,也只会分裂很少一部分。并且能减少数据的移动,每次插入都是插入到最后。总之就是减少分裂和移动的频率。

插入连续的数据:

img

插入非连续的数据:

img

总结

  1. B+Tree在新增数据时,会根据索引指定列的值对旧的B+Tree做调整。
  2. 从物理存储结构上说,B-Tree和B+Tree都以页(4K)来划分节点的大小,但是由于B+Tree中中间节点不存储数据,因此B+Tree能够在同样大小的节点中,存储更多的key,提高查找效率。
  3. 影响MySQL查找性能的主要还是磁盘IO次数,大部分是磁头移动到指定磁道的时间花费。
  4. MyISAM存储引擎下索引和数据存储是分离的,InnoDB索引和数据存储在一起。
  5. InnoDB存储引擎下索引的实现,(辅助索引)全部是依赖于主索引建立的(辅助索引中叶子结点存储的并不是数据的地址,还是主索引的值,因此,所有依赖于辅助索引的都是先根据辅助索引查到主索引,再根据主索引查数据的地址)。
  6. 由于InnoDB索引的特性,因此如果主索引不是自增的(id作主键),那么每次插入新的数据,都很可能对B+Tree的主索引进行重整,影响性能。因此,尽量以自增id作为InnoDB的主索引。

索引的建立原理

QQ图片20201110114919

B+Tree 构建过程

叶子节点:先将数据排序,生成叶子节点+指向相邻叶子节点的双向指针

枝节点:保存叶子节点范围+指向叶子节点的双向指针

根节点:保存枝节点范围+指向根节点的指针

区(簇)extent:连续的64pages,默认是1M的存储空间

页page:16KB大小,Mysql中最小的IO单元

IOT(Index-Organized Table)索引组织表:表面上看是一种表,实质上其数据是以索引的形式来存放的,占用的是索引段。MySQL的表数据存储是聚集索引组织表。

聚簇索引和二级索引

聚集索引(C)(聚簇(区)索引)

构建条件

InnoDB自动建立聚簇索引,索引列选择优先级如下:

  1. 首选主键,没有则向下选择
  2. 次选第一个非空且唯一(NOT NULLUNIQUE)的键,没有则向下选择
  3. 末选自动生成一个6字节的隐藏列

构建B树结构

叶子节点:因为Mysql使用聚簇索引组织表存储数据,在存数据时,就按照索引列的顺序,存储数据到连续的数据页(16K)中,所以原表数据存储结构就是叶子节点+指向相邻叶子节点的双向指针

枝节点:存储叶子节点中索引列的范围+指向叶子节点的双向指针

根节点:存储枝节点中索引列的范围+指向根节点的指针


能优化的查询

只能优化仅使用索引列的查询,局限性很大。


加快查询原理

通过聚集索引访问行是快速的,因为索引搜索直接导致包含所有行数据的页面。如果表很大,则与使用不同于索引记录的页面存储行数据的存储组织相比,聚集索引体系结构通常可以节省磁盘I / O操作。

辅助索引(S)(二级索引)

构建条件

需要人为按照需求创建,表中任何一个列都可以创建辅助索引列,只要名字不同即可,可以有多个


构建B树结构

叶子节点:将辅助索引列和主键提取出来,按照辅助索引列的值从小到大排序,均匀的存储到各个page中,生成叶子节点+指向相邻叶子节点的双向指针

枝节点:存储叶子节点中辅助索引列的范围+指向叶子节点的双向指针

根节点:存储枝节点中辅助索引列的范围+指向根节点的指针


能优化的查询

只能优化查询条件使用辅助索引列的查询。

会先扫描辅助索引,获得主键,再回到聚簇索引(回表查询),按照主键进行聚簇索引扫描,获得所需数据行.

最好的查询条件是同时使用辅助索引列和主键,直接可以获得所需数据行,不用回表查询,但有查询条件是主键可以直接使用聚簇索引...


加快查询原理

除聚集索引之外的所有索引都称为辅助索引。在InnoDB中,辅助索引中的每个记录都包含该行的主键列以及为辅助索引指定的列。 InnoDB使用此主键值在聚集索引中搜索行。

如果主键较长,则辅助索引将使用更多空间,因此具有短主键是有利的。


联合索引(覆盖索引)

辅助索引细分

  1. 单列辅助索引
  2. 联合索引(覆盖索引):多个列作为索引条件。
  3. 前缀索引
  4. 唯一索引:索引列的值都是唯一的。

构建B树结构

叶子节点:将辅助索引列a和辅助索引列b和主键提取出来,按照辅助索引列a,b的值从小到大联合排序,存储到各个page中,生成叶子节点+指向相邻叶子节点的双向指针

枝节点:存储叶子节点中最左列(除了主键)的范围+指向叶子节点的双向指针

根节点:存储枝节点中最左列(除了主键)的范围+指向根节点的指针


能优化的查询

只能优化查询条件至少使用a条件(最左前缀的列)的查询。

先通过a条件,扫描联合索引的根节点和枝节点,得到叶子节点范围,如果最终得到的主键是多个,就要再执行多次回表查询。如果能再通过b条件过滤,最终得到的主键更精确,理论上能减少回表的次数。回表查询是随机IO的,会大大增加查询时间。


加快查询原理(最左原则)

  • 建立联合索引时,选择基数大(重复值少)的列作为最左列
  • 查询条件中必须要包含最左列条件

索引树高度

索引树高度应当越低越好,一般维持在3-4层最佳,3层能存储2000万+行数(量级)


影响索引树高度的因素

  1. 数据量级,解决方法:

    分区表:

    定期归档:一般按照时间字段,定期归档到历史库中, pt-archiver

    分库,分表,分布式

  2. 索引列值过长,解决方法:

    业务允许,尽量选择字符长度短的列作为索引列

    业务不允许,采用前缀索引.

  3. 数据类型:

    变长长度字符串,使用了char,解决方案:变长长度字符串使用 varchar

    enum使用短字符代替长字符

    enum ('山东','河北','黑龙江','吉林','辽宁','陕西'......)
    enum (  1      2      3      4      5     6        )
    

回表问题

回表是辅助索引列扫描之后,得到主键,再回到聚簇索引查询的过程

回表是随机IO,会导致IO的次数(IOPS)和量(吞吐量)增加

IOPS(Input/Output Operations Per Second):每秒的读写次数,用于衡量随机访问的性能

吞吐量:单位时间内成功地传送数据的数量(例如 200M/s)

减少回表的方法:

  1. 建立覆盖索引时,尽可能多的将查询条件需要的数据包含在联合索引中
  2. 业务层面,精细查询条件(> and < , limit)
  3. 使用索引时,查询条件要符和联合索引规则,包含建立联合索引的列越多越好

执行计划的获取及分析

为什么要分析执行计划

场景一:如果业务中出现了执行比较慢的语句,需要借助对执行计划的评估,分析优化方案。

场景二:上线新业务,语句执行前,先看执行计划,可以有效的防止性能较差的语句带来的性能问题


执行计划获取

SQL语句没有真正运行,到优化器选择完成后停止并显示信息,获取到的是优化器选择完成,认为代价最小的执行计划.

desc SQL语句;
explain SQL语句;

输出说明


执行计划分析

mysql> desc select * from world.city where countrycode='CHN' \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: city
   partitions: NULL
         type: ref
possible_keys: CountryCode,idx_c_p
          key: CountryCode
      key_len: 3
          ref: const
         rows: 363
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

重点关注的信息

select_type: SIMPLE                    查询类型:简单查询
table: city                            操作的表 
type: ref                              操作类型(全表\索引)
possible_keys: CountryCode,idx_co_po   可能会走的索引
key: CountryCode                       真正走的索引
key_len: 3                             索引覆盖长度(联合索引)
rows: 363                              预估需要扫描的行数
Extra: Using index condition           额外信息

select_type

SIMPLE:简单查询
SUBQUERY:简单子查询
PRIMARY:最外面的SELECT
DERIVED:用于FROM中的子查询
UNION:UNION语句的第一个之后的SELECT语句
UNION RESULT:匿名临时表


type

从上往下效率依次升高

  1. ALL:全表扫描,不走索引
① 查询全表
DESC SELECT * FROM world.city;
② 查询条件出现以下语句
DESC SELECT * FROM world.city WHERE countrycode LIKE '%HN';
③ 辅助索引列的查询条件出现以下语句
DESC SELECT * FROM world.city WHERE countrycode != 'CHN';
DESC SELECT * FROM world.city WHERE countrycode <> 'CHN';
DESC SELECT * FROM world.city WHERE countrycode NOT IN ('CHN','USA' );
DESC SELECT * FROM world.city WHERE Population > 100 and Population < 1000000;
DESC SELECT * FROM world.city WHERE Population between 100 and 1000000;

注意:对于聚集索引,使用例③语句,依然会走RANGE索引

DESC SELECT * FROM world.city WHERE id <> 10;
DESC SELECT * FROM world.city WHERE id != 10;
DESC SELECT * FROM world.city WHERE id NOT IN ('1','2' );
DESC SELECT * FROM world.city WHERE id > 10 and id < 1000;
DESC SELECT * FROM world.city WHERE id between 10 and 1000;
  1. INDEX:全索引扫描
1. 查询需要获取整颗索引树,才能得到想要的结果时
DESC  SELECT countrycode FROM world.city;
2. 联合索引中,任何一个非最左列作为查询条件时
idx_a_b_c(a,b,c)  ---> a  ab  abc

SELECT * FROM t1 WHERE b
SELECT * FROM t1 WHERE c
  1. RANGE:索引范围扫描,生产中容忍的底线

    • 辅助索引列 > < >= <= LIKE IN OR

      DESC SELECT * FROM world.city WHERE Population > 1000000;
      DESC SELECT * FROM world.city WHERE Population < 100;
      DESC SELECT * FROM world.city WHERE Population >= 1000000;
      DESC SELECT * FROM world.city WHERE Population <= 100;
      DESC SELECT * FROM world.city WHERE countrycode LIKE 'CH%';
      DESC SELECT * FROM world.city WHERE countrycode IN ('CHN','USA');
      DESC SELECT * FROM world.city WHERE Population < 100 OR Population > 1000000;
      
    • 主键列 > < >= <= IN OR <> != NOT IN and between and

      DESC SELECT * FROM world.city WHERE id<5;
      DESC SELECT * FROM world.city WHERE id>5;
      DESC SELECT * FROM world.city WHERE id<=5;
      DESC SELECT * FROM world.city WHERE id>=5;
      DESC SELECT * FROM world.city WHERE id IN ('1','2' );
      DESC SELECT * FROM world.city WHERE id < 5 OR id > 10;
      DESC SELECT * FROM world.city WHERE id <> 10;
      DESC SELECT * FROM world.city WHERE id != 10;
      DESC SELECT * FROM world.city WHERE id NOT IN ('1','2' );
      DESC SELECT * FROM world.city WHERE id > 10 and id < 1000;
      DESC SELECT * FROM world.city WHERE id between 10 and 1000;
      

注意:

IN不能享受到B+树的双向指针

DESC SELECT * FROM world.city WHERE countrycode IN ('CHN','USA');

可以改写为

DESC SELECT * FROM world.city WHERE countrycode='CHN'
UNION ALL 
SELECT * FROM world.city WHERE countrycode='USA';

具体效果看压测结果

如果索引列的重复值太多, 改写为UNION没有意义

  1. ref:非唯一性索引的等值查询
DESC SELECT * FROM world.city WHERE countrycode='CHN';
  1. eq_ref:在多表连接查询时,连接条件使用了主键或唯一键索引(uk pK)的非驱动表
DESC SELECT b.name,a.name 
FROM city a 
JOIN country b 
ON a.countrycode=b.code 
WHERE a.population <100;
  1. const(system):唯一索引的等值查询
DESC SELECT * FROM world.city WHERE id=10;

key_len

索引覆盖长度(字节),根据这个值可以判断索引的使用情况,特别是在判断联合索引有多少部分被使用到时。

计算索引列的key_len

key_len和每个索引列的最大预留长度有关

  • 如果没有NOT NULL标记,占用1个字节。如果有NOT NULL标记,不占用字节。

  • 字符类型受字符集影响:latin1编码1个字符1个字节,gbk编码的为1个字符2个字节,utf8编码的1个字符3个字节,utf8mb4编码一个字符4个字节。

  • 变长数据类型,比如:varchar,还有长度信息,需要占用2个字节。

  • 前缀索引指定的索引长度优先,例如:

    alter table test add index uri(uri(30));
    

    长度30指的是字符的个数,如果为utf8编码的varchar(255),key_len=30*3+2=92个字节。

数据类型 字符集: utf8mb4 没有 NOT NULL
tinyint 1 1
int 4 1
char(n) 4*n 1
varchar(n) 4*n+2 1

注:key_len只统计用于where条件过滤时使用的索引列,不包含order by/group by部分使用的索引列。


联合索引应用细节

联合索引 idx(a,b,c) 可能有以下应用情况

  • 完全覆盖:
    • a=? and b=? and c=?
    • c=? and b=? and a=? ... 等值乱序的(5.6+优化器自动调整顺序)
    • a=? and b=? and c范围
    • a=? and b字符范围 and c=?
  • 部分覆盖:
    • a=? and b=?
    • a=?
    • a=? and c=?
    • a=? and b数字范围 and c=?
    • a范围 and b=? and c=?
  • 完全不覆盖
    • b=?
    • c=?
    • b=? and c=?
    • c=? and b=?

优化案例:a=? and b数字范围 and c=?

idx(k1,num,k2)

desc select * from t100w where k1='Vs' and num<27779 and k2='mnij';

优化方案:修改索引为idx(k1,k2,num)。

索引列的基数是重要的参考,对效率的影响更大。

到底是要保证唯一值多的列放在最左侧,还是保证联合索引的覆盖度,最终由压测结果决定。


Extra

  1. Using index :使用覆盖索引

  2. Using where :使用where回表扫描,部分条件列没有用到索引

    1. 查找出问题的表
    2. 查找原始查询语句中的条件列
    3. 查询列的索引情况(show index from 表名)
    4. 按需优化索引
  3. Using index condition :索引条件下推(ICP)

    -- 关闭ICP
    set global optimizer_switch='index_condition_pushdown=off';
    -- 获取执行计划
    desc select * from t100w where k1='Vs' and num<27779 and k2='mnij';
    
    # 压测
    mysqlslap --defaults-file=/etc/my.cnf \
    --concurrency=100 --iterations=1 --create-schema='test' \
    --query=" select * from t100w where k1='Vs' and num<27779 and k2='mnij'" engine=innodb \
    --number-of-queries=2000 -uroot -p123 -verbose
    
    -- 开启ICP
    set global optimizer_switch='index_condition_pushdown=on';
    -- 获取执行计划
    desc select * from t100w where k1='Vs' and num<27779 and k2='mnij';
    
    # 压测
    mysqlslap --defaults-file=/etc/my.cnf \
    --concurrency=100 --iterations=1 --create-schema='test' \
    --query=" select * from t100w where k1='Vs' and num<27779 and k2='mnij'" engine=innodb \
    --number-of-queries=2000 -uroot -p123 -verbose
    
  4. Using filesort :使用额外排序(ORDER BY,GROUP BY,DISTINCT)

    1. 查找出问题的表
    2. 查找原始查询语句中的:order by | group by | distinct
    3. 查询列的索引情况(show index from 表名)
    4. 按需优化索引
    -- 发现执行计划Extra出现Using filesort
    DESC SELECT * FROM world.city WHERE countrycode='CHN' ORDER BY population;
    -- 查询列的索引情况
    SHOW INDEX FROM world.city;
    -- WHERE + (ORDER BY,GROUP BY,DISTINCT) 需要创建联合索引
    ALTER TABLE world.city ADD INDEX idx_c_p(CountryCode,Population);
    -- 发现执行计划Extra变为NULL
    DESC SELECT * FROM world.city WHERE countrycode='CHN' ORDER BY population;
    
  5. Using temp:临时表

    1. 条件范围过大
    2. 额外排序(having order by)
    3. 子查询临时使用

https://www.cnblogs.com/kerrycode/p/9909093.html


explain(desc)使用场景(面试题)

题目意思:我们公司业务慢,请你从数据库的角度分析原因

mysql出现性能问题,我总结有两种情况:

  1. 应急性的慢:突然夯住

    应急情况:数据库hang(卡了,资源耗尽)

    处理过程:

    1. 获取到导致数据库hang的语句show processlist;
    2. explain 分析SQL的执行计划,有没有走索引,索引的类型情况
    3. 建索引,改语句
  2. 一段时间慢(持续性的):

    1. 记录慢日志slowlog,分析slowlog
    2. explain 分析SQL的执行计划,有没有走索引,索引的类型情况
    3. 建索引,改语句

索引的优化器算法

-- 查看优化器算法
select @@optimizer_switch;

Optimizer Hints:优化器提示 指定单个语句使用的优化器算法

ICP

ICP:索引条件下推 默认启用,是针对MySQL使用辅助索引从表中检索行后的一种优化。

SET optimizer_switch = 'index_condition_pushdown=off';
SET optimizer_switch = 'index_condition_pushdown=on';

QQ图片20201110142917

如果不使用ICP,辅助索引扫描获取索引元组后,存储引擎将遍历整个基表定位行,并将其返回给MySQL服务器评估WHERE行的条件。

QQ图片20201110142926

启用ICP后,辅助索引扫描获取索引元组后,如果WHERE可以仅使用索引中的列来评估部分条件,则MySQL服务器会将这部分WHERE条件下降到存储引擎,存储引擎使用索引中的列来评估获取的索引元组,并且只有在满足此条件的情况下,才从基表中读取行。

ICP可以减少存储引擎必须访问基表的次数以及MySQL服务器必须访问存储引擎的次数。


具体示例:

SELECT * FROM people
  WHERE zipcode='95054'
  AND lastname LIKE '%etrunia%';

如果没有ICP,此查询必须先遍历整个基表,检索符和 zipcode='95054'条件的表行,然后再将其返回给MySQL服务器评估lastname LIKE '%etrunia%'条件。

启用ICP,此查询将在遍历整个基表,检索符和 zipcode='95054'条件的表行前,存储引擎先检查lastname LIKE '%etrunia%'条件。


适用情况:

  • ICP用于 rangerefeq_ref,和 ref_or_null访问方法时,有一个需要访问的全部表行。
  • ICP可用于InnoDBMyISAM表,包括分区表InnoDBMyISAM表。
  • InnoDB表,ICP仅用于二级索引。ICP的目标是减少全行读取次数,从而减少I/O操作。对于 InnoDB聚集索引,完整的记录已被读入InnoDB 缓冲区。使用ICP不会减少I/O。
  • InnoDB 支持虚拟生成的列上的二级索引。在虚拟生成的列上创建的二级索引不支持ICP。
  • 引用子查询的条件不能下推。
  • 涉及存储功能的条件不能下推。存储引擎无法调用存储功能。
  • 触发条件不能下推。

MRR

MRR: 多范围读取优化 默认开启,由优化器选择是否使用,关闭则强制使用MRR

SET optimizer_switch = 'mrr_cost_based=off';
SET optimizer_switch = 'mrr_cost_based=on';

不使用MRR:

QQ图片20201110145656

使用MRR:

QQ图片20201110145702

MRR可以减少随机磁盘访问的次数,对基表数据进行更顺序的扫描。

具体示例:

DESC SELECT * FROM world.city WHERE countrycode IN ('CHN','USA');

索引的应用规范

特别提示:

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

建立索引的原则(DBA运维规范)

  1. 必须要有主键,业务无关列

  2. 为经常作为where,ORDER BY,GROUP BY,join on,distinct的条件(业务: 产品功能+用户行为)的列建立索引

  3. 最好使用唯一值多的列作为索引列,如果索引列重复值较多,可以建立联合索引。

  4. 字段的值很长的索引列,建议使用前缀索引,前缀索引长度注意评估基数

    select count(distinct left(name,19)) from city;
    
  5. 降低索引的数目,不要创建没用的索引,清理不常用或很少使用的索引(pt-duplicate-key-checker)

    1. 每个索引都需要占用磁盘空间,索引越多,需要的磁盘空间就越大。
    2. 修改表时,对索引的重构和更新很麻烦。越多的索引,会使更新表变得很浪费时间。
    3. 优化器的负担会很重,有可能会影响到优化器的选择.
    4. 分析索引是否有用的工具 percona-toolkit

    查看冗余索引

    • 查询系统表参数
    select table_schema,table_name, redundant_index_name, redundant_index_columns from sys.schema_redundant_indexes;
    
    • SQLyog表信息

    image-20201110112844669

  6. 索引维护要避开业务繁忙期,建议使用pt-osc

  7. 联合索引最左原则


不走索引的情况(开发规范)

  1. 没有查询条件,或者查询条件没有建立索引

    SQL审核和审计

  2. 查询结果集是原表中的大部分数据,应该是15-25%以上,优化器觉得就没有必要走索引了(和Mysql的预读功能有关)。

    优化:

    • 如果业务允许,改写语句
      • 使用limit精确控制查找范围
      • 分段查询 >500 and <1000
    • 尽量不要在mysql存放这个数据了,放到redis里面。
  3. 索引本身失效,统计数据不真实(过旧)

    索引有自我维护的能力,在表内容变化比较频繁的情况下,有可能会出现索引失效。

    -- 查看索引统计
    select * from mysql.innodb_index_stats;
    -- 查看表统计
    select * from mysql.innodb_table_stats;
    -- 查看当前时间
    select now();
    -- 1. 删除索引重建
    -- 2. 重新收集统计信息
    ANALYZE TABLE 表名;
    
  4. 查询条件在索引列上使用函数运算,算术运算包括(+,-,*,/,! 等)或者子查询

    -- 错误的SQL
    select * from test where id-1=9;
    -- 正确的SQL
    select * from test where id=10;
    
  5. 隐式转换导致索引失效,是开发中经常会犯的错误,这一点应当引起重视.

    mysql> alter table tab add index inx_tel(telnum);
    Query OK, 0 rows affected (0.03 sec)
    Records: 0  Duplicates: 0  Warnings: 0
    mysql> desc tab;
    +--------+-------------+------+-----+---------+-------+
    | Field  | Type        | Null | Key | Default | Extra |
    +--------+-------------+------+-----+---------+-------+
    | id     | int(11)     | YES  |     | NULL    |       |
    | name   | varchar(20) | YES  |     | NULL    |       |
    | telnum | varchar(20) | YES  | MUL | NULL    |       |
    +--------+-------------+------+-----+---------+-------+
    3 rows in set (0.01 sec)
    -- 正确的SQL
    mysql> select * from tab where telnum='1333333';
    +------+------+---------+
    | id   | name | telnum  |
    +------+------+---------+
    |    1 | a    | 1333333 |
    +------+------+---------+
    1 row in set (0.00 sec)
    -- 错误的SQL
    mysql> select * from tab where telnum=1333333;
    +------+------+---------+
    | id   | name | telnum  |
    +------+------+---------+
    |    1 | a    | 1333333 |
    +------+------+---------+
    1 row in set (0.00 sec)
    mysql> explain select * from tab where telnum='1333333';
    +----+-------------+-------+------+---------------+---------+---------+-------+------+-----------------------+
    | id | select_type | table | type | possible_keys | key    | key_len | ref  | rows | Extra                |
    +----+-------------+-------+------+---------------+---------+---------+-------+------+-----------------------+
    |  1 | SIMPLE      | tab  | ref  | inx_tel      | inx_tel | 63      | const |    1 | Using index condition |
    +----+-------------+-------+------+---------------+---------+---------+-------+------+-----------------------+
    1 row in set (0.00 sec)
    mysql> explain select * from tab where telnum=1333333;
    +----+-------------+-------+------+---------------+------+---------+------+------+-------------+
    | id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra      |
    +----+-------------+-------+------+---------------+------+---------+------+------+-------------+
    |  1 | SIMPLE      | tab  | ALL  | inx_tel      | NULL | NULL    | NULL |    2 | Using where |
    +----+-------------+-------+------+---------------+------+---------+------+------+-------------+
    1 row in set (0.00 sec)
    
  6. <>not in!=不走索引(辅助索引)

    EXPLAIN  SELECT * FROM teltab WHERE telnum <> '110';
    EXPLAIN  SELECT * FROM teltab WHERE telnum NOT IN ('110','119');
    EXPLAIN  SELECT * FROM teltab WHERE telnum != '110';
    

    ><in 可能走,也可能不走,和结果集有关,尽量结合业务添加limit
    orin 尽量改成UNION ALL

  7. like "%_" 百分号或者下划线开头不走索引

    EXPLAIN SELECT * FROM teltab WHERE telnum LIKE '%110'
    

    %linux%类的搜索需求,可以使用专门做搜索服务的数据库产品 elasticsearch + mongodb

  8. 尽量避免NULL,含有 NULL 的索引将很难进行优化,复合索引如果有NULL值,不走索引

posted @ 2021-01-06 16:49  原因与结果  阅读(192)  评论(0编辑  收藏  举报