MySQL 学习笔记②


若文章内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系博主删除。



  • 推荐观看黑马官方提供的 PDF 文档,内容非常详细。官方提供的 MD 文件 ,内容十分精简。
  • 我自己的这个博客只是写给自己看看,加深印象的。

写这篇博客旨在制作笔记,方便个人在线阅览,巩固知识。

博客的内容主要来自视频内容和资料中提供的学习笔记。


推荐阅读博客


0.总目录



本博客内容概述:存储引擎索引SQL 优化视图 / 存储过程 / 触发器InnoDB引擎MySQL管理

  • 存储引擎MySQL体系结构,存储引擎的简介、特点、选择
  • 索引:索引的概述、结构、分类、语法,SQL 性能分析,索引的使用和设计原则
  • SQL优化:插入数据、主键优化、order bygroup bylimitcount 优化、update 优化
  • 视图存储过程存储函数触发器
  • :全局锁、表级锁、行级锁
  • InnoDB:逻辑存储结构、架构、事务原理、MVCC
  • MySQL 管理:系统数据库、常用工具

1.存储引擎


1.1.MySQL体系结构


在这里插入图片描述


  1. 连接层

最上层是一些客户端和链接服务,包含本地 sock 通信和大多数基于客户端/服务端工具实现的类似于 TCP/IP 的通信。

主要完成一些类似于连接处理、授权认证、及相关的安全方案。

在该层上引入了线程池的概念,为通过认证安全接入的客户端提供线程。

同样在该层上可以实现基于 SSL 的安全链接。

服务器也会为安全接入的每个客户端验证它所具有的操作权限。


  1. 服务层

第二层架构主要完成大多数核心服务功能,如 SQL 接口,并完成缓存的查询,SQL 的分析和优化,部分内置函数的执行。

所有跨存储引擎的功能也在这一层实现,如 过程、函数等。

在该层,服务器会解析查询并创建相应的内部解析树,并对其完成相应的优化如确定表的查询的顺序,是否利用索引等,最后生成相应的执行操作。

如果是 select 语句,服务器还会查询内部的缓存,如果缓存空间足够大,这样在解决大量读操作的环境中能够很好的提升系统的性能。


  1. 引擎层

存储引擎层, 存储引擎真正的负责了 MySQL 中数据的存储和提取,服务器通过 API 和存储引擎进行通信。

不同的存储引擎具有不同的功能,这样我们可以根据自己的需要,来选取合适的存储引擎。

数据库中的索引是在存储引擎层实现的。


  1. 存储层

数据存储层, 主要是将数据(如:redologundolog、数据、索引、二进制日志、错误日志、查询日志、慢查询日志等)存储在文件系统之上,并完成与存储引擎的交互。


和其他数据库相比,MySQL 有点与众不同,它的架构可以在多种不同场景中应用并发挥良好作用。

主要体现在存储引擎上,插件式的存储引擎架构,将查询处理和其他的系统任务以及数据的存储提取分离。

这种架构可以根据业务的需求和实际需要选择合适的存储引擎。


1.2.存储引擎简介


存储引擎是 MySQL 数据库的核心,我们也需要在合适的场景选择合适的存储引擎。

存储引擎就是存储数据、建立索引、更新 / 查询数据 等技术的实现方式 。

存储引擎是基于表的,而不是基于库的,所以存储引擎也可被称为表类型。

我们可以在创建表的时候,来指定选择的存储引擎,如果没有指定将自动选择默认的存储引擎。


  1. 建表时指定存储引擎
CREATE TABLE 表名(
	字段1 字段1类型 [ COMMENT 字段1注释 ] ,
	......
	字段n 字段n类型 [COMMENT 字段n注释 ]
) ENGINE = INNODB [ COMMENT 表注释 ] ;

  1. 查询当前数据库支持的引擎
SHOW ENGINES;

  1. 查询建表语句

这里使用该指令的目的是查看建表时设置(未设置时则系统默认)的存储引擎

SHOW CREATE TABLE account;

在这里插入图片描述
可以看到,创建表时,即使我们没有指定存储引擎,数据库也会自动选择默认的存储引擎。

  • MySQL 5.5.5 版本之前,MyISAMMySQL 的默认存储引擎。
  • MySQL 5.5.5 版本之后,InnoDBMySQL 的默认存储引擎。

  1. 查询当前数据库支持的存储引擎
SHOW ENGINES;

在这里插入图片描述


示例

  1. 创建表my_myisam,并指定 MyISAM 存储引擎
  2. 创建表my_memory,指定 Memory 存储引擎
CREATE TABLE my_myisam(
	id INT,
	NAME VARCHAR(10)
) ENGINE = MYISAM ;
CREATE TABLE my_memory(
	id INT,
	NAME VARCHAR(10)
) ENGINE = MEMORY ;

1.3.InnoDB


1.3.1.简单介绍


  • 介绍

InnoDB 是一种兼顾高可靠性和高性能的通用存储引擎。

MySQL 5.5 之后,InnoDB 是默认的 MySQL 存储引擎。


  • 特点

DML 操作遵循 ACID 模型,支持事务;

行级锁,提高并发访问性能;

支持外键 FOREIGN KEY 约束,保证数据的完整性和正确性。


1.3.2.ibd文件


xxx.ibd:xxx 代表的是表名

innoDB 引擎的每张表都会对应这样一个表空间文件,存储该表的表结构(frm-早期的 、sdi-新版的)、数据和索引。

参数:innodb_file_per_table

SHOW VARIABLES LIKE 'innodb_file_per_table';

在这里插入图片描述
如果该参数开启,代表对于 InnoDB 引擎的表,每一张表都对应一个 ibd 文件。

我们直接打开 MySQL 的数据存放目录: C:\ProgramData\MySQL\MySQL Server 8.0\Data

这个目录下有很多文件夹,不同的文件夹代表不同的数据库,我们直接打开 itcast 文件夹。

可以看到里面有很多的 ibd 文件,每一个ibd文件就对应一张表。

比如:我们有一张表 account,就有这样的一个 account.ibd 文件。

在这个 ibd 文件中不仅存放表结构、数据,还会存放该表对应的索引信息。

该文件是基于二进制存储的,不能直接基于记事本打开。

我们可以使用 MySQL 提供的一个指令 ibd2sdi 来从 ibd 文件中提取 sdi 信息。

ibd2sdi 表.sdi

sdi 数据字典信息中就包含该表的表结构,sdi 文件是可以用记事本工具打开的。

在这里插入图片描述


1.3.3.InnoDB逻辑存储结构


在这里插入图片描述


  • 表空间InnoDB存储引擎逻辑结构的最高层,ibd文件其实就是表空间文件,在表空间中可以包含多个Segment段。
  • :表空间是由各个段组成的, 常见的段有数据段、索引段、回滚段等。
    InnoDB中对于段的管理,都是引擎自身完成,不需要人为对其控制,一个段中包含多个区。
  • :区是表空间的单元结构,每个区的大小为 1M。
    默认情况下,InnoDB存储引擎页大小为 16K, 即一个区中一共有 64 个连续的页。
  • :页是组成区的最小单元,页也是InnoDB存储引擎磁盘管理的最小单元,每个页的大小默认为 16KB。
    为了保证页的连续性,InnoDB存储引擎每次从磁盘申请 4-5 个区。
  • InnoDB存储引擎是面向行的,也就是说数据是按行进行存放的。
    在每一行中除了定义表时所指定的字段以外,还包含两个隐藏字段。

1.4.MyISAM


  • 介绍MySQL 5.5.5 版本之前,MyISAMMySQL 的默认存储引擎。

  • 特点
    • 不支持事务,不支持外键
    • 支持表锁,不支持行锁
    • 访问速度快

  • 文件
    • xxx.sdi:存储表结构信息
    • xxx.MYD:存储数据
    • xxx.MYI:存储索引

这里通过工具 VSCode 打开的 sdi 文件(并且格式化了)

在这里插入图片描述


1.5.Memory


  • 介绍

Memory 擎的表数据时存储在内存中的,由于受到硬件问题、或断电问题的影响,只能将这些表作为临时表或缓存使用。


  • 特点

内存存放

hash 索引(默认)


  • 文件

xxx.sdi:存储表结构信息


1.6.三种引擎的对比


特点InnoDBMyISAMMemory
存储限制64 TB
事务安全支持--
锁机制行锁表锁表锁
B+tree 索引支持支持支持
Hash 索引--支持
全文索引支持(5.6 版本之后)支持-
空间使用N/A
内存使用中等
批量插入速度
支持外键支持--

面试题InnoDB 引擎与 MyISAM 引擎之间的区别

  1. InnoDB 引擎,支持事务;而 MyISAM 不支持。
  2. InnoDB 引擎,支持行锁和表锁;而 MyISAM 仅支持表锁, 不支持行锁。
  3. InnoDB 引擎,支持外键;而 MyISAM 是不支持的。

当然,也可以从索引结构、存储限制等方面,更加深入的回答

具体参考如下官方文档


InnoDB

  • 索引分为 聚簇索引 与 二级索引
    • 聚簇索引:主键值作为索引数据,叶子节点还包含了所有字段数据,索引和数据是存储在一起的
    • 二级索引:除主键外的其它字段建立的索引称为二级索引。被索引的字段值作为索引数据,叶子节点还包含了主键值
  • 支持事务
    • 通过 undo log 支持事务回滚、当前读(多版本查询)
    • 通过 redo log 实现持久性
    • 通过两阶段提交实现一致性
    • 通过当前读、锁实现隔离性
  • 支持行锁、间隙锁
  • 支持外键

MyISAM

  • 索引只有一种
  • 被索引字段值作为索引数据,叶子节点还包含了该记录数据页地址,数据和索引是分开存储的
  • 不支持事务,没有 undo logredo log
  • 仅支持表锁
  • 不支持外键
  • 会保存表的总行数

InnoDB 索引特点

  • 聚簇索引:主键值作为索引数据,叶子节点还包含了所有字段数据,索引和数据是存储在一起的
  • 二级索引:除主键外的其它字段建立的索引称为二级索引。被索引的字段值作为索引数据,叶子节点还包含了主键值。

MyISAM 索引特点

  • 被索引字段值作为索引数据,叶子节点还包含了该记录数据页地址,数据和索引是分开存储的

1.7.三种引擎的选择


在选择存储引擎时,应该根据应用系统的特点选择合适的存储引擎。

对于复杂的应用系统,还可以根据实际情况选择多种存储引擎进行组合。

  • InnoDB:是 MySQL 的默认存储引擎,支持事务、外键。
    如果应用对事务的完整性有比较高的要求,在并发条件下要求数据的一致性,数据操作除了插入和查询之外,还包含很多的更新、删除操作,那么 InnoDB 存储引擎是比较合适的选择。

  • MyISAM:如果应用是以读操作和插入操作为主,只有很少的更新和删除操作,并且对事务的完整性、并发性要求不是很高,那么选择这个存储引擎是非常合适的。

  • MEMORY:将所有数据保存在内存中,访问速度快,通常用于临时表及缓存。
    该引擎的缺陷就是对表的大小有限制,太大的表无法缓存在内存中,而且无法保障数据的安全性。


1.8.存储引擎小结


  1. 体系结构
连接层、服务层、引擎层、存储层

  1. 存储引擎简介
SHOW ENGINES;

CREATE TABLE XXX(...)ENGINE=INNODB;

  1. 存储引擎特点
INNODB 与 MyISAM:事务、外键、行级锁

  1. 存储引擎应用
INNODB:存储业务系统中对于事务、数据完整性要求较高的核心数据。

MyISAM:存储业务系统的非核心事务。

日常的生产环境、测试环境、开发环境大多都是在 Linux 下进行的

下载 LinuxMySQL 安装包:https://downloads.mysql.com/archives/community/

在这里插入图片描述

黑马官方提供的资料里给出了详细的安装文档。

或者也可以略览一下我之前写过的博客(https://blog.csdn.net/yanzhaohanwei/article/details/125305548 )中的安装 MySQL 部分

之后的许多操作我都是在 CentOS7 下进行的

此外,下载安装过程中可能会遇到一些问题,可以在 MySQL 官方文档 中找到解决方法


2.索引


2.1.索引概述


索引是帮助 MySQL 高效获取数据数据结构(有序)

在数据之外,数据库系统还维护着满足特定查找算法的数据结构。

这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查询算法,这种数据结构就是索引。


2.2.优缺点


优点

  • 提高数据检索效率,降低数据库的IO成本
  • 通过索引列对数据进行排序,降低数据排序的成本,降低CPU的消耗

缺点

  • 索引列也是要占用空间的
  • 索引大大提高了查询效率,但降低了更新的速度,比如 INSERTUPDATEDELETE

2.3.索引结构


2.3.1.概述


MySQL 的索引是在存储引擎层实现的,不同的存储引擎有不同的索引结构

索引结构描述
B+Tree最常见的索引类型,大部分引擎都支持B+树索引
Hash底层数据结构是用哈希表实现,只有精确匹配索引列的查询才有效,不支持范围查询
R-Tree空间索引空间索引是 MyISAM 引擎的一个特殊索引类型,主要用于地理空间数据类型,通常使用较少
Full-Text全文索引是一种通过建立倒排索引,快速匹配文档的方式,类似于 LuceneSolrES

不同的存储引擎对于索引结构的支持情况

索引InnoDBMyISAMMemory
B+Tree 索引支持支持支持
Hash 索引不支持不支持支持
R-Tree 索引不支持支持不支持
Full-Text5.6 版本后支持支持不支持

  • 注意: 我们平常所说的索引,如果没有特别指明,都是指 B+Tree 结构组织的索引。

2.3.2.二叉树的问题


  1. 假设 MySQL 的索引结构采用二叉树的数据结构

在这里插入图片描述


如果选择二叉树作为索引结构,会存在以下缺点:

  • 顺序插入时,会形成一个链表,查询性能大大降低。
  • 大数据量情况下,层级较深,检索速度慢。

  1. 假设 MySQL 的索引结构采用红黑树的数据结构

任意节点的左右两个子树的高度差不超过 1,任意节点的左右两个子树都是一颗平衡二叉树

红黑树是一颗自平衡二叉树,即使是顺序插入数据,最终形成的数据结构也是一颗平衡的二叉树。

红黑树不是高度平衡的,它的平衡是通过 “红黑规则” 进行实现的

在这里插入图片描述

因为红黑树也是一颗二叉树,所以依旧会存在一个缺点:在大数据量的情况下,层级较深,检索速度慢。


2.3.3.B-Tree


推荐观看视频:进阶-索引-结构-B-Tree

B-Tree 是一种多叉路衡查找树,相对于二叉树,B-Tree 每个节点可以有多个分支,即多叉。


以一颗最大度数(max-degree)为 5(5 阶)的 b-tree 为例,那这个 B-Tree 每个节点最多存储 4 个 key,5 个指针

补充:树的度数指的是一个节点的子节点个数。

我们可以通过一个数据结构可视化的网站来简单演示一下:https://www.cs.usfca.edu/~galles/visualization/BTree.html

在这里插入图片描述
插入一组数据:100 65 169 368 900 556 780 35 215 1200 234 888 158 90 1000 88120 268 250 。

然后观察一些数据插入过程中,节点的变化情况。

在这里插入图片描述


特点

  • 5 阶的 B-Tree,每一个节点最多存储 4 个 key,对应 5 个指针。
  • 一旦节点存储的 key 数量到达 5,就会裂变,中间元素向上分裂。
  • B-Tree 中,非叶子节点和叶子节点都会存放数据。

2.3.4.B+Tree


推荐观看视频:进阶-索引-结构-B+Tree

B+TreeB-Tree 的变种,我们以一颗最大度数(max-degree)为 4(4 阶)的B+Tree为例,来看一下其结构示意图:

在这里插入图片描述
我们可以看到两部分

  • 绿色框框起来的部分,是索引部分,仅仅起到索引数据的作用,不存储数据。
  • 红色框框起来的部分,是数据存储部分,在其叶子节点中要存储具体的数据。

我们可以通过一个数据结构可视化的网站来简单演示一下:https://www.cs.usfca.edu/~galles/visualization/BPlusTree.html

在这里插入图片描述
插入一组数据:100 65 169 368 900 556 780 35 215 1200 234 888 158 90 1000 88 120 268 250。

然后观察一些数据插入过程中,节点的变化情况。

在这里插入图片描述

B+TreeB-Tree 相比,主要有以下三点区别:

  • 所有的数据都会出现在叶子节点。
  • 叶子节点形成一个单向链表。
  • 非叶子节点仅仅起到索引数据作用,具体的数据都是在叶子节点存放的。

上述我们所看到的结构是标准的 B+Tree 的数据结构。


接下来,我们再来看看 MySQL中优化之后的 B+Tree

MySQL 索引数据结构对经典的 B+Tree 进行了优化。

在原 B+Tree 的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的 B+Tree,提高区间访问的性能,利于排序。

在这里插入图片描述


2.3.5.Hash


MySQL 中除了支持 B+Tree 索引,还支持一种索引类型:Hash 索引。


  1. 结构

哈希索引就是采用一定的 hash 算法,将键值换算成新的 hash 值,映射到对应的槽位上,然后存储在 hash 表中

在这里插入图片描述

如果两个(或多个)键值,映射到一个相同的槽位上,他们就产生了 Hash 冲突(也称为 Hash 碰撞),可以通过链表来解决。

在这里插入图片描述


  1. 特点

Hash 索引只能用于对等比较=in),不支持范围查询(between>< ,…)

无法利用索引完成排序操作

查询效率高,通常(前提是不存在 Hash 冲突的情况)只需要一次检索就可以了,效率通常要高于 B+Tree 索引


  1. 存储引擎支持

MySQL 中,支持 hash 索引的是 Memory 存储引擎。

InnoDB 中具有自适应 hash 功能hash 索引是 InnoDB 存储引擎根据 B+Tree 索引在指定条件下自动构建的。


2.3.6.思考题


思考题:为什么 InnoDB 存储引擎选择使用 B+Tree 索引结构?

  • 相对于二叉树,层级更少,搜索效率高。
  • 对于 B-Tree ,无论是叶子节点还是非叶子节点,都会保存数据。
    这样导致一页中存储的键值减少,指针跟着减少。
    要同样保存大量数据,只能增加树的高度,导致性能降低。
  • 相对 Hash 索引,B+Tree 支持范围匹配及排序操作。

