MySQL面经整理(迭代)

257 阅读1小时+

MySQL基础

什么是数据库, 数据库管理系统, 数据库系统, 数据库管理员?

  • 数据库 : 数据库(DataBase 简称 DB)就是信息的集合或者说数据库是由数据库管理系统管理的数据的集合。
  • 数据库管理系统 : 数据库管理系统(Database Management System 简称 DBMS)是一种操纵和管理数据库的大型软件,通常用于建立、使用和维护数据库。
  • 数据库系统 : 数据库系统(Data Base System,简称 DBS)通常由软件、数据库和数据管理员(DBA)组成。
  • 数据库管理员 : 数据库管理员(Database Administrator, 简称 DBA)负责全面管理和控制数据库系统。

什么是元组, 码, 候选码, 主码, 外码, 主属性, 非主属性?

  • 元组 : 元组(tuple)是关系数据库中的基本概念,关系是一张表,表中的每行(即数据库中的每条记录)就是一个元组,每列就是一个属性。 在二维表里,元组也称为行。
  • :码就是能唯一标识实体的属性,对应表中的列。
  • 候选码 : 若关系中的某一属性或属性组的值能唯一的标识一个元组,而其任何、子集都不能再标识,则称该属性组为候选码。例如:在学生实体中,“学号”是能唯一的区分学生实体的,同时又假设“姓名”、“班级”的属性组合足以区分学生实体,那么{学号}和{姓名,班级}都是候选码。
  • 主码 : 主码也叫主键。主码是从候选码中选出来的。 一个实体集中只能有一个主码,但可以有多个候选码。
  • 外码 : 外码也叫外键。如果一个关系中的一个属性是另外一个关系中的主码则这个属性为外码。
  • 主属性 : 候选码中出现过的属性称为主属性。比如关系 工人(工号,身份证号,姓名,性别,部门). 显然工号和身份证号都能够唯一标示这个关系,所以都是候选码。工号、身份证号这两个属性就是主属性。如果主码是一个属性组,那么属性组中的属性都是主属性。
  • 非主属性: 不包含在任何一个候选码中的属性称为非主属性。比如在关系——学生(学号,姓名,年龄,性别,班级)中,主码是“学号”,那么其他的“姓名”、“年龄”、“性别”、“班级”就都可以称为非主属性。

主键和外键有什么区别?

  • 主键(主码) :主键用于唯一标识一个元组,不能有重复,不允许为空。一个表只能有一个主键。
  • 外键(外码) :外键用来和其他表建立联系用,外键是另一表的主键,外键是可以有重复的,可以是空值。一个表可以有多个外键。

为什么不推荐使用外键与级联?

不得使用外键与级联,一切外键概念必须在应用层解决。

说明: 以学生和成绩的关系为例,学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群; 级联更新是强阻塞,存在数据库更新风暴的风 险; 外键影响数据库的插入速度

  • 增加了复杂性
  • 增加了额外工作
  • 对分库分表不友好
  • 保证了数据库数据的一致性和完整性;
  • 级联操作方便,减轻了程序代码量;

什么是 ER 图?

E-R 图 也称实体-联系图(Entity Relationship Diagram),提供了表示实体类型、属性和联系的方法,用来描述现实世界的概念模型。 它是描述现实世界关系概念模型的有效方法。 是表示概念关系模型的一种方式。

数据库范式了解吗?

1NF(第一范式) ——所有字段都不可在拆分

属性(对应于表中的字段)不能再被分割,也就是这个字段只能是一个值,不能再分为多个其他的字段了。1NF 是所有关系型数据库的最基本要求 ,也就是说关系型数据库中创建的表一定满足第一范式。

2NF(第二范式) ——每一行数据必须有一个主键,所有非主键字段必须依赖于主键字段

3NF——所有非主键字段不能依赖于其他非主键字段,必须完全依赖于主键字段

反三范式——实际开发中,通常会为数据添加一些冗余字段,减少多表查询。

什么是存储过程?

阿里巴巴 Java 开发手册里要求禁止使用存储过程。

禁止使用存储过程,存储过程难以调试和拓展,更没有移植性。

阿里巴巴 Java 开发手册里要求禁止使用存储过程。

drop、delete 与 truncate 区别?

  • drop(丢弃数据): drop table 表名 ,直接将表都删除掉,在删除表的时候使用。
  • truncate (清空数据) : truncate table 表名 ,只删除表中的数据,再插入数据的时候自增长 id 又从 1 开始,在清空表中数据的时候使用。
  • delete(删除数据) : delete from 表名 where 列名=值,删除某一行的数据,如果不加 where 子句和truncate table 表名作用类似。

truncatedrop 属于 DDL(数据定义语言)语句,操作立即生效,原数据不放到 rollback segment 中,不能回滚,操作不触发 trigger。而 delete 语句是 DML (数据库操作语言)语句,这个操作会放到 rollback segement 中,事务提交之后才生效。

执行速度不同

MySQL 基础架构

img

  • 连接器: 系统(客户端)访问MySQL服务器前,做的第一件事就是建立TCP连接

    经过三次握手建立连接成功后,MySQL服务器对TCP传输过来的账号密码做身份认证、权限获取

  • 查询缓存: 执行查询语句的时候,会先查询缓存(MySQL 8.0 版本后移除,因为 这个功能不太实用)。

  • 逻辑架构 服务层还有SQL Interface: SQL接口,接收用户的SQL命令,并且返回用户需要查询的结果。

  • Parser: 解析器

    • 在解析器中对 SQL 语句进行语法分析语义分析。将SQL语句分解成数据结构,并将这个结构传递到后续步骤,以后SQL语句的传递和处理就是基于这个结构的。如果在分解构成中遇到错误,那么就说明这个SQL语句是不合理的。
    • 在SQL命令传递到解析器的时候会被解析器验证和解析,并为其创建语法树,并根据数据字典丰富查询语法树,会验证该客户端是否具有执行该查询的权限。创建好语法树后,MySQL还会对SQl查询进行语法上的优化,进行查询重写。
  • Optimizer: 查询优化器

    • SQL语句在语法解析之后、查询之前会使用查询优化器确定 SQL 语句的执行路径,生成一个执行计划。
    • 这个执行计划表明应该使用哪些索引进行查询(全表检索还是使用索引检索),表之间的连接顺序如何,最后会按照执行计划中的步骤调用存储引擎提供的方法来真正的执行查询,并将查询结果返回给用户。
    • 它使用“选取-投影-连接”策略进行查询。
  • 执行器: 执行语句,然后从存储引擎返回数据。 执行语句之前会先判断是否有权限,如果没有权限的话,就会报错。

  • 插件式存储引擎 : 主要负责数据的存储和读取对物理服务器级别维护的底层数据执行操作,采用的是插件式架构,支持 InnoDB、MyISAM、Memory 等多种存储引擎。

数据库缓冲池(buffer pool)

InnoDB 存储引擎是以页为单位来管理存储空间的,我们进行的增删改查操作其实本质上都是在访问页面(包括读页面、写页面、创建新页面等操作)。而磁盘 I/O 需要消耗的时间很多,而在内存中进行操作,效率则会高很多,为了能让数据表或者索引中的数据随时被我们所用,DBMS 会申请 占用内存来作为 数据缓冲池 ,在真正访问页面之前,需要把在磁盘上的页缓存到内存中Buffer Pool 之后才可以访问。

这样做的好处是可以让磁盘活动最小化,从而 减少与磁盘直接进行 I/O 的时间 。要知道,这种策略对提升 SQL 语句的查询性能来说至关重要。如果索引的数据在缓冲池里,那么访问的成本就会降低很多。

缓冲池是主内存中的一部分空间,用来缓存已使用的表和索引数据。缓冲池使得经常被使用的数据能够直接在内存中获得,从而提高速度。

缓冲池.png

MySQL 存储引擎架构了解吗?

MySQL 存储引擎采用的是插件式架构,支持多种存储引擎,我们甚至可以为不同的数据库表设置不同的存储引擎以适应不同场景的需要。存储引擎是基于表的,而不是数据库。

并且,你还可以根据 MySQL 定义的存储引擎实现标准接口来编写一个属于自己的存储引擎。这些非官方提供的存储引擎可以称为第三方存储引擎,区别于官方存储引擎。像目前最常用的 InnoDB 其实刚开始就是一个第三方存储引擎,后面由于过于优秀,其被 Oracle 直接收购了。

MyISAM 和 InnoDB 的区别是什么?

1.是否支持行级锁

MyISAM 只有表级锁(table-level locking),而 InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。

2.是否支持事务

MyISAM 不提供事务支持。

InnoDB 提供事务支持,实现了 SQL 标准定义了四个隔离级别,具有提交(commit)和回滚(rollback)事务的能力。并且,InnoDB 默认使用的 REPEATABLE-READ(可重读)隔离级别是可以解决幻读问题发生的(基于 MVCCNext-Key Lock)。

3.是否支持外键

MyISAM 不支持,而 InnoDB 支持。

