MySQL索引及优化

一、索引

1.1 虾米是索引?

首先,索引是帮助MySQL或其他数据库进程高效获取数据的 排好序的数据结构在这里插入图片描述
我们要知道数据库都是放在磁盘上的,就是一堆文件
在这里插入图片描述
每张表对应若干个文件
在这里插入图片描述
我们的进程比如MySQL
在这里插入图片描述
它每次查询数据都是一次访问磁盘的IO操作,磁盘IO相对来说是很慢的,因此优化查询速度的关键之一就是减少磁盘IO的操作次数,也就是减少进程对磁盘的查询次数,这就是索引的作用。它用额外的磁盘空间保存索引数据,每次插入删除数据也要先维护这个索引数据,这么麻烦还浪费磁盘空间就是为了提升查询效率。

想象如果一堆快递杂乱无章放在菜鸟驿站(磁盘上),找你是肯定能找到,但是要找多少次?不得而知,可能一下子就找到了,可能找到最后一个才找到,这就是在没建立索引的表中找一个数据的情况,查找的次数是未知的,很烦!因此我们想尽量缩短每次查找的次数,想让整个查找的过程在一定程度上可控,于是设计的快递货架,编号123,每个货架分一二三层,这样先找货架再找层数最后找快递的方式就比原来一件一件快递找快多了,整个过程就是在一级一级地缩小查找范围,从而提升效率。那么索引就是这么一个东西,就是那一套货架和编号。

1.2 索引的数据结构(索引方法)

索引的数据结构有很多种:二叉树、红黑树(平衡二叉树)、Hash表、B-Tree、B+Tree等等

每次基于索引的查找,都是先把根节点从磁盘加载到内存作比对,然后一路跟着树结构一个节点一个节点的找,所以索引数据结构的节点越大,一次加载到内存的数据就越多,查找起来就快很多(因为查找是计算动作,是ALU在做逻辑计算,从内存中读写的速度远比磁盘IO快得多)。如B+Tree这个多叉树,缺点就是占用内存相对大一点,但并不是常驻内存中(它某些时候会把所有索引节点都缓存到内存中,直到叶子节点才去做磁盘IO),但只要将节点大仙限制在一定范围内,并不会产生很大的副作用。

1.2.1 B+Tree

在这里插入图片描述
这种分段拆分的索引结构天生具有范围查找的优势。

  • 关于B+Tree的高度(深度):
    因为磁盘分块大小固定(如4KB),那么单个索引数据占据的字节越多,则每个节点能存放的索引数据就少,层级就会越多,高度就会越高,查询线路就越长,效率越低,反之则越短,效率越高。

  • 关于索引的区分度:
    区分度就是指索引字段的值的区分度,如果索引字段的值唯一性越高,那它的区分度就越高(比如一个班级用姓名作为索引),这个索引的效率就越高。如果索引值之间相同的值越多(比如一个班级用性别作为索引),那么区分度就越低,这个索引的效率就越低。

  • 关于左侧优先:
    因为索引是有序数据,所有查找都从左(小)往右(大)。

  • 优点:

    • 更快的读性能,没有遍历的过程,可以直接索引到某个确定的位置。
    • 更好的事务支持,锁可以直接定义到树的结点中。
    • 因为在磁盘上维护了全局的顺序结构,支持更快的范围查找。
  • 缺点:

    • 每次至少写两次,一次写入预写日志,一次写入树本身。
    • 磁盘碎片化,写入内容可能不足4KB也会占用一个块。占用比实际内容更大的存储空间

B+Tree叶子节点之间的 双向指针 是为了范围查找而设计的,根据上限和下限位置,然后通过双向指针快速取出数据而不用多次索引取值。

索引数据一般是有序数据,基于这组有序数据的定量分段其中的量是指分支因子,简单理解就是间隔。分支因子为5004KB块的四级树可以存储的空间就高达256TB,所以大多数B-tree就3-4层的深度就可以满足大部分需求。

  1. MySQL用的存储引擎是Innodb,所用的索引数据结构是B+Tree,每个节点默认分配的大小是16kb。
    可以通过语句查看:
    SHOW GLOBAL STATUS like 'Innodb_page_size';
    
    在这里插入图片描述
  2. 计算每个节点最多能存多少个索引
    每个完整的 索引元素 都是一个 索引值 + 分叉指针 组成,
    索引值 我们常用bigint类型,在 java 中占8 Byte
    分叉指针 MySQL默认分配6 Byte
    MySQL默认分页大小为16 KB
    所以每个子节点能够存储的最大 索引元素 个数 = (16 * 1024) / (8 + 6) = 1170 个。
    假设叶子节点的 索引值 + 数据1016 Byte = 1024 kB,那么每个叶子节点最多能存 (16 * 1024) / (1024) = 16 个元素
    如此如果是一个3B+Tree 能存储的索引元素 = 1170 * 1170 * 16 = 2千万,一般我们上到这个数量级就要考虑分库分表了。