2.4.索引分类


MySQL 数据库,将索引的具体类型主要分为以下几类:主键索引、唯一索引、常规索引、全文索引。

分类含义特点关键字
主键索引针对于表中主键创建的索引默认自动创建,只能有一个PRIMARY
唯一索引避免同一个表中某数据列中的值重复可以有多个UNIQUE
常规索引快速定位特定数据可以有多个
全文索引全文索引查找的是文本中的关键词,而不是比较索引中的值可以有多个FULLTEXT

InnoDB 存储引擎中,根据索引的存储形式,可以分为聚集索引、二级索引。


2.5.聚集索引和二级索引


2.5.1.概述


InnoDB 存储引擎中,根据索引的存储形式,可以分为聚集索引、二级索引。

分类含义特点
聚集索引Clustered Index将数据存储与索引放一块,索引结构的叶子节点保存了行数据必须有,而且只有一个
二级索引Secondary Index将数据与索引分开存储,索引结构的叶子节点关联的是对应的主键可以存在多个

2.5.2.选取规则


  • 如果存在主键,主键索引就是聚集索引
  • 如果不存在主键,将使用第一个唯一UNIQUE索引作为聚集索引
  • 如果表没有主键或没有合适的唯一索引,则 InnoDB自动生成一个 rowid 作为隐藏的聚集索引

2.5.3.具体结构


在这里插入图片描述

  • 聚集索引的叶子节点下挂的是这一行的数据。
  • 二级索引的叶子节点下挂的是该字段值对应的主键值。

2.5.4.查找过程


接下来,我们来分析一下,当我们执行如下的 SQL 语句时,具体的查找过程是什么样子的。

在这里插入图片描述
具体过程如下

  1. 由于是根据 name 字段进行查询,所以先根据 name = 'Arm'name 字段的二级索引中进行匹配查找。
    但是在二级索引中只能查找到 Arm 对应的主键值 10。
  2. 由于查询返回的数据是 *,所以此时,还需要根据主键值 10,到聚集索引中查找 10 对应的记录,最终找到 10 对应的行 row
  3. 最终拿到这一行的数据,直接返回即可。

回表查询这种先到二级索引中查找数据,找到主键值,然后再到聚集索引中根据主键值,获取数据的方式,就称之为回表查询。


2.5.5.思考题1


思考题 1:以下两条 SQL 语句,那个执行效率高? 为什么?

  • A.select * from user where id = 10;
  • B.select * from user where name = 'Arm';

备注:id 为主键,name 字段创建的有索引;


解答:A 语句的执行性能要高于 B 语句。

因为 A 语句直接走聚集索引,直接返回数据。

而 B 语句需要先查询 name 字段的二级索引,然后再查询聚集索引,也就是需要进行回表查询。


2.5.6.思考题2


思考题 2InnoDB 主键索引的 B+Tree 高度为多高呢?

在这里插入图片描述


假设

  • 一行数据大小为 1k,一页中可以存储 16 行这样的数据。
  • InnoDB 的指针占用 6 个字节的空间,主键即使为 bigint,占用字节数为 8。

如果树的高度为 2

  • n * 8 + (n + 1) * 6 = 16*1024,算出 n 约为 1170。
  • 1171* 16 = 18736
  • 也就是说,如果树的高度为 2,则可以存储 18000 多条记录。

如果树的高度为 3

  • 1171 * 1171 * 16 = 21939856
  • 也就是说,如果树的高度为 3,则可以存储 2200W 左右的记录。

2.6.索引语法


  • 创建索引
CREATE [ UNIQUE | FULLTEXT ] INDEX index_name ON table_name (index_col_name,... ) ;
  • 查看索引
SHOW INDEX FROM table_name ;
  • 删除索引
DROP INDEX index_name ON table_name ;

2.7.案例演示


2.7.0.数据准备


CREATE DATABASE
IF
	NOT EXISTS itcast DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;

CREATE TABLE tb_user (
	id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
	name VARCHAR ( 50 ) NOT NULL COMMENT '用户名',
	phone VARCHAR ( 11 ) NOT NULL COMMENT '手机号',
	email VARCHAR ( 100 ) COMMENT '邮箱',
	profession VARCHAR ( 11 ) COMMENT '专业',
	age TINYINT UNSIGNED COMMENT '年龄',
	gender CHAR ( 1 ) COMMENT '性别 , 1: 男, 2: 女',
	status CHAR ( 1 ) COMMENT '状态',
	createtime DATETIME COMMENT '创建时间' 
) COMMENT '系统用户表';

INSERT INTO tb_user ( name, phone, email, profession, age, gender, status, createtime )
VALUES
	( '吕布', '17799990000', 'lvbu666@163.com', '软件工程', 23, '1', '6', '2001-02-02 00:00:00' );
	
INSERT INTO tb_user ( name, phone, email, profession, age, gender, status, createtime )
VALUES
	( '曹操', '17799990001', 'caocao666@qq.com', '通讯工程', 33, '1', '0', '2001-03-05 00:00:00' );
	
INSERT INTO tb_user ( name, phone, email, profession, age, gender, status, createtime )
VALUES
	( '赵云', '17799990002', '17799990@139.com', '英语', 34, '1', '2', '2002-03-02 00:00:00' );
	
INSERT INTO tb_user ( name, phone, email, profession, age, gender, status, createtime )
VALUES
	( '孙悟空', '17799990003', '17799990@sina.com', '工程造价', 54, '1', '0', '2001-07-02 00:00:00' );
	
INSERT INTO tb_user ( name, phone, email, profession, age, gender, status, createtime )
VALUES
	( '花木兰', '17799990004', '19980729@sina.com', '软件工程', 23, '2', '1', '2001-04-22 00:00:00' );
	
INSERT INTO tb_user ( name, phone, email, profession, age, gender, status, createtime )
VALUES
	( '大乔', '17799990005', 'daqiao666@sina.com', '舞蹈', 22, '2', '0', '2001-02-07 00:00:00' );
INSERT INTO tb_user ( name, phone, email, profession, age, gender, status, createtime )
VALUES
	( '露娜', '17799990006', 'luna_love@sina.com', '应用数学', 24, '2', '0', '2001-02-08 00:00:00' );
	
INSERT INTO tb_user ( name, phone, email, profession, age, gender, status, createtime )
VALUES
	( '程咬金', '17799990007', 'chengyaojin@163.com', '化工', 38, '1', '5', '2001-05-23 00:00:00' );
	
INSERT INTO tb_user ( name, phone, email, profession, age, gender, status, createtime )
VALUES
	( '项羽', '17799990008', 'xiaoyu666@qq.com', '金属材料', 43, '1', '0', '2001-09-18 00:00:00' );
	
INSERT INTO tb_user ( name, phone, email, profession, age, gender, status, createtime )
VALUES
	( '白起', '17799990009', 'baiqi666@sina.com', '机械工程及其自动化', 27, '1', '2', '2001-08-16 00:00:00' );
	
INSERT INTO tb_user ( name, phone, email, profession, age, gender, status, createtime )
VALUES
	( '韩信', '17799990010', 'hanxin520@163.com', '无机非金属材料工程', 27, '1', '0', '2001-06-12 00:00:00' );
	
INSERT INTO tb_user ( name, phone, email, profession, age, gender, status, createtime )
VALUES
	( '荆轲', '17799990011', 'jingke123@163.com', '会计', 29, '1', '0', '2001-05-11 00:00:00' );
INSERT INTO tb_user ( name, phone, email, profession, age, gender, status, createtime )
VALUES
	( '兰陵王', '17799990012', 'lanlinwang666@126.com', '工程造价', 44, '1', '1', '2001-04-09 00:00:00' );
	
INSERT INTO tb_user ( name, phone, email, profession, age, gender, status, createtime )
VALUES
	( '狂铁', '17799990013', 'kuangtie@sina.com', '应用数学', 43, '1', '2', '2001-04-10 00:00:00' );
	
INSERT INTO tb_user ( name, phone, email, profession, age, gender, status, createtime )
VALUES
	( '貂蝉', '17799990014', '84958948374@qq.com', '软件工程', 40, '2', '3', '2001-02-12 00:00:00' );
	
INSERT INTO tb_user ( name, phone, email, profession, age, gender, status, createtime )
VALUES
	( '妲己', '17799990015', '2783238293@qq.com', '软件工程', 31, '2', '0', '2001-01-30 00:00:00' );
	
INSERT INTO tb_user ( name, phone, email, profession, age, gender, status, createtime )
VALUES
	( '芈月', '17799990016', 'xiaomin2001@sina.com', '工业经济', 35, '2', '0', '2000-05-03 00:00:00' );
	
INSERT INTO tb_user ( name, phone, email, profession, age, gender, status, createtime )
VALUES
	( '嬴政', '17799990017', '8839434342@qq.com', '化工', 38, '1', '1', '2001-08-08 00:00:00' );
INSERT INTO tb_user ( name, phone, email, profession, age, gender, status, createtime )
VALUES
	( '狄仁杰', '17799990018', 'jujiamlm8166@163.com', '国际贸易', 30, '1', '0', '2007-03-12 00:00:00' );

INSERT INTO tb_user ( name, phone, email, profession, age, gender, status, createtime )
VALUES
	( '安琪拉', '17799990019', 'jdodm1h@126.com', '城市规划', 51, '2', '0', '2001-08-15 00:00:00' );

INSERT INTO tb_user ( name, phone, email, profession, age, gender, status, createtime )
VALUES
	( '典韦', '17799990020', 'ycaunanjian@163.com', '城市规划', 52, '1', '2', '2000-04-12 00:00:00' );
	
INSERT INTO tb_user ( name, phone, email, profession, age, gender, status, createtime )
VALUES
	( '廉颇', '17799990021', 'lianpo321@126.com', '土木工程', 19, '1', '3', '2002-07-18 00:00:00' );
	
INSERT INTO tb_user ( name, phone, email, profession, age, gender, status, createtime )
VALUES
	( '后羿', '17799990022', 'altycj2000@139.com', '城市园林', 20, '1', '0', '2002-03-10 00:00:00' );
	
INSERT INTO tb_user ( name, phone, email, profession, age, gender, status, createtime )
VALUES
	( '姜子牙', '17799990023', '37483844@qq.com', '工程造价', 29, '1', '4', '2003-05-26 00:00:00' );

表结构中插入的数据如下(这里使用的工具是 Navicat Premium

在这里插入图片描述


2.7.1.创建索引


  1. name 字段为姓名字段,该字段的值可能会重复,为该字段创建索引
CREATE INDEX idx_user_name ON tb_user ( name );
  1. phone手机号字段的值,是非空,且唯一的,为该字段创建唯一索引
CREATE UNIQUE INDEX idx_user_phone ON tb_user ( phone );
  1. professionagestatus 创建联合索引
CREATE INDEX idx_user_pro_age_sta ON tb_user ( profession, age, status );
  1. email 建立合适的索引来提升查询效率
CREATE INDEX idx_user_email on tb_user( email );

2.7.2.查看索引


完成上述的需求之后,我们再查看 tb_user 表的所有的索引数据。

SHOW INDEX FROM tb_user;

此处使用的工具是 SQLYog

在这里插入图片描述


2.8.SQL性能优化


2.8.1.SQL执行频率


MySQL 客户端连接成功后,通过 show [session|global] status 命令可以提供服务器状态信息。

通过如下指令,可以查看当前数据库的 INSERTUPDATEDELETESELECT 的访问频次

-- session status 是查看当前会话的状态信息;

-- global status 是查询全局数据的状态信息;

SHOW GLOBAL STATUS LIKE 'Com_______';

在这里插入图片描述

我们可以在当前数据库再执行几次查询操作,然后再次查看执行频次,可以发现 Com_select 参数的值发生了变化。

这里就不贴图了。


通过上述指令,我们可以查看到当前数据库到底是以查询为主,还是以增删改为主,从而为数据库优化提供参考依据。

如果是以增删改为主,我们可以考虑不对其进行索引的优化。

如果是以查询为主,那么就要考虑对数据库的索引进行优化了。


2.8.2.慢查询日志


慢查询日志记录了所有执行时间超过指定参数(long_query_time,单位:秒,默认 10 秒)的所有 SQL 语句的日志。

MySQL 的慢查询日志默认没有开启,我们可以查看一下系统变量 slow_query_log

SHOW VARIABLES LIKE 'slow_query_log';

在这里插入图片描述


如果要开启慢查询日志,需要在 MySQL 的配置文件(/etc/my.cnf)中配置如下信息\

# 开启 MySQL 慢日志查询开关
slow_query_log=1

# 设置慢日志的时间为 2 秒,SQL 语句执行时间超过 2 秒,就会视为慢查询,记录慢查询日志
long_query_time=2

配置完毕之后,通过以下指令重新启动 MySQL 服务器进行测试

systemctl restart mysqld

之后再次查看慢日志是否开启

在这里插入图片描述


查看慢日志文件中记录的信息 /var/lib/mysql/虚拟机名称-slow.log

# 我自己的虚拟机设置的名称就是 centos7

cat /var/lib/mysql/centos7-slow.log

在这里插入图片描述


执行如下 SQL 语句

select * from tb_user; -- 这条 SQL 执行效率比较高, 执行耗时 0.00sec
select count(*) from tb_sku; -- 由于 tb_sku 表中, 预先存入了 1000w 的记录, count 一次,耗时 13.35sec

在这里插入图片描述

检查慢查询日志,发现 虚拟机名称-slow.log 只会记录执行时间超多我们预设时间(2s)的 SQL,执行较快的 SQL 是不会记录的。

在这里插入图片描述
那这样,通过慢查询日志,就可以定位出执行效率比较低的 SQL,从而有针对性的进行优化。


2.8.3.profile详情


show profiles 能够在做 SQL 优化时帮助我们了解时间都耗费到哪里去了。

通过 have_profiling 参数,能够看到当前 MySQL 是否支持 profile 操作

在这里插入图片描述
可以看到,当前 MySQL 是支持 profile 操作的,但是开关是关闭的(0 为关闭,1 为开启)。

可以通过 set 语句在 session/global 级别开启 profiling

SET profiling = 1;

开关已经打开了,接下来,我们所执行的 SQL 语句,都会被 MySQL 记录,并记录执行时间消耗到哪儿去了。

我们直接执行如下的 SQL 语句

SELECT * FROM tb_user;

SELECT * FROM tb_user WHERE id = 1;

SELECT * FROM tb_user WHERE name = '白起';

SELECT COUNT(*) FROM tb_sku;

执行一系列的业务 SQL 的操作,然后通过如下指令查看指令的执行耗时

SHOW PROFILES;
-- 查看指定 query_id 的 SQL 语句各个阶段的耗时情况
SHOW PROFILE FOR QUERY query_id;
-- 查看指定 query_id 的 SQL 语句 CPU 的使用情况
SHOW PROFILE cpu FOR QUERY query_id;

查看每一条 SQL 的耗时情况

在这里插入图片描述

查看指定 SQL 各个阶段的耗时情况

在这里插入图片描述


2.8.4.explain执行计划


EXPLAIN 或者 DESC 命令获取 MySQL 如何执行 SELECT 语句的信息,包括在 SELECT 语句执行过程中表如何连接和连接的顺序。


语法

-- 直接在 select 语句之前加上关键字 explain / desc

EXPLAIN SELECT 字段列表 FROM 表名 WHERE 条件 ;

在这里插入图片描述


Explain 执行计划中各个字段的含义

字段含义
idselect 查询的序列号
表示查询中执行 select 子句或者是操作表的顺序(id 相同,执行顺序从上到下;id 不同,值越大,越先执行)。
select_type表示 SELECT 的类型
常见的取值有:
  SIMPLE(简单表,即不使用表连接或者子查询)
  PRIMARY(主查询,即外层的查询)
  UNIONUNION 中的第二个或者后面的查询语句)
  SUBQUERYSELECT/WHERE 之后包含了子查询)等
type表示连接类型
性能由好到差的连接类型为 NULL、systemconsteq_refrefrangeindexall
possible_key显示可能应用在这张表上的索引,一个或多个。
key实际使用的索引
如果为 NULL,则没有使用索引。
key_len表示索引中使用的字节数
该值为索引字段最大可能长度,并非实际使用长度,在不损失精确性的前提下, 长度越短越好 。
rowsMySQL 认为必须要执行查询的行数
innodb 引擎的表中,是一个估计值,可能并不总是准确的。
filtered表示返回结果的行数占需读取行数的百分比, filtered 的值越大越好。

接下来我们着重讲解演示几个重要的字段帮助诸位理解


  • id
    • select 查询的序列号。表示查询中执行 select 子句或者是操作表的顺序
    • id 相同,执行顺序从上到下;id 不同,值越大,越先执行)。

此处要用到之前用过的三张表,下图是这三张表的关系

在这里插入图片描述


id 序号相同的情况时,执行顺序是从上到下的。

在这里插入图片描述
在这里插入图片描述


id 不同时,值越大,执行顺序越靠前

查询选修了 MySQL 课程的学生(子查询方式)

在这里插入图片描述
在这里插入图片描述


  • type
    • 表示连接类型
    • 性能由好到差的连接类型为 NULL、systemconsteq_refrefrangeindexall

在这里插入图片描述

对唯一索引进行查询,typeconst

对于非唯一性索引进行查询,typeref

对于不涉及到表的查询时,typenull


  • key_len
    • 表示索引中使用的字节数
    • 该值为索引字段最大可能长度,并非实际使用长度,在不损失精确性的前提下, 长度越短越好 。

在这里插入图片描述


2.9.索引使用①


2.9.1.验证索引效率


tb_sku 中有 1000w 的记录。(相关 SQL 脚本在资料中)

在这里插入图片描述
这张表中 id 为主键,有主键索引,而其他字段是没有建立索引的。

在这里插入图片描述
显然,没有索引的字段的查询效率是非常低的。


此处针对字段创建索引

CREATE INDEX idx_sku_sn ON tb_sku(sn) ;

在这里插入图片描述


在这里插入图片描述

显然,sn 字段建立了索引之后,查询性能大大提升。

建立索引前后,查询耗时都不是一个数量级的。


在这里插入图片描述


2.9.2.最左前缀法则


如果索引了多列(联合索引),要遵守最左前缀法则。