外键对于维护数据一致性非常有帮助,但是对性能有一定的损耗。因此,通常情况下,我们是不建议在实际生产项目中使用外键的,在业务代码中进行约束即可!

总结:一般我们也是不建议在数据库层面使用外键的,应用层面可以解决。不过,这样会对数据的一致性造成威胁。具体要不要使用外键还是要根据你的项目来决定。

4.是否支持数据库异常崩溃后的安全恢复

MyISAM 不支持,而 InnoDB 支持。

使用 InnoDB 的数据库在异常崩溃后,数据库重新启动的时候会保证数据库恢复到崩溃前的状态。这个恢复的过程依赖于 redo log

5.是否支持 MVCC

MyISAM 不支持,而 InnoDB 支持。MVCC 可以看作是行级锁的一个升级,可以有效减少加锁操作,提高性能

6.索引实现不一样。

虽然 MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,但是两者的实现方式不太一样。

InnoDB 引擎中,其数据文件本身就是索引文件。相比 MyISAM索引文件和数据文件是分离的,其表数据文件本身就是按 B+Tree 组织的一个索引结构,树的叶节点 data 域保存了完整的数据记录。

MyISAM优势是访问的速度快 ,对事务完整性没有要求或者以SELECTINSERT为主的应用针对数据统计有额外的常数存储。故而 count(*) 的查询效率很高(直接读取数据表保存的行记录数并返回)

Innodb写效率稍差,占用的数据空间相对比较大

Memory 引擎:置于内存的表

MySQL 查询缓存

执行查询语句的时候,会先查询缓存。不过,MySQL 8.0 版本后移除,因为这个功能不太实用

my.cnf 加入以下配置,重启 MySQL 开启查询缓存

query_cache_type=1
query_cache_size=600000

MySQL 执行以下命令也可以开启查询缓存

set global  query_cache_type=1;
set global  query_cache_size=600000;

开启查询缓存后在同样的查询条件以及数据情况下,会直接在缓存中返回结果。这里的查询条件包括查询本身、当前要查询的数据库、客户端协议版本号等一些可能影响结果的信息。查询缓存不命中的情况:(1) 因此任何两个查询在任何字符上的不同都会导致缓存不命中。此外,查询缓存不命中的情况:(2) 如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、MySQL 库中的系统表,其查询结果也不会被缓存。

查询缓存不命中的情况:(3) 缓存建立之后,MySQL 的查询缓存系统会跟踪查询中涉及的每张表,如果这些表(数据或结构)发生变化,那么和这张表相关的所有缓存数据都将失效。

缓存虽然能够提升数据库的查询性能,但是缓存同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁。 因此,开启查询缓存要谨慎,尤其对于写密集的应用来说更是如此。如果开启,要注意合理控制缓存空间大小,一般来说其大小设置为几十 MB 比较合适。此外,还可以通过 sql_cache 和 sql_no_cache 来控制某个查询语句是否需要缓存:

select sql_no_cache count(*) from usr;

SQL语句执行流程

1、若是 Select 语句,先从查询缓存中查看是否存在同样的语句的查询结果缓存,若有,直接返回 2、由查询分析器解析查询语句 3、由优化器进行查询语句优化,主要是选择是否使用索引,以及使用哪一个索引 4、由执行器使用接口调用存储引擎进行存储,MySQL 中存储引擎是插件式的,根据用户选择使用具体引擎,默认使用 InnoDB 引擎 5、在 InnoDB 引擎中,首先查看缓存池中是否存在 sql 语句对应的缓存数据,若没有,从数据库加载 6、执行增删改语句时,直接在缓存中进行修改 7、因为在缓存中修改的数据会由于 MySQL 主机宕机而消失,是脏数据,InnoDB 引擎使用 redo-log 来保证事务的持久性,在对缓存中数据进行修改时,还会将修改后的数据写入一个 redo-log Buffer 缓存中,并定时刷入磁盘中的 redo-log,在 MySQL 宕机重启后,就会通过 redo-log 来恢复已经提交了的修改事务 8、InnoDB 引擎还通过 undo-log 中的版本链来实现事务的回滚以及 MVCC 的 read-view,因此在修改缓存中数据时,还会将每一次修改后的 数据版本存入 undo-log,主要存储:数据行的主键 ID/进行本次修改的事务 ID/指向上一个版本的指针 9、MySQL 还会将所有的修改操作记入 bin-log 中,用于实现主从复制,以及可以搭配 canal 中间件进行多写一致 10、当事务完成提交时,首先会将 redo-log Buffer 中的数据刷入磁盘,然后将 bin-log 日志刷入磁盘,bin-log 刷入磁盘后,在 redo-log 中添加 commit 标记,此时事务才算成功提交 11、MySQL 会在自认为空闲时,由一个后台线程将缓存中的数据写入磁盘

能用 MySQL 直接存储文件(比如图片)吗?

可以是可以,直接存储文件对应的二进制数据即可。不过,还是建议不要在数据库中存储文件,会严重影响数据库性能,消耗过多存储空间。

可以选择使用云服务厂商提供的开箱即用的文件存储服务,成熟稳定,价格也比较低。

也可以选择自建文件存储服务,实现起来也不难,基于FastDFS、MinIO(推荐) 等开源项目就可以实现分布式文件服务。

数据库只存储文件地址信息,文件由文件存储服务负责存储。

MySQL 如何存储 IP 地址?

可以将 IP 地址转换成整形数据存储,性能更好,占用空间也更小。

MySQL 提供了两个方法来处理 ip 地址

  • INET_ATON() : 把 ip 转为无符号整型 (4-8 位)
  • INET_NTOA() :把整型的 ip 转为地址

插入数据前,先用 INET_ATON() 把 ip 地址转为整型,显示数据时,使用 INET_NTOA() 把整型的 ip 地址转为地址显示即可。

数据类型(列类型)

/* 数据类型(列类型) */ ------------------
1. 数值类型
-- a. 整型 ----------
    类型         字节     范围(有符号位)
    tinyint     1字节    -128 ~ 127      无符号位:0 ~ 255
    smallint    2字节    -32768 ~ 32767
    mediumint   3字节    -8388608 ~ 8388607
    int         4字节
    bigint      8字节
    int(M)  M表示总位数
    - 默认存在符号位,unsigned 属性修改
    - 显示宽度,如果某个数不够定义字段时设置的位数,则前面以0补填,zerofill 属性修改
        例:int(5)   插入一个数'123',补填后为'00123'
    - 在满足要求的情况下,越小越好。
    - 1表示bool值真,0表示bool值假。MySQL没有布尔类型,通过整型01表示。常用tinyint(1)表示布尔型。
-- b. 浮点型 ----------
    类型             字节     范围
    float(单精度)     4字节
    double(双精度)    8字节
    浮点型既支持符号位 unsigned 属性,也支持显示宽度 zerofill 属性。
        不同于整型,前后均会补填0.
    定义浮点型时,需指定总位数和小数位数。
        float(M, D)     double(M, D)
        M表示总位数,D表示小数位数。
        M和D的大小会决定浮点数的范围。不同于整型的固定范围。
        M既表示总位数(不包括小数点和正负号),也表示显示宽度(所有显示符号均包括)。
        支持科学计数法表示。
        浮点数表示近似值。
-- c. 定点数 ----------
    decimal -- 可变长度
    decimal(M, D)   M也表示总位数,D表示小数位数。
    保存一个精确的数值,不会发生数据的改变,不同于浮点数的四舍五入。
    将浮点数转换为字符串来保存,每9位数字保存为4个字节。
2. 字符串类型
-- a. char, varchar ----------
    char    定长字符串,速度快,但浪费空间
    varchar 变长字符串,速度慢,但节省空间
    M表示能存储的最大长度,此长度是字符数,非字节数。
    不同的编码,所占用的空间不同。
    char,最多255个字符,与编码无关。
    varchar,最多65535字符,与编码有关。
    一条有效记录最大不能超过65535个字节。
        utf8 最大为21844个字符,gbk 最大为32766个字符,latin1 最大为65532个字符
    varchar 是变长的,需要利用存储空间保存 varchar 的长度,如果数据小于255个字节,则采用一个字节来保存长度,反之需要两个字节来保存。
    varchar 的最大有效长度由最大行大小和使用的字符集确定。
    最大有效长度是65532字节,因为在varchar存字符串时,第一个字节是空的,不存在任何数据,然后还需两个字节来存放字符串的长度,所以有效长度是65535-1-2=65532字节。
    例:若一个表定义为 CREATE TABLE tb(c1 int, c2 char(30), c3 varchar(N)) charset=utf8; 问N的最大值是多少? 答:(65535-1-2-4-30*3)/3
-- b. blob, text ----------
    blob 二进制字符串(字节字符串)
        tinyblob, blob, mediumblob, longblob
    text 非二进制字符串(字符字符串)
        tinytext, text, mediumtext, longtext
    text 在定义时,不需要定义长度,也不会计算总长度。
    text 类型在定义时,不可给default-- c. binary, varbinary ----------
    类似于charvarchar,用于保存二进制字符串,也就是保存字节字符串而非字符字符串。
    char, varchar, text 对应 binary, varbinary, blob.