1.2.2 Hash

HashMap作为数据结构存储索引,以索引值为key进行 一次 hash计算就可以定位出value(数据存储的地址),很多时候比B+Tree索引更加高效。
在这里插入图片描述

  • 局限性
    • hash索引必须全部放在内存中,因为它需要大量的随机访问IO,如果放在磁盘上性能会
      变得很糟糕。当数据量很大继续增长时,继续增长的代价昂贵,并且hash冲突时需要处理复杂的逻辑
      性能也会有明显的下降
    • 仅能满足=IN,不支持范围查询,只能用逐个查找的方式去查找每个键。
  1. 哈希结构只有单层结构,随着索引增加,哈希表也会不断地增大,扩容的过程非常消耗性能。
  2. 现实工作时几乎不用Hash索引,因为它不支持范围查找。

1.2.3 LSM-Tree

其实是优化版的HASH:

把索引字段进行排序,分段,然后在内存中的哈希表只维护每个段的地址,这样就大大地减少的哈希表的体量,相当于构建了一个稀疏的哈希表(类似跳表),使之前的单层结构变成了多层,类似于BTREE。
在这里插入图片描述

  • 优点:
    • 更好的写入性能,因为是顺序写入,磁盘的写入性能要比随机写入高的多。
    • 2,更好的压缩合并算法,无空间碎片,占用更少的空间
  • 缺点:
    • 在不同段中可能具有相同键的多个副本,且在顺序段中还是需要进行遍历,查询性能较低
    • 因为只是局部有序,对范围查找的支持不如B-Tree

拓展:二叉树、红黑树、B树 和 B+树

二叉树、红黑树:

  • 每个节点只存一个键值对(索引和对应的数据地址),且最多有两个子节点
  • 没有数据冗余
    在这里插入图片描述
    B树:
  • 每个节点可以存多个值(多个键值对),对应的就有多个节点,所以也叫多叉树
  • 每个节点都存储着索引和值(数据地址),但相比二叉树存储同等量数据的情况下层级更少,结构更加扁平(通过增加宽度缩减深度)
  • 没有数据冗余
    在这里插入图片描述

B+树:

  • 节点不存储数据,只存储索引,叶子节点才存储数据。
  • 相比B树,等量数据下进一步减少层级,更加扁平(通过增加宽度缩减深度)。
  • 以冗余索引的方式建立索引树,叶子节点包含所有索引和数据(数据地址)。
  • 叶子节点之间通过双向指针横向连接,提高区间访问性能(快速范围取值)。
     在这里插入图片描述

MySQL用的存储引擎是Innodb,所用的索引数据结构是B+Tree,每个节点默认分配的大小是16kb

1.3 索引类型

  1. Normal 普通索引
    表示普通索引,大多数情况下都可以使用

  2. Unique 唯一索引
    表示唯一的,不允许重复的索引,如果该字段信息保证不会重复例如身份证号用作索引时,可设置为unique,约束唯一标识数据库表中的每一条记录,即在单表中不能用每条记录是唯一的(例如身份证就是唯一的),Unique(要求列唯一)和Primary Key(primary key = unique + not null 列唯一)约束均为列或列集合中提供了唯一性的保证,Primary Key是拥有自动定义的Unique约束,但是每个表中可以有多个Unique约束,但是只能有一个Primary Key约束。
    mysql中创建Unique约束

  3. Full Text 全文索引
    表示全文收索,在检索长文本的时候,效果最好,短文本建议使用Index,但是在检索的时候数据量比较大的时候,现将数据放入一个没有全局索引的表中,然后在用Create Index创建的Full Text索引,要比先为一张表建立Full Text然后在写入数据要快的很多

  4. FULLTEXT 用于搜索很长一篇文章的时候,效果最好。用在比较短的文本,如果就一两行字的,普通的 INDEX 也可以。

  5. SPATIAL 空间索引
    空间索引是对空间数据类型的字段建立的索引,MYSQL中的空间数据类型有4种,分别是GEOMETRY、POINT、LINESTRING、POLYGON。MYSQL使用SPATIAL关键字进行扩展,使得能够用于创建正规索引类型的语法创建空间索引。创建空间索引的列,必须将其声明为NOT NULL,空间索引只能在存储引擎为MYISAM的表中创建