最左前缀法则指的是查询从索引的最左列开始,并且不跳过索引中的列。

如果跳跃某一列,索引将会部分失效(后面的字段索引失效)。


tb_user 表为例,我们先来查看一下之前 tb_user 表所创建的索引。

在这里插入图片描述
tb_user 表中,有一个联合索引,这个联合索引涉及到三个字段,顺序分别为:professionagestatus

对于最左前缀法则指的是,查询时,最左变的列,也就是 profession 必须存在,否则索引全部失效。

而且中间不能跳过某一列,否则该列后面的字段索引将失效。


SELECT * FROM tb_user WHERE profession = '软件工程' AND age = 31 AND `STATUS` = '0' ;

在这里插入图片描述

接下来,我们来就以这条 SQL 语句演示几组案例,看一下具体的执行计划


EXPLAIN SELECT * FROM tb_user WHERE profession = '软件工程' AND age = 31 AND `STATUS` = '0' ;

在这里插入图片描述


EXPLAIN SELECT * FROM tb_user WHERE profession = '软件工程'  AND age = 31;

在这里插入图片描述


EXPLAIN SELECT * FROM tb_user WHERE profession = '软件工程' ;

在这里插入图片描述


以上的这三组测试中,我们发现只要联合索引最左边的字段 profession 存在,索引就会生效,只不过索引的长度不同。

而且由以上三组测试,我们也可以推测出 profession 字段索引长度为 47、age 字段索引长度为 2、status 字段索引长度为 5。


EXPLAIN SELECT * FROM tb_user WHERE age = 31 AND STATUS = '0';

在这里插入图片描述


EXPLAIN SELECT * FROM tb_user WHERE STATUS = '0';

在这里插入图片描述


而通过上面的这两组测试,我们也可以看到索引并未生效。

原因是因为不满足最左前缀法则,联合索引最左边的列 profession 不存在。


EXPLAIN SELECT * FROM tb_user WHERE profession = '软件工程' AND STATUS = '0';

在这里插入图片描述


上述的 SQL 查询时,存在 profession 字段,最左边的列是存在的,索引满足最左前缀法则的基本条件。

但是查询时,跳过了 age 这个列,所以后面的列索引是不会使用的,也就是索引部分生效,所以索引的长度就是 47。


  • 注意
    • 最左前缀法则中指的最左边的列,是指在查询时,联合索引的最左边的字段即是第一个字段必须存在
    • 与我们编写 SQL 时,条件编写的先后顺序无关。

在这里插入图片描述


2.10.索引失效情况


2.10.1.范围查询


联合索引中,出现范围查询(><),范围查询右侧的列索引失效。


SELECT * FROM tb_user WHERE profession = '软件工程' AND age > 30 AND STATUS = '0';

在这里插入图片描述


EXPLAIN SELECT * FROM tb_user WHERE profession = '软件工程' AND age > 30 AND STATUS = '0';

在这里插入图片描述
当范围查询使用 >< 时,走联合索引了。

但是索引的长度为 49,就说明范围查询右边的 status 字段是没有走索引的。


EXPLAIN SELECT * FROM tb_user WHERE profession = '软件工程' AND age >= 30 AND STATUS = '0';

在这里插入图片描述
当范围查询使用 >=<= 时,走联合索引了。

索引的长度为 54,就说明所有的字段都是走索引的。


所以,在业务允许的情况下,尽可能的使用类似于 >=<= 这类的范围查询,而避免使用 ><


2.10.2.索引列运算


不要在索引列上进行运算操作, 索引将失效。


tb_user 表中,除了前面介绍的联合索引之外,还有一个索引,是 phone 字段的单列索引。

在这里插入图片描述


当根据 phone 字段进行等值匹配查询时,索引生效。

EXPLAIN SELECT * FROM tb_user WHERE phone = '17799990015';

在这里插入图片描述


当根据 phone 字段进行函数运算操作之后,索引失效。

EXPLAIN SELECT * FROM tb_user WHERE SUBSTRING(phone,10,2) = '15';

在这里插入图片描述


2.10.3.字符串不加引号


字符串类型字段使用时,不加引号,索引将失效。


EXPLAIN SELECT * FROM tb_user WHERE profession = '软件工程' AND age = 31 AND STATUS = '0';
EXPLAIN SELECT * FROM tb_user WHERE profession = '软件工程' AND age = 31 AND STATUS = 0;

在这里插入图片描述


EXPLAIN SELECT * FROM tb_user WHERE phone = '17799990015';
EXPLAIN SELECT * FROM tb_user WHERE phone = 17799990015;

在这里插入图片描述


经过上面两组示例,我们会明显的发现,如果字符串不加单引号,对于查询结果,没什么影响。

但是数据库存在隐式类型转换,此时索引失效。


2.10.4.模糊查询


如果仅仅是尾部模糊匹配,索引不会失效。如果是头部模糊匹配,索引失效。


由于下面查询语句中,都是根据 profession 字段查询,符合最左前缀法则,联合索引是可以生效的。

我们主要看一下,模糊查询时,% 加在关键字之前,和加在关键字之后的影响。

EXPLAIN SELECT * FROM tb_user WHERE profession LIKE '软件%';
EXPLAIN SELECT * FROM tb_user WHERE profession LIKE '%工程';
EXPLAIN SELECT * FROM tb_user WHERE profession LIKE '%工%';

在这里插入图片描述


经过上述的测试,我们发现,在 like 模糊查询中,在关键字后面加 %,索引可以生效。

而如果在关键字前面加了 %,索引将会失效。


2.10.5.or 连接条件


or 分割开的条件, 如果 or 前的条件中的列有索引,而后面的列中没有索引,那么涉及的索引都不会被用到。


EXPLAIN SELECT * FROM tb_user WHERE id = 10 OR age = 23;
EXPLAIN SELECT * FROM tb_user WHERE phone = '17799990017' OR age = 23;

在这里插入图片描述
由于 age 没有索引,所以即使 idphone 有索引,索引也会失效。


创建索引

CREATE INDEX idx_user_age ON tb_user(age);

建立了索引之后,我们再次执行上述的 SQL 语句,看看前后执行计划的变化。

在这里插入图片描述
在这里插入图片描述

最终,我们发现,当 or 连接的条件,左右两侧字段都有索引时,索引才会生效。


2.10.6.数据分布影响


如果 MySQL 评估使用索引比全表更慢,则不使用索引。


SELECT * FROM tb_user WHERE phone >= '17799990005';
SELECT * FROM tb_user WHERE phone >= '17799990015';

在这里插入图片描述
经过测试我们发现,相同的 SQL 语句,只是传入的字段值不同,最终的执行计划也完全不一样,这是为什么呢?

就是因为 MySQL 在查询时,会评估使用索引的效率与走全表扫描的效率,如果走全表扫描更快,则放弃索引,走全表扫描。

因为索引是用来索引少量数据的,如果通过索引查询返回大批量的数据,则还不如走全表扫描来的快,此时索引就会失效。


接下来,我们再来看看 is nullis not null 操作是否走索引。

EXPLAIN SELECT * FROM tb_user WHERE profession IS NULL;
EXPLAIN SELECT * FROM tb_user WHERE profession IS NOT NULL;

在这里插入图片描述


接下来,我们做一个操作将 profession 字段值全部更新为 null

UPDATE tb_user SET profession = null;

然后,再次执行上述的两条 SQL,查看 SQL 语句的执行计划。

在这里插入图片描述

最终我们看到,一模一样的 SQL 语句,先后执行了两次,结果查询计划是不一样的。

为什么会出现这种现象?这和数据库的数据分布有关系。


查询时 MySQL 会评估,走索引快,还是全表扫描快。如果全表扫描更快,则放弃索引走全表扫描。

因此,is nullis not null 是否走索引,得具体情况具体分析,并不是固定的。


2.11.索引使用②


2.11.0.数据准备


tb_user 中的数据如下

在这里插入图片描述


tb_user 索引的情况如下

在这里插入图片描述


把上述的 idx_user_ageidx_email 这两个之前测试使用过的索引直接删除。

DROP INDEX idx_user_age ON tb_user;
DROP INDEX idx_email ON tb_user;

2.11.1.SQL提示


EXPLAIN SELECT * FROM tb_user WHERE profession = '软件工程';

在这里插入图片描述
显然,该查询经过了联合索引。


创建 profession 的单列索引

CREATE INDEX idx_user_pro ON tb_user(profession);

创建单列索引后,再次查看执行计划,看看到底走哪个索引。

在这里插入图片描述
从测试结果中我们可以看到,possible_keys 中有两个索引,最终 MySQL 选择了 idx_user_pro_age_sta 索引。

这是 MySQL 自动选择的结果。

那么,我们能不能在查询的时候,自己来指定使用哪个索引呢?

答案是肯定的,此时就可以借助于 MySQLSQL 提示 来完成。


SQL 提示 是优化数据库的一个重要手段。

简单来说,就是在 SQL 语句中加入一些人为的提示来达到优化操作的目的。


  1. use index:建议 MySQL 使用哪一个索引完成此次查询(仅仅是建议,MySQL 内部还会再次进行评估)。
EXPLAIN SELECT * FROM tb_user USE INDEX(idx_user_pro) WHERE profession = '软件工程';

在这里插入图片描述


  1. ignore index: 忽略指定的索引。
EXPLAIN SELECT * FROM tb_user IGNORE INDEX(idx_user_pro) WHERE profession = '软件工程';

在这里插入图片描述


  1. force index: 强制使用索引。
EXPLAIN SELECT * FROM tb_user FORCE INDEX(idx_user_pro) WHERE profession = '软件工程';

在这里插入图片描述


2.11.2.覆盖索引


尽量使用覆盖索引,减少 select *

那么什么是覆盖索引呢?

覆盖索引是指 查询使用了索引,并且需要返回的列,在该索引中已经全部能够找到 。


我们先删除掉一些之前创建的索引,避免对接下来的测试造成干扰。

DROP INDEX idx_user_age ON tb_user;
DROP INDEX idx_user_email ON tb_user;
DROP INDEX idx_user_pro ON tb_user;

接下来,我们来看一组 SQL 的执行计划,看看执行计划的差别,然后再来具体做一个解析。

EXPLAIN SELECT id, profession FROM tb_user WHERE profession = '软件工程' AND age = 31 AND status = '0';

EXPLAIN SELECT id, profession, age, STATUS FROM tb_user WHERE profession = '软件工程' AND age = 31 AND status = '0';

EXPLAIN SELECT id, profession, age, STATUS, NAME FROM tb_user WHERE profession = '软件工程' AND age = 31 AND status = '0';

EXPLAIN SELECT * FROM tb_user WHERE profession = '软件工程' AND age = 31 AND status = '0';

在这里插入图片描述

  • 这图取自官方提供的 pdf 里的图。
  • 我自己使用的是 MySQL5.7,这里出现的 Extra 字段,前两条记录是 Using index,后两条记录是 NULL

从上述的执行计划我们可以看到,这四条 SQL 语句的执行计划前面所有的指标都是一样的,看不出来差异。

但是此时,我们主要关注的是后面的 Extra

  • 前面两条 SQL 的结果为 Using where; Using Index;
  • 而后面两条 SQL 的结果为:Using index condition
Extra含义
Using where; Using Index查找使用了索引,但是需要的数据都在索引列中能找到,所以不需要回表查询数据
Using index condition查找使用了索引,但是需要回表查询数据

因为,在 tb_user 表中有一个联合索引 idx_user_pro_age_sta

该索引关联了三个字段 professionagestatus ,而这个索引也是一个二级索引,所以叶子节点下面挂的是这一行的主键 id

所以当我们查询返回的数据在 idprofessionagestatus 之中,则直接走二级索引直接返回数据了。

如果超出这个范围,就需要拿到主键 id,再去扫描 聚集索引,再获取额外的数据了,这个过程就是 回表

而我们如果一直使用 select * 查询返回所有字段值,很容易就会造成 回表查询(除非是根据主键查询,此时只会扫描 聚集索引)。


为了大家更清楚的理解,什么是覆盖索引,什么是回表查询,我们一起再来看下面的这组 SQL 的执行过程。


  1. 表结构及索引示意图

在这里插入图片描述
id 是主键,是一个聚集索引。

name 字段建立了普通索引,是一个二级索引(辅助索引)。


  1. 执行的 SQL 语句:select * from tb_user where id = 2;

在这里插入图片描述

根据 id 查询,直接走聚集索引查询,一次索引扫描,直接返回数据,性能高。


  1. 执行的 SQL 语句:selet id,name from tb_user where name = 'Arm';

在这里插入图片描述

此处是根据 name 字段查询,查询二级索引。

因为查询返回的字段(idname)在 name 的二级索引中,所以这两个值都是可以直接获取到的。此即为覆盖索引。

因为覆盖索引不需要回表查询,所以性能高。


  1. 执行的 SQL 语句:selet id,name,gender from tb_user where name = 'Arm';

在这里插入图片描述
因为在 name 的二级索引中,不包含 gender,所以需要两次索引扫描,也就是需要回表查询,性能相对较差一点。


思考题

  • 一张表, 有四个字段( id, username, password, status
  • 由于数据量大, 需要对以下 SQL 语句进行优化
    • select id,username,password from tb_user where username = 'itcast';
  • 请问该如何进行才是最优方案?

答案

  • 针对于 username, password 建立联合索引
  • SQL 语句 为:create index idx_user_name_pass on tb_user(username,password);
  • 这样可以避免上述的 SQL 语句,在查询的过程中,出现回表查询。

2.11.3.前缀索引


当字段类型为字符串(varchartextlongtext 等)时,有时候需要索引很长的字符串,这会让索引变得很大。

查询时,会浪费掉大量的磁盘 IO, 影响查询效率。

此时可以只将字符串的一部分前缀,建立索引,这样可以大大节约索引空间,从而提高索引效率。


  1. 语法
CREATE INDEX idx_xxxx ON table_name(COLUMN(n)) ;

  1. 前缀长度

可以根据索引的选择性来决定。

选择性是指不重复的索引值(基数)和数据表的记录总数的比值。

索引选择性越高则查询效率越高。

唯一索引的选择性是 1,这是最好的索引选择性,性能也是最好的。

SELECT COUNT(DISTINCT email)/COUNT(*) FROM tb_user;
SELECT COUNT( DISTINCT SUBSTRING( email, 1, 5 ) )/ COUNT(*) FROM tb_user;

在这里插入图片描述


示例:为 tb_user 表的 email 字段,建立长度为 5 的前缀索引。

CREATE INDEX idx_email_5 ON tb_user ( email ( 5 ) );

在这里插入图片描述

EXPLAIN SELECT * FROM tb_user WHERE email = 'lvbu666@163.com';

在这里插入图片描述


  1. 前缀索引的查询流程

执行的 SQL 语句:SELECT * FROM tb_user WHERE email = 'lvbu666@163.com';

在这里插入图片描述


2.11.4.单列索引与联合索引


  • 单列索引:即一个索引只包含单个列。

  • 联合索引:即一个索引包含了多个列。

  • 注意事项:多条件联合查询时,MySQL 优化器会评估哪个字段的索引效率高,会选择该索引完成本次查询。


EXPLAIN SELECT id, phone, name FROM tb_user WHERE phone = '17799990010' AND name = '韩信';

在这里插入图片描述

通过上述执行计划我们可以看出来,在 and 连接的两个字段 phonename 上都是有单列索引的

但是最终 MySQL 只会选择一个索引,也就是说,只能走一个字段的索引,此时是会回表查询的。


紧接着,我们再来创建一个 phonename 字段的联合索引来查询一下执行计划(phone 字段本身就是 UNIQUE

CREATE UNIQUE INDEX idx_user_phone_name ON tb_user ( phone, name );

此处因为单列索引的干扰,所以我们需要指定索引

  • EXPLAIN SELECT id, phone, name FROM tb_user USE INDEX( idx_user_phone_name ) WHERE name = '韩信' AND phone = '17799990010';

在这里插入图片描述
指定查询索引后,查询时走了联合索引

在联合索引中包含 phonename 的信息,在叶子节点下挂的是对应的主键 id,所以查询是无需回表查询的。

在业务场景中,如果存在多个查询条件,考虑针对于查询字段建立索引时,建议建立联合索引,而非单列索引。


如果查询使用的是联合索引,具体的结构示意图如下

在这里插入图片描述


2.12.索引设计原则


  1. 针对于数据量较大且查询比较频繁的表建立索引

  2. 针对于常作为查询条件where)、排序order by)、分组group by操作的字段建立索引

  3. 尽量选择区分度高的列作为索引尽量建立唯一索引区分度越高使用索引的效率越高

  4. 如果是字符串类型的字段字段的长度较长可以针对于字段的特点建立前缀索引

  5. 尽量使用联合索引减少单列索引查询时联合索引很多时候可以覆盖索引节省存储空间避免回表提高查询效率

  6. 要控制索引的数量索引并不是多多益善索引越多维护索引结构的代价也就越大会影响增删改的效率

  7. 如果索引列不能存储 NULL请在创建表时使用 NOT NULL 约束它
    当优化器知道每列是否包含 NULL 值时它可以更好地确定哪个索引最有效地用于查询


2.13.索引小结


  1. 索引概述
索引是高效获取数据的数据结构
  1. 索引结构
B+Tree

Hash
  1. 索引分类
主键索引、唯一索引、常规索引、全文索引

聚集索引、二级索引
  1. 索引语法
create [unique] index xxx on xxx(xxx);

show index from xxx;

drop index xxx on xxx;
  1. SQL 性能分析
执行频次、慢查询日志、profile、explain
  1. 索引使用
联合索引、索引失效、SQL提示、覆盖索引、前缀索引、单列/联合索引
  1. 索引设计原则
表、字段、索引

3.SQL优化


3.1.插入数据


3.1.1.insert


INSERT INTO tb_test VALUES(1,'tom');

INSERT INTO tb_test VALUES(2,'cat');

INSERT INTO tb_test VALUES(3,'jerry');

.....

如果我们需要一次性往数据库表中插入多条记录,可以从三个方面进行优化:批量插入、手动提交事务、主键顺序插入

  1. 批量插入
Insert into tb_test values(1,'Tom'),(2,'Cat'),(3,'Jerry');
  1. 手动提交事务
START TRANSACTION;