3. 日期时间类型
    一般用整型保存时间戳,因为PHP可以很方便的将时间戳进行格式化。
    datetime    8字节    日期及时间     1000-01-01 00:00:009999-12-31 23:59:59
    date        3字节    日期         1000-01-019999-12-31
    timestamp   4字节    时间戳        197001010000002038-01-19 03:14:07
    time        3字节    时间         -838:59:59838:59:59
    year        1字节    年份         1901 - 2155
datetime    YYYY-MM-DD hh:mm:ss
timestamp   YY-MM-DD hh:mm:ss
date        YYYY-MM-DD
time        hh:mm:ss
year        YYYY
4. 枚举和集合
-- 枚举(enum) ----------
enum(val1, val2, val3...)
    在已知的值中进行单选。最大数量为65535.
    枚举值在保存时,以2个字节的整型(smallint)保存。每个枚举值,按保存的位置顺序,从1开始逐一递增。
    表现为字符串类型,存储却是整型。
    NULL值的索引是NULL。
    空字符串错误值的索引值是0-- 集合(set) ----------
set(val1, val2, val3...)
    create table tab ( gender set('男', '女', '无') );
    insert into tab values ('男, 女');
    最多可以有64个不同的成员。以bigint存储,共8个字节。采取位运算的形式。
    当创建表时,SET成员值的尾部空格将自动被删除。

索引

索引

降低数据库的IO成本保证数据库表中每一行数据的唯一性加速表和表之间的连接

(1)创建索引和维护索引要 耗费时间 ,并且随着数据量的增加,所耗费的时间也会增加。 (2)索引需要占 磁盘空间 ,除了数据表占数据空间之外,每一个索引还要占一定的物理空间, 存储在磁盘上 ,如果有大量的索引,索引文件就可能比数据文件更快达到最大文件尺寸。(3)虽然索引大大提高了查询速度,同时却会 降低更新表的速度 。当对表中的数据进行增加、删除和修改的时候,索引也要动态地维护,这样就降低了数据的维护速度。

提示:在突发插入频繁的情况下,由于索引可以提高查询的速度,但是会影响插入记录的速度。这种情况下,可以先删除表中的索引,然后插入数据,插入完成后再创建索引。

b+树.png

my1.png

MyISAM中索引检索的算法为:首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取相应数据记录

聚簇索引

所有的用户记录都存在了叶子节点,数据即索引,索引即数据

  • 页内的记录是按照主键的大小顺序排成一个单向链表
  • 各个存放 用户记录的页也是根据页中用户记录的主键大小顺序排成一个 双向链表
  • 存放 目录项记录的页 分为不同的层次,在同一层次中的页也是根据页中目录项记录的主键大小顺序排成一个 双向链表

二级索引(辅助索引、非聚簇索引)

回表

想根据c2列的值查找到完整的用户记录的话,仍然需要到 聚簇索引 中再查一遍,这个过程称为 回表 。也就是根据c2列的值查询一条完整的用户记录需要使用到 2 棵B+树

联合索引

可以同时以多个列的大小作为排序规则,也就是同时为多个列建立索引,比方说我们想让B+树按照 c2和c3列 的大小进行排序,这个包含两层含义:

  • 先把各个记录和页按照c2列进行排序。
  • 在记录的c2列相同的情况下,采用c3列进行排序
全文索引

全文索引技术就是将各种信息,文档中所有的文字序列都作为检索对象,找出包含检索词汇的信息或文档。

索引下推

Index Condition Pushdown(ICP)是MySQL 5.6中新特性,是一种在存储引擎层使用索引过滤数据的一种优化方式。

索引下推是把本应该在 server 层进行筛选的条件,下推到存储引擎层来进行筛选判断,这样能有效减少回表。

举例说明:

首先使用联合索引(name,age),现在有这样一个查询语句:

select *  from t_user where name like 'L%' and age = 17;
这条语句从最左匹配原则上来说是不符合的,原因在于只有name用的索引,但是age并没有用到。
​
不用索引下推的执行过程:
第一步:利用索引找出name带'L'的数据行:LiLei、Lili、Lisa、Lucy 这四条索引数据
第二步:再根据这四条索引数据中的 id 值,逐一进行回表扫描,从聚簇索引中找到相应的行数据,将找到的行数据返回给 server 层。
第三步:在server层判断age = 17,进行筛选,最终只留下 Lucy 用户的数据信息。
​
使用索引下推的执行过程:
第一步:利用索引找出name带'L'的数据行:LiLei、Lili、Lisa、Lucy 这四条索引数据
第二步:根据 age = 17 这个条件,对四条索引数据进行判断筛选,最终只留下 Lucy 用户的数据信息。
(注意:这一步不是直接进行回表操作,而是根据 age = 17 这个条件,对四条索引数据进行判断筛选)
第三步:将符合条件的索引对应的 id 进行回表扫描,最终将找到的行数据返回给 server 层。

比较二者的第二步我们发现,索引下推的方式极大的减少了回表次数。

索引下推需要注意的情况:下推的前提是索引中有 age 列信息,如果是其它条件,如 gender = 0,这个即使下推下来也没用

  • 如果没有ICP,存储引擎会遍历索引以定位基表中的行,并将它们返回给MySQL服务器,由MySQL服务器评估WHERE后面的条件是否保留行。

  • 启用ICP后,如果部分WHERE条件可以仅使用索引中的列进行筛选,则MySQL服务器会把这部分WHERE条件放到存储引擎筛选。然后,存储引擎通过使用索引条目来筛选数据,并且只有在满足这一条件时才从表中读取行。

    • 好处: ICP可以减少存储引擎必须访问基表的次数和MySQL服务器必须访问存储引擎的次数。
    • 但是,ICP的加速效果取决于在存储引擎内通过ICP筛选掉的数据的比例。
#打开索引下推
SET optimizer_switch = 'index_condition_pushdown=off ' ;
#关闭索引下推
SET optimizer_switch = 'index_condition_pushdown=on ' ;
复制代码

当使用索引条件下推时,EXPLAIN语句输出结果中Extra列内容显示为Using index condition

为啥不用hash结构

hash.png

二叉搜索树

如果我们利用二叉树作为索引结构,那么磁盘的IO次数和索引树的高度是相关的。

搜索树.png

但是存在特殊的情况,就是有时候二叉树的深度非常大

AVL树(平衡二叉搜索树)

avl.png

B-TreeVSB+Tree

B树的英文是Balance Tree,也就是多路平衡查找树。简写为B-Tree,它的高度远小于平衡二叉树的高度

b树.png

b树1.png

b3.png

bb+.png

B+ 树和 B 树的差异
  1. 有 k 个孩子的节点就有 k 个关键字。也就是孩子数量 = 关键字数,而 B 树中,孩子数量 = 关键字数+1。
  2. 非叶子节点的关键字也会同时存在在子节点中,并且是在子节点中所有关键字的最大(或最小)。
  3. 非叶子节点仅用于索引,不保存数据记录,跟记录有关的信息都放在叶子节点中。而 B 树中, 非叶子节点既保存索引,也保存数据记录 。
  4. 所有关键字都在叶子节点出现,叶子节点构成一个有序链表,而且叶子节点本身按照关键字的大小从小到大顺序链接。

为了减少IO ,索引树会一次性加载吗?

B+ 树的存储能力如何?为何说一般查找行记录,最多只需1~3次磁盘IO

为什么说B+树比B-树更适合实际应用中操作系统的文件索引和数据库索引?

Hash 索引与 B+ 树索引的区别

隐藏索引

在MySQL 5.7版本及之前,只能通过显式的方式删除索引。此时,如果发现删除索引后出现错误,又只能通过显式创建索引的方式将删除的索引创建回来。如果数据表中的数据量非常大,或者数据表本身比较大,这种操作就会消耗系统过多的资源,操作成本非常高。

从MySQL 8.x开始支持隐藏索引(invisible indexes),只需要将待删除的索引设置为隐藏索引,使查询优化器不再使用这个索引(即使使用force index(强制使用索引),优化器也不会使用该索引)确认将索引设置为隐藏索引后系统不受任何响应,就可以彻底删除索引。这种通过先将索引设置为隐藏索引,再删除索引的方式就是软删除

同时,你想验证某个索引删除之后的查询性能影响,就可以暂时先隐藏该索引

索引的创建与设计原则

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

哪些情况适合创建索引

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

业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。

说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的。

2.频繁作为 WHERE 查询条件的字段

3.经常 GROUP BY 和 ORDER BY 的列

索引就是让数据按照某种顺序进行存储或检索,索引就是让数据按照某种顺序进行存储或检索

4.UPDATE、DELETE 的 WHERE 条件列

对数据按照某个条件进行查询后再进行 UPDATE 或 DELETE 的操作,如果对 WHERE 字段创建了索引,就能大幅提升效率。原理是因为我们需要先根据 WHERE 条件列检索出来这条记录,然后再对它进行更新或删除。