1.4 存储引擎

存储引擎是修饰数据库表的,不同引擎存储一张表数据的形式不同。
在这里插入图片描述

存储引擎有很多种
在这里插入图片描述

1.4.1 MyISAM

MyISAM存储引擎采用索引文件和数据文件分离的形式(非聚集/簇)存储数据库表。
在这里插入图片描述
本地文件分为 3 Part
在这里插入图片描述

1.4.2 Innodb

表数据文件本身就是按B+Tree组织的一个索引结构文件,而叶子节点包含了完整的数据记录,所以数据文件也是索引文件,以数据索引二合一的形式(聚集/簇)存储数据库表。
在这里插入图片描述

在这里插入图片描述

聚簇索引查询速度会比非聚簇索引快,因为要先查询索引然后再去另外一个表查数据(回表)。

同为InnoDB表,同为B+Tree索引结构,主键索引和非主键索引在叶子节点上会有所区别:
在这里插入图片描述
如果对一个非主键字段创建索引,InnoDB引擎会自动在表中所有列找一个区分度最高的列做映射,一般都是主键列,也就是把非主键列和主键做一个映射,这个非主键索引创建的索引树的叶子节点存着的就是对应的主键,之后查询的时候先根据非主键列索引找到对应主键,然后在用主键去主键索引找到数据。这也是为什么
建议 InnoDB 表必须建主键,并且推荐使用整型的自增主键(保证主键唯一性和有序性)的原因。

  1. 一个索引对应一张索引表,如果一张表有一个主键索引一个非主键索引,在只用到非主键索引查找时就先查非主键索引表拿到主键,再到主键索引表拿数据。这样做节省了空间。
  2. 如果InnoDB没有找到唯一性索引列(主键列),它会自动帮你增加并维护一个隐藏列,以自增整型的主键形式保证唯一性。
  3. 有一些情况有人用UUID作为InnoDB表的主键,虽然它也有唯一性保证,但是它不能保证有序性,而且做主键运算比较时比整型数据慢。
  4. 自增主键的好处是,在插入索引树的时候,它永远都是在树的末尾增加,而不会插入树的前面或中间,也就是自增型有序主键在插入数据时可以把对树节点的影响(自动平衡操作,节点的拆分重排)降到最低

1.5 索引优化

1.5.1 联合索引

一般一张表不建议建太多个单值索引,因为每多一个索引MySQL就要多维护一个索引树,数据库资源是非常珍贵的。因此在实际工作中,我们一般会用联合索引而少用单值索引, MySQL中联合索引最多可以包括16个字段。

每次比较依次比较多个索引字段,直到比较结果为不同为止(因此要保证表有唯一性索引)。
在这里插入图片描述

单值索引:基于同一表中某个列创建的索引称为单值索引。
联合索引:基于同一表中多个列创建的索引称为联合索引。

1.5.2 EXPLAIN工具

使用EXPLAIN关键字可以模拟优化器执行SQL语句,分析你的查询语句或是结构的性能瓶颈。因此在select语句之前增加explain关键字,MySQL会在查询上设置一个标记,执行查询会返回执行计划的信息,而不是执行这条SQL。

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

1.5.3 最左前缀原则

例:
employees表中,有一个基于nameageposition三个字段创建的联合索引,

KEY `idx_name_age_position` (`name`,`age`,`position`) USING BTREE

有以下三条SQL语句,哪条会走索引?

EXPLAIN SELECT FROM employees WHERE name = 'Bill' and age = 31;     // 符合规则
EXPLAIN SELECT FROM employees WHERE name = 'Bill' position = 'dev'; // 半违规,name会走索引,position不会
EXPLAIN SELECT FROM employees WHERE age = 30 AND position = 'dev';  // 违规,跳过了name字段
EXPLAIN SELECT FROM employees WHERE position = 'manager';           // 违规,跳过了name,age字段