INSERT INTO tb_test VALUES(1,'Tom'),(2,'Cat'),(3,'Jerry');
INSERT INTO tb_test VALUES(4,'Tom'),(5,'Cat'),(6,'Jerry');
INSERT INTO tb_test VALUES(7,'Tom'),(8,'Cat'),(9,'Jerry');

COMMIT;
  1. 主键顺序插入,性能要高于乱序插入。
主键乱序插入 : 8 1 9 21 88 2 4 15 89 5 7 3

主键顺序插入 : 1 2 3 4 5 7 8 9 15 21 88 89

3.1.2.大批量插入数据


如果一次性需要插入大批量数据(比如:几百万的记录),使用 insert 语句插入性能较低

此时可以使用 MySQL 数据库提供的 load 指令进行插入。

操作如下

在这里插入图片描述

  • 客户端连接服务端时,加上参数 -–local-infile
mysql –-local-infile -u root -p
  • 设置全局参数 -–local-infile 为 1,开启从本地加载文件导入数据的开关
set global local_infile = 1;
  • 执行 load 指令将准备好的数据,加载到表结构中
load data local infile '/root/sql1.log' into table tb_user fields terminated by ',' lines terminated by '\n' ;
  • 在执行 load 命令时,主键顺序插入性能高于乱序插入

示例