5.DISTINCT 字段需要创建索引

这是因为索引会对数据按照某种顺序进行排序,所以在去重的时候也会快很多。 因为紧挨着所以去重特别方便

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

连接表的数量尽量不要超过 3 张、WHERE 条件创建索引、对用于连接的字段创建索引

7.使用列的类型小的创建索引

我们这里所说的类型大小指的就是该类型表示的数据范围的大小

数据类型越小,索引占用的存储空间就越少,在一个数据页内就可以放下更多的记录,从而减少磁盘I/0带来的性能损耗,也就意味着可以把更多的数据页缓存在内存中,从而加快读写效率。

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

通过截取字段的前面一部分内容建立索引,这个就叫前缀索引。这样在查找记录时虽然不能精确的定位到记录的位置,但是能定位到相应前缀所在的位置,然后根据前缀相同的记录的主键值回表查询完整的字符串值。既节约空间,又减少了字符串的比较时间,还大体能解决排序的问题。

索引列前缀对排序的影响

二级索引中不包含完整的address列信息,所以无法对前12个字符相同,后边的字符不同的记录进行排序,也就是使用索引列前缀的方式无法支持使用索引排序,只能使用文件排序。

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

select count(distinct a)/count(*) from t1计算区分度,越接近1越好,一般超过33%就算是比较高效的索引了。 select count(distinct a)/count(*) from t1计算区分度,越接近1越好,一般超过33%就算是比较高效的索引了。

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

哪些情况不适合创建索引

1. 在where中使用不到的字段,不要设置索引

2. 数据量小的表最好不要使用索引

3. 有大量重复数据的列上不要建立索引

4.避免对经常更新的表创建过多的索引

5.不建议用无序的值作为索引

6.删除不再使用或者很少使用的索引

7.不要定义冗余或重复的索引

建立索引原则

1、对于经常要用做查询条件的字段 2、索引长度最好不要太长,特别是主键索引 3、遵从最左前缀法则,建立复合索引时,最常用的条件放最左边,经常作为精确查询的条件放左边,范围查找放右边 4、对于经常要进行增删改的字段,根据维护索引数据结构的开销慎重考虑是否建立索引 5、对于区分度低的字段,例如性别,建立索引也提高不了多少查询性能,不需要建索引

前缀索引,也就是使用索引列前缀的方式无法支持使用索引排序

select count(distinct left(address, 10 )) / count(*) as sub10, -- 截取前 10 个字符的选择度

性能分析工具的使用

统计SQL的查询成本:last_query_cost

定位执行慢的 SQL:慢查询日志

慢查询日志分析工具:mysqldumpslow

查看 SQL 执行成本:SHOW PROFILE

分析查询语句:EXPLAIN

索引失效案例

最佳左前缀法则

MySQL可以为多个字段创建索引,一个索引可以包括16个字段。对于多列索引, 过滤条件要使用索引必须按照索引建立时的顺序,依次满足,一旦跳过某个字段,索引后面的字段都无法被使用。如果查询条件中没有使用这些字段中第1个字段时,多列(或联合)索引不会被使用。

让主键具有 AUTO_INCREMENT ,让存储引擎自己为表生成主键

使用InnoDB存储引擎的表来说,表中的数据实际上都是存储在聚簇索引的叶子节点的。而记录又是存储在数据页中的,数据页和记录又是按照记录主键值从小到大的顺序进行排序,所以如果我们插入的记录的主键值是依次增大的话,那我们每插满一个数据页就换到下一个数据页继续插

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

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

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

is null可以使用索引,is not null无法使用索引(在查询中使用not like 也无法使用索引,导致全表扫描)

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

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

数据库和表的字符集统一使用utf8mb4

事务&日志&锁

MySQL 事务

事务是逻辑上的一组操作,要么都执行,要么都不执行。

何谓数据库事务?

大多数情况下,我们在谈论事务的时候,如果没有特指分布式事务,往往指的就是数据库事务

数据库事务在我们日常开发中接触的最多了。如果你的项目属于单体架构的话,你接触到的往往就是数据库事务了。

那数据库事务有什么作用呢?

简单来说,数据库事务可以保证多个对数据库的操作(也就是 SQL 语句)构成一个逻辑上的整体。构成这个逻辑上的整体的这些数据库操作遵循:要么全部执行成功,要么全部不执行

# 开启一个事务
START TRANSACTION;
# 多条 SQL 语句
SQL1,SQL2...
## 提交事务
COMMIT;

关系型数据库(例如:MySQL、SQL Server、Oracle 等)事务都有 ACID 特性:

  1. 原子性Atomicity) : 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  2. 一致性Consistency): 执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;
  3. 隔离性Isolation): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
  4. 持久性Durabilily): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。也就是说 A、I、D 是手段,C 是目的! 想必大家也和我一样

那么事务的四种特性到底是基于什么机制实现呢?

  • 事务的隔离性由 锁机制 实现。

  • 而事务的原子性、一致性和持久性由事务的 redo 日志和undo 日志来保证。

    • REDO LOG 称为 重做日志 ,提供再写入操作,恢复提交事务修改的页操作,用来保证事务的持久性
    • UNDO LOG 称为 回滚日志回滚行记录到某个特定版本,用来保证事务的原子性、一致性

有的DBA或许会认为 UNDO 是 REDO 的逆过程,其实不然。其实不然。REDO和UNDO都可以视为是一种恢厦操作

  • redo log:是存储引擎层(innodb)生成的日志,记录的是"物理级别"上的页修改操作,比如页号xx、偏移量ywy写入了'zzz'数据。主要为了保证数据的可靠性;
  • undo log:是存储引擎层(innodb)生成的日志,记录的是逻辑操作日志,比如对某一行数据进行了INSERT语句操作,那么undo log就记录一条与之相反的DELETE操作。主要用于事务的回滚(undo log 记录的是每个修改操作的逆操作)和一致性非锁定读(undo log回滚行记录到某种特定的版本---MVCC,即多版本并发控制)。

redo log日志

InnoDB存储引擎是以页为单位来管理存储空间的。在真正访问页面之前需要把在磁盘上的页缓存到内存中的 Buffer Pool之后才可以访问。所有的变更都必须先更新缓冲池中的数据,然后缓冲池中的脏页会以一定的频率被刷入磁盘( checkPoint机制),通过缓冲池来优化CPU和磁盘之间的鸿沟,这样就可以保证整体的性能不会下降太快。

为什么需要REDO日志

一方面,缓冲池可以帮助我们消除CPU和磁盘之间的鸿沟,checkpoint机制可以保证数据的最终落盘,然而由于checkpoint 并不是每次变更的时候就触发 的,而是master线程隔一段时间去处理的。所以最坏的情况就是事务提交后,刚写完缓冲池,数据库宕机了,那么这段数据就是丢失的,无法恢复。

另一方面,事务包含 持久性 的特性,就是说对于一个已经提交的事务,在事务提交后即使系统发生了崩溃,这个事务对数据库中所做的更改也不能丢失。

InnoDB引擎的事务采用了WAL技术(Write-Ahead Logging ),这种技术的思想就是先写日志,再写磁盘,只有日志写入成功,才算事务提交成功,这里的日志就是redo log。当发生宕机且数据未刷到磁盘的时候,可以通过redo log来恢复,保证ACID中的D,这就是redo log的作用。

image-20220328141502562.png

REDO日志的好处、特点

好处

  • redo日志降低了刷盘频率
  • 存储表空间ID页号偏移量以及需要更新的值,所需的存储空间是很小的,刷盘快。

特点

  • redo日志是顺序写入磁盘的

    在执行事务的过程中,每执行一条语句,就可能产生若干条redo日志,这些日志是按照产生的顺序写入磁盘的,也就是使用顺序IO,效率比随机IO快。

  • 事务执行过程中,redo log不断记录

    redo logbin log的区别,redo log存储引擎层产生的,而bin log数据层产生的。假设一个事务,对表做10万行的记录插入,在这个过程中,一直不断的往redo log顺序记录,而bin log不会记录,直到这个事务提交,才会一次写入到bin log文件中。

redo的组成

  • 重做日志的缓冲 (redo log buffer) ,保存在内存中,是易失的。

在服务器启动时就向操作系统申请了一大片称之为redo log buffer连续内存空间,翻译成中文就是redo日志缓冲区。这片内存空间被划分成若干个连续的redo log block。一个redo log block占用512字节大小。

  • 重做日志文件(redo log file),保存在硬盘中,是持久的。

    REDO日志文件,其中的ib_logfile0ib_logfile1即为redo log日志。

image-20220328142105871.png

第1步:先将原始数据从磁盘中读入内存中来,修改数据的内存拷贝

第2步:生成一条重做日志并写入redo log buffer,记录的是数据被修改后的值

第3步:当事务commit时,将redo log buffer中的内容刷新到 redo log file,对 redo log file采用追加写的方式

第4步:定期将内存中修改的数据刷新到磁盘中

redo log的刷盘策略