通过Explain工具分析,只有第一条SQL走了索引,为什么?

最左前缀原则是指在使用联合索引的查询语句中,必须遵循一定规则才能够走索引:

  • 使用索引时必须按照联合索引创建时的索引字段顺序调用才会生效。
  • 以最左边的为起点任何连续的索引都能匹配上。
  • 不能跳过联合索引前面的索引字段,只使用后面的索引字段

阿里巴巴索引规约:

  1. 单表索引数量控制5个以内
  2. 不允许存在重复索引和冗余索引
  3. 防止字段隐式转换导致的索引失效
  4. SQL优化目标:至少达到range级别
  5. 利用覆盖索引避免回表操作
  6. 禁止超过三个表的join
  7. varchar上建立索引,指定索引长度
  8. 索引字段值不允许设置为null,必须设置默认值
  9. 单表数据量控制在1000w以内
  10. 字段列数量建议在30以内
  11. 不建议使用MySQL分区表
  12. 单表行数超过500w或者单表容量超过2G建议分库分表

1.6 索引失效的几种情况

1.6.1 没有遵循最左前缀原则

1.6.2 索引字段参与计算、函数、类型转换(自动或手动)导致索引失效

1.6.3 范围条件右边的列索引失效

因为前一个条件相同的情况下 当前条件才会是有序的。当前一个条件是范围,那么查找到的数据是范围数据,是无序的,那么无法保证当前条件为有序的 所以索引失效。
所以在实际开发中范围查询,例如:金额查询,日期查询往往都是范围查询。应将范围查询条件放置 WHERE 语句的最后面。(创建的联合索引中,务必把范围涉及到的字段写在最后

1.6.4 不等于( != 或者 <> )索引失效

对索引字段使用!=<>不会走索引。

1.6.5 is null 可以使用索引,is not null 无法使用索引

is null相当于= null,是唯一确定的值。
is not null相当于范围查找,值是不确定的。

1.6.6 like 以通配符 % 开头索引失效

Alibaba《Java开发手册》,强制要求:页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决

1.6.7 OR 前后存在非索引的列,索引失效

数据库和表没有统一使用字符集

统一使用 utf8mb4 (5.5.3版本以上支持)兼容性更好,统一字符集可以避免由于字符集转换产生的乱码。不同的字符集进行比较前需要进行转换会造成索引失效。

二、MySQL读写分离

当性能瓶颈出现在MySQL时,可以把对数据库的读操作和写操作分开来提升效率。要做读写分离,就要做把数据库做成主从架构,一个主库一个从库或者一个主库多个从库,一般主库做写操作,从库做读操作,然后主从之间做好数据同步。

2.1 主从复制

  1. MySQL数据库默认支持主从复制,是数据库自带功能,无需借助第三方工具,只需要在数据库中简单的配置即可。
  2. MySQL的主从复制是一个 异步 的复制过程,底层是基于Mysql数据库自带的二进制日志功能。就是一台或多台MySQL数据库 从另一台MySQL数据库进行日志的复制,然后再解析日志并应用到自身,最终实现 从库 的数据和 主库 的数据保持一致。
  3. 二进制日志binlog记录了所有的DDL(数据定义语言)语句和DML(数据操纵语言)语句,但是不包括数据查询语句。此日志对于灾难时的数据恢复起着极其重要的作用,MySQL的主从复制就是通过该binlog实现的。默认MySQL是未开启该日志的。
  4. 简单来说就是一台服务器中的mysql数据库根据另一台服务器中的mysql数据库的日志文件进行分析然后执行sql语句进行数据复制。
    在这里插入图片描述

2.2 查看binlog日志

show binary logs;

在这里插入图片描述

binlog文件不止一个

看看目master服务正在使用哪个日志文件

show master status;

在这里插入图片描述
然后看看日志记录的事件

show binlog events in 'mysql-bin.000003';

2.3 数据一致性问题

主从同步提高了数据库的读写性能,但是也产生 数据一致性 的问题,因为主从同步是异步的,所以在某一时刻非常可能产生主库写入了数据,但从库还没通过日志复制,此时查询这个数据就会查不到。

2.3.1 数据半同步

正常情况下,主库插入数据后,在binlog生成日志记录就完成了插入数据这个操作。但是数据半同步是在binlog生成日志记录后,把它复制到relaylog才算完成插入数据操作。这样可以在一定程度上保证数据一致性(因网络引起的),但同时也降低的写入效率,所以要根据实际做取舍。
在这里插入图片描述

如果是一主多从的结构,那么数据半同步为了尽量减少对主库写入效率的影响,一般只对其中一个从库做半同步。

数据全同步把整个插入过程拉长到了读取中继日志执行插入指令完成数据复制为止。这个过程以为涉及到了SQL线程,为了不影响数据的查询效率,一般不会选择这种全同步的方式。

查看binlog是否开启:
① 打开命令窗口:
在这里插入图片描述
② 执行show variables like 'log_bin' ;
在这里插入图片描述
③ 开启binlog:
打开mysql 的配置文件my.ini
在mysqld配置项下面加上log_bin=mysql_bin

[mysqld]
log_bin = mysql_bin

2.4 常用主从架构

在这里插入图片描述

  • 一主一从:主要做数据热备(实时备份),提高容灾性(但不能代替手动数据备份工作)。
  • 一主多从:从节点一般2~4个,优化查询效率和对数据做有效隔离方便多种业务执行(比如其中一个从库专门用来做数据监控)。但是同步压力比较大。用得最多
  • 双主:存在写入性能瓶颈,提升写入并发性。
  • 环形多主:京东促销使用过(也挂了),性能非常高,但风险非常高,一个主节点挂了就挂了,双刃剑。
  • 级联同步:在一主多从的基础上变化而来,降低同步压力。

2.5 读写分离架构方案

2.5.1 通过代理分离

Atlas
在这里插入图片描述
搭建:(基于Docker部署)

1.配置主库:
在这里插入图片描述
2.配置从库:
在这里插入图片描述
在这里插入图片描述

三、一些经验

一、分类讨论

一条 SQL 语句执行的很慢,那是每次执行都很慢呢?还是大多数情况下是正常的,偶尔出现很慢呢?所以我觉得,我们还得分以下两种情况来讨论。

1、大多数情况是正常的,只是偶尔会出现很慢的情况。

2、在数据量不变的情况下,这条SQL语句一直以来都执行的很慢。

针对这两种情况,我们来分析下可能是哪些原因导致的。

二、针对偶尔很慢的情况

一条 SQL 大多数情况正常,偶尔才能出现很慢的情况,针对这种情况,我觉得这条SQL语句的书写本身是没什么问题的,而是其他原因导致的,那会是什么原因呢?

1、数据库在刷新脏页(flush)我也无奈啊
当我们要往数据库插入一条数据、或者要更新一条数据的时候,我们知道数据库会在内存中把对应字段的数据更新了,但是更新之后,这些更新的字段并不会马上同步持久化到磁盘中去,而是把这些更新的记录写入到 redo log 日记中去,等到空闲的时候,在通过 redo log 里的日记把最新的数据同步到磁盘中去。

当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。

刷脏页有下面4种场景(后两种不用太关注“性能”问题):

  • redolog写满了:
    redo log 里的容量是有限的,如果数据库一直很忙,更新又很频繁,这个时候 redo log 很快就会被写满了,这个时候就没办法等到空闲的时候再把数据同步到磁盘的,只能暂停其他操作,全身心来把数据同步到磁盘中去的,而这个时候,就会导致我们平时正常的SQL语句突然执行的很慢,所以说,数据库在在同步数据到磁盘的时候,就有可能导致我们的SQL语句执行的很慢了。

  • 内存不够用了:
    如果一次查询较多的数据,恰好碰到所查数据页不在内存中时,需要申请内存,而此时恰好内存不足的时候就需要淘汰一部分内存数据页,如果是干净页,就直接释放,如果恰好是脏页就需要刷脏页。

  • MySQL 认为系统“空闲”的时候: 这时系统没什么压力。

  • MySQL 正常关闭的时候:

  • 这时候,MySQL 会把内存的脏页都 flush 到磁盘上,这样下次 MySQL 启动的时候,就可以直接从磁盘上读数据,启动速度会很快。

2、拿不到锁我能怎么办
这个就比较容易想到了,我们要执行的这条语句,刚好这条语句涉及到的表,别人在用,并且加锁了,我们拿不到锁,只能慢慢等待别人释放锁了。或者,表没有加锁,但要使用到的某个一行被加锁了,这个时候,我也没办法啊。

如果要判断是否真的在等待锁,我们可以用 show processlist这个命令来查看当前的状态哦,这里我要提醒一下,有些命令最好记录一下,反正,我被问了好几个命令,都不知道怎么写,呵呵。

下来我们来访分析下第二种情况,我觉得第二种情况的分析才是最重要的

三、针对一直都这么慢的情况

如果在数据量一样大的情况下,这条 SQL 语句每次都执行的这么慢,那就就要好好考虑下你的 SQL 书写了,下面我们来分析下哪些原因会导致我们的 SQL 语句执行的很不理想。

我们先来假设我们有一个表,表里有下面两个字段,分别是主键 id,和两个普通字段 c 和 d。

mysql> CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `c` int(11) DEFAULT NULL,
  `d` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

1、扎心了,没用到索引
没有用上索引,我觉得这个原因是很多人都能想到的,例如你要查询这条语句

select * from t where 100 <c and c < 100000;
(1)、字段没有索引

刚好你的 c 字段上没有索引,那么抱歉,只能走全表扫描了,你就体验不会索引带来的乐趣了,所以,这回导致这条查询语句很慢。

(2)、字段有索引,但却没有用索引

好吧,这个时候你给 c 这个字段加上了索引,然后又查询了一条语句

select * from t where c - 1 = 1000;
我想问大家一个问题,这样子在查询的时候会用索引查询吗?

答是不会,如果我们在字段的左边做了运算,那么很抱歉,在查询的时候,就不会用上索引了,所以呢,大家要注意这种字段上有索引,但由于自己的疏忽,导致系统没有使用索引的情况了。

正确的查询应该如下

select * from t where c = 1000 + 1;
有人可能会说,右边有运算就能用上索引?难道数据库就不会自动帮我们优化一下,自动把 c - 1=1000 自动转换为 c = 1000+1。

不好意思,确实不会帮你,所以,你要注意了。

(3)、函数操作导致没有用上索引

如果我们在查询的时候,对字段进行了函数操作,也是会导致没有用上索引的,例如

select * from t where pow(c,2) = 1000;
这里我只是做一个例子,假设函数 pow 是求 c 的 n 次方,实际上可能并没有 pow(c,2)这个函数。其实这个和上面在左边做运算也是很类似的。

所以呢,一条语句执行都很慢的时候,可能是该语句没有用上索引了,不过具体是啥原因导致没有用上索引的呢,你就要会分析了,我上面列举的三个原因,应该是出现的比较多的吧。

2、呵呵,数据库自己选错索引了
我们在进行查询操作的时候,例如

select * from t where 100 < c and c < 100000;

我们知道,主键索引和非主键索引是有区别的,主键索引存放的值是整行字段的数据,而非主键索引上存放的值不是整行字段的数据,而且存放主键字段的值。不大懂的可以看这篇文章: 【思维导图-索引篇】搞定数据库索引就是这么简单 里面有说到主键索引和非主键索引的区别

也就是说,我们如果走 c 这个字段的索引的话,最后会查询到对应主键的值,然后,再根据主键的值走主键索引,查询到整行数据返回。

好吧扯了这么多,其实我就是想告诉你,就算你在 c 字段上有索引,系统也并不一定会走 c 这个字段上的索引,而是有可能会直接扫描扫描全表,找出所有符合 100 < c and c < 100000 的数据。

为什么会这样呢?

其实是这样的,系统在执行这条语句的时候,会进行预测:究竟是走 c 索引扫描的行数少,还是直接扫描全表扫描的行数少呢?显然,扫描行数越少当然越好了,因为扫描行数越少,意味着I/O操作的次数越少。

如果是扫描全表的话,那么扫描的次数就是这个表的总行数了,假设为 n;而如果走索引 c 的话,我们通过索引 c 找到主键之后,还得再通过主键索引来找我们整行的数据,也就是说,需要走两次索引。而且,我们也不知道符合 100 c < and c < 10000 这个条件的数据有多少行,万一这个表是全部数据都符合呢?这个时候意味着,走 c 索引不仅扫描的行数是 n,同时还得每行数据走两次索引。

所以呢,系统是有可能走全表扫描而不走索引的。那系统是怎么判断呢?

判断来源于系统的预测,也就是说,如果要走 c 字段索引的话,系统会预测走 c 字段索引大概需要扫描多少行。如果预测到要扫描的行数很多,它可能就不走索引而直接扫描全表了。

那么问题来了,系统是怎么预测判断的呢?这里我给你讲下系统是怎么判断的吧,虽然这个时候我已经写到脖子有点酸了。

系统是通过索引的区分度来判断的,一个索引上不同的值越多,意味着出现相同数值的索引越少,意味着索引的区分度越高。我们也把区分度称之为基数,即区分度越高,基数越大。所以呢,基数越大,意味着符合 100 < c and c < 10000 这个条件的行数越少。

所以呢,一个索引的基数越大,意味着走索引查询越有优势。

那么问题来了,怎么知道这个索引的基数呢?

系统当然是不会遍历全部来获得一个索引的基数的,代价太大了,索引系统是通过遍历部分数据,也就是通过采样的方式,来预测索引的基数的。

扯了这么多,重点的来了,居然是采样,那就有可能出现失误的情况,也就是说,c 这个索引的基数实际上是很大的,但是采样的时候,却很不幸,把这个索引的基数预测成很小。例如你采样的那一部分数据刚好基数很小,然后就误以为索引的基数很小。然后就呵呵,系统就不走 c 索引了,直接走全部扫描了。

所以呢,说了这么多,得出结论:由于统计的失误,导致系统没有走索引,而是走了全表扫描,而这,也是导致我们 SQL 语句执行的很慢的原因。

这里我声明一下,系统判断是否走索引,扫描行数的预测其实只是原因之一,这条查询语句是否需要使用使用临时表、是否需要排序等也是会影响系统的选择的。

不过呢,我们有时候也可以通过强制走索引的方式来查询,例如

select * from t force index(a) where c < 100 and c < 100000;

我们也可以通过

show index from t;

来查询索引的基数和实际是否符合,如果和实际很不符合的话,我们可以重新来统计索引的基数,可以用这条命令

analyze table t;
来重新统计分析。

既然会预测错索引的基数,这也意味着,当我们的查询语句有多个索引的时候,系统有可能也会选错索引哦,这也可能是 SQL 执行的很慢的一个原因。

好吧,就先扯这么多了,你到时候能扯出这么多,我觉得已经很棒了,下面做一个总结。

四、总结

以上是我的总结与理解,最后一个部分,我怕很多人不大懂数据库居然会选错索引,所以我详细解释了一下,下面我对以上做一个总结。

一个 SQL 执行的很慢,我们要分两种情况讨论:

1、大多数情况下很正常,偶尔很慢,则有如下原因

(1)、数据库在刷新脏页,例如 redo log 写满了需要同步到磁盘。

(2)、执行的时候,遇到锁,如表锁、行锁。

2、这条 SQL 语句一直执行的很慢,则有如下原因。

(1)、没有用上索引:例如该字段没有索引;由于对字段进行运算、函数操作导致无法用索引。

(2)、数据库选错了索引。

数据结构模拟网站:cs.usfca.edu/~galles/visualization/Algorithms.html

参考资料:

  1. 哔哩哔哩 山中孔聖 十分钟讲透索引
    https://www.bilibili.com/video/BV1sL411373d?share_source=copy_web
  2. 哔哩哔哩 图灵课堂Java视频教程 【MySQL完整版】这可能是B站讲得最好的MySQL索引优化教程了(B树+底层数据结构+MySQL读写分离+底层执行原理)
    https://www.bilibili.com/video/BV1RU4y1P7Ae?p=2&share_source=copy_web
  3. 微信公众号 JavaGuide 苦逼的码农 腾讯面试:一条SQL语句执行得很慢的原因有哪些?—不看后悔系列
    https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485185&idx=1&sn=66ef08b4ab6af5757792223a83fc0d45&chksm=cea248caf9d5c1dc72ec8a281ec16aa3ec3e8066dbb252e27362438a26c33fbe842b0e0adf47&token=79317275&lang=zh_CN#rd
  4. CSDN 秋天的童话_ 如何使用MySQL进行主从复制
    https://blog.csdn.net/han1725692339/article/details/125912816
  5. CSDN 花小疯 索引失效的几种情况
    https://blog.csdn.net/dd2016124/article/details/125076815
    3.CSDN 徊忆羽菲 详细介绍mysql索引类型:FULLTEXT、NORMAL、SPATIAL、UNIQUE
    https://blog.csdn.net/guo_qiangqiang/article/details/88794971
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值