CREATE DATABASE itheima;
CREATE TABLE `tb_user` (
	`id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
	`username` VARCHAR ( 50 ) NOT NULL,
	`password` VARCHAR ( 50 ) NOT NULL,
	`name` VARCHAR ( 20 ) NOT NULL,
	`birthday` DATE DEFAULT NULL,
	`sex` CHAR ( 1 ) DEFAULT NULL,
	PRIMARY KEY ( `id` ),
UNIQUE KEY `unique_user_username` ( `username` ) 
) ENGINE = INNODB DEFAULT CHARSET = utf8;
-- 客户端连接服务端时,加上参数 -–local-infile
mysql –-local-infile -u root -p
-- 设置全局参数 local_infile 为 1,开启从本地加载文件导入数据的开关
set global local_infile = 1;
load data local infile '/root/load_user_100w_sort.sql ' into table tb_user fields terminated by ',' lines terminated by '\n' ;

在这里插入图片描述
我们看到,插入100w 的记录,17s 就完成了,性能很好。


3.2.主键优化


3.2.1.数据组织方式


InnoDB 存储引擎中,表数据都是根据主键顺序组织存放的,这种存储方式的表称为 索引组织表index organized table IOT)。

在这里插入图片描述
行数据,都是存储在聚集索引的叶子节点上的。

下图为 InnoDB 的逻辑结构图

在这里插入图片描述
InnoDB 引擎中,数据行是记录在逻辑结构 page 页中的,而每一个页的大小是固定的,默认16K。

那也就意味着, 一个页中所存储的行也是有限的。

如果插入的数据行 row 在该页存储不小,将会存储到下一个页中,页与页之间会通过指针连接。


3.2.2.页分裂


页可以为空,也可以填充一半,也可以填充 100%。

每个页包含了 2-N 行数据(如果一行数据很大,会发生行溢出),根据主键排列。


主键顺序插入效果

  1. 从磁盘中申请页, 主键顺序插入

在这里插入图片描述

  1. 第一个页没有满,继续往第一页插入

在这里插入图片描述

  1. 当第一个也写满之后,再写入第二个页,页与页之间会通过指针连接

在这里插入图片描述

  1. 当第二页写满了,再往第三页写入

在这里插入图片描述


主键乱序插入效果

  1. 加入 1#, 2# 页都已经写满了,存放了如图所示的数据

在这里插入图片描述

  1. 此时再插入 id50 的记录,我们来看看会发生什么现象

问:会再次开启一个页,写入新的页中吗?

在这里插入图片描述

答:不会。因为,索引结构的叶子节点是有顺序的。按照顺序,应该存储在 47 之后。

在这里插入图片描述

但是 47 所在的 1# 页,已经写满了,存储不了 50 对应的数据了。 那么此时会开辟一个新的页 3#。

在这里插入图片描述

但是并不会直接将 50 存入3# 页,而是会将 1# 页后一半的数据,移动到 3# 页,然后在 3# 页,插入 50。

在这里插入图片描述

移动数据,并插入 id 为 50 的数据之后,那么此时,这三个页之间的数据顺序是有问题的。

1# 的下一个页,应该是 3#, 3# 的下一个页是 2#。 所以,此时,需要重新设置链表指针。

在这里插入图片描述

上述的这种现象,称之为 “页分裂”,是比较耗费性能的操作。


3.2.3.页合并


目前表中已有数据的索引结构(叶子节点)如下

在这里插入图片描述

当我们对已有数据进行删除时,具体的效果如下:

  • 当删除一行记录时,实际上记录并没有被物理删除,只是记录被标记(flaged)为删除
  • 并且被标记的空间变得允许被其他记录声明使用。

在这里插入图片描述

当我们继续删除 2# 的数据记录

在这里插入图片描述

当页中删除的记录达到 MERGE_THRESHOLD默认为页的 50%

InnoDB 会开始寻找最靠近的页(前或后)看看是否可以将两个页合并以优化空间使用。

在这里插入图片描述

删除数据,并将页合并之后,再次插入新的数据 21,则直接插入 3# 页

在这里插入图片描述
这个里面所发生的合并页的这个现象,就称之为 “页合并”。


  • 注意事项
    • MERGE_THRESHOLD合并页的阈值可以自己设置在创建表或者创建索引时指定

3.2.4.索引设计原则


  • 满足业务需求的情况下,尽量降低主键的长度。
  • 插入数据时,尽量选择顺序插入,选择使用 AUTO_INCREMENT 自增主键
  • 尽量不要使用 UUID 做主键或者是其他自然主键,如身份证号。
  • 业务操作时,避免对主键的修改。

在这里插入图片描述


3.3.order by 优化


3.3.1.两种优化方式


MySQL 的排序,有两种方式:

  • Using filesort
    • 通过表的索引或全表扫描,读取满足条件的数据行
      然后在排序缓冲区sort buffer 中完成排序操作
      所有不是通过索引直接返回排序结果的排序都叫 FileSort 排序
  • Using index
    • 通过有序索引顺序扫描直接返回有序数据,这种情况即为 using index
      不需要额外排序,操作效率高

对于以上的两种排序方式,Using index 的性能高,而 Using filesort 的性能低,我们在优化排序操作时,尽量要优化为 Using index


3.3.2.测试


  1. 数据准备

把之前测试时,为 tb_user 表所建立的部分索引直接删除掉

drop index idx_user_phone on tb_user;

drop index idx_user_phone_name on tb_user;

drop index idx_user_name on tb_user;

执行上述操作后,表 tb_user 还剩如下索引

在这里插入图片描述


  1. 执行排序 SQL
explain select id, age, phone from tb_user order by age ;
explain select id, age, phone from tb_user order by age, phone ;

在这里插入图片描述
在这里插入图片描述

由于 agephone 都没有索引,所以此时再排序时,出现 Using filesort,排序性能较低。


  1. 创建索引
create index idx_user_age_phone_aa on tb_user( age, phone );

  1. 创建索引后根据 agephone 进行升序排序
explain select id, age, phone from tb_user order by age;
explain select id, age, phone from tb_user order by age , phone;

在这里插入图片描述
在这里插入图片描述

建立索引之后,再次进行排序查询,就由原来的 Using filesort, 变为了 Using index,性能就是比较高的了。


  1. 创建索引后根据 agephone 进行降序排序
explain select id, age, phone from tb_user order by age desc , phone desc ;

在这里插入图片描述

此处也出现了 Using index, 但是此时 Extra 中出现了 Backward index scan,这个代表反向扫描索引。

因为在 MySQL 中我们创建的索引,默认索引的叶子节点是从小到大排序的

而此时我们查询排序时,是从大到小,所以,在扫描时,就是反向扫描,就会出现 Backward index scan

  • MySQL 8 版本中,支持降序索引,我们也可以创建降序索引。
  • MySQL 5.7 并不支持降序索引。
  • 有兴趣的话,可以看看这篇博客:MySQL8.0 新特性之降序索引

  1. 根据 phoneage 进行升序排序phone 在前age 在后
explain select id, age, phone from tb_user order by phone, age ;

在这里插入图片描述

排序时,也需要满足最左前缀法则,否则也会出现 filesort

因为在创建索引的时候, age 是第一个字段,phone 是第二个字段

所以排序时,也就该按照这个顺序来,否则就会出现 Using filesort


  1. 根据 agephone 进行降序一个升序,一个降序
explain select id, age, phone from tb_user order by age asc , phone desc ;

在这里插入图片描述
因为创建索引时,如果未指定顺序,默认都是按照升序排序的。

而查询时,一个升序,一个降序,此时就会出现 Using filesort

在这里插入图片描述
为了解决上述的问题,我们可以创建一个索引,这个联合索引中 age 升序排序,phone 倒序排序。


  1. 创建联合索引age 升序排序,phone 倒序排序
create index idx_user_age_phone_ad on tb_user( age asc , phone desc ) ;

在这里插入图片描述


  1. 然后再次执行如下 SQL
explain select id, age, phone from tb_user order by age asc , phone desc ;

在这里插入图片描述


升序 / 降序联合索引结构图示

在这里插入图片描述
在这里插入图片描述


3.3.3.order by 优化原则


由上述的测试,我们得出 order by 优化原则:

  1. 根据排序字段建立合适的索引,多字段排序时,也遵循最左前缀法则。
  2. 尽量使用覆盖索引。
  3. 多字段排序, 一个升序一个降序,此时需要注意联合索引在创建时的规则(ASC/DESC)。
  4. 如果不可避免的出现 filesort,大数据量排序时,可以适当增大排序缓冲区大小 sort_buffer_size(默认 256k)。

3.4.group by 优化


分组操作,我们主要来看看索引对于分组操作的影响。


首先我们先将 tb_user 表的索引全部删除掉 。

drop index idx_user_pro_age_sta on tb_user;

drop index idx_email_5 on tb_user;

drop index idx_user_age_phone_aa on tb_user;

drop index idx_user_age_phone_ad on tb_user;

接下来,在没有索引的情况下,执行如下 SQL,查询执行计划

explain select profession , count(*) from tb_user group by profession ;

在这里插入图片描述

ExtraMySQL 5 中(explain select profession , count(*) from tb_user group by profession ;)还多了一个 Using filesort

然后,我们在针对于 professionagestatus 创建一个联合索引。

create index idx_user_pro_age_sta on tb_user(profession , age , status);

紧接着,再执行前面相同的 SQL 查看执行计划。

explain select profession , count(*) from tb_user group by profession ;

在这里插入图片描述

再执行如下的分组查询 SQL,查看执行计划:

在这里插入图片描述

  • 教学视频中使用的是 MySQL 8 ,而我使用的是 MySQL 5
  • ExtraMySQL 5 中(explain select age , count(*) from tb_user group by age ;)还多了一个 Using filesort
  • Using filesort 表示在索引之外,需要额外进行外部的排序动作。
  • 导致该问题的原因一般和 order by 有者直接关系,一般可以通过合适的索引来减少或者避免。
  • MySQL 8.0 以前的版本会对 group by 操作进行隐式排序
  • 有兴趣的话可以看看这篇博客:MySQL 中出现 using filesort 现象的一般情况与分析

我们发现,如果仅仅根据 age 分组,就会出现 Using temporary

而如果是 根据 professionage 两个字段同时分组,则不会出现 Using temporary

原因是因为对于分组操作,在联合索引中,也是符合最左前缀法则的。

所以,在分组操作中,我们需要通过以下两点进行优化,以提升性能:

  1. 在分组操作时,可以通过索引来提高效率。
  2. 分组操作时,索引的使用也是满足最左前缀法则的。

3.5.limit 优化


在数据量比较大时,如果进行 limit 分页查询,在查询时,越往后,分页查询效率越低。

我们一起来看看执行 limit 分页查询耗时对比:

cat /var/lib/mysql/centos7-slow.log;

在这里插入图片描述

通过测试我们会看到,越往后,分页查询效率越低,这就是分页查询的问题所在。

因为,当在进行分页查询时,如果执行 limit 2000000 , 10,此时需要 MySQL 排序前 2000010 记录,仅仅返回 2000000 - 2000010 的记录,其他记录丢弃,查询排序的代价非常大 。


优化思路:一般分页查询时,通过创建覆盖索引能够比较好地提高性能

这里可以通过覆盖索引子查询形式进行优化。

explain select * from tb_sku t , (select id from tb_sku order by id limit 2000000, 10) a where t.id = a.id ;

3.6.count 优化


3.6.1.概述


SELECT COUNT(*) FROM tb_user ;

在之前的测试中,我们发现,如果数据量很大,在执行 count 操作时,是非常耗时的。

MyISAM 引擎

  • 这个引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个数,效率很高;
  • 但是如果是带条件的 countMyISAM 也慢。

InnoDB 引擎

  • 这个引擎就麻烦了,它执行 count(*) 的时候,需要把数据一行一行地从引擎里面读出来,然后累积计数。
  • 如果说要大幅度提升 InnoDB 表的 count 效率,主要的优化思路有:自己计数
    • 可以借助于 redis 这样的数据库进行,但是如果是带条件的 count 又比较麻烦了。

3.6.2.count 用法


count() 是一个聚合函数,对于返回的结果集,一行行地判断

  • 如果 count 函数的参数不是 NULL,累计值就加 1
  • 否则不加,最后返回累计值。

用法:count(*)count(主键)count(字段)count(数字)


count 用法含义
count(主键)InnoDB 引擎会遍历整张表,把每一行的 主键 id 值都取出来,返回给服务层。
服务层拿到主键后,直接按行进行累加(主键不可能为 null
count(字段)没有 not null 约束:
    InnoDB 引擎会遍历整张表把每一行的字段值都取出来,返回给服务层,服务层判断是否为 null,不为 null,计数累加。

not null 约束:
    InnoDB 引擎会遍历整张表把每一行的字段值都取出来,返回给服务层,直接按行进行累加。
count(数字)InnoDB 引擎遍历整张表,但不取值。
服务层对于返回的每一行,放一个数字 “1” 进去,直接按行进行累加。
count(*)InnoDB 引擎并不会把全部字段取出来,而是专门做了优化,不取值,服务层直接按行进行累加。

按照效率排序的话,count(字段) < count(主键 id) < count(1)count(*),所以尽量使用 count(*)


3.7.update 优化


我们主要需要注意一下 update 语句执行时的注意事项。

update course set name = 'javaEE' where id = 1 ;

当我们在执行删除的 SQL 语句时,会锁定 id 为 1 这一行的数据,然后事务提交之后,行锁释放。

但是当我们在执行如下 SQL 时。

update course set name = 'SpringBoot' where name = 'PHP' ;

当我们开启多个事务,在执行上述的 SQL 时,我们发现行锁升级为了表锁。 导致该 update 语句的性能大大降低。


  • 注意事项InnoDB 的行锁是针对 索引 加的锁不是针对记录加的锁并且该索引不能失效否则会从行锁升级为表锁

3.8.SQL优化小结


  1. 插入数据
insert:批量插入、手动控制事务、主键顺序插入

大批量插入:load data local infile
  1. 主键优化
主键长度尽量短、顺序插入 

AUTO_INCREMENT

不要使用 UUID 这类很长的数据作为主键
  1. order by 优化
using index:直接通过索引返回数据,性能高

using filesort:需要将返回的结果在排序缓存区排序
  1. group by 优化
索引,多字段分组满足最左前缀法则
  1. limit 优化
覆盖索引 + 子查询
  1. count 优化
性能:count(字段) < count(主键 id) < count(1)count(*)
  1. update 优化
尽量根据 主键/索引 字段进行数据更新

4.视图


4.1.介绍


视图(View)是一种虚拟存在的表。

视图中的数据并不在数据库中实际存在,行和列数据来自定义视图的查询中使用的表,并且是在使用视图时动态生成的。

通俗的讲,视图只保存了查询的 SQL 逻辑,不保存查询结果。所以我们在创建视图的时候,主要的工作就落在创建这条 SQL 查询语句上。


4.2.基本语法


  1. 创建
CREATE [OR REPLACE] VIEW 视图名称[(列名列表)] AS SELECT语句 [ WITH [ CASCADED | LOCAL ] CHECK OPTION ]
  1. 查询

查看创建视图语句

SHOW CREATE VIEW 视图名称;

查看视图数据

SELECT * FROM 视图名称 ...... ;
  1. 修改

方式一

CREATE [OR REPLACE] VIEW 视图名称[(列名列表)] AS SELECT语句 [ WITH [ CASCADED | LOCAL ] CHECK OPTION ]

方式二

ALTER VIEW 视图名称[(列名列表)] AS SELECT语句 [ WITH [ CASCADED | LOCAL ] CHECK OPTION ]
  1. 删除
DROP VIEW [IF EXISTS] 视图名称 [,视图名称] ...

演示案例

  • 创建视图
CREATE OR REPLACE VIEW stu_v_1 AS SELECT id, name FROM student WHERE id <= 10;
  • 查询视图
SHOW CREATE VIEW stu_v_1;

在这里插入图片描述

SELECT * FROM stu_v_1;
SELECT * FROM stu_v_1 WHERE id < 3;
  • 修改视图
CREATE OR REPLACE VIEW stu_v_1 AS SELECT id , name , no FROM student WHERE id <= 10;
ALTER VIEW stu_v_1 AS SELECT id , name FROM student WHERE id <= 10 ;
  • 删除视图
DROP VIEW IF EXISTS stu_v_1;

上述我们演示了,视图应该如何创建、查询、修改、删除,那么我们能不能通过视图来插入、更新数据呢?

接下来,做一个测试。

CREATE OR REPLACE VIEW stu_v_1 AS SELECT id , name FROM student WHERE id <= 10 ;
SELECT * FROM stu_v_1;
INSERT INTO stu_v_1 VALUES( 6 , 'Tom' );
INSERT INTO stu_v_1 VALUES( 17 , 'Tom22' );

执行上述的 SQL,我们会发现,id 为 6 和 17 的数据都是可以成功插入的。

但是我们执行查询,查询出来的数据,却没有 id 为 17 的记录。

在这里插入图片描述
因为我们在创建视图的时候,指定的条件为 id <= 10

id 为 17 的数据,是不符合条件(id <= 10)的,所以没有查询出来,但是这条数据确实是已经成功的插入到了基表中。

如果我们定义视图时就指定了条件,然后我们在插入、修改、删除数据时,是否可以做到必须满足条件才能操作,否则不能够操作呢?

答案是可以的,这就需要借助于视图的检查选项了。

CREATE OR REPLACE VIEW stu_v_1 AS SELECT id , name  FROM student WHERE id <= 20 WITH CASCADED CHECK OPTION ;

在这里插入图片描述


4.3.检查选项


当使用 WITH CHECK OPTION 子句创建视图时,MySQL 会通过视图检查正在更改的每个行,例如 插入,更新,删除,以使其符合视图的定义。

MySQL 允许基于另一个视图创建视图,它还会检查依赖视图中的规则以保持一致性。

为了确定检查的范围MySQL 提供了两个选项: CASCADEDLOCAL,默认值为 CASCADED


  1. CASCADED级联

比如,v2 视图是基于 v1 视图的,如果在 v2 视图创建的时候指定了检查选项为 cascaded,但是 v1 视图创建时未指定检查选项。

在执行检查时,不仅会检查 v2,还会级联检查 v2 的关联视图 v1

即使 v1 视图 没有加语句 with cascaded check option,也照样进行条件的检查,这也是级联检查的含义。

在这里插入图片描述


  1. LOCAL本地

比如,v2 视图是基于 v1 视图的,如果在 v2 视图创建的时候指定了检查选项为 local,但是 v1 视图创建时未指定检查选项。

则在执行检查时,只会检查 v2,不会检查 v2 的关联视图 v1

在这里插入图片描述


4.4.视图的更新


要使视图可更新,视图中的基础表中的之间必须存在一对一的关系。

如果视图包含以下任何一项,则该视图不可更新:

  1. 聚合函数或窗口函数(SUM()MIN()MAX()COUNT() 等)
  2. DISTINCT
  3. GROUP BY
  4. HAVING
  5. UNION 或者 UNION ALL

示例演示

CREATE VIEW stu_v_count AS SELECT COUNT(*) FROM student;

在这里插入图片描述

INSERT INTO stu_v_count VALUES(10) ;

在这里插入图片描述


4.5.视图作用


  1. 简单

视图不仅可以简化用户对数据的理解,也可以简化他们的操作。

那些被经常使用的查询可以被定义为视图,从而使得用户不必为以后的操作每次指定全部的条件。


  1. 安全

数据库可以授权,但不能授权到数据库特定行和特定的列上。

通过视图用户只能查询和修改他们所能见到的数据


  1. 数据独立

视图可帮助用户屏蔽真实表结构变化带来的影响。

视图可返回与底层表的表示和格式不同的数据。


4.6.案例


  1. 为了保证数据库表的安全性,开发人员在操作 tb_user表 时,只能看到的用户的基本字段,屏蔽手机号和邮箱两个字段。
CREATE VIEW tb_user_view 
AS SELECT id, name, profession, age, gender, status, createtime 
FROM
	tb_user;
SELECT * FROM tb_user_view ;

  1. 查询每个学生所选修的课程(三张表联查),这个功能在很多的业务中都有使用到,为了简化操作,定义一个视图。
CREATE VIEW tb_stu_course_view 
AS SELECT
	s.name student_name, s.no student_no, c.name course_name 
FROM
	student s, student_course sc, course c 
WHERE
	s.id = sc.studentid 
	AND sc.courseid = c.id;
SELECT * FROM tb_stu_course_view;

5. 存储过程


5.1.介绍


存储过程是事先经过编译并存储在数据库中的一段 SQL 语句的集合

调用存储过程可以简化应用开发人员的很多工作,减少数据在数据库和应用服务器之间的传输

对于提高数据处理的效率是有好处的。

存储过程思想上很简单,就是数据库 SQL 语言层面的代码封装与重用。

在这里插入图片描述


特点

  • 封装,复用
    • 可以把某一业务 SQL 封装在存储过程中,需要用到的时候直接调用即可。
  • 可以接收参数,也可以返回数据
    • 在存储过程中,可以传递参数,也可以接收返回值。
  • 减少网络交互,效率提升
    • 如果涉及到多条 SQL,每执行一次都是一次网络传输。 而如果封装在存储过程中,我们只需要网络交互一次可能就可以了。

5.2.基本语法


  1. 创建
CREATE PROCEDURE 存储过程名称 ([ 参数列表 ])
BEGIN
	-- SQL语句
END ;

  1. 调用
CALL 名称 ([ 参数 ]);

  1. 查看

查询指定数据库的存储过程及状态信息

SELECT * FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = 'xxx'; 

查询某个存储过程的定义

SHOW CREATE PROCEDURE 存储过程名称 ; 

  1. 删除
DROP PROCEDURE [ IF EXISTS ] 存储过程名称 ;

  • 注意在命令行中,执行创建存储过程的 SQL 时,需要通过关键字 delimiter 指定 SQL 语句的结束符。

  • 示例

创建

CREATE PROCEDURE p1()
BEGIN
	SELECT COUNT(*) FROM student;
END;

调用

CALL p1();

查看

SELECT * FROM information_schema.routines WHERE routine_schema = 'itcast'; 
SHOW CREATE PROCEDURE p1;

删除

DROP PROCEDURE IF EXISTS p1;

5.3.变量


MySQL 中变量分为三种类型:系统变量、用户定义变量、局部变量。


5.3.1.系统变量


系统变量 是 MySQL 服务器提供,不是用户定义的,属于服务器层面。

分为全局变量GLOBAL)、会话变量SESSION)。


  1. 查看系统变量

查看所有系统变量

SHOW [ SESSION | GLOBAL ] VARIABLES ; 

可以通过 LIKE 模糊匹配方式查找变量

SHOW [ SESSION | GLOBAL ] VARIABLES LIKE '......'; 

查看指定变量的值

SELECT @@[SESSION | GLOBAL] 系统变量名;

  1. 设置系统变量
SET [ SESSION | GLOBAL ] 系统变量名 =;
SET @@[SESSION | GLOBAL]系统变量名 =;

  • 注意事项

    • 如果没有指定 SESSION/GLOBAL,默认是 SESSION,会话变量。
      • MySQL 服务重新启动之后,所设置的全局参数会失效,要想不失效,可以在 /etc/my.cnf 中配置。
    • 全局变量(GLOBAL)针对于所有的会话。
    • 会话变量(SESSION)针对于单个会话,在另外一个会话窗口就不生效了。

演示示例

  • 查看系统变量
show session variables ;

show session variables like 'auto%';

show global variables like 'auto%';

select @@global.autocommit;

select @@session.autocommit;
  • 设置系统变量
-- 关闭自动提交
set session autocommit = 1;

-- 测试一条 sql 语句
insert into course(id, name) VALUES (6, 'ES');

-- 开启自动提交
set global autocommit = 1;

select @@global.autocommit;

5.3.2.用户定义变量


用户定义变量 是用户根据需要自己定义的变量,用户变量不用提前声明,在用的时候直接用 "@变量名" 使用就可以。

其作用域为当前连接。


  1. 赋值

方式一

SET @var_name = expr [, @var_name = expr] ... ;
SET @var_name := expr [, @var_name := expr] ... ;

赋值时,可以使用 =,也可以使用 :=

方式二

SELECT @var_name := expr [, @var_name := expr] ... ;
SELECT 字段名 INTO @var_name FROM 表名;

  1. 使用
SELECT @var_name ;

  • 注意:用户定义的变量无需对其进行声明或初始化,只不过获取到的值为 NULL

示例

  • 赋值
SET @myname = 'itcast';

SET @myage := 10;

SET @mygender := '男' , @myhobby := 'java';

SELECT @mycolor := 'red';
SELECT COUNT(*) INTO @mycount FROM tb_user;
  • 使用
SELECT @myname , @myage , @mygender , @myhobby ;

SELECT @mycolor , @mycount;
SELECT @abc;

5.3.3.局部变量


  • 局部变量是根据需要定义的在局部生效的变量,访问之前,需要 DECLARE 声明。
  • 局部变量可用作存储过程内的局部变量和输入参数
  • 局部变量的范围是在其内声明的 BEGIN ... END 块。

  1. 声明
DECLARE 变量名 变量类型 [DEFAULT ... ] ;

变量类型就是数据库字段类型:INTBIGINTCHARVARCHARDATETIME 等。


  1. 赋值
SET 变量名 =;

SET 变量名 :=;

SELECT 字段名 INTO 变量名 FROM 表名 ... ;

演示示例

-- 声明局部变量 - declare
-- 赋值
CREATE PROCEDURE p2()
BEGIN
	DECLARE stu_count INT DEFAULT 0;
	SELECT COUNT( * ) INTO stu_count FROM student;
	SELECT stu_count;
END
CALL p2();

5.4.if


  • 介绍

if 用于做条件判断,具体的语法结构为

IF 条件1 THEN
	.....
ELSEIF 条件2 THEN 		-- 可选
	.....
ELSE 					-- 可选
	.....
END IF;

if 条件判断的结构中,ELSE IF 结构可以有多个,也可以没有。

ELSE 结构可以有,也可以没有。


  • 案例

根据定义的分数 score 变量,判定当前分数对应的分数等级。

  1. score >= 85 分,等级为优秀。
  2. score >= 60 分score < 85 分,等级为及格。
  3. score < 60 分,等级为不及格。
CREATE PROCEDURE p3 () 
BEGIN 
	DECLARE score INT DEFAULT 58;
	DECLARE result VARCHAR ( 10 );
		
	IF score >= 85 THEN
		SET result := '优秀';
	ELSEIF score >= 60 THEN
		SET result := '及格';
	ELSE 
		SET result := '不及格';	
	END IF;
	
	SELECT result;
END
CALL p3();

上述的需求我们虽然已经实现了,但是也存在一些问题

比如:score 分数我们是在存储过程中定义死的,而且最终计算出来的分数等级,我们也仅仅是最终查询展示出来而已。

那么我们能不能,把 score 分数动态的传递进来,计算出来的分数等级是否可以作为返回值返回呢 ?

答案是肯定的,我们可以通过接下来所讲解的 参数 来解决上述的问题。


5.5.参数


  1. 介绍

参数的类型,主要分为以下三种:INOUTINOUT

具体的含义如下

类型含义备注
IN该类参数作为输入,也就是需要调用时传入值默认
OUT该类参数作为输出,也就是该参数可以作为返回值
INOUT既可以作为输入参数,也可以作为输出参数

  1. 用法
CREATE PROCEDURE 存储过程名称 ([ IN/OUT/INOUT 参数名 参数类型 ])
BEGIN
	-- SQL语句
END ;

  1. 案例

案例一:根据传入的参数 score,判定当前分数对应的分数等级,并返回。

  1. score >= 85 分,等级为优秀。
  2. score >= 60 分score < 85 分,等级为及格。
  3. score < 60 分,等级为不及格。
CREATE PROCEDURE p4 (IN score INT, OUT result VARCHAR(10)) 
BEGIN 
	IF score >= 85 THEN
		SET result := '优秀';
	ELSEIF score >= 60 THEN
		SET result := '及格';
	ELSE 
		SET result := '不及格';	
	END IF;
	
	SELECT result;
END
CALL p4( 99 , @result );

CALL p4( 69 , @result );

CALL p4( 39 , @result );

案例二:将传入的 200 分制的分数,进行换算,换算成百分制,然后返回。

CREATE PROCEDURE p5(INOUT score DOUBLE)
BEGIN
	SET score := score * 0.5;
END
SET @score = 78;

CALL p5( @score );

SELECT @score;

5.6 case


  1. 介绍

case 结构及作用,和流程控制函数很类似。

有两种语法格式:

  • 语法一
-- 含义:
-- 当 case_value 的值为 when_value1 时,执行 statement_list1
-- 当值为 when_value2时,执行 statement_list2
-- 否则就执行 statement_list

CASE case_value
	WHEN when_value1 THEN statement_list1
	[ WHEN when_value2 THEN statement_list2] ...
	[ ELSE statement_list ]
END CASE;
  • 语法二
-- 含义:
-- 当条件 search_condition1 成立时,执行 statement_list1
-- 当条件 search_condition2 成立时,执行 statement_list2
-- 否则就执行 statement_list

CASE
	WHEN search_condition1 THEN statement_list1
	[WHEN search_condition2 THEN statement_list2] ...
	[ELSE statement_list]
END CASE;

案例:根据传入的月份,判定月份所属的季节(要求采用 case 结构)。

  • 1-3 月份,为第一季度
  • 4-6 月份,为第二季度
  • 7-9 月份,为第三季度
  • 10-12 月份,为第四季度
CREATE PROCEDURE p6( IN month INT )
BEGIN
	DECLARE result VARCHAR( 10 );
	
	CASE 
		WHEN  month >= 1 and month <= 3  THEN
			SET result := '第一季度';
		WHEN month >= 4 and month <= 6 THEN
			SET result := '第二季度';
		WHEN month >= 7 and month <= 9 THEN
			SET result := '第三季度';
		WHEN month >= 10 and month <= 12 THEN
			SET result := '第四季度';
		ELSE
			SET result := '非法参数';
	END CASE;

	
	SELECT CONCAT('您输入的月份是:', month, ',其所属的季度是:', result);
END
CALL p6( 16 );

CALL p6( 11 );

  • 注意:如果判定条件有多个,多个条件之间,可以使用 andor 进行连接。

5.7.while


  1. 介绍

while 循环是有条件的循环控制语句。

满足条件后,再执行循环体中的 SQL 语句。

具体语法为:

-- 先判定条件,如果条件为 true,则执行逻辑
-- 否则,不执行逻辑

WHILE 条件 DO
	SQL逻辑...
END WHILE;

  1. 案例:计算从 1 累加到 n 的值,n 为传入的参数值。
-- A. 定义局部变量, 记录累加之后的值;
-- B. 每循环一次, 就会对 n 进行减 1 , 如果 n 减到 0, 则退出循环

CREATE PROCEDURE p7( IN n INT )
BEGIN
	DECLARE total int DEFAULT 0;
	
	WHILE n > 0 DO
		set total := total + n;
		set n := n-1;
	END WHILE;

	SELECT total;

END;
CALL p7( 100 );

5.8.repeat


介绍:repeat 是有条件的循环控制语句, 当满足 until 声明的条件的时候,则退出循环 。


具体语法

-- 先执行一次逻辑,然后判定 UNTIL 条件是否满足
-- 如果满足,则退出。
-- 如果不满足,则继续下一次循环

REPEAT
	SQL逻辑...
	UNTIL 条件
END REPEAT;

案例:计算从 1 累加到 n 的值,n 为传入的参数值。(使用 repeat 实现)

CREATE PROCEDURE p8( IN n INT )
BEGIN

	DECLARE total INT DEFAULT 0;
	
	REPEAT
		set total := total + n;
		set n := n - 1;
	UNTIL n <= 0 END REPEAT;
	
	SELECT total;
END;

5.9.loop


  • 介绍

LOOP 实现简单的循环,如果不在 SQL 逻辑中增加退出循环的条件,可以用其来实现简单的死循环。

LOOP 可以配合一下两个语句使用:

  • LEAVE:配合循环使用,退出循环。
  • ITERATE:必须用在循环中,作用是跳过当前循环剩下的语句,直接进入下一次循环。
[begin_label:] LOOP
	SQL逻辑...
END LOOP [end_label];
LEAVE label; 	-- 退出指定标记的循环体

ITERATE label; 	-- 直接进入下一次循环

上述语法中出现的 begin_labelend_labellabel 指的都是我们所自定义的标记。


  • 案例一:计算从 1 累加到 n 的值,n 为传入的参数值。
-- A. 定义局部变量, 记录累加之后的值;
-- B. 每循环一次, 就会对 n 进行 -1 , 如果 n 减到 0, 则退出循环 ----> leave xx

CREATE PROCEDURE p9 ( IN n INT ) 
BEGIN
	DECLARE total INT DEFAULT 0;
	
	sum : LOOP
		IF n <= 0 THEN
			LEAVE sum;
		END IF;
		
		SET total := total + n;
		SET n := n - 1;
	END LOOP sum;
	
	SELECT total;
END;
CALL p9(100);

  • 案例二:计算从 1 到 n 之间的偶数累加的值,n 为传入的参数值。
-- A. 定义局部变量, 记录累加之后的值;
-- B. 每循环一次, 就会对 n 进行 -1 , 如果 n 减到 0, 则退出循环 ----> leave xx
-- C. 如果当次累加的数据是奇数, 则直接进入下一次循环. --------> iterate xx

CREATE PROCEDURE p10 ( IN n INT ) BEGIN
	DECLARE total INT DEFAULT 0;
	
	sum : LOOP
		IF n <= 0 THEN
			LEAVE sum;
		END IF;
		
		IF n % 2 = 0 THEN
			SET n := n - 1;
			ITERATE sum;
		END IF;
		
		SET total := total + n;
		SET n := n - 1;
	END LOOP sum;
	
	SELECT total;
END;
CALL p10( 10 );

5.10.游标


  • 介绍

游标CURSOR是用来 存储 查询结果集 的数据类型在存储过程和函数中可以使用游标对结果集进行 循环 的处理


游标的使用包括 游标的声明OPENFETCHCLOSE

  • 语法
    • 声明游标
      DECLARE 游标名称 CURSOR FOR 查询语句 ;
      
    • 打开游标
      OPEN 游标名称 ;
      
    • 获取游标记录
      FETCH 游标名称 INTO 变量 [, 变量 ] ;
      
    • 关闭游标
      CLOSE 游标名称 ;
      

  • 案例

根据传入的参数 uage,来查询用户表 tb_user 中,所有的用户年龄小于等于 uage 的用户姓名(name)和专业(profession

并将用户的姓名和专业插入到所创建的一张新表(idnameprofession)中。

-- 逻辑
-- A. 声明游标, 存储查询结果集
-- B. 准备: 创建表结构
-- C. 开启游标
-- D. 获取游标中的记录
-- E. 插入数据到新表中
-- F. 关闭游标
CREATE PROCEDURE p11(IN uage INT)
BEGIN
	DECLARE uname VARCHAR( 100 ) ;
	DECLARE upro VARCHAR( 100 ) ;
	DECLARE u_cursor CURSOR FOR SELECT `name` , profession FROM tb_user WHERE age <= uage ;
	
	DROP TABLE IF EXISTS tb_user_pro ;
	
	CREATE TABLE IF NOT EXISTS tb_user_pro(
		id INT PRIMARY KEY auto_increment ,
		`name` VARCHAR( 100 ) ,
		profession VARCHAR( 100 )
	);
	
	OPEN u_cursor ;
	
	WHILE TRUE DO
		FETCH u_cursor INTO uname , upro ;
		INSERT INTO tb_user_pro VALUES ( null , uname , upro) ;
	END WHILE;
	
	CLOSE u_cursor ;

END;
CALL p11( 40 );

上述的存储过程,最终我们在调用的过程中,会报错,之所以报错是因为上面的 while 循环中,并没有退出条件。

当游标的数据集获取完毕之后,再次获取数据,就会报错,从而终止了程序的执行。

在这里插入图片描述
但是此时,tb_user_pro 表结构及其数据都已经插入成功了,我们可以直接刷新表结构,检查表结构中的数据。

在这里插入图片描述
上述的功能,虽然我们实现了,但是逻辑并不完善,而且程序执行完毕,获取不到数据,数据库还报错。

接下来,我们就需要来完成这个存储过程,并且解决这个问题。

要想解决这个问题,就需要通过 MySQL 中提供的 条件处理程序 Handler 来解决。


5.11.条件处理程序


  1. 介绍

条件处理程序Handler)可以用来定义在流程控制结构执行过程中遇到问题时相应的处理步骤。

具体语法如下

DECLARE handler_action HANDLER FOR condition_value [, condition_value] ... statement ;

handler_action 的取值

  • CONTINUE:继续执行当前程序
  • EXIT:终止执行当前程序

condition_value 的取值

  • SQLSTATE sqlstate_value:状态码,如 02000
  • SQLWARNING:所有以 01 开头的 SQLSTATE 代码的简写
  • NOT FOUND:所有以 02 开头的 SQLSTATE 代码的简写
  • SQLEXCEPTION:所有没有被 SQLWARNINGNOT FOUND 捕获的 SQLSTATE 代码的简写

  1. 上一节的案例

我们继续来完成在上一小节提出的这个需求,并解决其中的问题。

根据传入的参数 uage,来查询用户表 tb_user 中,所有的用户年龄小于等于 uage 的用户姓名(name)和专业(profession

并将用户的姓名和专业插入到所创建的一张新表(idnameprofession)中。

  • 通过 SQLSTATE 指定具体的状态码
CREATE DEFINER=`root`@`%` PROCEDURE `p11`(IN uage INT)
BEGIN
	DECLARE uname VARCHAR( 100 ) ;
	DECLARE upro VARCHAR( 100 ) ;
	DECLARE u_cursor CURSOR FOR SELECT `name` , profession FROM tb_user WHERE age <= uage ;
	
	-- 声明条件处理程序:当 SQL 语句执行抛出的状态码为 02000 时,将关闭游标 u_cursor,并退出
	DECLARE EXIT HANDLER FOR SQLSTATE '02000' CLOSE u_cursor ;
	
	DROP TABLE IF EXISTS tb_user_pro ;
	
	CREATE TABLE IF NOT EXISTS tb_user_pro(
		id INT PRIMARY KEY auto_increment ,
		`name` VARCHAR( 100 ) ,
		profession VARCHAR( 100 )
	);
	
	OPEN u_cursor ;
	
	WHILE TRUE DO
		FETCH u_cursor INTO uname , upro ;
		INSERT INTO tb_user_pro VALUES ( null , uname , upro) ;
	END WHILE;
	
	CLOSE u_cursor ;

END

  1. 上一节的案例

02 开头的状态码,代码简写为 NOT FOUND

  • 通过 SQLSTATE 的代码简写方式 NOT FOUND
CREATE DEFINER=`root`@`%` PROCEDURE p12(IN uage INT)
BEGIN
	DECLARE uname VARCHAR( 100 ) ;
	DECLARE upro VARCHAR( 100 ) ;
	DECLARE u_cursor CURSOR FOR SELECT `name` , profession FROM tb_user WHERE age <= uage ;
	
	-- 声明条件处理程序 : 当 SQL 语句执行抛出的状态码为 02 开头时,将关闭游标 u_cursor,并退出
	-- 补充:NOT FOUND,即所有以 02 开头的 SQLSTATE 代码的简写
	DECLARE EXIT HANDLER FOR NOT FOUND CLOSE u_cursor ; 
	
	DROP TABLE IF EXISTS tb_user_pro ;
	
	CREATE TABLE IF NOT EXISTS tb_user_pro(
		id INT PRIMARY KEY auto_increment ,
		`name` VARCHAR( 100 ) ,
		profession VARCHAR( 100 )
	);
	
	OPEN u_cursor ;
	
	WHILE TRUE DO
		FETCH u_cursor INTO uname , upro ;
		INSERT INTO tb_user_pro VALUES ( null , uname , upro) ;
	END WHILE;
	
	CLOSE u_cursor ;

END

5.12.存储函数


  • 介绍:存储函数是有返回值的存储过程存储函数的参数只能是 IN 类型的

  • 具体语法
CREATE FUNCTION 存储函数名称 ([ 参数列表 ])
RETURNS type [characteristic ...]
BEGIN
	-- SQL语句
	RETURN ...;
END ;
  • characteristic 说明
    • DETERMINISTIC:相同的输入参数总是产生相同的结果
    • NO SQL:不包含 SQL 语句。
    • READS SQL DATA:包含读取数据的语句,但不包含写入数据的语句。

  • 案例:计算从 1 累加到 n 的值,n 为传入的参数值。
CREATE FUNCTION fun1(n INT)
RETURNS INT DETERMINISTIC
BEGIN
	DECLARE total INT DEFAULT 0;
	
	WHILE n > 0 DO
		SET total := total + n;
		SET n := n - 1;
	END WHILE;
	
	RETURN total;
END;
SELECT fun1( 100 );

MySQL 8.0 版本中 binlog 默认是开启的

一旦开启了,MySQL 就要求在定义存储过程时,需要指定 characteristic 特性,否则就会报如下错误

在这里插入图片描述


6. 触发器


6.1.介绍


触发器是与表有关的数据库对象

指在 insert/update/delete 之前(BEFORE)或之后(AFTER),触发并执行触发器中定义的 SQL 语句集合。


触发器的这种特性可以协助应用在数据库端确保数据的完整性、日志记录、数据校验等操作。

使用别名 OLDNEW 来引用触发器中发生变化的记录内容,这与其他的数据库是相似的。

现在触发器还只支持行级触发,不支持语句级触发。

触发器类型NEWOLD
INSERT 型触发器NEW 表示将要或者已经新增的数据
UPDATE 型触发器OLD 表示修改之前的数据,NEW 表示将要或已经修改后的数据
DELETE 型触发器OLD 表示将要或者已经删除的数据

6.2.语法


  1. 创建
CREATE TRIGGER trigger_name
BEFORE/AFTER INSERT/UPDATE/DELETE
ON tbl_name FOR EACH ROW -- 行级触发器
BEGIN
	trigger_stmt ;
END;

  1. 查看
SHOW TRIGGERS ;

  1. 删除
DROP TRIGGER [schema_name.]trigger_name ; -- 如果没有指定 schema_name,默认为当前数据库 。

6.3.案例


通过触发器记录 tb_user 表的数据变更日志

将变更日志插入到日志表 user_logs 中, 包含增加,修改,删除。


表结构准备

-- 准备工作 : 日志表 user_logs
CREATE TABLE user_logs (
	id INT ( 11 ) NOT NULL auto_increment,
	operation VARCHAR ( 20 ) NOT NULL COMMENT '操作类型, insert/update/delete' ,
	operate_time datetime NOT NULL COMMENT '操作时间' ,
	operate_id INT ( 11 ) NOT NULL COMMENT '操作的ID' ,
	operate_params VARCHAR ( 500 ) COMMENT '操作参数' ,
	PRIMARY KEY ( `id` ) 
) ENGINE = INNODB DEFAULT CHARSET = utf8;

  • 插入数据触发器
CREATE TRIGGER tb_user_insert_trigger 
	AFTER INSERT ON tb_user FOR EACH ROW
BEGIN
		INSERT INTO user_logs ( id, operation, operate_time, operate_id, operate_params )
		VALUES
			(
				NULL, 'insert', now(), new.id,
				CONCAT( '插入的数据内容是:id = ', NEW.id, ',name = ', NEW.name, ',phone = ', NEW.phone, ',email = ', NEW.email , ',profession = ', NEW.profession) 
			);
END;
  • 测试
SHOW TRIGGERS;
INSERT INTO tb_user ( id, `name`, phone, email, profession, age, gender, `status`, createtime )
VALUES
	( 26, '三皇子', '18809091212', 'erhuangzi@163.com', '软件工程', 23, '1', '1', now() );

测试完毕之后,检查日志表中的数据是否可以正常插入,以及插入数据的正确性。


  • 修改数据触发器
CREATE TRIGGER tb_user_update_trigger 
	AFTER UPDATE ON tb_user FOR EACH ROW
BEGIN
		INSERT INTO user_logs ( id, operation, operate_time, operate_id, operate_params )
		VALUES
			(
				NULL, 'update', now(), new.id,
				CONCAT( '更新前的数据内容是:id = ', OLD.id, ',name = ', OLD.name, ',phone = ', OLD.phone, ',email = ', OLD.email , ',profession = ', OLD.profession, 
						'| 更新后的数据内容是:id = ', NEW.id, ',name = ', NEW.name, ', phone = ', NEW.phone, ',email = ', NEW.email , ',profession = ', NEW.profession
					) 
			);
END;
  • 测试
SHOW TRIGGERS;
UPDATE tb_user SET profession = '会计' WHERE id = 23;
UPDATE tb_user SET profession = '会计' WHERE id <= 5;

测试完毕之后,检查日志表中的数据是否可以正常插入,以及插入数据的正确性。


  • 删除数据触发器
CREATE TRIGGER tb_user_delete_trigger 
	AFTER DELETE ON tb_user FOR EACH ROW
BEGIN
		INSERT INTO user_logs ( id, operation, operate_time, operate_id, operate_params )
		VALUES
			(NULL, 'delete', now(), OLD.id,
				CONCAT( '删除前的数据内容是:id = ', OLD.id, ',name = ', OLD.name, ',phone = ', OLD.phone, ',email = ', OLD.email , ',profession = ', OLD.profession) );
END;
  • 测试
SHOW TRIGGERS;
DELETE FROM tb_user WHERE id = 26;

测试完毕之后,检查日志表中的数据是否可以正常插入,以及插入数据的正确性。


7.锁


7.1.概述


锁是计算机协调多个进程或线程并发访问某一资源的机制。

在数据库中,除传统的计算资源(CPURAMI/O)的争用以外,数据也是一种供许多用户共享的资源。

如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。

从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。


MySQL 中的锁,按照锁的粒度分,分为以下三类:

  • 全局锁:锁定数据库中的所有表。
  • 表级锁:每次操作锁住整张表。
  • 行级锁:每次操作锁住对应的行数据。

7.2.全局锁


7.2.1.介绍


全局锁就是对整个数据库实例加锁,加锁后整个实例就处于只读状态

后续的 DML 的写语句,DDL 语句,已经更新操作的事务提交语句都将被阻塞。

其典型的使用场景是做全库的逻辑备份,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性。


为什么全库逻辑备份,就需要加全就锁呢?


  1. 我们一起先来分析一下不加全局锁,可能存在的问题。

假设在数据库中存在这样三张表:tb_stock 库存表,tb_order 订单表,tb_orderlog 订单日志表。

在这里插入图片描述

在进行数据备份时,先备份了 tb_stock 库存表。

然后接下来,在业务系统中,执行了下单操作,扣减库存,生成订单(更新 tb_stock 表,插入 tb_order 表)。

然后再执行备份 tb_order 表的逻辑。

业务中执行插入订单日志操作。

最后,又备份了 tb_orderlog 表。

此时备份出来的数据,是存在问题的。

因为备份出来的数据,tb_stock 表与 tb_order 表的数据不一致(有最新操作的订单信息,但是库存数没减)。

那如何来规避这种问题呢?

此时就可以借助于 MySQL 的全局锁来解决。


  1. 再来分析一下加了全局锁后的情况

在这里插入图片描述

对数据库进行进行逻辑备份之前,先对整个数据库加上全局锁

一旦加了全局锁之后,其他的 DDLDML 全部都处于阻塞状态

但是可以执行 DQL 语句,也就是处于只读状态,而数据备份就是查询操作。

那么数据在进行逻辑备份的过程中,数据库中的数据就是不会发生变化的,这样就保证了数据的一致性和完整性。


7.2.2.语法


  1. 全局锁
FLUSH TABLES WITH READ LOCK ;
  1. 数据备份
mysqldump -uroot –p1234 itcast > itcast.sql

数据备份的相关指令,在后面 MySQL 管理章节,还会详细讲解。

  1. 释放锁
UNLOCK TABLES ;

7.2.3.特点


数据库中加全局锁,是一个比较重的操作,存在以下问题:

  • 如果在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆。
  • 如果在从库上备份,那么在备份期间从库不能执行主库同步过来的二进制日志(binlog),会导致主从延迟。

InnoDB 引擎中,我们可以在备份时加上参数 --single-transaction 参数来完成不加锁的一致性数据备份。

mysqldump --single-TRANSACTION -uroot –p123456 itcast > itcast.sql

7.3.表级锁


7.3.1.介绍


表级锁,每次操作锁住整张表。

锁定粒度大,发生锁冲突的概率最高,并发度最低。

应用在 MyISAMInnoDBBDB 等存储引擎中。


对于表级锁,主要分为三类:表锁、元数据锁(meta data lockMDL)、意向锁


7.3.2.表锁


对于表锁,分为两类:

  • 表共享读锁(read lock
  • 表独占写锁(write lock

语法

  • 加锁:lock tables 表名... read/write
  • 释放锁:unlock tables 或 断开客户端连接

特点

  1. 读锁

在这里插入图片描述

在这里插入图片描述

左侧为客户端一,对指定表加了读锁,不会影响右侧客户端二的读,但是会阻塞右侧客户端的写。

  1. 写锁

在这里插入图片描述

在这里插入图片描述

左侧为客户端一,对指定表加了写锁,会阻塞右侧客户端的读和写。


  • 结论:读锁不会阻塞其他客户端的读,但是会阻塞写。写锁既会阻塞其他客户端的读,又会阻塞其他客户端的写。

7.3.3.元数据锁


元数据锁(meta data lock,简写是 MDL

  • MDL 加锁过程是系统自动控制,无需显式使用,在访问一张表的时候会自动加上。
  • MDL 锁主要作用是维护表元数据的数据一致性,在表上有活动事务的时候,不可以对元数据进行写入操作。
  • 为了避免 DML DDL 冲突保证读写的正确性

这里的元数据,大家可以简单理解为就是一张表的表结构。

也就是说,某一张表涉及到未提交的事务时,是不能够修改这张表的表结构的。


MySQL 5.5 中引入了 MDL

  • 当对一张表进行增删改查的时候,加 MDL 读锁(共享);
  • 当对表结构进行变更操作的时候,加 MDL 写锁(排他)。

常见的 SQL 操作时,所添加的元数据锁

对应 SQL锁类型说明
lock tables xxx read / writeSHARED_READ_ONLY
SHARED_NO_READ_WRITE
selectselect ... lock in share modeSHARED_READSHARED_READSHARED_WRITE 兼容
EXCLUSIVE 互斥
insertupdatedeleteselect ... for updateSHARED_WRITESHARED_READSHARED_WRITE 兼容
EXCLUSIVE 互斥
alter table ...EXCLUSIVE与其他的 MDL 都互斥

演示


当执行 SELECTINSERTUPDATEDELETE 等语句时

添加的是元数据共享锁(SHARED_READ / SHARED_WRITE),之间是兼容的。

在这里插入图片描述


当执行 SELECT 语句时

添加的是元数据共享锁(SHARED_READ),会阻塞元数据排他锁(EXCLUSIVE),之间是互斥的。

在这里插入图片描述


我们可以通过下面的 SQL,来查看当前数据库中的元数据锁的情况

SELECT 
	object_type , object_schema , object_name , lock_type , lock_duration 
FROM 
	performance_schema.metadata_locks ;

我们在操作过程中,可以通过上述的 SQL 语句,来查看元数据锁的加锁情况。

在这里插入图片描述


7.3.4.意向锁


  1. 简单介绍

为了避免 DML 在执行时加的行锁与表锁的冲突,在 InnoDB 中引入了意向锁

加入意向锁之后,使得表锁不用检查每行数据是否加锁,使用意向锁来减少表锁的检查。


假如没有意向锁,客户端一对表加了行锁后,客户端二如何给表加表锁呢?

这里通过示意图简单分析一下:

首先客户端一,开启一个事务,然后执行 DML 操作,在执行 DML 语句时,会对涉及到的行加行锁。

在这里插入图片描述

当客户端二,想对这张表加表锁时,会检查当前表是否有对应的行锁

如果没有,则添加表锁,此时就会从第一行数据,检查到最后一行数据,效率较低。

在这里插入图片描述


有了意向锁之后:

客户端一,在执行 DML 操作时,会对涉及的行加行锁,同时也会对该表加上意向锁。

在这里插入图片描述

而其他客户端,在对这张表加表锁的时候,会根据该表上所加的意向锁来判定是否可以成功加表锁,而不用逐行判断行锁情况了。

在这里插入图片描述


  1. 分类

意向共享锁IS)

  • 由语句 select ... lock in share mode 添加 。
  • 与 表锁共享锁(read)兼容,与表锁排他锁(write)互斥。

意向排他锁IX

  • insertupdatedeleteselect ...for update 添加 。
  • 与 表锁共享锁(read)及 排他锁(write)都互斥,意向锁 之间不会互斥。

  • 注意事项:一旦事务提交了,意向共享锁、意向排他锁,都会自动释放。

可以通过以下 SQL,查看意向锁及行锁的加锁情况

SELECT object_schema , object_name , index_name , lock_type , lock_mode , lock_data FROM performance_schema.data_locks ;

  • 演示(一)意向共享锁与表读锁是兼容的

在这里插入图片描述


  • 演示(二)意向排他锁与表读锁、写锁都是互斥的

在这里插入图片描述


7.4.行级锁


7.4.1.介绍


行级锁,每次操作锁住对应的行数据。

锁定粒度最小,发生锁冲突的概率最低,并发度最高。

应用在 InnoDB 存储引擎中。

InnoDB 的数据是基于索引组织的,行锁是通过对索引上的索引项加锁来实现的,而不是对记录加的锁。


对于行级锁,主要分为三类:行锁间隙锁临键锁

  • 行锁Record Lock
    • 锁定单个行记录的锁,防止其他事务对此行进行 updatedelete
    • RCRR 隔离级别下都支持。

在这里插入图片描述

  • 间隙锁Gap Lock
    • 锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行 insert,产生幻读。
    • RR 隔离级别下都支持。

在这里插入图片描述

  • 临键锁Next-Key Lock
    • 可以简单理解为行锁和间隙锁组合,同时锁住数据,并锁住数据前面的间隙 Gap
    • RR 隔离级别下支持。

在这里插入图片描述


7.4.2.行锁


  1. 介绍

InnoDB 实现了以下两种类型的行锁:

  • 共享锁(S:允许一个事务去读一行,阻止其他事务获得相同数据集的排它锁。
  • 排他锁(X:允许获取排他锁的事务更新数据,阻止其他事务获得相同数据集的共享锁和排他锁。

两种行锁的兼容情况如下

在这里插入图片描述

常见的 SQL 语句,在执行时,所加的行锁如下

SQL行锁类型说明
INSERT ...排他锁自动加锁
UPDATE ...排他锁自动加锁
DELETE ...排他锁自动加锁
SELECT正常不加任何锁
SELECT ... LOCK IN SHARE MODE共享锁需要手动在 SELECT 之后加 LOCK IN SHARE MODE
SELECT ... FOR UPDATE排他锁需要手动在 SELECT 之后加 FOR UPDATE

  • 默认情况下,InnoDBREPEATABLE READ 事务隔离级别运行,InnoDB 使用 next-key 锁进行搜索和索引扫描,以防止幻读。
  • 针对唯一索引进行检索时,对已存在的记录进行等值匹配时,将会自动优化为行锁。
  • InnoDB 的行锁是针对于索引加的锁,不通过索引条件检索数据,那么 InnoDB 将对表中的所有记录加锁,此时就会升级为表锁。

  1. 演示

可以通过以下 SQL,查看意向锁及行锁的加锁情况:

SELECT
	object_schema, object_name, index_name, lock_type, lock_mode, lock_data 
FROM
	PERFORMANCE_SCHEMA.data_locks;
  • 数据准备
CREATE TABLE `stu` (
	`id` int NOT NULL PRIMARY KEY AUTO_INCREMENT,
	`name` varchar(255) DEFAULT NULL,
	`age` int NOT NULL
) ENGINE = InnoDB CHARACTER SET = utf8mb4;
INSERT INTO `stu` VALUES (1, 'tom', 1);
INSERT INTO `stu` VALUES (3, 'cat', 3);
INSERT INTO `stu` VALUES (8, 'rose', 8);
INSERT INTO `stu` VALUES (11, 'jetty', 11);
INSERT INTO `stu` VALUES (19, 'lily', 19);
INSERT INTO `stu` VALUES (25, 'luci', 25);

演示行锁的时候,我们就通过上面这张表来演示一下。

  • A.普通的 select 语句,执行时,不会加锁

在这里插入图片描述

  • B.select...lock in share mode,加共享锁,共享锁与共享锁之间兼容。

在这里插入图片描述

共享锁与排他锁之间互斥。

在这里插入图片描述

客户端一获取的是 id 为 1 这行的共享锁,客户端二是可以获取 id 为 3 这行的排它锁的,因为不是同一行数据。

而如果客户端二想获取 id 为1这行的排他锁,会处于阻塞状态,以为共享锁与排他锁之间互斥。

  • C.排它锁与排他锁之间互斥

在这里插入图片描述
当客户端一,执行 update 语句,会为 id 为 1 的记录加排他锁;

客户端二,如果也执行 update 语句更新 id 为 1 的数据,也要为 id 为 1 的数据加排他锁,

但是客户端二会处于阻塞状态,因为排他锁之间是互斥的。

直到客户端一,把事务提交了,才会把这一行的行锁释放,此时客户端二,解除阻塞。

  • D.无索引行锁升级为表锁

在这里插入图片描述

我们在两个客户端中执行如下操作

在这里插入图片描述

在客户端一中,开启事务,并执行 update 语句,更新 name 为 Lily 的数据,也就是 id 为 19 的记录 。

然后在客户端二中更新 id 为 3 的记录,却不能直接执行,会处于阻塞状态,为什么呢?

原因就是因为此时,客户端一,根据 name 字段进行更新时, name 字段是没有索引的,

如果没有索引,此时行锁会升级为表锁(因为行锁是对索引项加的锁,而 name 没有索引)。

接下来,我们再针对 name 字段建立索引,索引建立之后,再次做一个测试:

在这里插入图片描述

此时我们可以看到,客户端一,开启事务,然后依然是根据 name 进行更新。

而客户端二,在更新 id 为 3 的数据时,更新成功,并未进入阻塞状态。

这样就说明,我们根据索引字段进行更新操作,就可以避免行锁升级为表锁的情况


7.4.3.间隙锁&临键锁


默认情况下,InnoDBREPEATABLE READ 事务隔离级别运行,InnoDB 使用 next-key 锁进行搜索和索引扫描,以防止幻读。

  • 索引上的等值查询(唯一索引),给不存在的记录加锁时, 优化为间隙锁 。
  • 索引上的等值查询(非唯一普通索引),向右遍历时最后一个值不满足查询需求时,next-key lock 退化为间隙锁。
  • 索引上的范围查询(唯一索引)–会访问到不满足条件的第一个值为止。

  • 注意事项
    • 间隙锁唯一目的是防止其他事务插入间隙。
    • 间隙锁可以共存,一个事务采用的间隙锁不会阻止另一个事务在同一间隙上采用间隙锁。

示例演示

  • 索引上的等值查询(唯一索引),给不存在的记录加锁时, 优化为间隙锁 。

在这里插入图片描述

  • 索引上的等值查询(非唯一普通索引),向右遍历时最后一个值不满足查询需求时,next-key lock 退化为间隙锁。

此处介绍分析一下:

  • 我们知道 InnoDBB+ 树索引,叶子节点是有序的双向链表。
  • 假如,我们要根据这个二级索引查询值为 18 的数据,并加上共享锁,我们是只锁定 18 这一行就可以了吗?
  • 并不是,因为是非唯一索引,这个结构中可能有多个 18 的存在
  • 所以,在加锁时会继续往后找,找到一个不满足条件的值(当前案例中也就是 29)。
  • 此时会对 18 加临键锁,并对 29 之前的间隙加锁。

在这里插入图片描述
在这里插入图片描述

  • 索引上的范围查询(唯一索引)–会访问到不满足条件的第一个值为止。

在这里插入图片描述
查询的条件为 id >= 19,并添加共享锁。

此时我们可以根据数据库表中现有的数据,将数据分为三个部分:[19]、(19, 25]、(25, +∞]

所以数据库在数据加锁时,就是将 19 加了行锁,25 的临键锁(包含 25 及 25 之前的间隙),正无穷的临键锁(正无穷及之前的间隙)。


8. InnoDB引擎


8.1.逻辑存储结构


在这里插入图片描述


  1. 表空间

表空间是 InnoDB 存储引擎逻辑结构的最高层。

如果用户启用了参数 innodb_file_per_table(在 8.0 版本中默认开启) ,则每张表都会有一个表空间(xxx.ibd)。

一个 MySQL 实例可以对应多个表空间,用于存储记录、索引等数据。


段,分为数据段(Leaf node segment)、索引段(Non-leaf node segment)、回滚段(Rollback segment)。

InnoDB 是索引组织表,数据段就是 B+ 树的叶子节点, 索引段即为 B+ 树的非叶子节点。

段用来管理多个 Extent(区)。


区,表空间的单元结构。每个区的大小为 1M。

默认情况下, InnoDB 存储引擎页大小为 16 K, 即一个区中一共有 64 个连续的页。


页,是 InnoDB 存储引擎磁盘管理的最小单元。每个页的大小默认为 16 KB。

为了保证页的连续性,InnoDB 存储引擎每次从磁盘申请 4-5 个区。


行,InnoDB 存储引擎数据是按行进行存放的。

在行中,默认有两个隐藏字段:

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

在这里插入图片描述


8.2.架构


8.2.1.概述


MySQL5.5 版本开始,默认使用 InnoDB 存储引擎,它擅长事务处理,具有崩溃恢复特性,在日常开发中使用非常广泛。

下面是 InnoDB 架构图,左侧为内存结构,右侧为磁盘结构。

在这里插入图片描述


8.2.2.内存结构


MySQL 内存结构中,主要分为这么四大块儿:Buffer PoolChange BufferAdaptiveHash IndexLog Buffer


  1. Buffer Pool

InnoDB 存储引擎基于磁盘文件存储,访问物理硬盘和在内存中进行访问,速度相差很大。

为了尽可能弥补这两者之间的 I/O 效率的差值,就需要把经常使用的数据加载到缓冲池中,避免每次访问都进行磁盘 I/O

InnoDB 的缓冲池中不仅缓存了索引页和数据页,还包含了 undo 页、插入缓存、自适应哈希索引以及 InnoDB 的锁信息等等。

缓冲池 Buffer Pool,是主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据。

在执行增删改查操作时,先操作缓冲池中的数据(若缓冲池没有数据,则从磁盘加载并缓存),

然后再以一定频率刷新到磁盘,从而减少磁盘 I/O,加快处理速度。


缓冲池以 Page 页为单位,底层采用链表数据结构管理 Page

根据状态,将 Page 分为三种类型:

  • free page:空闲 page,未被使用。
  • clean page:被使用 page,数据没有被修改过。
  • dirty page:脏页,被使用 page,数据被修改过,也中数据与磁盘的数据产生了不一致。

在专用服务器上,通常将多达 80% 的物理内存分配给缓冲池 。

参数:innodb_buffer_pool_size

SHOW VARIABLES LIKE 'innodb_buffer_pool_size';

在这里插入图片描述


  1. Change Buffer

MySQL 5 版本时只有 Insert BufferMySQL 8.0 版本引入了 Change Buffer


Change Buffer,更改缓冲区(针对于非唯一二级索引

在执行 DML 语句时,如果这些数据 Page 没有在 Buffer Pool 中,不会直接操作磁盘,而会将数据变更存在更改缓冲区 Change Buffer 中,

在未来数据被读取时,再将数据合并恢复到 Buffer Pool 中,再将合并后的数据刷新到磁盘中。


Change Buffer 的意义是什么呢?

先来看一幅图(二级索引的结构图)

在这里插入图片描述

与聚集索引不同,二级索引通常是非唯一的,并且以相对随机的顺序插入二级索引。

同样,删除和更新可能会影响索引树中不相邻的二级索引页,如果每一次都操作磁盘,会造成大量的磁盘 IO

有了 ChangeBuffer 之后,我们可以在缓冲池中进行合并处理,减少磁盘 IO


  1. Adaptive Hash Index

自适应 hash 索引,用于优化对 Buffer Pool 数据的查询。

MySQLInnoDB 引擎中虽然没有直接支持 hash 索引,但是给我们提供了一个功能就是这个自适应 hash 索引。

因为前面我们讲到过,hash索引在进行等值匹配时,一般性能是要高于 B+ 树

因为 hash 索引一般只需要一次 IO 即可,而 B+ 树,可能需要几次匹配,所以 hash 索引 的效率要高。

但是 hash 索引又不适合做范围查询、模糊匹配等。

InnoDB 存储引擎 会监控对表上各索引页的查询,

如果观察到在特定的条件下 hash 索引 可以提升速度,则建立 hash 索引,称之为自适应 hash 索引

自适应哈希索引,无需人工干预,是系统根据情况自动完成。

参数: adaptive_hash_index

SHOW VARIABLES LIKE '%hash_index';

  1. Log Buffer

Log Buffer:日志缓冲区,用来保存要写入到磁盘中的 log 日志数据(redo logundo log),

默认大小为 16 MB,日志缓冲区的日志会定期刷新到磁盘中。

如果需要更新、插入或删除许多行的事务,增加日志缓冲区的大小可以节省磁盘 I/O


参数:

  • innodb_log_buffer_size:缓冲区大小
  • innodb_flush_log_at_trx_commit:日志刷新到磁盘时机。取值主要包含以下三个:
    • 1:日志在每次事务提交时写入并刷新到磁盘,默认值。
    • 0:每秒将日志写入并刷新到磁盘一次。
    • 2:日志在每次事务提交后写入,并每秒刷新到磁盘一次。

在这里插入图片描述


以下图片截图自视频


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


8.2.3.磁盘结构


  1. System Tablespace

系统表空间是更改缓冲区的存储区域。如果表是在系统表空间而不是每个表文件或通用表空间中创建的,它也可能包含表和索引数据。

(在 MySQL5.X 版本中还包含 InnoDB 数据字典、undolog 等)

  • 参数:Innodb_data_file_path

在这里插入图片描述

系统表空间,默认的文件名叫 ibdata1


  1. File-Per-Table Tablespaces

如果开启了 innodb_file_per_table 开关 ,则每个表的文件表空间包含单个 InnoDB 表的数据和索引 ,并存储在文件系统上的单个数据文件中。

开关参数:innodb_file_per_table,该参数默认开启。

在这里插入图片描述

那也就是说,我们每创建一个表,都会产生一个表空间文件,如图:

在这里插入图片描述


  1. General Tablespaces

通用表空间,需要通过 CREATE TABLESPACE 语法创建通用表空间,在创建表时,可以指定该表空间。

  • A.创建表空间
CREATE TABLESPACE ts_name ADD DATAFILE 'file_name' ENGINE = engine_name;

在这里插入图片描述

  • B.创建表时指定表空间
CREATE TABLE xxx ... TABLESPACE ts_name;

在这里插入图片描述


  1. Undo Tablespaces

撤销表空间,MySQL 实例在初始化时会自动创建两个默认的 undo 表空间(初始大小 16 M),用于存储 undo log 日志。


  1. Temporary Tablespaces

InnoDB 使用会话临时表空间和全局临时表空间。存储用户创建的临时表等数据。


  1. Doublewrite Buffer Files

双写缓冲区,InnoDB 引擎将数据页从 Buffer Pool 刷新到磁盘前,先将数据页写入双写缓冲区文件中,便于系统异常时恢复数据。

在这里插入图片描述


  1. Redo Log

重做日志,是用来实现事务的持久性。

该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log),前者是在内存中,后者在磁盘中。

当事务提交之后会把所有修改信息都会存到该日志中,用于在刷新脏页到磁盘时,发生错误时,进行数据恢复使用。

以循环方式写入重做日志文件,涉及两个文件

在这里插入图片描述


以下图片截取自视频


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


前面我们介绍了 InnoDB 的内存结构,以及磁盘结构,那么内存中我们所更新的数据,又是如何到磁盘中的呢?

此时,就涉及到一组后台线程,接下来,就来介绍一些 InnoDB 中涉及到的后台线程。

在这里插入图片描述


8.2.4.后台线程


InnoDB 的后台线程中,分为 4 类,分别是:Master ThreadIO ThreadPurge ThreadPage Cleaner Thread

在这里插入图片描述


  1. Master Thread

核心后台线程,负责调度其他线程,还负责将缓冲池中的数据异步刷新到磁盘中, 保持数据的一致性,还包括脏页的刷新、合并插入缓存、undo 页的回收 。


  1. IO Thread

InnoDB 存储引擎中大量使用了 AIO 来处理 IO 请求,这样可以极大地提高数据库的性能,而 IO Thread 主要负责这些 IO 请求的回调。

线程类型默认个数职责
Read thread4负责读操作
Write thread4负责写操作
Log thread1负责将日志缓冲区刷新到磁盘
Insert buffer thread1负责将写缓冲区内容刷新到磁盘

我们可以通过以下的这条指令,查看到 InnoDB 的状态信息,其中就包含 IO Thread 信息

show engine innodb status \G;

在这里插入图片描述


3.Purge Thread

主要用于回收事务已经提交了的 undo log,在事务提交之后,undo log 可能不用了,就用它来回收。


4.Page Cleaner Thread

协助 Master Thread 刷新脏页到磁盘的线程,它可以减轻 Master Thread 的工作压力,减少阻塞。


该图截取自视频

在这里插入图片描述


8.3.事务原理


8.3.1.事务基础


  1. 事务

事务 是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。


  1. 特性

原子性Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。

一致性Consistency):事务完成时,必须使所有的数据都保持一致状态。

隔离性Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。

持久性Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。

那实际上,我们研究事务的原理,就是研究 MySQLInnoDB 引擎是如何保证事务的这四大特性的。

在这里插入图片描述

而对于这四大特性,实际上分为两个部分。

  • 其中的原子性、一致性、持久化,实际上是由 InnoDB 中的两份日志来保证的,一份是 redo log 日志,一份是 undo log 日志。
  • 而持久性是通过数据库的锁,加上 MVCC 来保证的。

在这里插入图片描述

我们在讲解事务原理的时候,主要就是来研究一下 redologundolog 以及 MVCC


8.3.2.redo log


重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性。


该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log file),前者是在内存中,后者在磁盘中。

当事务提交之后会把所有修改信息都存到该日志文件中,用于在刷新脏页到磁盘,发生错误时, 进行数据恢复使用。


如果没有 redolog,可能会存在什么问题的? 我们一起来分析一下。

我们知道,在 InnoDB 引擎中的内存结构中,主要的内存区域就是缓冲池,在缓冲池中缓存了很多的数据页。

当我们在一个事务中,执行多个增删改的操作时,InnoDB 引擎会先操作缓冲池中的数据。

如果缓冲区没有对应的数据,会通过后台线程将磁盘中的数据加载出来,存放在缓冲区中,然后将缓冲池中的数据修改,修改后的数据页我们称为脏页。

而脏页则会在一定的时机,通过后台线程刷新到磁盘中,从而保证缓冲区与磁盘的数据一致。

而缓冲区的脏页数据并不是实时刷新的,而是一段时间之后将缓冲区的数据刷新到磁盘中。

假如刷新到磁盘的过程出错了,而提示给用户事务提交成功,而数据却没有持久化下来,这就出现问题了,没有保证事务的持久性。

在这里插入图片描述


那么,如何解决上述的问题呢?

InnoDB 中提供了一份日志 redo log,接下来我们再来分析一下,看看如何通过 redolog 来解决这个问题。

在这里插入图片描述

有了 redolog 之后,当对缓冲区的数据进行增删改之后,会首先将操作的数据页的变化,记录在 redo log buffer 中。

在事务提交时,会将 redo log buffer 中的数据刷新到 redo log 磁盘文件中。

过一段时间之后,如果刷新缓冲区的脏页到磁盘时,发生错误,此时就可以借助于 redo log 进行数据恢复,这样就保证了事务的持久性。

而如果脏页成功刷新到磁盘 或 或者涉及到的数据已经落盘,此时 redolog 就没有作用了,就可以删除了,所以存在的两个 redolog 文件是循环写的。


那为什么每一次提交事务,要刷新 redo log 到磁盘中呢,而不是直接将 buffer pool 中的脏页刷新到磁盘呢 ?

因为在业务操作中,我们操作数据一般都是随机读写磁盘的,而不是顺序读写磁盘。

redo log 在往磁盘文件中写入数据,由于是日志文件,所以都是顺序写的。顺序写的效率,要远大于随机写。

这种先写日志的方式,称之为 WALWrite-Ahead Logging)。


8.3.3.undo log


回滚日志,用于记录数据被修改前的信息 , 作用包含两个 : 提供回滚(保证事务的原子性)和 MVCC(多版本并发控制)。


undo logredo log 记录物理日志不一样,它是逻辑日志。

  • 可以认为当 delete 一条记录时,undo log 中会记录一条对应的 insert 记录;
  • 反之亦然,当 update 一条记录时,它记录一条对应相反的 update 记录。
  • 当执行 rollback 时,就可以从 undo log 中的逻辑记录读取到相应的内容并进行回滚。

Undo Log 销毁

  • undo log 在事务执行时产生,事务提交时,并不会立即删除 undo log,因为这些日志可能还用于 MVCC

Undo Log 存储

  • undo log 采用段的方式进行管理和记录,存放在前面介绍的 rollback segment 回滚段中,内部包含 1024 个 undo log segment

8.4.MVCC


8.4.1.基本概念


  1. 当前读

读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。

对于我们日常的操作,如:select ... lock in share mode(共享锁),select ... for updateupdateinsertdelete(排他锁)都是一种当前读。


在这里插入图片描述

在上面的测试截图中我们可以看到,即使是在默认的 RR 隔离级别下,事务 A 中依然可以读取到事务 B 最新提交的内容,因为在查询语句后面加上了 lock in share mode 共享锁,此时是当前读操作。

当然,当我们加排他锁的时候,也是当前读操作。


  1. 快照读

简单的 select(不加锁)就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。

  • Read Committed:每次 select,都生成一个快照读。
  • Repeatable Read:开启事务后第一个 select 语句才是快照读的地方。
  • Serializable:快照读会退化为当前读。

在这里插入图片描述

在测试中,我们看到即使事务 B 提交了数据,事务 A 中也查询不到。

原因就是因为普通的 select 是快照读,而在当前默认的 RR 隔离级别下,开启事务后第一个 select 语句才是快照读的地方,后面执行相同的 select 语句都是从快照中获取数据,可能不是当前的最新数据,这样也就保证了可重复读。


  1. MVCC

全称 Multi-Version Concurrency Control,多版本并发控制。

指维护一个数据的多个版本,使得读写操作没有冲突,快照读为 MySQL 实现 MVCC 提供了一个非阻塞读功能。

MVCC 的具体实现,还需要依赖于数据库记录中的三个隐式字段、undo log 日志、readView

接下来,我们再来介绍一下 InnoDB 引擎的表中涉及到的隐藏字段 、undolog 以及 readview,从而来介绍一下 MVCC 的原理。


8.4.2.隐藏字段


在这里插入图片描述

当我们创建了上面的这张表,我们在查看表结构的时候,就可以显式的看到这三个字段。

实际上除了这三个字段以外,InnoDB 还会自动的给我们添加三个隐藏字段及其含义分别是:

隐藏字段含义
DB_TRX_ID最近修改事务 ID,记录插入这条记录或最后一次修改该记录的事务 ID
DB_ROLL_PTR回滚指针,指向这条记录的上一个版本,用于配合 undo log,指向上一个版本。
DB_ROW_ID隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段。

而上述的前两个字段是肯定会添加的, 是否添加最后一个字段 DB_ROW_ID,得看当前表有没有主键,如果有主键,则不会添加该隐藏字段。


  • 测试 1:查看有主键的表 stu

进入服务器中的 /var/lib/mysql/itcast/,查看 stu 的表结构信息, 通过如下指令:

ibd2sdi stu.ibd

查看到的表结构信息中,有一栏 columns,在其中我们会看到处理我们建表时指定的字段以外,还有额外的两个字段 分别是:DB_TRX_IDDB_ROLL_PTR,因为该表有主键,所以没有 DB_ROW_ID 隐藏字段。(此处就不贴图了)


  • 测试 2:查看没有主键的表 employee

建表语句

create table employee (id int , name varchar(10));

此时,我们再通过以下指令来查看表结构及其其中的字段信息

ibd2sdi employee.ibd

查看到的表结构信息中,有一栏 columns,在其中我们会看到处理我们建表时指定的字段以外,还有额外的三个字段 分别是:DB_TRX_IDDB_ROLL_PTRDB_ROW_ID,因为 employee 表是没有指定主键的。(此处就不贴图了)


8.4.3.undolog


回滚日志,在 insertupdatedelete 的时候产生的便于数据回滚的日志。

insert 的时候,产生的 undo log 日志只在回滚时需要,在事务提交后,可被立即删除。

updatedelete 的时候,产生的 undo log 日志不仅在回滚时需要,在快照读时也需要,不会立即被删除。


该图截自视频

在这里插入图片描述


接下来详细解释一下上面的截图


有一张表的原始数据是

在这里插入图片描述

  • DB_TRX_ID:代表最近修改事务 ID,记录插入这条记录或最后一次修改该记录的事务 ID,是自增的。
  • DB_ROLL_PTR:由于这条数据是才插入的,没有被更新过,所以该字段值为 null

然后,有四个并发事务同时在访问这张表。


  • 第一步

在这里插入图片描述
当事务 2 执行第一条修改语句时,会记录 undo log 日志,记录数据变更之前的样子;

然后更新记录,并且记录本次操作的事务 ID,回滚指针,回滚指针用来指定发生回滚时应该回滚到哪一个版本。

在这里插入图片描述


  • 第二步

在这里插入图片描述

当事务 3 执行第一条修改语句时,也会记录 undo log 日志,记录数据变更之前的样子;

然后更新记录,并且记录本次操作的 事务 ID,回滚指针,回滚指针用来指定当回滚发生时,回滚到哪一个版本。

在这里插入图片描述


  • 第三步

在这里插入图片描述

当事务 4 执行第一条修改语句时,也会记录 undo log 日志,记录数据变更之前的样子;

然后更新记录,并且记录本次操作的事务 ID,回滚指针,回滚指针用来指定如果发生回滚,回滚到哪一个版本。

在这里插入图片描述


最终我们发现,不同事务或相同事务对同一条记录进行修改,会导致该记录的 undolog 生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录。


8.4.4.readview


ReadView(读视图)是 快照读 SQL 执行时 MVCC 提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id


ReadView 中包含了四个核心字段:

字段含义
m_ids当前活跃的事务 ID 集合
min_trx_id最小活跃事务 ID
max_trx_id预分配事务 ID,当前最大事务 ID + 1(因为事务 ID 是自增的)
creator_trx_idReadView 创建者的事务 ID

而在 readview 中就规定了版本链数据的访问规则:(其中 trx_id 代表当前 undolog 版本链对应事务 ID

条件是否可以访问说明
trx_id == creator_trx_id可以访问该版本成立,说明数据是当前这个事务更改的。
trx_id < min_trx_id可以访问该版本成立,说明数据已经提交了。
trx_id > max_trx_id不可以访问该版本成立,说明该事务是在 ReadView 生成后才开启。
min_trx_id <= trx_id <= max_trx_id如果 trx_id 不在 m_ids 中,是可以访问该版本的如果 trx_id 不在 m_ids 中,说明数据已经提交。

不同的隔离级别,生成 ReadView 的时机不同:

  • READ COMMITTED:在事务中每一次执行快照读时生成 ReadView
  • REPEATABLE READ:仅在事务中第一次执行快照读时生成 ReadView,后续复用该 ReadView

原理分析

8.4.5.RC 隔离级别


RC 隔离级别下,在事务中每一次执行快照读时生成 ReadView


我们就来分析事务 5 中,两次快照读读取数据,是如何获取数据的。

在事务 5 中,查询了两次 id 为 30 的记录,由于隔离级别为 Read Committed,所以每一次进行快照读都会生成一个 ReadView,那么两次生成的 ReadView 如下。

在这里插入图片描述

那么这两次快照读在获取数据时,就需要根据所生成的 ReadView 以及 ReadView 的版本链访问规则,到 undolog 版本链中匹配数据,最终决定此次快照读返回的数据。


  • A.先来看第一次快照读具体的读取过程

在这里插入图片描述

在进行匹配时,会从 undo log 的版本链,从上到下进行挨个匹配:

  • 注意:下面的分析是错误的,不应该从 4 开始,而是应该从 3 开始(因为事务 4 此前并没有对记录做出什么修改更新的操作)。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


  • B.再来看第二次快照读具体的读取过程

在这里插入图片描述

在进行匹配时,会从 undo log 的版本链,从上到下进行挨个匹配

在这里插入图片描述
在这里插入图片描述


8.4.6.RR 隔离级别


RR 隔离级别下,仅在事务中第一次执行快照读时生成 ReadView,后续复用该 ReadView


RR 是可重复读,在一个事务中,执行两次相同的 select 语句,查询到的结果是一样的。

MySQL 是如何做到可重复读的呢? 我们简单分析一下就知道了

在这里插入图片描述
我们看到,在 RR 隔离级别下,只是在事务中第一次快照读时生成 ReadView,后续都是复用该 ReadView,那么既然 ReadView 都一样, ReadView 的版本链匹配规则也一样, 那么最终快照读返回的结果也是一样的。


所以呢,MVCC 的实现原理就是通过 InnoDB 表的隐藏字段、UndoLog 版本链、ReadView 来实现的。

MVCC + 锁,则实现了事务的隔离性。 而一致性则是由 redolog 与 undolog 保证。

在这里插入图片描述


9.MySQL管理


9.1.系统数据库


MySQL 数据库安装完成后,自带了一下四个数据库,具体作用如下

数据库含义
mysql存储 MySQL 服务器正常运行所需要的各种信息 (时区、主从、用户、权限等)
information_schema提供了访问数据库元数据的各种表和视图,包含数据库、表、字段类型及访问权限等
performance_schemaMySQL 服务器运行时状态提供了一个底层监控功能,主要用于收集数据库服务器性能参数
sys包含了一系列方便 DBA 和开发人员利用 performance_schema 性能数据库进行性能调优和诊断的视图

9.2.常用工具


9.2.1.mysql


mysql 不是指 MySQL 服务,而是指 mysql 的客户端工具。


  • 语法
mysql [options] [database]
  • 选项
-u, --user=name 			#指定用户名

-p, --password[=name] 		#指定密码

-h, --host=name 			#指定服务器IP或域名

-P, --port=port 			#指定连接端口

-e, --execute=name 			#执行SQL语句并退出

-e 选项可以在 MySQL 客户端执行 SQL 语句,而不用连接到 MySQL 数据库再执行,对于一些批处理脚本,这种方式尤其方便。

  • 示例
mysql -uroot –p123456 db01 -e "select * from stu";

在这里插入图片描述


9.2.2.mysqladmin


mysqladmin 是一个执行管理操作的客户端程序。

可以用它来检查服务器的配置和当前状态、创建并删除数据库等。


  • 通过帮助文档查看选项
mysqladmin --help

在这里插入图片描述


  • 语法
mysqladmin [options] command ...
  • 选项
-u, --user=name 		#指定用户名
-p, --password[=name] 	#指定密码
-h, --host=name 		#指定服务器IP或域名
-P, --port=port 		#指定连接端口
  • 示例
mysqladmin -uroot –p1234 drop 'test01';
mysqladmin -uroot –p1234 version;

在这里插入图片描述


9.2.3.mysqlbinlog


由于服务器生成的二进制日志文件以二进制格式保存,所以如果想要检查这些文本的文本格式,就会使用到 mysqlbinlog 日志管理工具。


  • 语法
mysqlbinlog [options] log-files1 log-files2 ...
  • 选项
-d, --database=name 							指定数据库名称,只列出指定的数据库相关操作。

-o, --offset=# 									忽略掉日志中的前n行命令。

-r, --result-file=name 							将输出的文本格式日志输出到指定文件。

-s, --short-form 								显示简单格式, 省略掉一些信息。
--start-datatime=date1 --stop-datetime=date2 	指定日期间隔内的所有日志。

--start-position=pos1 --stop-position=pos2 		指定位置间隔内的所有日志。
  • 示例:查看 binlog.000008 这个二进制文件中的数据信息

在这里插入图片描述

上述查看到的二进制日志文件数据信息量太多了,不方便查询。 我们可以加上一个参数 -s 来显示简单格式。

在这里插入图片描述


9.2.4.mysqlshow


mysqlshow 客户端对象查找工具,用来很快地查找存在哪些数据库、数据库中的表、表中的列或者索引。


  • 语法
mysqlshow [options] [db_name [table_name [col_name]]]
  • 选项
--count 	显示数据库及表的统计信息(数据库,表 均可以不指定)

-i 			显示指定数据库或者指定表的状态信息
  • 示例
# 查询 test 库中每个表中的字段书,及行数

mysqlshow -uroot -p2143 test --count
# 查询 test 库中 book 表的详细情况

mysqlshow -uroot -p2143 test book --count

  • 示例一:查询每个数据库的表的数量及表中记录的数量
mysqlshow -uroot -p1234 --count

在这里插入图片描述


  • 示例二:查看数据库 db01 的统计信息
mysqlshow -uroot -p1234 db01 --count

在这里插入图片描述


  • 示例三:查看数据库 db01 中的 course 表的信息
mysqlshow -uroot -p1234 db01 course --count

在这里插入图片描述


  • 示例四:查看数据库 db01 中的 course 表的 id 字段的信息
mysqlshow -uroot -p1234 db01 course id --count

在这里插入图片描述


9.2.5.mysqldump


mysqldump 客户端工具用来备份数据库或在不同数据库之间进行数据迁移。备份内容包含创建表,及插入表的 SQL 语句。


  • 语法
mysqldump [options] db_name [tables]
mysqldump [options] --database/-B db1 [db2 db3...]
mysqldump [options] --all-databases/-A
  • 连接选项
-u, --user=name 		指定用户名

-p, --password[=name] 	指定密码

-h, --host=name 		指定服务器 ip 或域名

-P, --port=# 			指定连接端口
  • 输出选项
--add-drop-database 	在每个数据库创建语句前加上 drop database 语句

--add-drop-table 		在每个表创建语句前加上 drop table 语句 , 默认开启 ; 不开启 (--skip-add-drop-table)

-n, --no-create-db 		不包含数据库的创建语句

-t, --no-create-info 	不包含数据表的创建语句

-d --no-data 			不包含数据

-T, --tab=name 			自动生成两个文件:一个.sql文件,创建表结构的语句;一个.txt文件,数据文件

  • 示例一:备份 db01 数据库
mysqldump -uroot -p1234 db01 > db01.sql

在这里插入图片描述

可以直接打开 db01.sql,来查看备份出来的数据到底什么样。

在这里插入图片描述

备份出来的数据包含:

  • 删除表的语句
  • 创建表的语句
  • 数据插入语句

如果我们在数据备份时,不需要创建表,或者不需要备份数据,只需要备份表结构,都可以通过对应的参数来实现。


  • 示例二:备份 db01 数据库中的表数据,不备份表结构(-t
mysqldump -uroot -p1234 -t db01 > db01.sql

在这里插入图片描述

打开 db02.sql,来查看备份的数据,只有 insert 语句,没有备份表结构。

在这里插入图片描述


  • 示例三:将 db01 数据库的表的表结构与数据分开备份(-T
mysqldump -uroot -p1234 -T /root db01 score

在这里插入图片描述

执行上述指令,会出错,数据不能完成备份。

原因是因为我们所指定的数据存放目录 /rootMySQL 认为是不安全的,需要存储在 MySQL 信任的目录下。

那么,哪个目录才是 MySQL 信任的目录呢?可以查看一下系统变量 secure_file_priv

执行结果如下:

在这里插入图片描述
在这里插入图片描述

上述的两个文件 score.sql 中记录的就是表结构文件。

score.txt 就是表数据文件,但是需要注意表数据文件,并不是记录一条条的 insert 语句,而是按照一定的格式记录表结构中的数据。

如下:

在这里插入图片描述


9.2.6.mysqlimport/source


  1. mysqlimport

mysqlimport 是客户端数据导入工具,用来导入 mysqldump-T 参数后导出的文本文件。

  • 语法
mysqlimport [options] db_name textfile1 [textfile2...]
  • 示例
mysqlimport -uroot -p2143 test /tmp/city.txt

在这里插入图片描述


  1. source

如果需要导入 sql 文件,可以使用 MySQL 中的 source 指令

  • 语法
source /root/xxxxx.sql

9.3.MySQL管理小结


  1. mysql
MySQL 客户端管理工具,-e z执行 SQL 并退出
  1. mysqladmin
MySQL 管理工具
  1. mysalbinlog
二级制日志查看工具
  1. mysqlshow
查看数据库、表、字段的统计信息
  1. mysqldump
数据备份工具
  1. mysqlimprot / source
数据导入工具

  • 7
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值