redo log的写入并不是直接写入磁盘的,InnoDB引擎会在写redo log的时候先写redo log buffer,之后以 一定的频率刷入到真正的redo log file 中。

redo log buffer刷盘到redo log file的过程并不是真正的刷到磁盘中去,只是刷入到 文件系统缓存(page cache)中去(这是现代操作系统为了提高文件写入效率做的一个优化),真正的写入会交给系统自己来决定(比如page cache足够大了)。那么对于InnoDB来说就存在一个问题,如果交给系统来同步,同样如果系统宕机,那么数据也丢失了(虽然整个系统宕机的概率还是比较小的)。

针对这种情况,InnoDB给出 innodb_flush_log_at_trx_commit 参数,该参数控制 commit提交事务时,如何将 redo log buffer 中的日志刷新到 redo log file 中。它支持三种策略:

  • 设置为0 :表示每次事务提交时不进行刷盘操作。(系统默认master thread每隔1s进行一次重做日志的同步)最快最不安全
  • 设置为1 :表示每次事务提交时都将进行同步,刷盘操作( 默认值最慢 但最安全
  • 设置为2 :表示每次事务提交时都只把 redo log buffer 内容写入 page cache,不进行同步。由os自己决定什么时候同步到磁盘文件。

除了后台线程每秒1次的轮询操作,还有一种情况,当redo log buffer占用的空间即将达到innodb_log_buffer_size(这个参数默认是16M)的一半的时候,后台线程会主动刷盘。

checkpoint

在整个日志文件组中还有两个重要的属性,分别是write poscheckpoint

  • write pos是当前记录的位置,一边写一边后移
  • checkpoint是当前要擦除的位置,也是往后推移

每次刷盘redo log记录到日志文件组中,write pos位置就会后移更新。每次MySQL加载日志文件组恢复数据时,会清空加载过的redo log记录,并把 checkpoint后移更新。write poscheckpoint之间的还空着的部分可以用来写入新的redo log记录。

如果 write pos 追上 checkpoint ,表示日志文件组满了,这时候不能再写入新的 redo log记录,MySQL 得停下来,清空一些记录,把 checkpoint 推进一下。

image-20220328143310242.png

Undo日志

undo log是事务原子性的保证。在事务中 更新数据前置操作 其实是要先写入一个 undo log

undo log 会产生redo log,也就是undo log的产生会伴随着redo log的产生,这是因为undo log也需要持久性的保护

Undo日志的作用

回滚数据

用户对undo日志可能有误解: undo用于将数据库物理地恢复到执行语句或事务之前的样子。但事实并非如此。undo是逻辑日志,因此只是将数据库逻辑地恢复到原来的样子。所有修改都被逻辑地取消了,但是数据结构和页本身在回滚之后可能大不相同。

这是因为在多用户并发系统中,可能会有数十、数百甚至数千个并发事务。数据库的主要任务就是协调对数据记录的并发访问。比如,一个事务在修改当前一个页中某几条记录,同时还有别的事务在对同一个页中另几条记录进行修改。因此,不能将一个页回滚到事务开始的样子,因为这样会影响其他事务正在进行的工作

MVCC

undo的另一个作用是MVCC,即在InnoDB存储引擎中MVCC的实现是通过undo来完成。当用户读取一行记录时,若该记录已经被其他事务占用,当前事务可以通过undo读取之前的行版本信息以此实现非锁定读取

undo的存储结构

InnoDBundo log的管理采用段的方式,也就是 回滚段(rollback segment) 。每个回滚段记录了1024undo log segment ,而在每个undo log segment段中进行 undo页 的申请。

undo页的重用

当我们开启一个事务需要写undo log的时候,就得先去undo log segment中去找到一个空闲的位置,当有空位的时候,就去申请undo页,在这个申请到的undo页中进行undo log的写入。我们知道mysql默认一页的大小是16k。

当事务提交时,并不会立刻删除undo页。因为重用,所以这个undo页可能混杂着其他事务的undo logundo logcommit后,会被放到一个链表中,然后判断undo页的使用空间是否小于3/4,如果小于3/4的话,则表示当前的undo页可以被重用,那么它就不会被回收,其他事务的undo log可以记录在当前undo页的后面。由于undo log是离散的,所以清理对应的磁盘空间时,效率不高。

回滚段与事务

每个事务只会使用一个回滚段(rollback segment),一个回滚段在同一时刻可能会服务于多个事务。

当一个事务开始的时候,会制定一个回滚段,在事务进行的过程中,当数据被修改时,原始的数据会被复制到回滚段

在回滚段中,事务会不断填充盘区,直到事务结束或所有的空间被用完。如果当前的盘区不够用,事务会在段中请求扩展下一个盘区,如果所有已分配的盘区都被用完,事务会覆盖最初的盘区或者在回滚段允许的情况下扩展新的盘区来使用

回滚段存在于undo表空间中,在数据库中可以存在多个undo表空间,但同一时刻只能使用一个undo表空间。

当事务提交时,InnoDB存储引擎会做以下两件事情:

  • undo log放入列表中,以供之后的purge操作
  • 判断undo log所在的页是否可以重用(低于3/4可以重用),若可以分配给下个事务使用
回滚段中的数据分类
  1. 未提交的回滚数据(uncommitted undo information)

    该数据所关联的事务并未提交,用于实现读一致性,所以该数据不能被其他事务的数据覆盖。

  2. 已经提交但未过期的回滚数据(committed undo information)

    该数据关联的事务已经提交,但是仍受到undo retention参数的保持时间的影响。

  3. 事务已经提交并过期的数据(expired undo information)

    事务已经提交,而且数据保存时间已经超过undo retention参数指定的时间,属于已经过期的数据。当回滚段满了之后,会优先覆盖"事务已经提交并过期的数据"。

事务提交后并不能马上删除undo logundo log所在的页。这是因为可能还有其他事务需要通过undo log来得到行记录之前的版本。故事务提交时将undo log放入一个链表中,是否可以最终删除undo logundo log所在页由purge线程来判断。

undo的类型
  • insert undo log

insert undo log是指在insert操作中产生的undo log。因为insert操作的记录,只对事务本身可见,对其他事务不可见(这是事务隔离性的要求),故该undo log可以在事务提交后直接删除。不需要进行purge操作。

  • update undo log

update undo log记录的是对deleteupdate操作产生的undo log。该undo log可能需要提供MVCC机制,因此不能在事务提交时就进行删除。提交时放入undo log链表,等待purge线程进行最后的删除。

undo log的生命周期

以下是undo+redo事务的简化过程

假设有2个数值,分别为A=1和B=2,然后将A修改为3,B修改为4

1. start transaction;
2.记录A=1到undo log;
3. update A = 3;
4.记录A=3 到redo log;
5.记录B=2到undo loq;
6. update B = 4;
7.记录B = 4到redo log;
8.将redo log刷新到磁盘;
9. commit
复制代码
  • 在1-8步骤的任意一步系统宕机,事务未提交,该事务就不会对磁盘上的数据做任何影响。

  • 如果在8-9之间宕机。

    • redo log 进行恢复
    • undo log 发现有事务没完成进行回滚。
  • 若在9之后系统宕机,内存映射中变更的数据还来不及刷回磁盘,那么系统恢复之后,可以根据redo log把数据刷回磁盘。

对于InnoDB引擎来说,每个行记录除了记录本身的数据之外,还有几个隐藏的列:

  • DB_ROW_ID: 如果没有为表显式的定义主键,并且表中也没有定义唯一索引,那么InnoDB会自动为表添加一个row_id的隐藏列作为主键。

  • DB_TRX_ID︰每个事务都会分配一个事务ID,当对某条记录发生变更时,就会将这个事务的事务ID写入trx_id中。

    疑问, 就一个字段,如果有两个事务怎么办。两个事务会不会有锁呢?

  • DB_ROLL_PTR:回滚指针,本质上就是指向undo log的指针。

image-20220328144018899.png

插入的数据都会生成一条insert undo log,并且数据的回滚指针会指向它。undo log会记录undo log的序号、插入主键的列和值...,那么在进行rollback的时候,通过主键直接把对应的数据删除即可。

当我们执行UPDATE时:

对于更新的操作会产生update undo log,并且会分更新主键的和不更新主键的.

purge线程两个主要作用是:清理undo页清除page里面带有Delete_Bit标识的数据行。在InnoDB中,事分中的Delete操作实际上并不是真正的删除掉数据行,而是一种Delete Mark操作,在记录上标识Delete_Bit,而不删除记录。是一种"假删除"只是做了个标记,真正的删除工作需要后台purge线程去完成。

并发事务带来的问题

在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题。

  • 脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
  • 丢失修改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数/7据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务 1 读取某表中的数据 A=20,事务 2 也读取 A=20,事务 1 修改 A=A-1,事务 2 也修改 A=A-1,最终结果 A=19,事务 1 的修改被丢失。
  • 不可重复读(Unrepeatable read): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
  • 幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

不可重复读和幻读有什么区别呢?

  • 不可重复读的重点是内容修改或者记录减少比如多次读取一条记录发现其中某些记录的值被修改;
  • 幻读的重点在于记录新增比如多次执行同一条查询语句(DQL)时,发现查到的记录增加了。

幻读其实可以看作是不可重复读的一种特殊情况,单独把区分幻读的原因主要是解决幻读和不可重复读的方案不一样。

举个例子:执行 deleteupdate 操作的时候,可以直接对记录加锁,保证事务安全。而执行 insert 操作的时候,由于记录锁(Record Lock)只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁(Gap Lock)。也就是说执行 insert 操作的时候需要依赖 Next-Key LockRecord Lock+Gap Lock) 进行加锁来保证不出现幻读。

怎么解决脏读、不可重复读、幻读这些问题呢?

  • 方案一:读操作利用多版本并发控制(MVCC),写操作进行加锁。(读-写操作彼此并不冲突,性能更高。)

所谓的MVCC,就是生成一个ReadView,通过ReadView找到符合条件的记录版本(历史版本由undo日志构建)。查询语句只能到在生成ReadView之前已提交事务所做的更改,在生成ReadView之前未提交的事务或者之后才开启的事务所做的更改是看不到的。而写操作肯定针对的是最新版本的记录,读记录的历史版本和改动记录的最新版本本身并不冲突,也就是采用MVCC时,读-写操作并不冲突。

普通的SELECT语句在READ COMMITTEDREPEATABLE READ隔离级别下会使用到MVCC读取记录。

  • READ COMMITTED隔离级别下,一个事务在执行过程中每次执行SELECT操作时都会生成一个ReadViewReadView的存在本身就保证了事务不可以读取到未提交的事务所做的更改,也就是避免了脏读现象;
  • REPEATABLE READ隔离级别下,一个事务在执行过程中只有第一次执行SELECT操作才会生成一个ReadView,之后的SELECT操作都复用这个ReadView,这样也就避免了不可重复读和幻读的问题。
  • 方案二:读、写操作都采用加锁的方式。(读-写操作彼此需要排队执行,影响性能。)

脏读的产生是因为当前事务读取了另一个未提交事务写的一条记录,如果另一个事务在写记录的时候就给这条记录加锁,那么当前事务就无法继续读取该记录了,所以也就不会有脏读问题的产生了。

不可重复读的产生是因为当前事务先读取一条记录,另外一个事务对该记录做了改动之后并提交之后,当前事务再次读取时会获得不同的值,如果在当前事务读取记录时就给该记录加锁那么另一个事务就无法修改该记录,自然也不会发生不可重复读了。

幻读问题的产生是因为当前事务读取了一个范围的记录,然后另外的事务向该范围内插入了新记录,当前事务再次读取该范围的记录时发现了新插入的新记录。采用加锁的方式解决幻读问题就有一些麻烦,因为当前事务在第一次读取记录时幻影记录并不存在,所以读取的时候加锁就有点尴尬(因为你并不知道给谁加锁)。

事务的状态

活动的(active)

事务对应的数据库操作正在执行过程中时,我们就说该事务处在 活动的 状态。

部分提交的(partially committed)

当事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并 没有刷新到磁盘时,我们就说该事务处在 部分提交的 状态。

失败的(failed)

当事务处在 活动的 或者 部分提交的 状态时,可能遇到了某些错误(数据库自身的错误、操作系统错误或者直接断电等)而无法继续执行,或者人为的停止当前事务的执行,我们就说该事务处在 失败的 状态。

中止的(aborted)

如果事务执行了一部分而变为 失败的 状态,那么就需要把已经修改的事务中的操作还原到事务执行前的状态。换句话说,就是要撤销失败事务对当前数据库造成的影响。我们把这个撤销的过程称之为 回滚 。当 回滚 操作执行完毕时,也就是数据库恢复到了执行事务之前的状态,我们就说该事务处在了 中止的 状态。

提交的(committed)

当一个处在部分提交的状态的事务将修改过的数据都 同步到磁盘 上之后,我们就可以说该事务处在了 提交的 状态。

image-20220328105721251.png

SQL 标准定义了哪些事务隔离级别?

SQL 标准定义了四个隔离级别:

  • READ-UNCOMMITTED(读取未提交) : 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
  • READ-COMMITTED(读取已提交) : 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
  • REPEATABLE-READ(可重复读) : 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • SERIALIZABLE(可串行化) : 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。

隔离级别脏读不可重复读幻读
READ-UNCOMMITTED
READ-COMMITTED×
REPEATABLE-READ××
SERIALIZABLE×××

MySQL 的隔离级别是基于锁实现的吗?

MySQL 的隔离级别基于锁和 MVCC 机制共同实现的

SERIALIZABLE 隔离级别,是通过锁来实现的。除了 SERIALIZABLE 隔离级别,其他的隔离级别都是基于 MVCC 实现。

不过, SERIALIZABLE 之外的其他隔离级别可能也需要用到锁机制,就比如 REPEATABLE-READ当前读情况下需要使用加锁读来保证不会出现幻读

MySQL 的默认隔离级别是什么?

MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读) 。我们可以通过SELECT @@tx_isolation;命令来查看,MySQL 8.0 该命令改为SELECT @@transaction_isolation;

MySQL 锁

image-20220328182847407.png

表级锁和行级锁了解吗?有什么区别?

MyISAM 仅仅支持表级锁(table-level locking),一锁就锁整张表,这在并发写的情况下性非常差。

InnoDB 不光支持表级锁(table-level locking),还支持行级锁(row-level locking),默认为行级锁。行级锁的粒度更小,仅对相关的记录上锁即可(对一行或者多行记录加锁),所以对于并发写入操作来说, InnoDB 的性能更高。

表级锁和行级锁对比

  • 表级锁: MySQL 中锁定粒度最大的一种锁,是针对非索引字段加的锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。其锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM 和 InnoDB 引擎都支持表级锁。

    • 自增锁(AUTO-INC锁)AUTO-INC锁是当向使用含有AUTO_INCREMENT列的表中插入数据时需要获取的一种特殊的表级锁当我们向一个有AUTO_INCREMENT关键字的主键插入值的时候,每条语句都要对这个表锁进行竞争
    • 元数据锁(MDL锁)保证读写的正确性
  • 行级锁: MySQL锁定粒度最小的一种锁,是针对索引字段加的锁,只针对当前操作的行记录进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁

  • 记录锁(Record Locks)

  • 间隙锁(Gap Locks) 但是在使用加锁方案解决时有个大问题,就是事务在第一次执行读取操作时,那些幻影记录尚不存在,我们无法给这些幻影记录加上记录锁。InnoDB提出了一种称之为Gap Locks的锁,官方的类型名称为:LOCK_GAP,我们可以简称为gap锁

  • 临键锁(Next-Key Locks) 锁住某条记录,又想阻止其他事务在该记录前边的间隙插入新记录,所以InnoDB就提出了一种称之为Next-Key Locks的锁,官方的类型名称为:LOCK_ORDINARY,我们也可以简称为next-key锁。Next-Key Locks是在存储引擎innodb、事务级别在可重复读的情况下使用的数据库锁,innodb默认的锁就是Next-Key locks。

  • 插入意向锁(Insert Intention Locks) 一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了gap锁next-key锁也包含gap锁),如果有的话,插入操作需要等待,直到拥有gap锁的那个事务提交。但是 InnoDB规定事务在等待的时候也需要在内存中生成一个锁结构 ,表明有事务想在某个间隙插入新记录,但是现在在等待。InnoDB就把这种类型的锁命名为Insert Intention Locks,官方的类型名称为:LOCK_INSERT_INTENTION,我们称为插入意向锁。插入意向锁是一种Gap锁,不是意向锁,在insert操作时产生。

    插入意向锁是在插入一条记录行前,由 INSERT 操作产生的一种间隙锁。

页锁

页锁就是在页的粒度上进行锁定,锁定的数据资源比行锁要多,因为一个页中可以有多个行记录。当我们使用页锁的时候,会出现数据浪费的现象,但这样的浪费最多也就是一个页上的数据行。 页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般。

多版本并发控制(Multiversion Concurrency Control)

MVCC 是通过数据行的多个版本管理来实现数据库的并发控制。这项技术使得在InnoDB的事务隔离级别下执行一致性读操作有了保证。换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值,这样在做查询的时候就不用等待另一个事务释放锁。

隐藏字段、Undo Log版本链

回顾一下undo日志的版本链,对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列(字段)。

  • trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id赋值给trx_id隐藏列。
  • roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

insert undo只在事务回滚时起作用,当事务提交后,该类型的undo日志就没用了,它占用的UndoLog Segment也会被系统回收(也就是该undo日志占用的Undo页面链表要么被重用,要么被释放)。

MVCC 的实现依赖于: 隐藏字段、Undo Log、Read View

在MVCC机制中,多个事务对同一个行记录进行更新会产生多个历史快照,这些历史快照保存在Undo Log里。如果一个事务想要查询这个行记录,需要读取哪个版本的行记录呢?这时就需要用到ReadView了,它帮我们解决了行的可见性问题

ReadView就是事务在使用MVCC机制进行快照读操作时产生的读视图。当事务启动时,会生成数据库系统当前的一个快照,InnoDB每个事务构造了一个数组,用来记录并维护系统当前活跃事务的ID(“活跃"指的就是,启动了但还没提交)

使用READ UNCOMMITTED隔离级别的事务,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了。

使用SERIALIZABLE隔离级别的事务,InnoDB规定使用加锁的方式来访问记录。

使用READ COMMITTEDREPEATABLE READ隔离级别的事务,都必须保证读到已经提交了的事务修改过的记录。假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是需要判断一下版本链中的哪个版本是当前事务可见的,这是ReadView要解决的主要问题。

ReadView中主要包含 4 个比较重要的内容

creator_trx_id,创建这个 Read View 的事务 ID。

说明:只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务id,否则在一个只读事务中的事务id值都默认为 0 。

trx_ids,表示在生成ReadView时当前系统中活跃的读写事务的事务id列表

up_limit_id,活跃的事务中最小的事务 ID

low_limit_id,表示生成ReadView时系统中应该分配给下一个事务的id值。low_limit_id 是系统最大的事务id值,这里要注意是系统中的事务id,需要区别于正在活跃的事务ID。

ReadView的规则

有了这个ReadView,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见。

  • 如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问

  • 如果被访问版本的trx_id属性值小于ReadView中的up_limit_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问

  • 如果被访问版本的trx_id属性值大于或等于ReadView中的low_limit_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问

  • 如果被访问版本的trx_id属性值在ReadViewup_limit_idlow_limit_id之间,那就需要判断一下trx_id属性值是不是在trx_ids列表中。

    • 如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问。
    • 如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。

行级锁的使用有什么注意事项?

InnoDB 的行锁是针对索引字段加的锁,表级锁是针对非索引字段加的锁。当我们执行 UPDATEDELETE 语句时,如果 WHERE条件中字段没有命中唯一索引或者索引失效的话,就会导致扫描全表对表中的所有行记录进行加锁。这个在我们日常工作开发中经常会遇到,一定要多多注意!!!

不过,很多时候即使用了索引也有可能会走全表扫描,这是因为 MySQL 优化器的原因。

共享锁和排他锁呢?

  • LOCK TABLES t READ:InnoDB存储引擎会对表t加表级别的S锁。
  • LOCK TABLES t WRITE:InnoDB存储引擎会对表t加表级别的X锁。

不论是表级锁还是行级锁,都存在共享锁(Share Lock,S 锁)和排他锁(Exclusive Lock,X 锁)这两类:

  • 共享锁(S 锁) :又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
  • 排他锁(X 锁) :又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条事务加任何类型的锁(锁不兼容)。

排他锁与任何的锁都不兼容,共享锁仅和共享锁兼容。

S 锁X 锁
S 锁不冲突冲突
X 锁冲突冲突

由于 MVCC 的存在,对于一般的 SELECT 语句,InnoDB 不会加任何锁。不过, 你可以通过以下语句显式加共享锁或排他锁。

# 共享锁
SELECT ... LOCK IN SHARE MODE;
# 排他锁
SELECT ... FOR UPDATE;
1. 锁定读

在普通的SELECT语句后边加LOCK IN SHARE MODE

在普通的SELECT语句后边加FOR UPDATE

2.写操作

DELETE: 对一条记录做DELETE操作的过程其实是先在B+树中定位到这条记录的位置,然后获取这条记录的X锁,再执行delete mark.操作。我们也可以把这个定位待删除记录在B+树中位置的过程看成是一个获取X锁锁定读

UPDATE∶在对一条记录做UPDATE操作时分为三种情况:

  • 情况1: 未修改该记录的键值,并且被更新的列占用的存储空间在修改前后未发生变化

    则先在B+树中定位到这条记录的位置,然后再获取一下记录的X锁,最后在原记录的位置进行修改操作。我们也可以把这个定位待修改记录在B+树中位置的过程看成是一个获取X锁锁定读

  • 情况2∶未修改该记录的键值,并且至少有一个被更新的列占用的存储空间在修改前后发生变化。

    则先在B+树中定位到这条记录的位置,然后获取一下记录的X锁,将该记录彻底删除掉(就是把记录彻底移入垃圾链表),最后再插入一条新记录。这个定位待修改记录在B+树中位置的过程看成是一个获取×锁锁定读,新插入的记录由INSERT操作提供的隐式锁进行保护。

  • 情况3∶修改了该记录的键值,则相当于在原记录上做DELETE操作之后再来一次INSERT操作,加锁操作就需要按照DELETEINSERT的规则进行了。

INSERT :

一般情况下,新插入一条记录的操作并不加锁,通过一种称之为隐式锁的结构来保护这条新插入的记录在本事务提交前不被别的事务访问

意向锁有什么作用?

如果需要用到表锁的话,如何判断表中的记录没有行锁呢?一行一行遍历肯定是不行,性能太差。我们需要用到一个叫做意向锁的东东来快速判断是否可以对某个表使用表锁

意向锁是表级锁,共有两种:

  • 意向共享锁(Intention Shared Lock,IS 锁) :事务有意向对表中的某些加共享锁(S 锁),加共享锁前必须先取得该表的 IS 锁。
  • 意向排他锁(Intention Exclusive Lock,IX 锁) :事务有意向对表中的某些记录加排他锁(X 锁),加排他锁之前必须先取得该表的 IX 锁。

意向锁是有数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享 / 排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。

1、意向锁的存在是为了协调行锁和表锁的关系,支持多粒度(表锁与行锁)的锁并存。

2、意向锁是一种不与行级锁冲突表级锁

3、表明“某个事务正在某些行持有了锁或该事务准备去持有锁

现在有两个事务,分别是T1和T2,其中T2试图在该表级别上应用共享或排它锁,如果没有意向锁存在,那么T2就需要去检查各个页或行是否存在锁;如果存在意向锁,那么此时就会受到由T1控制的表级别意向锁的阻塞。T2在锁定该表前不必检查各个页或行锁,而只需检查表上的意向锁

如果我们给某一行数据加上了排它锁,数据库会自动给更大一级的空间,比如数据页或数据表加上意向锁,告诉其他人这个数据页或数据表已经有人上过排它锁了

意向锁在保证并发性的前提下,实现了行锁和表锁共存满足事务隔离性的要求。

意向锁之间是互相兼容的。

IS 锁IX 锁
IS 锁兼容兼容
IX 锁兼容兼容

意向锁和共享锁和排它锁互斥(这里指的是表级别的共享锁和排他锁,意向锁不会与行级的共享锁和排他锁互斥)。

IS 锁IX 锁
S 锁兼容互斥
X 锁互斥互斥

InnoDB 有哪几类行锁?

MySQL InnoDB 支持三种行锁定方式:

  • 记录锁(Record Lock) :也被称为记录锁,属于单个行记录上的锁。
  • 间隙锁(Gap Lock) :锁定一个范围,不包括记录本身。
  • 临键锁(Next-key Lock) :Record Lock+Gap Lock,锁定一个范围,包含记录本身。记录锁只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁。

InnoDB 的默认隔离级别 RR(可重读)是可以解决幻读问题发生的,主要有下面两种情况:

  • 快照读(一致性非锁定读) :由 MVCC 机制来保证不出现幻读。
  • 当前读 (一致性锁定读): 使用 Next-Key Lock 进行加锁来保证不出现幻读。

当前读和快照读有什么区别?

快照读一致性非锁定读)就是单纯的 SELECT 语句,但不包括下面这两类 SELECT 语句:

SELECT ... FOR UPDATE
SELECT ... LOCK IN SHARE MODE

快照即记录的历史版本,每行记录可能存在多个历史版本(多版本技术)。

快照读的情况下,如果读取的记录正在执行 UPDATE/DELETE 操作,读取操作不会因此去等待记录上 X 锁的释放,而是会去读取行的一个快照

只有在事务隔离级别 RC(读取已提交) 和 RR(可重读)下,InnoDB 才会使用一致性非锁定读:

  • RC 级别下,对于快照数据,一致性非锁定读总是读取被锁定行的最新一份快照数据
  • RR 级别下,对于快照数据,一致性非锁定读总是读取本事务开始时的行数据版本

快照读比较适合对于数据一致性要求不是特别高且追求极致性能的业务场景。

当前读 (一致性锁定读)就是给行记录加 X 锁或 S 锁。

当前读的一些常见 SQL 语句类型如下:

# 对读的记录加一个X锁
SELECT...FOR UPDATE
# 对读的记录加一个S锁
SELECT...LOCK IN SHARE MODE
# 对修改的记录加一个X锁
INSERT...
UPDATE...
DELETE...

如果采用MVCC方式的话,只能解决一致性非锁定读(也称之为快照读)的幻读问题,读-写操作彼此并不冲突,并发性能更高;

如果采用加锁方式的话,可以解决当前读的幻读情况,读-写操作彼此需要排队执行,影响性能;

通过MVCC我们可以解决:

  1. 读写之间阻塞的问题。通过MVCC可以让读写互相不阻塞,即读不阻塞写,写不阻塞读,这样就可以提升事 务并发处理能力。
  2. 降低了死锁的概率。这是因为MVCC采用了乐观锁的方式,读取数据时并不需要加锁,对于写操作,也只锁 定必要的行。
  3. 解决快照读的问题。当我们查询数据库在某个时间点的快照时,只能看到这个时间点之前事务提交更新的结 果,而不能看到这个时间点之后事务提交的更新结果。

MySQL 性能优化

性能分析工具的使用

分析查询语句:EXPLAIN

定位了查询慢的SQL之后,我们就可以使用EXPLAIN或DESCRIBE工具做针对性的分析查询语句。

能做什么?
  • 表的读取顺序
  • 数据读取操作的操作类型。
  • 哪些索引可以使用
  • 哪些索引被实际使用
  • 表之间的引用
  • 每张表有多少行被优化器查询

数据库

1、数据增删改不频繁的情况下,可以使用查询缓存,并设置较大的 query_cache_size

2、配置全局 InnoDB 引擎缓存大小,即适当增大 innodb_buffer_pool_size

3、若数据库单个事务很大,或是写入非常频繁,可以适当调大 binlog 缓存大小,binlog_cache_size

4、InnoDB 默认每秒进行一次 redo-log 缓存写入磁盘,若相信自己的 MySQL 服务器,可以设置为当 redo-log 缓存写满时再写入磁盘,并调大 innodb_log_buffer_size 大小

......

核心思想:MySQL 是 I/O 密集型应用,如何减少 I/O 次数是 MySQL 数据库优化的核心

索引

1、经常需要进行前导 like 模糊匹配的,不建索引

2、经常使用负向条件的,如 !=,不建索引

3、使用时,遵守最左前缀法则

4、字段过长的,可以用 prefix 作前缀索引

5、条件查询中 or 字段的条件没有使用索引时,其他索引也会失效

6、查询时发生运算,也会索引失效,最典型的字符串匹配,字符串值不加 ' ',MySQL 会 进行类型转换,相当于一次运算

7、利用覆盖索引,可以从索引中直接获取查询的字段,减少回表查询

......

数据类型

○ 1、尽量不要用 DOUBLE 存储小数,有内存和精度问题,可以使用 INT 按固定倍率进行存储

○ 2、尽量不要用 BLOB 数据存放图片等大型文件

○ 3、可以用 ENUM 枚举类型来存储状态值

○ ......

表结构

○ 1、适当添加冗余字段,减少多表查询

○ 2、大表拆分

■ 竖直拆分

● 垂直分表:将一张表的字段拆开分配到多张表中,一般将常用字段放在一个表中,将不常用字段放在另一张表中

● 垂直分库:将一张表中的字段按照业务分配到不同的表中,适合业务耦合度非常低的场景

● 优点:垂直分表可以使查询热点字段的效率更高,垂直分库可以使表的结构和逻辑清晰,便于维护

● 缺点

○ 1、垂直分库分表并没有减少数据数量

○ 2、垂直分库分表需要在服务器代码中对 SQL 语句进行大量修改,并且不可避免的带来了多表查询

○ 3、对于主键和一些所有业务通用的字段,垂直分库分表必然导致这些字段的冗余,从而消耗存储空间

■ 水平拆分

● 水平分表:在同一个数据库中,把一张表的数据分到多张表进行存储,一般按照主键 ID 进行划分

● 水平分库:将一张表的数据分到多个数据库中的多张表进行存储,类似于 Redis 分片

● 优点:水平分库分表解决了单表单库数据存储量上限的问题

● 缺点

○ 1、对于一些范围查找,会涉及到跨节点的 JOIN 连接,性能会非常差——可以在进行两次查询,在服务端代码实现数据的聚合

○ 2、事务的一致性会变差——可以将一个跨节点的事务分成多个小的单节点事务,或是使用分布式事务

○ 3、进行 MySQL 数据库扩容难度大,不易于维护

Explain语句解析 SQL 语句 (性能从高到底)

1、ID —— 表示将 SQL 语句划分为多个操作后,每个操作的顺序

2、select_type —— 表示 SQL 语句查询类型

○ Simple:单表查询,如果是多表联查就是多次 Simple

○ Primary & Subquery:表示嵌套查询中的外查询,以及用作 Where 子句中查询条件的子查询

○ derived:表示嵌套查询的子查询用作 From 子句中表的子查询

○ Union Select/Union Result:前者表示 Union 子句后的并列查询,后者表示将两者拼合的操作,效率最低

3、type —— 表示查询过程

○ null:没有进行查询,如 now() 函数获取当前时间

○ system:查询的表中只有一行数据

○ const:通过主键索引进行精确匹配查找

○ eq_ref:通过唯一索引进行精确匹配查找

○ ref:通过索引进行精确匹配查找,结果可能存在多条

○ range:通过索引进行范围查找,一般 sql 优化到这个等级即可

○ Index:遍历索引

○ all:遍历了表

4、possible keys/key:表示可以使用的索引以及执行时真正使用的索引,可以通过 Select Using/Ignore/Force Index() 推荐/忽视/强制使用某个索引

5、Extra:一些额外信息

○ Using Index/Using Index Condition:前者表示覆盖索引,没有进行回表, 后者表示只是使用了索引但没有覆盖,进行回表查询

○ Using Filesort:表示没有使用索引的 Order By 排序

○ Using Temporary:表示将查询结果作为中间表进行了排序,如 Order By/Group By 等

MySQL高性能优化规范

谨慎使用 MySQL 分区表

分区表在物理上表现为多个文件,在逻辑上表现为一个表;

谨慎选择分区键,跨分区查询效率可能更低;

建议采用物理分表的方式管理大数据

经常一起使用的列放到一个表中

避免更多的关联操作。

禁止在表中建立预留字段

  • 预留字段的命名很难做到见名识义。
  • 预留字段无法确认存储的数据类型,所以无法选择合适的类型。
  • 对预留字段类型的修改,会对表进行锁定。

禁止在数据库中存储文件(比如图片)这类大的二进制数据

在数据库中存储文件会严重影响数据库性能,消耗过多存储空间。

文件(比如图片)这类大的二进制数据通常存储于文件服务器,数据库只存储文件地址信息。

禁止在线上做数据库压力测试

禁止从开发环境,测试环境直接连接生产环境数据库

索引设计规范

限制每张表上的索引数量,建议单张表索引不超过 5 个

索引并不是越多越好!索引可以提高效率同样可以降低效率。

索引可以增加查询效率,但同样也会降低插入和更新的效率,甚至有些情况下会降低查询效率。

因为 MySQL 优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划,如果同时有很多个索引都可以用于查询,就会增加 MySQL 优化器生成执行计划的时间,同样会降低查询性能。

禁止使用全文索引

全文索引不适用于 OLTP 场景。

禁止给表中的每一列都建立单独的索引

5.6 版本之前,一个 sql 只能使用到一个表中的一个索引,5.6 以后,虽然有了合并索引的优化方式,但是还是远远没有使用一个联合索引的查询方式好。

如何选择索引列的顺序

建立索引的目的是:希望通过索引进行数据查找,减少随机 IO,增加查询性能 ,索引能过滤出越少的数据,则从磁盘中读入的数据也就越少。

  • 区分度最高的放在联合索引的最左侧(区分度=列中不同值的数量/列的总行数)
  • 尽量把字段长度小的列放在联合索引的最左侧(因为字段长度越小,一页能存储的数据量越大,IO 性能也就越好)
  • 使用最频繁的列放到联合索引的左侧(这样可以比较少的建立一些索引)

避免建立冗余索引和重复索引(增加了查询优化器生成执行计划的时间)

  • 重复索引示例:primary key(id)、index(id)、unique index(id)
  • 冗余索引示例:index(a,b,c)、index(a,b)、index(a)

对于频繁的查询优先考虑使用覆盖索引

覆盖索引:就是包含了所有查询字段 (where,select,order by,group by 包含的字段) 的索引

覆盖索引的好处:

  • 避免 InnoDB 表进行索引的二次查询: InnoDB 是以聚集索引的顺序来存储的,对于 InnoDB 来说,二级索引在叶子节点中所保存的是行的主键信息,如果是用二级索引查询数据的话,在查找到相应的键值后,还要通过主键进行二次查询才能获取我们真实所需要的数据。而在覆盖索引中,二级索引的键值中可以获取所有的数据,避免了对主键的二次查询 ,减少了 IO 操作,提升了查询效率。
  • 可以把随机 IO 变成顺序 IO 加快查询效率: 由于覆盖索引是按键值的顺序存储的,对于 IO 密集型的范围查找来说,对比随机从磁盘读取每一行的数据 IO 要少的多,因此利用覆盖索引在访问时也可以把磁盘的随机读取的 IO 转变成索引查找的顺序 IO。