MySql进阶篇---006:存储引擎,索引,SQL优化,视图、存储过程、变量、流程控制、游标、存储函数、触发器,主键如何设计,事务

1. 存储引擎

1.1 MySQL体系结构

在这里插入图片描述

1).连接层
最上层是一些客户端和链接服务,包含本地sock 通信和大多数基于客户端/服务端工具实现的类似于TCP/IP的通信。主要完成一些类似于连接处理、授权认证、及相关的安全方案。在该层上引入了线程池的概念,为通过认证安全接入的客户端提供线程。同样在该层上可以实现基于SSL的安全链接。服务 器也会为安全接入的每个客户端验证它所具有的操作权限。

2).服务层
第二层架构主要完成大多数的核心服务功能,如SQL接口,并完成缓存的查询,SQL的分析和优化,部分内置函数的执行。所有跨存储引擎的功能也在这一层实现,如 过程、函数等。在该层,服务器会解析查询并创建相应的内部解析树,并对其完成相应的优化如确定表的查询的顺序,是否利用索引等, 最后生成相应的执行操作。如果是select语句,服务器还会查询内部的缓存,如果缓存空间足够大, 这样在解决大量读操作的环境中能够很好的提升系统的性能。

3).引擎层
存储引擎层, 存储引擎真正的负责了MySQL中数据的存储和提取,服务器通过API和存储引擎进行通信。不同的存储引擎具有不同的功能,这样我们可以根据自己的需要,来选取合适的存储引擎。数据库中的索引是在存储引擎层实现的,也就是说不同的存储引擎它的索引结构是不同的

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

和其他数据库相比,MySQL有点与众不同,它的架构可以在多种不同场景中应用并发挥良好作用。主要体现在存储引擎上,插件式的存储引擎架构,将查询处理和其他的系统任务以及数据的存储提取分离。 这种架构可以根据业务的需求和实际需要选择合适的存储引擎。

1.2 存储引擎介绍

大家可能没有听说过存储引擎,但是一定听过引擎这个词,引擎就是发动机,是一个机器的核心组件。 比如,对于舰载机、直升机、火箭来说,他们都有各自的引擎,是他们最为核心的组件。而我们在选择 引擎的时候,需要在合适的场景,选择合适的存储引擎,就像在直升机上,我们不能选择舰载机的引擎 一样。

而对于存储引擎,也是一样,他是mysql数据库的核心,我们也需要在合适的场景选择合适的存储引 擎。接下来就来介绍一下存储引擎。

存储引擎就是存储数据、建立索引、更新/查询数据等技术的实现方式 。存储引擎是基于表的,而不是基于库的所以存储引擎也可被称为表类型。我们可以在创建表的时候,来指定选择的存储引擎,如果没有指定将自动选择默认的存储引擎(mysql5.5后默认选用的存储引擎是INNODB )。

1).建表时指定存储引擎

CREATE TABLE 表名(

	字段1 字段1类型 [ COMMENT 字段1注释 ] ,
	......
	字段n 字段n类型 [COMMENT 字段n注释 ]
	
) ENGINE = INNODB [ COMMENT 表注释 ] ;

2).查询当前数据库支持的存储引擎

show engines;

示例演示:

A. 查询建表语句 ---- 默认存储引擎: InnoDB

show create table account;

在这里插入图片描述

  • AUTO_INCREMENT:id是自增的,那当我们插入下一条数据时,申请的id是多少。
  • CHARSET:指定当前表的字符集
  • COLLATE:排序方式
  • COMMENT :注释信息

我们可以看到,创建表时,即使我们没有指定存储引擎,数据库也会自动选择默认的存储引擎。

B. 查询当前数据库支持的存储引擎

show engines ;

在这里插入图片描述

  • Engine:有哪些存储引擎
  • Support:是否支持
  • Comment:注释
  • Transactions:是否支持事务
  • XA:是否支持XA协议
  • Savepoints:是否支持保存点

C. 创建表 my_myisam , 并指定MyISAM存储引擎

create table my_myisam(

	id int,
	name varchar(10)
	
) engine = MyISAM ;

D.创建表 my_memory , 指定Memory存储引擎

create table my_memory(

	id int,
	name varchar(10)
	
) engine = Memory ;

1.3 存储引擎特点

上面我们介绍了什么是存储引擎,以及如何在建表时如何指定存储引擎,接下来我们就来介绍下来上面 重点提到的三种存储引擎 InnoDB、MyISAM、Memory的特点。

1.3.1 InnoDB

1).介绍

InnoDB是一种兼顾高可靠性和高性能的通用存储引擎,在 MySQL 5.5 之后,InnoDB是默认的MySQL 存储引擎。

2).特点

  • DML(增删改)操作遵循ACID模型(事务4大特性),支持事务
  • 行级锁,提高并发访问性能;
  • 支持外键FOREIGN KEY约束,保证数据的完整性和正确性;

3).文件

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

参数:innodb_file_per_table

这个参数决定到底是多张表共用一个共享表空间,还是每一个表都对应一个表空间文件。mysql8.0,默认是on打开的代表每一张表都对应一个表空间文件。

#查看系统的变量:通过以下指令进行模糊匹配,根据系统命令名查看系统变量值
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 ,进入到此目录的cmd窗口,通过该指令就可以从ibd文件中提取sdi信息,而sdi数据字典信息中就包含该表的表结构。

#account.ibd:想要打开的ibd文件名
bd2sdi account.ibd

在这里插入图片描述

4).逻辑存储结构

在这里插入图片描述

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

1.3.2 MyISAM

1).介绍

MyISAM是MySQL早期的默认存储引擎。

2).特点

不支持事务,不支持外键

支持表锁,不支持行锁

访问速度快

3).文件

xxx.sdi:存储表结构信息 (文本文件可以直接打开,里面是json类型的数据)

xxx.MYD: 存储数据

xxx.MYI: 存储索引

在这里插入图片描述

1.3.3 Memory

1).介绍

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

2).特点

内存存放

hash索引(默认)

3).文件

xxx.sdi:存储表结构信息

1.3.4 区别及特点

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

面试题:

  • InnoDB引擎与MyISAM引擎的区别 ?

  • ①. InnoDB引擎, 支持事务, 而MyISAM不支持。

  • ②. InnoDB引擎, 支持行锁和表锁, 而MyISAM仅支持表锁, 不支持行锁。

  • ③. InnoDB引擎, 支持外键, 而MyISAM是不支持的。

  • 主要是上述三点区别,当然也可以从索引结构、存储限制等方面,更加深入的回答,具体参考如下官方文档:

  • https://dev.mysql.com/doc/refman/8.0/en/innodb-introduction.html

  • https://dev.mysql.com/doc/refman/8.0/en/myisam-storage-engine.html

1.4 存储引擎选择

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

  • InnoDB: 是Mysql的默认存储引擎,支持事务、外键。如果应用对事务的完整性有比较高的要求,在并发条件下要求数据的一致性,数据操作除了插入和查询之外,还包含很多的更新、删除操 作,那么InnoDB存储引擎是比较合适的选择。
    • 常用
  • MyISAM : 如果应用是以读操作和插入操作为主,只有很少的更新和删除操作,并且对事务的完整性、并发性要求不是很高,那么选择这个存储引擎是非常合适的。
    • eg:业务系统中的日志相关数据、电商中的评论相关数据。
    • 很少用,在业务中一般使用NoSql中的mongodb 代替
  • MEMORY:将所有数据保存在内存中,访问速度快,通常用于临时表及缓存。MEMORY的缺陷就是 对表的大小有限制,太大的表无法缓存在内存中,而且无法保障数据的安全性。
    • 很少用,在业务中一般使用NoSql中的redis代替

1.5 总结

在这里插入图片描述

2.索引(重要)

安装MySql(linux版本)

说明

  • 企业中日常的生产环境,测试环境,开发环境在使用的时候,绝大部分使用的都是linux系统。所以呢我们要去模拟一下真实的企业开发当中mysql的使用。
  • linux安装、卸载mysql8.0详情查看:MySQL-Linux安装、卸载

2.1 索引概述

2.1.1 介绍

索引(index)是帮助MySQL高效获取数据数据结构(有序)。在数据之外,数据库系统还维护着满足 特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。

在这里插入图片描述

一提到数据结构,大家都会有所担心,担心自己不能理解,跟不上节奏。不过在这里大家完全不用担 心,我们后面在讲解时,会详细介绍。

2.1.2 演示

表结构及其数据如下:

在这里插入图片描述

假如我们要执行的SQL语句为 : select * from user where age = 45;

1). 无索引情况

在这里插入图片描述

在无索引情况下,就需要从第一行开始扫描,一直扫描到最后一行,我们称之为 全表扫描,性能很低。

2). 有索引情况

如果我们针对于这张表建立了索引,假设索引结构就是二叉树,那么也就意味着,会对age这个字段建立一个二叉树的索引结构。

在这里插入图片描述

此时我们在进行查询时,只需要扫描三次就可以找到数据了,极大的提高的查询的效率。

备注: 这里我们只是假设索引的结构是二叉树,介绍一下索引的大概原理,只是一个示意图,并不是索引的真实结构,索引的真实结构,后面会详细介绍。

2.1.3 特点

优势劣势
提高数据检索的效率,降低数据库的IO成本索引列也是要占用空间的。
通过索引列对数据进行排序,降低数据排序的成本,降低CPU的消耗。索引大大提高了查询效率,同时却也降低更新表的速度, 如对表进行INSERT、UPDATE、DELETE时,效率降低。
  • 优点:提高查询效率,提高排序效率
  • 缺点:占用磁盘空间,提高查询效率降低增删改效率。
    • 这2个缺点可以忽略,现在磁盘很便宜,一个正常的业务逻辑增删改的比例较小,大部分是查询的业务。

2.2 索引结构

2.2.1 概述

MySQL的索引是在存储引擎层实现的不同的存储引擎有不同的索引结构,主要包含以下几种:

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

上述是MySQL中所支持的所有的索引结构,接下来,我们再来看看不同的存储引擎对于索引结构的支持情况

索引InnoDBMyISAMMemory
B+tree索引支持支持支持
Hash 索引不支持不支持支持
R-tree 索引不支持支持不支持
Full-text5.6版本之后支持支持不支持

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

2.2.2 二叉树

假如说MySQL的索引结构采用二叉树的数据结构,比较理想的结构如下:

特点:

  1. 每个结点最多有两颗子树,所以二叉树中不存在度大于2的结点。

  2. 左子树和右子树是有顺序的,次序不能任意颠倒。

  3. 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。譬如:

在这里插入图片描述

如果主键是顺序插入的,则会形成一个单向链表,结构如下:

在这里插入图片描述

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

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

此时大家可能会想到,我们可以选择红黑树,红黑树是一颗自平衡二叉树,那这样即使是顺序插入数据,最终形成的数据结构也是一颗平衡的二叉树,结构如下:

使用红黑树解决第一个问题

在这里插入图片描述

但是,即使如此,由于红黑树也是一颗二叉树,所以也会存在一个缺点:

  • 大数据量情况下,层级较深,检索速度慢

所以,在MySQL的索引结构中,并没有选择二叉树或者红黑树,而选择的是B+Tree,那么什么是B+Tree呢?在详解B+Tree之前,先来介绍一个B-Tree

2.2.3 B-Tree

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

多路:指的是一个节点下面可以包含多个子节点。

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

4个key:20,30,62,89

5 个指针:<20,20~30,30~62,62~89,>89
在这里插入图片描述

知识小贴士: 树的度数指的是一个节点的子节点个数

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

在这里插入图片描述

插入一组数据: 100 65 169 368 900 556 780 35 215 1200 234 888 158 90 1000 88 120 268 250 。然后观察一些数据插入过程中,节点的变化情况。

在这里插入图片描述

特点:

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

2.2.4 B+Tree

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

在这里插入图片描述

我们可以看到,两部分:

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

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

在这里插入图片描述

插入一组数据: 100 65 169 368 900 556 780 35 215 1200 234 888 158 90 1000 88 120 268 250 。然后观察一些数据插入过程中,节点的变化情况。

在这里插入图片描述

最终我们看到,B+Tree 与 B-Tree相比,主要有以下三点区别

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

上述我们所看到的结构是标准的B+Tree的数据结构,接下来,我们再来看看MySQL中优化之后的B+Tree。

MySQL索引数据结构对经典的B+Tree进行了优化。在原B+Tree的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的B+Tree,提高区间访问的性能,利于排序有序的双向链表)。

在这里插入图片描述

2.2.5 Hash

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

1). 结构

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

在这里插入图片描述

如果两个(或多个)键值,映射到一个相同的槽位上,他们就产生了hash冲突(也称为hash碰撞),可以通过链表来解决----即:通过链表在后面追加元素

在这里插入图片描述

2).特点

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

    • 因为:我们在进行等值匹配的时候,只需要计算这个key对应的hash值,根据hash值在链表中查找对应的元素即可,他在存储的时候是没有顺序的,所以没有办法进行范围查询。
  • 无法利用索引完成排序操作

    • 因为hash运算出来的结果是无序的。
  • 查询效率高,通常(不存在hash冲突的情况)只需要一次检索就可以了,效率通常要高于B+tree索引

3).存储引擎支持

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

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

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

2.3 索引分类

2.3.1 索引分类

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

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

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

1 . 普通索引

在创建普通索引时,不附加任何限制条件,只是用于提高查询效率。这类索引可以创建在任何数据类型中,其值是否唯一和非空,要由字段本身的完整性约束条件决定。建立索引以后,可以通过索引进行查询。例如,在表student的字段name 上建立一个普通索引,查询记录时就可以根据该索引进行查询。

2 . 唯一性索引

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

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

3 . 主键索引

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

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

4 . 单列索引

在表中的单个字段上创建索引。单列索引只根据该字段进行索引。单列索引可以是普通索引,也可以是唯一性索引,还可以是全文索引。只要保证该索引只对应一个字段即可。一个表可以有多个单列索引。

5 . 多列(组合、联合)索引

多列索引是在表的多个字段组合上创建一个索引。该索引指向创建时对应的多个字段,可以通过这几个字段进行查询,但是只有查询条件中使用了这些字段中的第一个字段时才会被使用。例如,在表中的字段id、name和gender上建立一个多列索引idx_id_name_gender,只有在查询条件中使用了字段id时该索引才会被使用。使用组合索引时遵循最左前缀集合

6 . 全文索引

全文索引(也称全文检索)是目前搜索引擎使用的一种关键技术。它能够利用【分词技术】等多种算法智能分析出文本文字中关键词的频率和重要性,然后按照一定的算法规则智能地筛选出我们想要的搜索结果。全文索引非常适合大型数据集,对于小的数据集,它的用处比较小。

使用参数FULLTEXT可以设置索引为全文索引。在定义索引的列上支持值的全文查找,允许在这些索引列中插入重复值和空值。全文索引只能创建在CHAR、VARCHARTEXT类型及其系列类型的字段上,查询数据量较大的字符串类型的字段时,使用全文索引可以提高查询速度。例如,表student的字段informationTEXT类型,该字段包含了很多文字信息。在字段information上建立全文索引后,可以提高查询字段information的速度。

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

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

MysQL数据库从3.23.23版开始支持全文索引,但MysQL5.6.4以前只有Myisam支持,5.6.4版本以后innodb才支持,但是官方版本不支持中文分词,需要第三方分词插件。在5.7.6版本,MysQL内置了ngram全文解析器,用来支持亚洲语种的分词。测试或使用全文索引时,要先看一下自己的MysQL版本、存储引擎和数据类型是否支持全文索引。

随着大数据时代的到来,关系型数据库应对全文索引的需求已力不从心,逐渐被 solr、ElasticSearch等专门的搜索引擎所替代。

7 . 补充:空间索引

使用参数SPATIAL 可以设置索引为空间索引。空间索引只能建立在空间数据类型上,这样可以提高系统获取空间数据的效率。MySQL中的空间数据类型包括GEOMETRY、POINT、LINESTRINGPOLYGON等。目前只有MyISAM存储引擎支持空间检索,而且索引的字段不能为空值。对于初学者来说,这类索引很少会用到。

2.3.2 聚集索引&二级索引

而在InnoDB存储引擎中,根据索引的存储形式,又可以分为以下两种:

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

聚集索引选取规则:

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

聚集索引和二级索引的具体结构如下:
在这里插入图片描述

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

接下来,我们来分析一下,当我们执行如下的SQL语句时,具体的查找过程是什么样子的。
在这里插入图片描述

具体过程如下:

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

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

思考题:

  • 以下两条SQL语句,那个执行效率高? 为什么?
  • A. select * from user where id = 10 ;
  • B. select * from user where name = ‘Arm’ ;
  • 备注: id为主键,name字段创建的有索引;

解答:

  • A 语句的执行性能要高于B 语句。
  • 因为A语句直接走聚集索引,直接返回数据。 而B语句需要先查询name字段的二级索引,然后再查询聚集索引,也就是需要进行回表查询。

思考题:

  • InnoDB主键索引的B+tree高度为多高呢?
    在这里插入图片描述

假设:

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

高度为2:

  • n * 8 + (n + 1) * 6 = 16*1024 , 算出n约为 1170
    • n:当前这个节点存储k的数量。
    • 8:主键占用的字节数
    • n+1:指针的数量
    • 6:指针占用的字节空间
    • 16*1024:1k等于1024个字节,16k等于16384个字节。
  • 1171* 16 = 18736
    • 以上可以算出这个节点下面最多有1171个指针,每个指针指向下面的一个子节点
    • 一个子节点下面最多可以存储16行数据
  • 也就是说,如果树的高度为2,则可以存储 18000 多条记录。

高度为3:

  • 1171 * 1171 * 16 = 21939856
  • 以上算出每个根节点最多有1171个子节点,每个子节点下面又有1171个子节点,所以直接再乘以1171即可。
  • 也就是说,如果树的高度为3,则可以存储 2200w 左右的记录。

2.4 索引语法

2.4.1 创建索引

MySQL支持多种方法在单个或多个列上创建索引:在创建表的定义语句CREATE TABLE 中指定索引列,使用ALTER TABLE语句在存在的表上创建索引,或者使用CREATE INDEX语句在已存在的表上添加索引。

1)创建表的时候创建索引

使用CREATE TABLE创建表时,除了可以定义列的数据类型外,还可以定义主键约束、外键约束或者唯一性约束,而不论创建哪种约束,在定义约束的同时相当于在指定列上创建了一个索引。

隐式的方式创建索引:

#隐式的方式创建索引。在声明有主键约束、唯一性约束、外键约束的字段上,会自动的添加相关的索引
CREATE DATABASE dbtest2;

USE dbtest2;

CREATE TABLE dept(
dept_id INT PRIMARY KEY AUTO_INCREMENT,
dept_name VARCHAR(20)
);

CREATE TABLE emp(
emp_id INT PRIMARY KEY AUTO_INCREMENT,
emp_name VARCHAR(20) UNIQUE,
dept_id INT,
CONSTRAINT emp_dept_id_fk FOREIGN KEY(dept_id) REFERENCES dept(dept_id)
);

在这里插入图片描述

显式的方式创建索引:

语法格式:

CREATE TABLE 表名 [字段名 字段类型]
[索引的类型] INDEX [索引的名字] (作用的字段 [字符串类型需要指明长度]) [索引在存储的时候是升序还是降序]

CREATE TABLE table_name [col_name data_type]
[UNIQUE | FULLTEXT | SPATIAL] [INDEX | KEY] [index_name] (col_name [length]) [ASC | DESC]
  • UNIQUE 、 FULLTEXT SPATIAL 为可选参数,分别表示唯一索引、全文索引和空间索引;
    • 注意主键索引只能是通过添加约束的这种隐式方式来创建
  • INDEX KEY 为同义词,两者的作用相同,用来指定创建索引,推荐使用index;
  • index_name 指定索引的名称,为可选参数,如果不指定索引名,那么MySQL默认col_name字段为索引名;
  • col_name 为需要创建索引的字段列(如果是联合索引中间用逗号隔开),该列必须从数据表中定义的多个列中选择;
  • length 为可选参数,表示索引的长度,只有字符串类型的字段才能指定索引长度(eg:如果长度为20表示只取前20个作为索引的创建);
  • ASC DESC 指定升序或者降序的索引值存储。

1 . 创建普通索引
在book表中的year_publication字段上建立普通索引,SQL语句如下:

CREATE TABLE book(
book_id INT ,
book_name VARCHAR(100),
AUTHORS VARCHAR(100),
info VARCHAR(100) ,
COMMENT VARCHAR(100),
year_publication YEAR,
#声明索引
INDEX idx_bname(book_name)
);

#通过命令查看索引 (也可以通过sqlyog在表下面查看索引)
#方式1:结果是使用一行展示,可以加\G分行展示(在命令行中支持,在SqlYog中报错)
SHOW CREATE TABLE book;

#方式2:
SHOW INDEX FROM book;

#性能分析工具:EXPLAIN 查看执行这条sql时是否用到了索引,用到了查询速度就快。
EXPLAIN SELECT * FROM book WHERE book_name = 'mysql高级';

在这里插入图片描述

2 . 创建唯一索引
举例:

# 声明有唯一索引的字段,在添加数据时,要保证唯一性,但是可以添加null,并且null值可以多次添加
# 原因:创建了一个唯一性约束就会有一个唯一性索引,反之创建了一个唯一性索引也会自动有个唯一性约束
CREATE TABLE book1(
book_id INT ,
book_name VARCHAR(100),
AUTHORS VARCHAR(100),
info VARCHAR(100) ,
COMMENT VARCHAR(100),
year_publication YEAR,
#声明索引
UNIQUE INDEX uk_idx_cmt(COMMENT)
);

SHOW INDEX FROM book1;#查看索引

#第一次执行成功,第二次执行失败,因为COMMENT字段添加唯一索引就相当于添加了唯一约束。
INSERT INTO book1(book_id,book_name,COMMENT)
VALUES(1,'Mysql高级','适合有数据库开发经验的人员学习');

#插入的COMMENT字段为null,执行几次都可以成功。
INSERT INTO book1(book_id,book_name,COMMENT)
VALUES(2,'Mysql高级',NULL);

SELECT * FROM book1;

在这里插入图片描述
3 . 主键索引
设定为主键后数据库会自动建立索引,innodb为聚簇索引,语法:

  • 随表一起建索引:
#通过定义主键约束的方式定义主键索引(只能是通过添加约束的这种隐式方式创建)
CREATE TABLE book2(
book_id INT PRIMARY KEY ,
book_name VARCHAR(100),
AUTHORS VARCHAR(100),
info VARCHAR(100) ,
COMMENT VARCHAR(100),
year_publication YEAR
);

SHOW INDEX FROM book2;
  • 删除主键索引:
#通过删除主键约束的方式删除主键索引
ALTER TABLE book2
DROP PRIMARY KEY;
  • 修改主键索引:必须先删除掉(drop)原索引,再新建(add)索引

4 . 创建单列索引
举例:

CREATE TABLE book3(
book_id INT ,
book_name VARCHAR(100),
AUTHORS VARCHAR(100),
info VARCHAR(100) ,
COMMENT VARCHAR(100),
year_publication YEAR,
#声明索引  (为一个字段创建的索引就是单列索引)
INDEX idx_bname(book_name)
);

SHOW INDEX FROM book3; #查看索引

5 . 创建组合索引:能否使用要看是否满足最左前缀法则
举例:创建表test3,在表中的id、name和age字段上建立组合索引,SQL语句如下:

CREATE TABLE book4(
book_id INT ,
book_name VARCHAR(100),
AUTHORS VARCHAR(100),
info VARCHAR(100) ,
COMMENT VARCHAR(100),
year_publication YEAR,
#声明索引  (为多个字段创建索引就是多列索引) 注意声明索引的顺序和是否可以使用索引有关联
INDEX mul_bid_bname_info(book_id,book_name,info)
);

SHOW INDEX FROM book4;

#分析
#可以使用索引,满足最左前缀法则
EXPLAIN SELECT * FROM book4 WHERE book_id = 1001 AND book_name = 'mysql';

#不可以使用索引,因为不满足最左前缀法则。
EXPLAIN SELECT * FROM book4 WHERE book_name = 'mysql';

6 . 创建全文索引
FULLTEXT全文索引可以用于全文搜索,并且只为CHARVARCHARTEXT列创建索引。索引总是对整个列进行,不支持局部(前缀)索引。

举例1:创建表test4,在表中的info字段上建立全文索引,SQL语句如下:

CREATE TABLE test4(
id INT NOT NULL,
name CHAR(30) NOT NULL,
age INT NOT NULL,
info VARCHAR(255),
#为info字段的前50个字符创建全文索引
FULLTEXT INDEX futxt_idx_info(info(50))
) ENGINE=MyISAM;

在MySQL5.7及之后版本中可以不指定最后的ENGINE了,因为在此版本中InnoDB支持全文索引。

语句执行完之后查看索引

SHOW INDEX FROM test4;

由结果可以看到,info字段上已经成功建立了一个名为futxt_idx_info的FULLTEXT索引。

在这里插入图片描述

举例2:

CREATE TABLE articles (
	id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
	title VARCHAR (200),
	body TEXT,
	FULLTEXT index (title, body)
) ENGINE = INNODB ;

创建了一个给title和body字段添加全文索引的表。

举例3:

CREATE TABLE `papers` (
	`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
	`title` varchar(200) DEFAULT NULL,
	`content` text,
	PRIMARY KEY (`id`),
	FULLTEXT KEY `title` (`title`,`content`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

不同于like方式的的查询:

SELECT * FROM papers WHERE content LIKE%查询字符串%;

全文索引用match+against方式查询:

SELECT * FROM papers WHERE MATCH(title,content) AGAINST (‘查询字符串’);

明显的提高查询效率。

注意点

  1. 使用全文索引前,搞清楚版本支持情况;
  2. 全文索引比 like + % 快 N 倍,但是可能存在精度问题;
  3. 如果需要全文索引的是大量数据,建议先添加数据,再创建索引。
    • 因为你不断添加数据的时候,索引也跟着变,反而导致添加数据变慢。

7 . 创建空间索引
空间索引创建中,要求空间类型的字段必须为 非空

举例:创建表test5,在空间类型为GEOMETRY的字段上创建空间索引,SQL语句如下:

CREATE TABLE test5(
geo GEOMETRY NOT NULL,
SPATIAL INDEX spa_idx_geo(geo)
) ENGINE=MyISAM;

语句执行完之后查看索引

SHOW INDEX FROM test4;

可以看到,test5表的geo字段上创建了名称为spa_idx_geo的空间索引。注意创建时指定空间类型字段值的非空约束,并且表的存储引擎为MylSAM。

2)在已经存在的表上创建索引

方式一:使用ALTER TABLE ADD语句创建索引 ,ALTER TABLE语句创建索引的基本语法如下:

ALTER TABLE 表名 ADD [索引的类型] INDEX 
[索引的名字] (作用的字段[字符类型需要指明长度],...) [索引在存储的时候是升序还是降序]

ALTER TABLE table_name ADD [UNIQUE | FULLTEXT | SPATIAL] [INDEX | KEY]
[index_name] (col_name[length],...) [ASC | DESC]

举例:

#先创建一个表
CREATE TABLE book5(
book_id INT ,
book_name VARCHAR(100),
AUTHORS VARCHAR(100),
info VARCHAR(100) ,
COMMENT VARCHAR(100),
year_publication YEAR
);

#查看这个表的索引情况
SHOW INDEX FROM book5;

#创建普通索引
ALTER TABLE book5 ADD INDEX idx_cmt(COMMENT);

#创建唯一索引
ALTER TABLE book5 ADD UNIQUE uk_idx_bname(book_name);

#创建联合索引
ALTER TABLE book5 ADD INDEX mul_bid_bname_info(book_id,book_name,info);

在这里插入图片描述

方式二:使用CREATE INDEX ON创建索引,CREATE INDEX语句可以在已经存在的表上添加索引,在MySQL中,CREATE INDEX被映射到一个ALTER TABLE语句上,基本语法结构为:

  • 关键字:CREATE INDEX ON
  • 索引类型
    • UNIQUE :创建的是一个唯一索引,要求该字段不能重复出现数据。
    • FULLTEXT :创建的是一个全文索引
    • SPATIAL:创建的是一个空间索引
  • 如果没有写索引类型,代表创建的是一个常规索引。
  • ...:表示一个索引可以关联多个字段
  • 如果一个索引只关联一个字段,那么称这个索引为单列索引
  • 如果一个索引关联了多个字段,那么称这个索引为联合索引或者组合索引
CREATE [索引类型] INDEX 索引的名字
ON 表名 (作用的字段[字符类型需要指明长度],...) [索引在存储的时候是升序还是降序]

CREATE [UNIQUE | FULLTEXT | SPATIAL] INDEX index_name
ON table_name (col_name[length],...) [ASC | DESC]

举例:

#创建表
CREATE TABLE book6(
book_id INT ,
book_name VARCHAR(100),
AUTHORS VARCHAR(100),
info VARCHAR(100) ,
COMMENT VARCHAR(100),
year_publication YEAR
);

#查看索引
SHOW INDEX FROM book6;

#创建普通索引
CREATE INDEX idx_cmt ON book6(COMMENT);

#创建唯一索引
CREATE UNIQUE INDEX  uk_idx_bname ON book6(book_name);

#创建组合索引
CREATE INDEX mul_bid_bname_info ON book6(book_id,book_name,info);

在这里插入图片描述

2.4.2 查看索引

#方式1:查看创建表的语法,针对的是在建表时候创建的索引
SHOW CREATE TABLE table_name ;

#方式2:查看指定表中的所有索引(啥时候创建的索引都可以查看)
SHOW INDEX FROM table_name ; 

其中各个主要参数的含义为

  1. Table表示创建索引的表。
  2. Non_unique表示索引非唯—,1代表非唯一索引,0代表唯一索引。
  3. Key_name表示索引的名称。
  4. Seq_in_index表示该字段在索引中的位置,单列索引该值为1,组合索引为每个字段在索引定义中的顺序。
  5. Column_name表示定义索引的列字段。
  6. Sub_part表示索引的长度。
  7. Null表示该字段是否能为空值。
  8. Index_type表示索引类型。

由结果可以看到,id字段上已经成功建立了一个名为mul_bid_bname_info的联合索引。

在这里插入图片描述

2.4.3 删除索引

1 . 使用ALTER TABLE删除索引 ALTER TABLE删除索引的基本语法格式如下:

#对应的是使用ALTER TABLE创建索引的方式
ALTER TABLE table_name DROP INDEX index_name;

2 . 使用DROP INDEX语句删除索引 DROP INDEX删除索引的基本语法格式如下:

#对应的是使用CREATE INDEX创建索引的方式
#删除哪一张表中的哪一个索引
DROP INDEX index_name ON table_name;

提示

  • 添加AUTO_INCREMENT约束字段的唯一索引不能被删除。
  • 原因:有唯一索引就会自动添加唯一约束,而自增作用的字段要么有主键约束要么有唯一约束,删除唯一索引也就没有唯一约束了,此时会报错,所以不能删除。

测试:

表book5没有删除之前的索引情况:普通索引,唯一索引,联合索引

在这里插入图片描述

# 02-索引的删除

SHOW INDEX FROM book5; #查看索引

#方式1:ALTER TABLE .... DROP INDEX ....
ALTER TABLE book5 
DROP INDEX idx_cmt;


#方式2:DROP INDEX ... ON ...
DROP INDEX uk_idx_bname ON book5;

#测试:删除联合索引中的相关字段,索引的变化
#效果:3个字段组成的联合索引,如果直接删除其中的一个字段,那么这个字段对应的索引也会被删除。

#删除表中的字段book_name
ALTER TABLE book5
DROP COLUMN book_name;

#删除表的字段book_id
ALTER TABLE book5
DROP COLUMN book_id;

#删除表的字段info
ALTER TABLE book5
DROP COLUMN info;

在这里插入图片描述

提示

  • 删除表中的列时,如果要删除的列为联合索引的组成部分,则该列也会从索引中删除。如果组成索引的所有列都被删除,则整个联合索引将被删除。

2.4.4 案例演示:

创建数据库 itcast:
在这里插入图片描述

先来创建一张表 tb_user,并且查询测试数据。


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');

表结构中插入的数据如下:

在这里插入图片描述
查看目前表中有哪些索引:只有主键索引

SHOW INDEX FROM tb_user; 

在这里插入图片描述

数据准备好了之后,接下来,我们就来完成如下需求:

A.name字段为姓名字段,该字段的值可能会重复,为该字段创建索引。

说明:字段可以重复说明创建的索引不可能是唯一索引,只能是常规索引

CREATE INDEX idx_user_name ON tb_user(name);

再次查看目前表中有哪些索引:
在这里插入图片描述

B.phone手机号字段的值,是非空,且唯一的,为该字段创建唯一索引

CREATE UNIQUE INDEX idx_user_phone ON tb_user(phone); 

再次查看目前表中有哪些索引:
在这里插入图片描述

C.为profession、age、status创建联合索引

#这个顺序是有讲究的,在索引的使用中讲解。
CREATE INDEX idx_user_pro_age_sta ON tb_user(profession,age,status);

再次查看目前表中有哪些索引:
在这里插入图片描述

D.为email建立合适的索引来提升查询效率。

说明:邮件,创建常规索引即可。

CREATE INDEX idx_email ON tb_user(email); 

再次查看目前表中有哪些索引:

show index from tb_user; 

在这里插入图片描述

2.5 SQL性能分析

  • 为什么要学习SQL性能分析的工具?

  • 因为要做sql优化,想要做sql优化首先要定位出对哪一类的sql进行优化,这个时候就需要知道每一个sql的执行性能是什么样的。

  • sql优化主要是针对的查询语句。

  • 优化查询语句的时候,索引这一块的优化占据主导地位。

2.5.1 SQL执行频率

  • 为什么要知道sql的执行频率?
  • 假如一个数据库表中的数据量很大,如果这张表只做插入不做查询,那么就没有必要做sql优化了,所以要通过sql的执行频率来判定当前数据库到底是以查询为主,还是以插入为主,还是以修改为主,还是以删除为主。

MySQL 客户端连接成功后,通过 show [session|global] status 命令可以提供服务器状态信息。通过如下指令,可以查看当前数据库的INSERT、UPDATE、DELETE、SELECT的访问频次:

-- session 是查看当前会话 ;
-- global 是查询全局数据 ;
SHOW GLOBAL STATUS LIKE 'Com_______';

在这里插入图片描述

Com_delete: 删除次数

Com_insert: 插入次数

Com_select: 查询次数

Com_update: 更新次数

我们可以在当前数据库再执行几次查询操作,然后再次查看执行频次,看看 Com_select 参数会不会变化。

执行2次查询,再次查看SQL执行频率,发现查询的频率由30变为36。

执行一次查询加3,可能和可视化工具有关。

在这里插入图片描述

通过上述指令,我们可以查看到当前数据库到底是以查询为主,还是以增删改为主,从而为数据库优化提供参考依据。 如果是以增删改为主,我们可以考虑不对其进行索引的优化。 如果是以查询为主,那么就要考虑对数据库的索引进行优化了。

那么通过查询SQL的执行频次,我们就能够知道当前数据库到底是增删改为主,还是查询为主。 那假如说是以查询为主,我们又该如何定位针对于那些查询语句进行优化呢? 次数我们可以借助于慢查询日志。

  • 即:当前通过sql的查询频次只知道select语句的权重比较高,那么我们到底要针对那些select语句进行优化呢????
  • 此时就需要借助数据库中的慢查询日志,来定位那些sql语句执行效率比较低,从而对这类的sql语句进行优化。

接下来,我们就来介绍一下MySQL中的慢查询日志。

2.5.2 慢查询日志

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

  • 即:只要我们执行了某一条sql,它的执行耗时超过10秒,那么Mysql就认为这一类的sql就是慢查询。此时就会记录在慢查询日志中。

MySQL的慢查询日志默认没有开启,我们可以查看一下开关的开启情况。

SHOW VARIABLES LIKE 'slow_query_log'

在这里插入图片描述

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

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

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

在这里插入图片描述

配置完毕之后,通过以下指令重新启动MySQL服务器进行测试,之后会生成一个日志文件。

# 重启Mysql服务器
systemctl restart mysqld

在这里插入图片描述

然后,再次查看开关情况,慢查询日志就已经打开了。

在这里插入图片描述

查看慢日志文件中记录的信息:

# 进入到此目录,发现会有一个后缀是-slow.log的日志文件
cd /var/lib/mysql/

# 查看次日志文件:只记录了一些基本的信息,比如数据库的版本,端口。
cat mysql8-slow.log

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

测试:

A.执行如下SQL语句 :

select * from tb_user; -- 这条SQL执行效率比较高, 执行耗时 0.00sec

#tb_sku表存放了1000万条记录,电脑太卡,这里不在演示,只显示最终结果
select count(*) from tb_sku; -- 由于tb_sku表中, 预先存入了1000w的记录, count一次,耗时13.35sec

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

B.检查慢查询日志 :

最终我们发现,在慢查询日志中,只会记录执行时间超多我们预设时间(2s)的SQL,执行较快的SQL 是不会记录的。

#查看慢日志文件尾部实时输出的内容,这要这个文件有新的内容追加上来,在这一块马上就可以刷新出来
tail -f mysql8-slow.log
  • 记录通过哪一个用户在哪一个主机上连接的
  • 耗时时长 锁了多少行 返回了多少记录
  • 用的哪一个数据库
  • 当前操作的时间
  • 执行的sql语句
    在这里插入图片描述
    在这里插入图片描述

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

2.5.3 profile详情

  • 通过慢查询日志记录的是,sql执行耗时超过了我们预设的指定时间后才会记录。比如我们预设的指定时间是2秒,那么超过2秒他才会记录在慢查询日志当中,假如一些sql的执行耗时为1.9秒,它是不会记录在慢查询日志当中的,假如说业务系统当中有一些SQL语句它的业务很简单,但是它的执行达到了1.9几秒,那么这类的sql实际上也相对来说性能是比较低的。那么我们也需要对这类sql进行优化,那么我们如何定位到这类sql呢???
  • 慢查询日志满足不了,可以通过profile详情进行定位。

show profiles 能够在做SQL优化时帮助我们了解时间都耗费到哪里去了。通过have_profiling 参数,能够看到当前MySQL是否支持profile操作:

SELECT @@have_profiling ;

在这里插入图片描述

可以看到,当前MySQL是支持 profile操作的,但是开关是关闭的。可以通过set语句在session/global级别开启profiling:

#查看这个默认的开关是否打开:0代表没有开启,1代表开启
SELECT @@profiling ;

#如果没有打开,可以设置为1打开开关
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的操作,然后通过如下指令查看指令的执行耗时:

-- 查看每一条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各个阶段的耗时情况 :

在这里插入图片描述

查看指定query_id的SQL语句CPU的使用情况

在这里插入图片描述

2.5.4 explain执行计划

  • 以上3种sql性能分析的工具,sql语句的执行频次、慢查询日志、以及通过profile详情查看sql语句耗时以及时间耗费在哪了,这些都是通过时间的层面来评判一条sql语句的性能,执行时间段说明sql的性能比较高,这种判定只是粗略的判断,并不能真正的评判一条sql语句的性能,要想真正的查看一条sql语句的性能还要借助第4种手段explain,来查看sql的执行计划。

EXPLAIN 或者 DESC命令获取 MySQL 如何执行 SELECT 语句的信息,包括在 SELECT 语句 它的执行过程中是否用到了索引,表的连接情况,表的连接顺序

语法:

-- 直接在任意的select语句之前加上关键字 explain / desc
EXPLAIN SELECT 字段列表 FROM 表名 WHERE 条件 ;

在这里插入图片描述

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

字段含义
idselect查询的序列号,表示查询中执行select子句或者是操作表的顺序(id相同,执行顺序从上到下;id不同,值越大,越先执行)。
select_type表示 SELECT 的类型,常见的取值有 SIMPLE(简单表,即不使用表连接或者子查询)、PRIMARY(主查询,即外层的查询)、UNION(UNION 中的第二个或者后面的查询语句)、SUBQUERY(SELECT/WHERE之后包含了子查询)等
type 表示连接类型,性能由好到差的连接类型为NULL、system、const、eq_ref、ref、range、 index、all 。
possible_key显示可能应用在这张表上的索引,一个或多个。
key实际使用的索引,如果为NULL,则没有使用索引。
key_len表示索引中使用的字节数, 该值为索引字段最大可能长度,并非实际使用长度,在不损失精确性的前提下, 长度越短越好 。
rowsMySQL认为必须要执行查询的行数,在innodb引擎的表中,是一个估计值, 可能并不总是准确的。
filtered表示返回结果的行数占需读取行数的百分比, filtered 的值越大越好。
Extra额外的信息,执行查询的过程中在前面这几个字段当中没有展示出来的值,将会在此处进行展示
  • type
    • NULL:业务系统中一般不太可能优化为null,一般是查询的时候不访问任何表才会出现
      在这里插入图片描述

    • system:访问系统表

    • const:主键、唯一索引

    • eq_ref

    • ref:非唯一索引

    • range

    • index:用了索引,但是他也会对索引进行扫描,遍历整个索引数,比all快但性能也比较低。

    • all:全表扫描,性能比较低

2.6 索引使用

2.6.1 验证索引效率

在讲解索引的使用原则之前,先通过一个简单的案例,来验证一下索引,看看是否能够通过索引来提升数据查询性能。在演示的时候,我们还是使用之前准备的一张表 tb_sku , 在这张表中准备了1000w 的记录。

执行耗时:11.03秒

在这里插入图片描述

这张表中id为主键,有主键索引,而其他字段是没有建立索引的。 我们先来查询其中的一条记录,看看里面的字段情况,执行如下SQL:

#\G:表中的数据比较多,显示的时候变形了,加上\G会把每一列转化为一行进行展示。
#   在命令行中使用,sqlyog中使用报错。
select * from tb_sku where id = 1\G;

执行耗时:0.00秒

在这里插入图片描述

可以看到即使有1000w的数据,根据id进行数据查询,性能依然很快,因为主键id是有索引的。 那么接下来,我们再来根据 sn 字段进行查询,执行如下SQL:

SELECT * FROM tb_sku WHERE sn = '100000003145001';

执行耗时: 20.78秒

在这里插入图片描述

我们可以看到根据sn字段进行查询,查询返回了一条数据,结果耗时 20.78sec,就是因为sn没有索引,而造成查询效率很低。

那么我们可以针对于sn字段,建立一个索引,建立了索引之后,我们再次根据sn进行查询,再来看一 下查询耗时情况。

创建索引:它的执行耗时也比较长

创建索引就是要去构建一种数据结构,为这1000万条数据构建b+tree这种数据结构,所以耗时。

create index idx_sku_sn on tb_sku(sn) ;

在这里插入图片描述

然后再次执行相同的SQL语句,再次查看SQL的耗时。

SELECT * FROM tb_sku WHERE sn = '100000003145001'\G;

执行耗时: 0.01秒

在这里插入图片描述

我们明显会看到,sn字段建立了索引之后,查询性能大大提升。建立索引前后,查询耗时都不是一个数量级的。

2.6.2 索引失效情况

1)最左前缀法则

主要针对联合索引

如果一个索引关联了多个字段(联合索引),要遵守最左前缀法则。最左前缀法则指的是查询从索引的最左列开始, 并且不跳过索引中的列。如果跳跃某一列,索引将会部分失效(后面的字段索引失效)

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

#查看表中的所有索引
SHOW INDEX FROM tb_user ;

在这里插入图片描述

在 tb_user 表中,有一个联合索引,这个联合索引涉及到三个字段,顺序分别为:profession,age,status。

对于最左前缀法则指的是,查询时,最左变的列,也就是profession必须存在,否则索引全部失效。 而且中间不能跳过某一列,否则该列后面的字段索引将失效。 接下来,我们来演示几组案例,看一下具体的执行计划:

#满足左前缀法则,走索引
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不存在。

#从最左边开始会走索引,由于跳过了age,所以后面的索引失效。
explain select * from tb_user where profession = '软件工程' and status = '0';

在这里插入图片描述

上述的SQL查询时,存在profession字段,最左边的列是存在的,索引满足最左前缀法则的基本条件。但是查询时,跳过了age这个列,所以后面的列索引是不会使用的,也就是索引部分生效,所以索引的长度就是47。

思考题:

  • 这个联合索引涉及到三个字段,顺序分别为:profession,age,status
  • 当执行SQL语句: explain select * from tb_user where age = 31 and
    status = ‘0’ and profession = ‘软件工程’; 时,是否满足最左前缀法则,走不走上述的联合索引,索引长度?
    在这里插入图片描述
  • 可以看到,是完全满足最左前缀法则的,索引长度54,联合索引是生效的。
  • 注意 : 最左前缀法则中指的最左边的列,是指在查询时,联合索引的最左边的字段(即是第一个字段)必须存在,与我们编写SQL时,条件编写的先后顺序无关
2)范围查询

主要针对联合索引

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

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,就说明所有的字段都是走索引的。

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

3)不等于(!= 或者<>)索引失效
#创建索引
CREATE INDEX idx_name ON student(NAME);

#索引失效
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name <> 'abc' ;

#索引失效
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name != 'abc' ;

在这里插入图片描述

4)计算、函数导致索引失效

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

2.4.4案例演示,在tb_user表中,除了前面介绍的联合索引之外,还创建了一个索引,是phone字段的唯一索引索引(单列索引)

#查看索引索引
SHOW INDEX FROM tb_user; 

在这里插入图片描述

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

explain select * from tb_user where phone = '17799990015';

在这里插入图片描述

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

explain select * from tb_user where substring(phone,10,2) = '15';

在这里插入图片描述

C.当根据stuno字段进行计算操作之后,索引失效。

#创建stuno字段的索引
CREATE INDEX idx_sno ON student(stuno);

#字段做运算了,导致索引失效
EXPLAIN SELECT SQL_NO_CACHE id, stuno, NAME FROM student WHERE stuno+1 = 900001;

#没有做运算,使用了索引
EXPLAIN SELECT SQL_NO_CACHE id, stuno, NAME FROM student WHERE stuno = 900000;

在这里插入图片描述

在这里插入图片描述

5)字符串不加引号(类型转换导致索引失效)

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

原因: 字符串不加引号会先进行隐式的转换,隐式转换用到了函数所以导致索引失效。

接下来,我们通过两组示例,来看看对于字符串类型的字段,加单引号与不加单引号的区别:

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;

满足最左前缀法则,走联合索引,长度发生变化,说明status没有走索引
在这里插入图片描述

explain select * from tb_user where phone = '17799990015';

explain select * from tb_user where phone = 17799990015;

不走唯一索引。
在这里插入图片描述

经过上面两组示例,我们会明显的发现,如果字符串不加单引号,对于查询结果,没什么影响,但是数据库存在隐式类型转换,索引将失效

6)模糊查询

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

接下来,我们来看一下这三条SQL语句的执行效果,查看一下其执行计划:

由于下面查询语句中,都是根据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模糊查询中,在关键字后面加%,索引可以生效。而如果在关键字 前面加了%,索引将会失效。

7)or连接条件

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

即:or 2侧都有索引,才会生效。

#id有主键索引,age没有索引:复合索引你单单只查询age所以不会用到复合索引
explain select * from tb_user where id = 10 or age = 23;

#phone有索引,age没索引
explain select * from tb_user where phone = '17799990017' or age = 23;

在这里插入图片描述

由于age没有索引,所以即使id、phone有索引,索引也会失效。所以需要针对于age也要建立索引。

然后,我们可以对age字段建立索引。

create index idx_user_age on tb_user(age);

在这里插入图片描述

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

在这里插入图片描述

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

8)数据分布影响(is null和is not null)

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

select * from tb_user where phone >= '17799990005';

select * from tb_user where phone >= '17799990015';

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

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

就是因为MySQL在查询时,会评估使用索引的效率与走全表扫描的效率,如果走全表扫描更快,则放弃索引,走全表扫描。 因为索引是用来索引少量数据的,如果通过索引查询返回大批量的数据,则还不如走全表扫描来的快,此时索引就会失效。

接下来,我们再来看看 is null 与 is not null 操作是否走索引。
执行如下两条语句 :

#走索引:所有的profession都是有值的,is null过滤出极少部分数据,所以使用索引速度更快。
explain select * from tb_user where profession is null;

#不走索引:所有的profession都是有值的,is not null返回大部分数据,此时mysql会评估走索引还不如全表扫描快,所以会放弃走索引。
explain select * from tb_user where profession is not null;

在这里插入图片描述

接下来,我们做一个操作将profession字段值全部更新为null。
在这里插入图片描述

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

#不走索引:profession都为null,此时is null返回大部分数据,所以走全表扫描
explain select * from tb_user where profession is null;

#走索引:profession都为null,此时is not null返回少部分数据,所以走索引
explain select * from tb_user where profession is not null;

在这里插入图片描述

最终我们看到,一模一样的SQL语句,先后执行了两次,结果查询计划是不一样的,为什么会出现这种现象,这是和数据库的数据分布有关系。查询时MySQL会评估,走索引快,还是全表扫描快,如果全表扫描更快,则放弃索引走全表扫描。 因此,is null 、is not null是否走索引,得具体情况具体分析,并不是固定的。

结论:最好在设计数据表的时候就将字段设置为 NOT NULL 约束,比如你可以将INT类型的字段,默认值设置为o。将字符类型的默认值设置为空字符串(')。

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

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

转换就使用到了函数,所以导致索引失效。

mysql8.0默认使用utf8mb4作为字符集

2.6.3 索引的使用建议

一般性建议:

  • 对于单列索引,尽量选择针对当前query过滤性更好的索引
  • 在选择组合索引的时候,当前query中过滤性最好的字段在索引字段顺序中,位置越靠前越好。
  • 在选择组合索引的时候,尽量选择能够包含当前query中的where子句中更多字段的索引。
  • 在选择组合索引的时候,如果某个字段可能出现范围查询时,尽量把这个字段放在索引次序的最后面。

总之,书写SQL语句时,尽量避免造成索引失效的情况。

2.6.4 SQL提示

目前tb_user表的数据情况如下:恢复更新的数据
在这里插入图片描述

索引情况如下:

在这里插入图片描述

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

drop index idx_user_age on tb_user;

drop index idx_email on tb_user;

A.执行SQL : explain select * from tb_user where profession = '软件工程';
在这里插入图片描述
符合最左前缀法则,查询走了联合索引。

B.执行SQL,创建profession的单列索引:create index idx_user_pro on tb_user(profession);

在这里插入图片描述

C.创建单列索引后,再次执行A中的SQL语句,查看执行计划,此时既有复合索引又有单列索引,看看到底走哪个索引。

在这里插入图片描述

测试结果,我们可以看到,possible_keys中 idx_user_pro_age_sta,idx_user_pro 这两个索引都可能用到,最终MySQL选择了idx_user_pro_age_sta 联合索引。这是MySQL自动选择的结果。

那么,我们能不能在查询的时候,自己来指定使用哪个索引呢? 答案是肯定的,此时就可以借助于MySQL的SQL提示来完成。 接下来,介绍一下SQL提示。

SQL提示,是优化数据库的一个重要手段,简单来说,就是在SQL语句中加入一些人为的提示来达到优化操作的目的。

1).use index建议MySQL使用哪一个索引完成此次查询(仅仅是建议,mysql内部还会再次进行评估,如果你指定的索引效率低,mysql可能不会接收你的建议)。

explain select * from tb_user use index(idx_user_pro) where profession = '软件工程';

2).ignore index忽略指定的索引。

explain select * from tb_user ignore index(idx_user_pro) where profession = '软件工程';

3).force index强制使用索引。

explain select * from tb_user force index(idx_user_pro) where profession = '软件工程';

示例演示:

A.use index

explain select * from tb_user use index(idx_user_pro) where profession = '软件工
程';

在这里插入图片描述

B.ignore index

explain select * from tb_user ignore index(idx_user_pro) where profession = '软件工

程';

在这里插入图片描述

C.force index

explain select * from tb_user force index(idx_user_pro_age_sta) where profession =

'软件工程';

在这里插入图片描述

2.6.5 覆盖索引(Extra)

尽量使用覆盖索引,减少select *(因为很容易出现回表查询)。 那么什么是覆盖索引呢? 覆盖索引是指查询使用了索引,并且需要返回的列,在该索引中已经全部能够找到

接下来,我们来看一组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';

上述这几条SQL的执行结果为:
在这里插入图片描述

从上述的执行计划我们可以看到,这四条SQL语句的执行计划前面所有的指标都是一样的,看不出来差异。但是此时,我们主要关注的是后面的Extra,前面两天SQL的结果为 Using where; Using Index ; 而后面两条SQL的结果为: Using index condition

这个Extra输出的值和mysql的版本有关,不同的版本输出的结果不同。

Extra含义
Using where; Using Index (性能高)查找使用了索引,但是需要的数据都在索引列中能找到,所以不需要回表查询数据
Using index condition (性能低)查找使用了索引,但是需要回表查询数据

因为,在tb_user表中有一个联合索引 idx_user_pro_age_sta,该索引关联了三个字段profession、age、status,而这个索引也是一个二级索引,所以叶子节点下面挂的是这一行的主键id。 所以当我们查询返回的数据在 id、profession、age、status 之中,则直接走二级索引直接返回数据了。 如果超出这个范围,就需要拿到主键id,再去扫描聚集索引,再获取额外的数据了,这个过程就是回表。 而我们如果一直使用select * 查询返回所有字段值,很容易就会造成回表查询(除非是根据主键查询,此时只会扫描聚集索引)。

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

A.表结构及索引示意图:
在这里插入图片描述

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

B.执行SQL : select * from tb_user where id = 2;
在这里插入图片描述

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

C.执行SQL:selet id,name from tb_user where name = ‘Arm’;

在这里插入图片描述

虽然是根据name字段查询,查询二级索引,但是由于查询返回在字段为 id,name,在name的二级索引中,这两个值都是可以直接获取到的,因为覆盖索引,所以不需要回表查询,性能高。

D.执行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语句,在查询的过程中,出现回表查询。
  • 思路:首先想要提高查询效率肯定需要建立索引,重点是建立什么样的索引才能是提高sql性能的最优方案
    • 情况1:针对username 字段建立单列索引,可以提高性能,但是查询返回的字段不仅仅包含id、username ,还包含password 所以会进行回表查询。
    • 情况2:针对于 username, password建立联合索引,这样直接在二级索引中就可以查询到想要的数据了,直接覆盖索引避免回表查询,性能比较好。

2.6.6 前缀索引

当字段类型为字符串(varchar,text,longtext等)时,有时候需要索引很长的字符串(eg:存储一篇文章),这会让索引变得很大(如果根据文章的内容进行查询,那我们直接对文章的内容去建立一个索引),查询时,浪费大量的磁盘IO, 影响查询效率。此时可以只将字符串的一部分前缀,建立索引,这样可以大大节约索引空间,从而提高索引效率。

1).语法

#和之前创建索引的语法基本一致,只需要在对应的写一个字段后面(n),表示我要提取这个字符串的前面几个字符来建立索引。
create index idx_xxxx on table_name(column(n)) ;

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

create index idx_email_5 on tb_user(email(5));

2).前缀长度

可以根据索引的选择性来决定,而选择性是指不重复的索引值(基数)和数据表的记录总数的比值, 索引选择性越高则查询效率越高, 唯一索引的选择性是1,这是最好的索引选择性,性能也是最好的。

  • 需求:想要知道tb_user表中email字段的选择性。
    • 首先查询当前表中的总记录数: count(*) ,24条
    • 其次查询表中email字段不重复的数量:count(distinct email),24条
    • 选择性:24/24=1,选择性为1的性能最好。
#选择性为1,性能最好
select count(distinct email) / count(*) from tb_user ;

#表示从email的第一个字符开始截取,截取5个。
select count(distinct substring(email,1,5)) / count(*) from tb_user ;

截取前10个,选择性为1,也就是说截取email的前10个字符数据也是不重复的。

截取前9个,选择性为0.9583,出现重复。
截取前8个,选择性为0.9583,出现重复。
截取前7个,选择性为0.9583,出现重复。
截取前6个,选择性为0.9583,出现重复。
截取前5个,选择性为0.9583,出现重复。

截取前4个,选择性为0.9167,出现重复。


总结:这个就带看我们业务系统中想要多大的选择性,如果选择性尽可能高那么就截取10个前缀,
如果想要平衡选择性和索引的体积(截取的长度),那么就可以考虑取前5个前缀。

3).前缀索引的查询流程

  • 针对主键构建一个聚集索引
  • 针对email的前5个字符构建一个前缀索引/二级索引
  • 执行:select * from tb_user where email = lvbu666@163.com的过程:
    • 首先到二级索引中进行匹配,注意不是整个字段匹配而是截取它的前5个前缀进行匹配,拿到二级索引中的id。
    • 之后在聚集索引中进行回表查询,根据id查询对应的一行数据,注意这里不是拿到这一行数据直接返回
    • 因为刚才在对比的时候我只是对比了前缀,拿到这一行数据我要从这一行数据中拿出email的值,然后再去看这一行的数据email的值是不是我说传递进来的email,如果是那么此时我就要将这一行的数据来查询到并且返回,然后在去查询当前lvbu6在往下一个节点走,因为它是一个链表,我再去找下一个元素是否为lvbu6,如果不是直接返回这一行数据,如果是那我接下来还需要再去查询下一行的数据,再把下一行的数据也拿到最终组装数据并返回。

在这里插入图片描述

2.6.7 单列索引与联合索引的选择

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

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

我们先来看看 tb_user 表中目前的索引情况:

  • id;主键索引
  • phone:单列索引、唯一索引
  • name:单列索引
  • profession、age、status:联合索引
  • email:前缀索引
    在这里插入图片描述

在查询出来的索引中,既有单列索引,又有联合索引。

接下来,我们来执行一条SQL语句,看看其执行计划:

explain select id,phone,name from tb_user 
where phone = '17799990010' and name = '韩信';

在这里插入图片描述

通过上述执行计划我们可以看出来,在and连接的两个字段 phone、name上都是有单列索引的,但是最终mysql只会选择一个索引,也就是说,只能走一个字段的索引phone,查询phone、name2个字段,在phone的二级索引中必然不包含name字段的值,所以此时是会回表查询的。

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

紧接着,我们再来创建一个phone和name字段的联合索引来查询一下执行计划。

#phone已经是一个唯一索引了,再加上name字段那么这个联合索引也是一个唯一的,所以
#  可以在前面加上一个关键字unique创建一个唯一索引。
create unique index idx_user_phone_name on tb_user(phone,name);

此时再次执行上面的查询语句,可能用到的索引有3个 phone的单列索引、name的单列索引、phone和name的组合索引。实际上使用的是phone的单列索引,这是mysql自己选择的结果。

  • Extra:显示Null,代表使用回表查询。
    在这里插入图片描述

使用sql提示指定使用联合索引:

explain select id,phone,name from tb_user use index(idx_user_phone_name) 
where phone = '17799990010' and name = '韩信';
  • Extra:显示Using index,代表使用的是覆盖索引,不需要使用回表查询。
    在这里插入图片描述

此时,查询时,就走了联合索引,而在联合索引中包含 phone、name的信息,在叶子节点下挂的是对应的主键id,所以查询是无需回表查询的。

  • 在业务场景中,如果存在多个查询条件,考虑针对于查询字段建立索引时,建议建立联合索引, 而非单列索引。
  • 因为:使用联合索引性能相对比较高,而且联合索引使用得当是可以避免使用回表查询,而使用单列索引很容易出现回表查询的,造成性能降低。

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

  • 每一个节点所存储的键值就是phone和name的组合情况,它会先按照phone手机号进行排序,如果手机号一致在按照name字段排序,最终构建成一个b+tree。当然所构建出的这个联合索引属于二级索引,叶子节点挂的是这一页行记录对应的主键。

  • 为什么不需要回表查询???

    • 因为我们查询返回的只是id、phone、name,走这个联合索引查询返回的恰好有id、phone、name,走这个二级索引已经获取到了想要的数据(覆盖索引),所以不需要进行回表查询。
  • 在创建联合索引的时候,对于哪一个字段放在前面哪一个字段放在后面对于查询的结果有没有影响???

    • 有影响,根据之前学习的最左前缀法则,每一次查询想要使用联合索引那么最左边的列必须存在。如果是(phone,name)表示最左边的phone必须存在,如果是(name,phone)表示最左边的name必须存在,所以在创建联合索引的时候我们需要考虑字段的顺序。
      在这里插入图片描述

2.7 索引设计原则

为了使索引的使用效率更高,在创建索引时,必须考虑在哪些字段上创建索引和创建什么类型的索引。索引设计不合理或者缺少索引都会对数据库和应用程序的性能造成障碍。高效的索引对于获得良好的性能非常重要。设计索引时,应该考虑相应准则。

2.7.1 数据准备

第1步:创建数据库、创建表

CREATE DATABASE atguigudb1;

USE atguigudb1;

#1.创建学生表和课程表
CREATE TABLE `student_info` (
 `id` INT(11) AUTO_INCREMENT,
 `student_id` INT NOT NULL ,
 `name` VARCHAR(20) DEFAULT NULL,
 `course_id` INT NOT NULL ,
 `class_id` INT(11) DEFAULT NULL,
 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
 PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

CREATE TABLE `course` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`course_id` INT NOT NULL ,
`course_name` VARCHAR(40) DEFAULT NULL,
PRIMARY KEY (`id`)
  #还可以设置自增的初始值
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

在这里插入图片描述

第2步:创建模拟数据必需的存储函数

#函数1:创建随机产生字符串函数(直接创建会报错,需要设置系统变量为1)
#报错原因:主从复制主机写操作记录在bin-log日志中,从机读取bin-log日志这样来保证
#          主从数据的一致,编写sql的时候默认情况下定义一些函数,就有可能函数记录到
#          bin-log日志中,在从机读的时候有可能回达到数据不一致的情况出现,所以默认
#          mysql服务器不相信这个创建函数,所以要想创建需要打开这个开关设置为1.
DELIMITER //
CREATE FUNCTION rand_string(n INT) 
	RETURNS VARCHAR(255) #该函数会返回一个字符串
BEGIN 
	DECLARE chars_str VARCHAR(100) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
	DECLARE return_str VARCHAR(255) DEFAULT '';
	DECLARE i INT DEFAULT 0;
	WHILE i < n DO 
       SET return_str =CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1));
       SET i = i + 1;
    END WHILE;
    RETURN return_str;
END //
DELIMITER ;

#解决:
#查看此系统变量:默认是0
SELECT @@log_bin_trust_function_creators;
#设置为1,再次执行上面的函数1就可以创建成功了
SET GLOBAL log_bin_trust_function_creators = 1;

在这里插入图片描述

#函数2:创建随机数函数
DELIMITER //
CREATE FUNCTION rand_num (from_num INT ,to_num INT) RETURNS INT(11)
BEGIN   
DECLARE i INT DEFAULT 0;  
SET i = FLOOR(from_num +RAND()*(to_num - from_num+1))   ;
RETURN i;  
END //
DELIMITER ;

在这里插入图片描述

创建函数,假如报错:

This function has none of DETERMINISTIC......

由于开启过慢查询日志bin-log, 我们就必须为我们的function指定一个参数。

主从复制,主机会将写操作记录在bin-log日志中。从机读取bin-log日志,执行语句来同步数据。如果使用函数来操作数据,会导致从机和主键操作时间不一致。所以,默认情况下,mysql不开启创建函数设置。

  • 查看mysql是否允许创建函数:
show variables like 'log_bin_trust_function_creators';
  • 命令开启:允许创建函数设置:
#global :全局
set global log_bin_trust_function_creators=1; # 不加global只是当前窗口有效。
  • mysqld重启,上述参数又会消失。永久方法:
    • windows下:my.ini[mysqld]加上:log_bin_trust_function_creators=1
    • linux下:/etc/my.cnf下my.cnf[mysqld]加上:log_bin_trust_function_creators=1

第3步:创建插入模拟数据的存储过程

# 存储过程1:创建插入课程表存储过程
DELIMITER //
CREATE PROCEDURE  insert_course( max_num INT )
BEGIN  
DECLARE i INT DEFAULT 0;   
 SET autocommit = 0;    #设置手动提交事务
 REPEAT  #循环
 SET i = i + 1;  #赋值
 INSERT INTO course (course_id, course_name ) VALUES (rand_num(10000,10100),rand_string(6));  
 UNTIL i = max_num  
 END REPEAT;  
 COMMIT;  #提交事务
END //
DELIMITER ;
# 存储过程2:创建插入学生信息表存储过程
DELIMITER //
CREATE PROCEDURE  insert_stu( max_num INT )
BEGIN  
DECLARE i INT DEFAULT 0;   
 SET autocommit = 0;    #设置手动提交事务
 REPEAT  #循环
 SET i = i + 1;  #赋值
 INSERT INTO student_info (course_id, class_id ,student_id ,NAME ) VALUES (rand_num(10000,10100),rand_num(10000,10200),rand_num(1,200000),rand_string(6));  
 UNTIL i = max_num  
 END REPEAT;  
 COMMIT;  #提交事务
END //
DELIMITER ;

在这里插入图片描述

第4步:调用存储过程

#课程表:100门课
CALL insert_course(100);
SELECT COUNT(*) FROM course;

#学生表:100万条记录
CALL insert_stu(1000000); #耗时49秒
SELECT COUNT(*) FROM student_info;

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

2.7.2 哪些情况适合创建索引(11种)

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

索引本身可以起到约束的作用,比如唯一索引、主键索引都是可以起到唯一性约束的,因此在我们的数据表中,如果某个字段是唯一性的,就可以直接创建唯一性索引,或者主键索引。这样可以更快速地通过该索引来确定某条记录。

解释:

  • 如果这个字段上已经添加了唯一性约束,那么就不需要在创建索引了,因为添加了唯一约束会自动创建索引。
    • 添加唯一性索引会自动添加唯一性约束,反过来添加唯一性约束也会自动创建唯一性索引。
  • 这里指的是这个字段明明是唯一的,但是他既没有唯一性约束有没有唯一性索引,这个时候就可以为这个字段添加唯一索引来提高查询的效率。

例如,学生表中学号是具有唯一性的字段,为该字段建立唯一性索引可以很快确定某个学生的信息,如果使用姓名的话,可能存在同名现象,从而降低查询速度。

  • 业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。(来源:Alibaba)
  • 说明:不要以为唯一索引影响了 insert 速度(因为添加数据的时候会维护这个唯一的索引,也要进行修改),这个速度损耗可以忽略,但提高查找速度是明显的。
2)频繁作为 WHERE 查询条件的字段

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

比如student_info数据表(含100万条数据),假设我们想要查询 student_id=123110 的用户信息。

#查看当前stduent_info表中的索引 
SHOW INDEX FROM student_info;

#目前只有主键id索引,student_id字段上没有索引的:
SELECT course_id, class_id, NAME, create_time, student_id 
FROM student_info
WHERE student_id = 123110; #耗时276ms

#给student_id字段添加索引
ALTER TABLE student_info
ADD INDEX idx_sid(student_id);

#student_id字段上有索引的:
SELECT course_id, class_id, NAME, create_time, student_id 
FROM student_info
WHERE student_id = 123110; #43ms
3)经常 GROUP BY 和 ORDER BY 的列

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

比如student_info数据表(含100万条数据),假设我们想要查询 student_id=123110 的用户信息。

测试1:GROUP BY

#student_id字段上有索引的:
SELECT student_id, COUNT(*) AS num 
FROM student_info 
GROUP BY student_id LIMIT 100; #41ms

#删除idx_sid索引
DROP INDEX idx_sid ON student_info;


#student_id字段上没有索引的:
SELECT student_id, COUNT(*) AS num 
FROM student_info 
GROUP BY student_id LIMIT 100; #866ms

测试2:ORDER BY

效果同上

测试3:GROUP BY + ORDER BY

如果同时有GROUP BY和ORDER BY的情况:比如我们按照student_id进行分组,同时按照创建时间(create_time)降序的方式进行排序,这时我们就需要同时进行GROUP BY和ORDER BY,那么是不是需要单独创建student_id的索引和create_time的索引呢? 还是创建它们2个的联合索引???

现在student_info这张表只有主键索引

在这里插入图片描述

情况1:对student_id和create_time分别创建索引

#添加单列索引
ALTER TABLE student_info
ADD INDEX idx_sid(student_id);

ALTER TABLE student_info
ADD INDEX idx_cre_time(create_time);


SELECT student_id, COUNT(*) AS num FROM student_info 
GROUP BY student_id 
ORDER BY create_time DESC 
LIMIT 100;  #5.212s

在这里插入图片描述
情况2:对student_id和create_time创建联合索引
student_id在前,create_time在后

#添加联合索引
#mysql8.0才支持索引的降序排序,5.7不支持
ALTER TABLE student_info
ADD INDEX idx_sid_cre_time(student_id,create_time DESC);


SELECT student_id, COUNT(*) AS num FROM student_info 
GROUP BY student_id 
ORDER BY create_time DESC 
LIMIT 100;  #0.257s

在这里插入图片描述
情况3:对student_id和create_time创建联合索引
create_time在前,student_id在后

#此时有2个联合索引:情况2的和现在情况3创建的联合索引,他们的区别是2个索引的字段顺序相反
ALTER TABLE student_info
ADD INDEX idx_cre_time_sid(create_time DESC,student_id);

#把情况2创建的联合索引删除,因为查询的sql是先执行分组在执行排序,
#    此时仍然使用的是原来情况2的联合索引,无法测试查看效果。
DROP INDEX idx_sid_cre_time ON student_info;

#现在还剩下主键索引,情况1创建的2个单独的索引,本情况3创建的联合索引
SELECT student_id, COUNT(*) AS num FROM student_info 
GROUP BY student_id 
ORDER BY create_time DESC 
LIMIT 100;  #3.790s

效果:发现使用的是student_id字段的单独索引,而没有使用此联合索引。

原因:sql执行顺序问题,先执行分组在执行排序,在执行分组的时候已经找到了索引所以此时优先使用分组的单独索引。
在这里插入图片描述

4)UPDATE、DELETE 的 WHERE 条件列

当我们对某条数据进行UPDATE或者DELETE操作的时候,是否也需要对WHERE的条件列创建索引呢?

#name字段没有索引
UPDATE student_info SET student_id = 10002 
WHERE NAME = '462eed7ac6e791292a79';  #0.633s

#添加索引
ALTER TABLE student_info
ADD INDEX idx_name(NAME);


UPDATE student_info SET student_id = 10001 
WHERE NAME = '462eed7ac6e791292a79'; #0.001s

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

5)DISTINCT 字段需要创建索引

有时候我们需要对某个字段进行去重,使用 DISTINCT,那么对这个字段创建索引,也会提升查询效率。

比如,我们想要查询课程表中不同的 student_id 都有哪些,如果我们没有对 student_id 创建索引,执行SQL 语句:

SELECT DISTINCT(student_id) FROM `student_info`;

运行结果(600637 条记录,运行时间0.683s):

如果我们对 student_id 创建索引,再执行 SQL 语句:

#因为使用索引后,相同字段挨在一起自然查询的速度就变快了。
SELECT DISTINCT(student_id) FROM `student_info`;

运行结果(600637 条记录,运行时间0.010s):

你能看到 SQL 查询效率有了提升,同时显示出来的 student_id 还是按照 递增的顺序 进行展示的。这是因为索引会对数据按照某种顺序进行排序,所以在去重的时候也会快很多。

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

首先, 连接表的数量尽量不要超过 3 张 ,因为每增加一张表就相当于增加了一次嵌套的循环,数量级增长会非常快,严重影响查询的效率。

其次, 对 WHERE 条件创建索引 ,因为 WHERE 才是对数据条件的过滤。如果在数据量非常大的情况下,没有 WHERE 条件过滤是非常可怕的。

最后,对用于连接的字段创建索引,并且该字段在多张表中的 类型必须一致 。比如 course_id 在student_info 表和 course 表中都为 int(11) 类型,而不能一个为 int 另一个为 varchar 类型。

#NAME 有索引
SELECT s.course_id, NAME, s.student_id, c.course_name 
FROM student_info s JOIN course c
ON s.course_id = c.course_id
WHERE NAME = '462eed7ac6e791292a79'; #0.001s

#删除索引
DROP INDEX idx_name ON student_info;

#NAME 没有索引
SELECT s.course_id, NAME, s.student_id, c.course_name 
FROM student_info s JOIN course c
ON s.course_id = c.course_id
WHERE NAME = '462eed7ac6e791292a79'; #0.227s

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

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

我们在定义表结构的时候要显式的指定列的类型,以整数类型为例,有TINYINT、MEDIUMINT、INT、BIGINT等,它们占用的存储空间依次递增,能表示的整数范围当然也是依次递增。如果我们想要对某个整数列建立索引的话,在表示的整数范围允许的情况下,尽量让索引列使用较小的类型,比如我们能使用INT就不要使用BIGINT,能使用MEDIUMINT就不要使用INT。这是因为:

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

这个建议对于表的主键来说更加适用,因为不仅是聚簇索引中会存储主键值,其他所有的二级索引的节点处都会存储一份记录的主键值,如果主键使用更小的数据类型,也就意味着节省更多的存储空间和更高效的/0。

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

假设我们的字符串很长,那存储一个字符串就需要占用很大的存储空间。在我们需要为这个字符串列建立索引时,那就意味着在对应的B+树中有这么两个问题:

  • B+树索引中的记录需要把该列的完整字符串存储起来,更费时。而且字符串越长,在索引中占用的存储空间越大。
  • 如果B+树索引中索引列存储的字符串很长,那在做字符串比较时会占用更多的时间。

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

例如,TEXT和BLOG类型的字段,进行全文检索会很浪费时间,如果只检索字段前面的若干字符,这样可以提高检索速度。

创建一张商户表,因为地址字段比较长,在地址字段上建立前缀索引

create table shop(address varchar(120) not null);
alter table shop add index(address(12));

问题是,截取多少呢?截取得多了,达不到节省索引存储空间的目的;截取得少了,重复内容太多,字段的散列度(选择性)会降低。怎么计算不同的长度的选择性呢?

先看一下字段在全部数据中的选择度:

select count(distinct address) / count(*) from shop;

通过不同长度去计算,与全表的选择性对比:

公式:

count(distinct left(列名, 索引长度))/count(*)

例如:

select count(distinct left(address,10)) / count(*) as sub10, -- 截取前10个字符的选择度
count(distinct left(address,15)) / count(*) as sub11, -- 截取前15个字符的选择度
count(distinct left(address,20)) / count(*) as sub12, -- 截取前20个字符的选择度
count(distinct left(address,25)) / count(*) as sub13 -- 截取前25个字符的选择度
from shop;

引申另一个问题:索引列前缀对排序的影响

如果使用了索引列前缀,比方说前边只把address列的前12个字符放到了二级索引中,下边这个查询可能就有点儿尴尬了:

select * from shop
order by address
limit 12;

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

拓展:Alibaba《Java开发手册》

强制】在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本
区分度决定索引长度。

说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会 高达 90% 以上 ,可以使用 count(distinct left(列名, 索引长度))/count(*)的区分度来确定。

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

列的基数指的是某一列中不重复数据的个数,比方说某个列包含值2,5,8,2,5,8,2,5,8,虽然有9条记录,但该列的基数却是3。也就是说,在记录行数一定的情况下,列的基数越大,该列中的值越分散;列的基数越小,该列中的值越集中。这个列的基数指标非常重要,直接影响我们是否能有效的利用索引。最好为列的基数大的列建立索引,为基数太小列的建立索引效果可能不好。

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

拓展:联合索引把区分度高(散列性高)的列放在前面。

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

这样也可以较少的建立一些索引。同时,由于"最左前缀原则",可以增加联合索引的使用率。

SELECT *
FROM student_info
WHERE student_id = 10013 AND course_id = 100;

# 为上面的这个查询语句创建联合索引的时候,把student_id放在左边 course_id 放在右边。
# 索引在使用的时候优先考虑使用频繁的字段作为索引,如果是把course_id 放在前面这个字段使用的不频繁
# 在有别的索引情况下可能使用了别的索引,而没有用到联合索引。
11)在多个字段都要创建索引的情况下,联合索引优于单值索引

2.7.3 限制索引的数目

在实际工作中,我们也需要注意平衡,索引的数目不是越多越好。我们需要限制每张表上的索引数量,建议单张表索引数量不超过6个。原因:

  1. 每个索引都需要占用磁盘空间,索引越多,需要的磁盘空间就越大。
  2. 索引会影响INSERT、DELETE、UPDATE等语句的性能,因为表中的数据更改的同时,索引也会进行调整和更新,会造成负担。
  3. 优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划,如果同时有很多个索引都可以用于查询,会增加MysQL优化器生成执行计划时间,降低查询性能。

2.7.4 哪些情况不适合创建索引(7种)

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

WHERE条件(包括GROUP BY、ORDER BY)里用不到的字段不需要创建索引,索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的。举个例子:

SELECT course_id, student_id, create_time 
FROM student_info
WHERE student_id = 41251;

因为我们是按照student_id 来进行检索的,所以不需要对其他字段创建索引,即使这些字段出现在SELECT字段中。

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

如果表记录太少,比如少于1000个,那么是不需要创建索引的。表记录太少,是否创建索引对查询效率的影响并不大。甚至说,查询花费的时间可能比遍历索引的时间还要短,索引可能不会产生优化效果。

举例:创建表1:

CREATE TABLE t_without_index(
a INT PRIMARY KEY AUTO_INCREMENT,
b INT
);

提供存储过程1:添加900条数据,b字段没有添加索引

#创建存储过程
DELIMITER //
CREATE PROCEDURE t_wout_insert()
BEGIN
	DECLARE i INT DEFAULT 1;
	WHILE i <= 900
	DO
		INSERT INTO t_without_index(b) SELECT RAND()*10000;
		SET i = i + 1;
	END WHILE;
	COMMIT;
END //
DELIMITER ;

#调用
CALL t_wout_insert();

创建表2:

CREATE TABLE t_with_index(
a INT PRIMARY KEY AUTO_INCREMENT,
b INT,
INDEX idx_b(b)
);

创建存储过程2:添加900条数据,b字段添加了索引

#创建存储过程
DELIMITER //
CREATE PROCEDURE t_with_insert()
BEGIN
	DECLARE i INT DEFAULT 1;
	WHILE i <= 900
	DO
		INSERT INTO t_with_index(b) SELECT RAND()*10000;
		SET i = i + 1;
	END WHILE;
	COMMIT;
END //
DELIMITER ;

#调用
CALL t_with_insert();

查询对比:

mysql> select * from t_without_index where b = 9879;
+------+------+
| a    | b    |
+------+------+
| 1242 | 9879 |
+------+------+
1 row in set (0.00 sec)
mysql> select * from t_with_index where b = 9879;
+-----+------+
| a   | b    |
+-----+------+
| 112 | 9879 |
+-----+------+
1 row in set (0.00 sec)

你能看到运行结果相同,但是在数据量不大的情况下,索引就发挥不出作用了。

结论:在数据表中的数据行数比较少的情况下,比如不到 1000 行,是不需要创建索引的。

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

在条件表达式中经常用到的不同值较多的列上建立索引,但字段中如果有大量重复数据,也不用创建索引。比如在学生表的“性别"字段上只有“男"与“女"两个不同值,因此无须建立索引。如果建立索引,不但不会提高查询效率,反而会严重降低数据更新速度

举例1:要在 100 万行数据中查找其中的 50 万行(比如性别为男的数据),一旦创建了索引,你需要先访问 50 万次索引,然后再访问 50 万次数据表,这样加起来的开销比不使用索引可能还要大。

举例2:假设有一个学生表,学生总数为 100 万人,男性只有 10 个人,也就是占总人口的 10 万分之 1。

学生表 student_gender 结构如下。其中数据表中的 student_gender 字段取值为 0 或 1,0 代表女性,1 代表男性。

CREATE TABLE student_gender(
	student_id INT(11) NOT NULL,
	student_name VARCHAR(50) NOT NULL,
	student_gender TINYINT(1) NOT NULL,
	PRIMARY KEY(student_id)
)ENGINE = INNODB;

如果我们要筛选出这个学生表中的男性,可以使用:

SELECT * FROM student_gender WHERE student_gender = 1

运行结果(10 条数据,运行时间 0.696s ):
在这里插入图片描述
你能看到在未创建索引的情况下,运行的效率并不高。如果针对student_gender’字段创建索引呢?

SELECT * FROM student_gender WHERE student_gender = 1

同样是10条数据,运行结果相同,时间却缩短到了0.052s,大幅提升了查询的效率。

其实通过这两个实验你也能看出来,索引的价值是帮你快速定位。如果想要定位的数据有很多,那么索引就失去了它的使用价值,比如通常情况下的性别字段。

结论:当数据重复度大,比如高于10%的时候,也不需要对这个字段使用索引。

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

第一层含义︰频繁更新的字段不一定要创建索引。因为更新数据的时候,也需要更新索引,如果索引太多,在更新索引的时候也会造成负担,从而影响效率。

第二层含义: 避免对经常更新的表创建过多的索引,并且索引中的列尽可能少。此时,虽然提高了查询速度,同时却会降低更新表的速度。|

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

例如身份证、UUID(在索引比较时需要转为ASCII,并且插入时可能造成页分裂)、MD5、HASH、无序长字符串等。

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

表中的数据被大量更新,或者数据的使用方式被改变后,原有的一些索引可能不再需要。数据库管理员应当定期找出这些索引,将它们删除,从而减少索引对更新操作的影响。

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

① 冗余索引

有时候有意或者无意的就对同一个列创建了多个索引,比如: index(a,b,c)相当于index(a)、index(a,b)、index(a,b,c)。

举例:建表语句如下

CREATE TABLE person_info(
	id INT UNSIGNED NOT NULL AUTO_INCREMENT,
	name VARCHAR(100) NOT NULL,
	birthday DATE NOT NULL,
	phone_number CHAR(11) NOT NULL,
	country varchar(100) NOT NULL,
	PRIMARY KEY (id),
	KEY idx_name_birthday_phone_number (name(10), birthday, phone_number),
	KEY idx_name (name(10))
);

我们知道,通过idx_name_birthday_phone_number索引就可以对 name 列进行快速搜索,再创建一个专门针对name列的索引就算是一个 冗余索引 ,维护这个索引只会增加维护的成本,并不会对搜索有什么好处。

② 重复索引

另一种情况,我们可能会对某个列 重复建立索引 ,比方说这样:

CREATE TABLE repeat_index_demo (
	col1 INT PRIMARY KEY,
	col2 INT,
	UNIQUE uk_idx_c1 (col1),
	INDEX idx_c1 (col1)
);

我们看到,col1 既是主键、又给它定义为一个唯一索引,还给它定义了一个普通索引,可是主键本身就会生成聚簇索引,所以定义的唯一索引和普通索引是重复的,这种情况要避免。

2.8 总结

索引是一把双刃剑,可提高查询效率,但也会降低插入和更新的速度并占用磁盘空间。

选择索引的最终目的是为了使查询的速度变快,上面给出的原则是最基本的准则,但不能拘泥于上面的准则,大家要在以后的学习和工作中进行不断的实践,根据应用的实际情况进行分析和判断,选择最合适的索引方式。

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

3.SQL优化----查询优化

都有哪些维度可以进行数据库调优? 简言之:

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

关于数据库调优的知识点非常分散。不同的DBMS,不同的公司,不同的职位,不同的项目遇到的问题都不尽相同。这里我们分为三个章节进行细致讲解。

虽然SQL查询优化的技术有很多,但是大方向上完全可以分成物理查询优化逻辑查询优化两大块。

  • 物理查询优化是通过索引表连接方式等技术来进行优化,这里重点需要掌握索引的使用。
  • 逻辑查询优化就是通过SQL等价变换提升查询效率,直白一点就是说,换一种查询写法执行效率可能更高。

3.1 准备数据

学员表 50万 条, 班级表 1万 条。

步骤1:建表

CREATE DATABASE atguigudb2;

USE atguigudb2;

CREATE TABLE `class` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`className` VARCHAR(30) DEFAULT NULL,
`address` VARCHAR(40) DEFAULT NULL,
`monitor` INT NULL ,
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

CREATE TABLE `student` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`stuno` INT NOT NULL ,
`name` VARCHAR(20) DEFAULT NULL,
`age` INT(3) DEFAULT NULL,
`classId` INT(11) DEFAULT NULL,
PRIMARY KEY (`id`)
#CONSTRAINT `fk_class_id` FOREIGN KEY (`classId`) REFERENCES `t_class` (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

步骤2:设置参数

命令开启:允许创建函数设置:

set global log_bin_trust_function_creators=1; # 不加global只是当前窗口有效。

步骤3:创建函数

保证每条数据都不同。

#随机产生字符串
DELIMITER //
CREATE FUNCTION rand_string(n INT) RETURNS VARCHAR(255)
BEGIN
DECLARE chars_str VARCHAR(100) DEFAULT
'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
DECLARE return_str VARCHAR(255) DEFAULT '';
DECLARE i INT DEFAULT 0;
WHILE i < n DO
SET return_str =CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1));
SET i = i + 1;
END WHILE;
RETURN return_str;
END //
DELIMITER ;


#假如要删除
#drop function rand_string;

随机产生班级编号

#用于随机产生多少到多少的编号
DELIMITER //
CREATE FUNCTION rand_num (from_num INT ,to_num INT) RETURNS INT(11)
BEGIN
DECLARE i INT DEFAULT 0;
SET i = FLOOR(from_num +RAND()*(to_num - from_num+1)) ;
RETURN i;
END //
DELIMITER ;
#假如要删除
#drop function rand_num;

步骤4:创建存储过程

#创建往stu表中插入数据的存储过程
DELIMITER //
CREATE PROCEDURE insert_stu( START INT , max_num INT )
BEGIN
DECLARE i INT DEFAULT 0;
SET autocommit = 0; #设置手动提交事务
REPEAT #循环
SET i = i + 1; #赋值
INSERT INTO student (stuno, name ,age ,classId ) VALUES
((START+i),rand_string(6),rand_num(1,50),rand_num(1,1000));
UNTIL i = max_num
END REPEAT;
COMMIT; #提交事务
END //
DELIMITER ;
#假如要删除
#drop PROCEDURE insert_stu;

创建往class表中插入数据的存储过程

#执行存储过程,往class表添加随机数据
DELIMITER //
CREATE PROCEDURE `insert_class`( max_num INT )
BEGIN
DECLARE i INT DEFAULT 0;
SET autocommit = 0;
REPEAT
SET i = i + 1;
INSERT INTO class ( classname,address,monitor ) VALUES
(rand_string(8),rand_string(10),rand_num(1,100000));
UNTIL i = max_num
END REPEAT;
COMMIT;
END //
DELIMITER ;
#假如要删除
#drop PROCEDURE insert_class;

步骤5:调用存储过程
class

#执行存储过程,往class表添加1万条数据
CALL insert_class(10000);

stu

#执行存储过程,往stu表添加50万条数据
CALL insert_stu(100000,500000);

步骤6:删除某表上的索引
目的:下面演示过程中可能会创建很多索引,避免测试影响需要删除索引,如果一个个的的删除太慢了,可以调用此存储过程删除表中的所有索引(除了主键索引)

创建存储过程

DELIMITER //
CREATE  PROCEDURE `proc_drop_index`(dbname VARCHAR(200),tablename VARCHAR(200))
BEGIN
       DECLARE done INT DEFAULT 0;
       DECLARE ct INT DEFAULT 0;
       DECLARE _index VARCHAR(200) DEFAULT '';
       DECLARE _cur CURSOR FOR  SELECT   index_name   FROM information_schema.STATISTICS   WHERE table_schema=dbname AND table_name=tablename AND seq_in_index=1 AND    index_name <>'PRIMARY'  ;
#每个游标必须使用不同的declare continue handler for not found set done=1来控制游标的结束
       DECLARE  CONTINUE HANDLER FOR NOT FOUND SET done=2 ;      
#若没有数据返回,程序继续,并将变量done设为2
        OPEN _cur;
        FETCH _cur INTO _index;
        WHILE  _index<>'' DO 
               SET @str = CONCAT("drop index " , _index , " on " , tablename ); 
               PREPARE sql_str FROM @str ;
               EXECUTE  sql_str;
               DEALLOCATE PREPARE sql_str;
               SET _index=''; 
               FETCH _cur INTO _index; 
        END WHILE;
   CLOSE _cur;
END //
DELIMITER ;

执行存储过程

CALL proc_drop_index("dbname","tablename");

3.2 关联查询优化

说明:

  • 多表查询分为:外连接和内连接
  • 在mysql中的满外连接使用UNION 关键字组合左外连接和右外连接实现的,左外连接和右外连接语法差不多,所以这里只讲解左外连接和内连接即可。

3.2.1 数据准备

#分类
CREATE TABLE IF NOT EXISTS `type` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`card` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`id`)
);
#图书
CREATE TABLE IF NOT EXISTS `book` (
`bookid` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`card` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`bookid`)
);

#向分类表中添加20条记录
INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));

#向图书表中添加20条记录
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));

3.2.2 采用左外连接

下面开始 EXPLAIN 分析

#没有索引时的sql语句,查询时是全表扫描
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card;

结果:上面的表叫做驱动表,下面的表叫做被驱动表。(根据table属性查看)

左外连接中:左边的表叫做驱动表,右边的表叫作被驱动表。
在这里插入图片描述
结论:type 有All(全表扫描,性能比较低)

添加索引优化

#给book表的card字段添加索引(被驱动表)
ALTER TABLE book ADD INDEX Y ( card); #【被驱动表】,可以避免全表扫描

#可以看到这条sql执行时使用了,被驱动表的索引
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card;

在这里插入图片描述

可以看到第二行的 type 变为了 ref,rows 也变成了优化比较明显。这是由左连接特性决定的。LEFT JOIN条件用于确定如何从右表搜索行,左边一定都有,所以 右边是我们的关键点,一定需要建立索引

#给type表的card字段添加索引(驱动表)
ALTER TABLE `type` ADD INDEX X (card); #【驱动表】,无法避免全表扫描

#可以看到这条sql执行时,驱动表和被驱动表的索引都用了
#注意:此时这2个字段(type.card = book.card)上都有索引并且都用上了,那么字段名可以不同但是类型一定相同
#因为:类型不一致还会做一个隐式的类型转换,导致索引失效。
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card;

在这里插入图片描述
接着:

#去掉被驱动表(book表)的索引
DROP INDEX Y ON book;

#此时只剩下驱动表的索引
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card;

在这里插入图片描述

3.2.3 采用内连接

先删除外连接测试中创建的索引

drop index X on type;

drop index Y on book;(如果已经删除了可以不用再执行该操作)

换成 inner join内连接(MySQL自动选择驱动表)

#没有索引
EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card=book.card;

在这里插入图片描述
添加索引优化

#给book表的card字段添加索引
ALTER TABLE book ADD INDEX Y ( card);

#执行内连接的sql用到了索引
EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card=book.card;

在这里插入图片描述

#给type表的card字段添加索引
ALTER TABLE type ADD INDEX X (card);

#这2个索引都用到了,查看table属性和左外连接相比它是先有book在有type,顺序恰好和左外连接相反
#结论:对于内连接来说,查询优化器可以决定谁作为驱动表,谁作为被驱动表出现的
#     上面的是驱动表,下面的是被驱动表
EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card=book.card;

在这里插入图片描述
接着:

#删除被驱动表(type)的索引
DROP INDEX X ON `type`;

#发现table属性的上下2个值交换了位置,被驱动表的索引删除后原先驱动表的索引现在充当被驱动表。
#结论:对于内连接来讲,如果表的连接条件中只能有一个字段有索引,则有索引的字段所在的表会被作为被驱动表出现。
EXPLAIN SELECT SQL_NO_CACHE * FROM TYPE INNER JOIN book ON type.card=book.card;

在这里插入图片描述
接着:

#在把type表的索引加上
ALTER TABLE `type` ADD INDEX X (card);

#发现table的属性又交换回去了,此时又变为book表充当驱动表,type表充当被驱动表
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` INNER JOIN book ON type.card=book.card;

在这里插入图片描述

目前book和type表中都是20条记录,此时往book表中在添加20条记录

#向图书表中添加20条记录
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));

再次查看这条sql的执行计划,发现table的属性再次进行了交换

# 此时type变为驱动表,book变为被驱动表
#结论:对于内连接来说,在两个表的连接条件都存在索引的情况下,会选择小表作为驱动表。“小表驱动大表”
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` INNER JOIN book ON type.card = book.card;

在这里插入图片描述

3.2.4 join语句原理

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

1)驱动表和被驱动表

驱动表就是主表,被驱动表就是从表、非驱动表。

  • 对于内连接来说:
select * from A join B on ...

A一定是驱动表吗?不一定,优化器会根据你查询语句做优化,决定先查哪张表。先查询的那张表就是驱动表,反之就是被驱动表。通过explain关键字可以查看。

  • 对于外连接来说:
select * from A left join B on ...select * from B right join A on ...

通常,大家会认为A就是驱动表,B就是被驱动表。但也未必。测试如下:

#创建2张表
CREATE TABLE a(f1 INT, f2 INT, INDEX(f1))ENGINE=INNODB;

CREATE TABLE b(f1 INT, f2 INT)ENGINE=INNODB;

#分别往表中插入数据
INSERT INTO a VALUES(1,1),(2,2),(3,3),(4,4),(5,5),(6,6);

INSERT INTO b VALUES(3,3),(4,4),(5,5),(6,6),(7,7),(8,8);


#测试1:左外连接查询,b作为驱动表   a作为被驱动表
EXPLAIN SELECT * FROM a LEFT JOIN b ON(a.f1=b.f1) WHERE (a.f2=b.f2);

#测试2:左外连接查询,a作为驱动表   b作为被驱动表
EXPLAIN SELECT * FROM a LEFT JOIN b ON(a.f1=b.f1) AND (a.f2=b.f2);

#测试3:内连接查询,b作为驱动表   a作为被驱动表
EXPLAIN SELECT * FROM a JOIN b ON(a.f1=b.f1) WHERE (a.f2=b.f2);

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

2)简单嵌套循环连接

Simple Nested-Loop Join:算法相当简单,从表A中取出一条数据1,遍历表B,将匹配到的数据放到result…以此类推,驱动表A中的每一条记录与被驱动表B的记录进行判断:

此时A和B都没有索引。
在这里插入图片描述
可以看到这种方式效率是非常低的,以上述表A数据100条,表B数据100o条计算,则A*B= 10万次。开销统计如下:

这里的数据条数指的是where条件过滤后的数量,不是这个表当中有多少条记录。
在这里插入图片描述
当然mysql肯定不会这么粗暴的去进行表的连接,所以就出现了后面的两种对Nested-Loop Join优化算法。

3)索引嵌套循环连接

Index Nested-Loop Join其优化的思路主要是为了减少内层表数据的匹配次数,所以要求被驱动表上必须有索引才行。通过外层表匹配条件直接与内层表索引进行匹配,避免和内层表的每条记录去进行比较,这样极大的减少了对内层表的匹配次数。
在这里插入图片描述
驱动表中的每条记录通过被驱动表的索引进行访问,因为索引查询的成本是比较固定的,故mysql优化器都倾向于使用记录数少的表作为驱动表(外表)。

在这里插入图片描述

如果被驱动表加索引,效率是非常高的,但如果索引不是主键索引,所以还得进行一次回表查询。相比,被驱动表的索引是主键索引,效率会更高。

4)块嵌套循环连接

Block Nested-Loop Join:如果存在索引,那么会使用index的方式进行join,如果join的列没有索引,被驱动表要扫描的次数太多了。每次访问被驱动表,其表中的记录都会被加载到内存中,然后再从驱动表中取一条与其匹配,匹配结束后清除内存,然后再从驱动表中加载一条记录,然后把被驱动表的记录在加载到内存匹配,这样周而复始,大大增加了10的次数。为了减少被驱动表的IO次数,就出现了Block Nested-Loop Join的方式。

不再是逐条获取驱动表的数据,而是一块一块的获取,引入了join buffer缓冲区,将驱动表join相关的部分数据列(大小受join buffer的限制)缓存到join buffer中,然后全表扫描被驱动表,被驱动表的每一条记录一次性和join buffer中的所有驱动表记录进行匹配(内存中操作),将简单嵌套循环中的多次比较合并成一次,降低了被驱动表的访问频率。

注意:

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

在这里插入图片描述
在这里插入图片描述
参数设置:

  • block_nested_loop

通过 show variables like '%optimizer_switch%'查看block_nested_loop状态。默认是开启的。

SHOW VARIABLES LIKE '%optimizer_switch%';

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

  • join_buffer_size

驱动表能不能一次加载完,要看join buffer能不能存储所有的数据,默认情况下join_buffer_size=256k

SHOW VARIABLES LIKE '%join_buffer%';

在这里插入图片描述
join_buffer_size的最大值在32位系统可以申请4G,而在64位操做系统下可以申请大于4的Join Buffer空间(64位Windows除外,其大值会被截断为4GB并发出警告)。

5)join小结

1、整体效率比较:INLJ(索引) >BNLJ (块)> SNLJ(简单)

2、永远用小结果集驱动大结果集(其本质就是减少外层循环的数据数量) (小的度量单位指的是表行数*每行大小)

小表驱动大表:不是指一个表的记录大小,而是指where条件过滤后的记录数*每行大小,运算的结果谁大谁就是大表。

#straight_join:不让查询优化器破坏表的前后顺序,也就是说sql语句谁写在前面谁就是
#  驱动表,谁写在后面谁就是被驱动表。

#t1驱动表    t2被驱动表
#查询t1一个字段,查询t2所有记录,所以t1数据少t2数据多,此时推荐使用t1作为驱动表t2作为被驱动表
#因为:t1作为驱动表它只有一个b字段的记录,把t1驱动表的内容放到缓存区中,剩余的空间都做缓冲,相同的缓存区大小下放的条目数更多些。
#注意:缓冲区缓存的不只是关联表的列,select后面的列也会缓存起来。你查询字段的列放在缓冲区占用的空间很多,
#     剩下做缓冲的空间当然变少了。
select t1.b,t2.* from t1 straight_join t2 on (t1.b=t2.b) where t2.id<=108; #推荐

#t2驱动表    t1被驱动表
#不推荐 t2作为驱动表t1作为被驱动表
#因为: t2作为驱动表用的字段很多,把t2驱动表的记录放到缓存区,导致缓冲区剩下的空间变得很小,一次性只能缓存很少的条目数,
#     这样访问的频率增加
select t1.b,t2.* from t2 straight_join t1 on (t1.b=t2.b) where t2.id<=100; #不推荐

3、为被驱动表匹配的条件增加索引(减少内层表的循环匹配次数,对应索引嵌套循环连接)

4、增大join buffer size的大小(一次缓存的数据越多,那么内层包的扫表次数就越少)

5、减少驱动表不必要的字段查询(字段越少,join buffer所缓存的数据就越多)

6)Hash Join

从MySQL的8.0.20版本开始将废弃BNLJ(块嵌套循环连接),因为从MySQL8.0.18版本开始就加入了hash join,默认都会使用hash join

  • Nested Loop:
    • 对于被连接的数据子集较小的情况,Nested Loop是个较好的选择。
  • Hash Join是做大数据集连接时的常用方式,优化器使用两个表中较小(相对较小)的表利用Join Key在内存中建立散列表,然后扫描较大的表并探测散列表,找出与Hash表匹配的行。
    • 这种方式适用于较小的表完全可以放于内存中的情况,这样总成本就是访问两个表的成本之和。
    • 在表很大的情况下并不能完全放入内存,这时优化器会将它分割成若干不同的分区,不能放入内存的部分就把该分区写入磁盘的临时段,此时要求有较大的临时段从而尽量提高I/O的性能。 Ⅰ
    • 它能够很好的工作于没有索引的大表和并行查询的环境中,并提供最好的性能。大多数人都说它是Join的重型升降机。Hash Join只能应用于等值连接(如WHERE A.COL1=B.COL2),这是由Hash的特点决定的。

在这里插入图片描述

3.2.5 小结

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

3.3 子查询优化

MySQL从4.1版本开始支持子查询,使用子查询可以进行SELECT语句的嵌套查询,即一个SELECT查询的结果作为另一个SELECT语句的条件。 子查询可以一次性完成很多逻辑上需要多个步骤才能完成的SQL操作

子查询是 MySQL 的一项重要的功能,可以帮助我们通过一个 SQL 语句实现比较复杂的查询。但是,子查询的执行效率不高。原因:

① 执行子查询时,MySQL需要为内层查询语句的查询结果 建立一个临时表 ,然后外层查询语句从临时表中查询记录。查询完毕后,再 撤销这些临时表 。这样会消耗过多的CPU和IO资源,产生大量的慢查询。

② 子查询的结果集存储的临时表,不论是内存临时表还是磁盘临时表都 不会存在索引 ,所以查询性能会受到一定的影响。

③ 对于返回结果集比较大的子查询,其对查询性能的影响也就越大。

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

举例1:查询学生表中是班长的学生信息

  • 使用子查询
#创建班级表中班长的索引
CREATE INDEX idx_monitor ON class(monitor);

#查询班长的信息
EXPLAIN SELECT * FROM student stu1
WHERE stu1.`stuno` IN (
SELECT monitor
FROM class c
WHERE monitor IS NOT NULL
);

在这里插入图片描述

  • 推荐:使用多表查询
EXPLAIN SELECT stu1.* FROM student stu1 JOIN class c 
ON stu1.`stuno` = c.`monitor`
WHERE c.`monitor` IS NOT NULL;

在这里插入图片描述
举例2:取所有不为班长的同学

  • 不推荐
#查询不为班长的学生信息
EXPLAIN SELECT SQL_NO_CACHE a.* 
FROM student a 
WHERE  a.stuno  NOT  IN (
			SELECT monitor FROM class b 
			WHERE monitor IS NOT NULL) 

执行结果如下
在这里插入图片描述

通过sql查询结果如下

SELECT SQL_NO_CACHE a.* 
FROM student a 
WHERE  a.stuno  NOT  IN (
			SELECT monitor FROM class b 
			WHERE monitor IS NOT NULL) 

在这里插入图片描述

  • 推荐
EXPLAIN SELECT SQL_NO_CACHE a.*
FROM  student a LEFT OUTER JOIN class b 
ON a.stuno =b.monitor
WHERE b.monitor IS NULL;

在这里插入图片描述
执行sql查询结果如下

SELECT SQL_NO_CACHE a.*
FROM  student a LEFT OUTER JOIN class b 
ON a.stuno =b.monitor
WHERE b.monitor IS NULL;

在这里插入图片描述
可以看到查询时间从1分26.74秒缩减为了22.83秒,极大的提升了效率。

结论:尽量不要使用NOT IN 或者 NOT EXISTS,用LEFT JOIN xxx ON xx WHERE xx IS NULL替代

3.4 order by优化(创建索引时可以指定排序)

MySQL的排序,有两种方式:

  • Using filesort : 通过表的索引或全表扫描,读取满足条件的数据行,然后在排序缓冲区sort buffer中完成排序操作,所有不是通过索引直接返回排序结果的排序都叫 FileSort 排序。

  • Using index : 通过有序索引顺序扫描直接返回有序数据,这种情况即为 using index,不需要额外排序,操作效率高。

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

接下来,我们来做一个测试:

A.数据准备

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

#切换数据库
use itcast;

show tables;

#查看索引
SHOW INDEX FROM tb_user; 

#删除索引
drop index idx_user_phone on tb_user;
drop index idx_user_name on tb_user;

在这里插入图片描述
剩下一个主键索引,一个联合索引,一个前缀索引
在这里插入图片描述

B.执行排序SQL

#根据年龄进行升序排序的执行计划
explain select id,age,phone from tb_user order by age ;

在这里插入图片描述

#先按照年龄进行升序排序,如果年龄相同在按照手机号进行升序排序的执行计划
explain select id,age,phone from tb_user order by age, phone ;

在这里插入图片描述

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

C.创建索引

-- 创建索引

create index idx_user_age_phone_aa on tb_user(age,phone);

D.创建索引后,根据age, phone进行升序排序

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,性能就是比较高的了。

E.创建索引后,根据age, phone进行降序排序

explain select id,age,phone from tb_user order by age desc , phone desc ;

在这里插入图片描述

也出现 Using index, 但是此时Extra中出现了 Backward index scan,这个代表反向扫描索引,因为在MySQL中我们创建的索引,默认索引的叶子节点是从小到大排序的,而此时我们查询排序 时,是从大到小,所以,在扫描时,就是反向扫描,就会出现 Backward index scan。 在MySQL8版本中,支持降序索引,我们也可以创建降序索引。

F.根据phone,age进行升序排序,phone在前,age在后。

explain select id,age,phone from tb_user order by phone , age;

在这里插入图片描述

此时既出现了index也出现了filesort, 排序时,也需要满足最左前缀法则,否则也会出现 filesort。因为在创建索引的时候, age是第一个字段,phone是第二个字段,所以排序时,也就该按照这个顺序来,否则就会出现 Using filesort。

G.根据age, phone进行降序一个升序,一个降序

explain select id,age,phone from tb_user order by age asc , phone desc ;

在这里插入图片描述

因为创建索引时,如果未指定顺序,默认都是按照升序排序的,而查询时,一个升序,一个降序,此时就会出现Using filesort额外的排序。

#查看索引的排序方式,这个创建的联合索引的排序方式为 A:asc升序
SHOW INDEX FROM tb_user; 

在这里插入图片描述

为了解决上述的问题,我们可以创建一个新的索引,这个联合索引中 age 升序排序,phone 倒序排序。

H.创建联合索引(age 升序排序,phone 倒序排序)

create index idx_user_age_phone_ad on tb_user(age asc ,phone desc);

#再次查看索引的排序方式,这个创建的联合索引的排序方式为 A:asc升序,D:desc降序
SHOW INDEX FROM tb_user; 

在这里插入图片描述

I.然后再次执行如下SQL,不会再出现filesort额外排序了

explain select id,age,phone from tb_user order by age asc , phone desc ;

在这里插入图片描述
J.创建索引后,根据age, phone进行升序排,也不会出现filesort

因为他用的之前的老索引idx_user_age_phone_aa,他默认按照是age升序,phone升序排序的。

explain select id,age,phone from tb_user order by age; 

在这里插入图片描述

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

在这里插入图片描述
在这里插入图片描述
由上述的测试,我们得出order by优化原则:

A.根据排序字段建立合适的索引,多字段排序时,也遵循最左前缀法则。

B.尽量使用覆盖索引。

C.多字段排序, 一个升序一个降序,此时需要注意联合索引在创建时的规则(ASC/DESC)。

D.如果不可避免的出现filesort,大数据量排序时,可以适当增大排序缓冲区大小sort_buffer_size(默认256k)

# 查看排序缓冲区大小
show variables like 'sort_buffer_size';

如果说在排序的时候这个缓冲区256k占满了,他会在磁盘文件中进行排序,性能比较低,所以如果实在大数据量排序的时候出现了filesort,可以适量的把这个参数往上调,从而提高排序效率。

在这里插入图片描述

3.5 group by优化

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

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

#查看当前表有哪些索引
SHOW INDEX FROM tb_user; 

#删除索引
drop index idx_user_pro_age_sta on tb_user;
drop index idx_email on tb_user;
drop index idx_user_age_phone_aa on tb_user;
drop index idx_user_age_phone_ad on tb_user;

在这里插入图片描述
只剩下一个主键索引。
在这里插入图片描述

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

#根据专业分组并统计每个专业人员的数量
select profession,count(*) from tb_user	 group by profession ;

#查看执行计划
explain select profession,count(*) from tb_user	 group by profession ;

效果:没有用到任何索引,用到了临时表效率比较低。
在这里插入图片描述

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

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,查看执行计划:

#修改为根据年龄进行分组,每组的年龄人数是多少,查看执行计划
explain select age, count(*) from tb_user group by age;

效果:出现 Using temporary临时表,性能不高。不满足最左前缀法则,不走索引所以出现了临时表。
在这里插入图片描述

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

#修改为根据专业、年龄2个字段进行分组,查看执行计划
explain select profession,age, count(*) from tb_user group by profession,age;

效果:没有出现Using temporary临时表,因为排序的字段和创建索引的字段,满足最左前缀法则,所以直接走索引。
在这里插入图片描述
执行如下的分组查询SQL,查看执行计划:

#修改为根据年龄字段进行分组,但是在分组之前先要对profession进行一个过滤,查看执行计划
#查询专业为软件工程的数据,并根据年龄进行分组。
explain select age, count(*) from tb_user where profession = "软件工程" group by age;

效果:没有出现Using temporary临时表,因为有第一个字段profession ,满足最左前缀法则,所以直接走索引。

在这里插入图片描述

我们发现,如果仅仅根据age分组,就会出现 Using temporary临时表,性能不高 ;而如果是根据profession,age两个字段同时分组,则不会出现 Using temporary。原因是因为对于分组操作, 在联合索引中,也是符合最左前缀法则的。

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

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

3.6 limit优化

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

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

tb_sku表,有1000万条数据。

# 查询第一页的数据,每页显示10条数据,耗时0.00秒
select * from tb_sku limit 0,10;

# 查询100万行记录开始之后的10条记录,耗时1.66秒
select * from tb_sku limit 1000000,10;

# 查询500万行记录开始之后的10条记录,耗时10.79秒
select * from tb_sku limit 5000000,10;

# 查询900万行记录开始之后的10条记录,耗时19.39秒
select * from tb_sku limit 9000000,10;

在这里插入图片描述

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

因为,当在进行分页查询时,如果执行 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;

测试:

原始查询方式:耗时19.39秒

# 查询900万行记录开始之后的10条记录,耗时19.39秒
select * from tb_sku limit 9000000,10;

覆盖索引优化:把*改为id,查询*肯定会用到回表查询,查询id直接可以从二级索引查询到,所以性能更高

#可以拿到要查询的id,耗时11.47秒
select id from tb_sku order by id limit 9000000,10;

在这里插入图片描述

使用子查询拿到id对应的数据:

#多行子查询方式:报错,当前mysql的版本不支持这种语法 在in之后使用limit关键字
select * from tb_sku where id in(select id from tb_sku order by id limit 9000000,10;)

在这里插入图片描述

解决:把这个select id from tb_sku order by id limit 9000000,10;返回id的查询结果看成是一张表,通过多表联查来实现。

#耗时11.46秒
select s.* from tb_sku s ,(select id from tb_sku order by id limit 9000000,10) a
where s.id = a.id;

在这里插入图片描述

3.7 全值匹配我最爱

系统中经常出现的sql语句如下:

#查看这条sql执行时使用的索引情况,此时只有主键索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30;
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30 AND classId=4;
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30 AND classId=4 AND NAME = 'abcd';

建立索引前执行:(关注执行时间)

#0.28sec
SELECT SQL_NO_CACHE * FROM student WHERE age=30 AND classId=4 AND NAME = 'abcd';

建立索引

#建立一个字段的索引,此时执行这个sql的耗时为 0.023sec
CREATE INDEX idx_age ON student(age);

#建立两个字段的索引,此时执行这个sql的耗时为 0.001sec
CREATE INDEX idx_age_classid ON student(age,classId);

#建立两个字段的索引,此时执行这个sql的耗时为 0sec
CREATE INDEX idx_age_classid_name ON student(age,classId,NAME);

建立索引后执行

#0.023sec     0.001sec    0sec
SELECT SQL_NO_CACHE * FROM student WHERE age=30 AND classId=4 AND NAME = 'abcd';

可以看到,创建索引前的查询时间是0 .28秒,创建索引后的查询时间是0.001秒,索引帮助我们极大的提高了查询效率。

where条件的字段建立的索引越多,执行速度越快,此时有3个索引(一个字段的、两个字段的、三个字段的),优先使用性能最高的全值匹配的索引。

4.SQL优化----其它优化

4.1 主键优化

在上一小节,我们提到,主键顺序插入的性能是要高于乱序插入的。 这一小节,就来介绍一下具体的原因,然后再分析一下主键又该如何设计。

4.1.1 数据组织方式

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

表数据都是根据主键顺序组织存放的:InnoDB存储引擎中按照存储方式分为聚集索引和二级索引,聚集索引的叶子节点下面挂的是这一行的行数据。一张表默认主键索引就是聚集索引,所以最终我们表中的数据在存放的时候,他就是根据主键进行顺序存放的。比如:6下面存放的就是6这个主键对应的这一行数据,12下面存放的就是12这个主键对应的数据。所以在InnoDB存储引擎中表数据都是根据主键顺序存放的。

在这里插入图片描述

行数据,都是存储在聚集索引的叶子节点上的。而我们之前也讲解过InnoDB的逻辑结构图:

在这里插入图片描述

在InnoDB引擎中,数据行是记录在逻辑结构 page 页中的,而每一个页的大小是固定的,默认16K。那也就意味着, 一个页中所存储的行也是有限的,如果插入的数据行row在该页存储不小,将会存储到下一个页中,页与页之间会通过指针连接。

4.1.2 页分裂

页可以为空,也可以填充一半,也可以填充100%。每个页包含了2-N行数据(如果一行数据过大,会行溢出),根据主键排列。

  • 在InnoDB引擎中规定每一个页当中至少包含2行数据,如果只包含一行就相当于一个链表了。

A.主键顺序插入效果
①. 从磁盘中申请页, 主键顺序插入

在这里插入图片描述

②. 第一个页没有满,继续往第一页插入
在这里插入图片描述

③. 当第一个也写满之后,再写入第二个页,页与页之间会通过指针连接
在这里插入图片描述

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

在这里插入图片描述

B.主键乱序插入效果

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

在这里插入图片描述

②. 此时再插入id为50的记录,我们来看看会发生什么现象会再次开启一个页,写入新的页中吗?

在这里插入图片描述

不会。因为,索引结构的叶子节点是有顺序的。按照顺序,应该存储在47之后。
在这里插入图片描述

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

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

移动数据,并插入id为50的数据之后,那么此时,这三个页之间的数据顺序是有问题的。 1#的下一个页,应该是3#, 3#的下一个页是2#。 所以,此时,需要重新设置链表指针。
在这里插入图片描述

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

4.1.3 页合并

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

在这里插入图片描述

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

当删除一行记录时,实际上记录并没有被物理删除(并不会直接从磁盘当中,将数据页当中的这一行数据干掉),只是记录被标记(flaged)为删除并且它的空间变得允许被其他记录声明使用。
在这里插入图片描述

当我们继续删除2#的数据记录
在这里插入图片描述

当页中删除的记录达到 MERGE_THRESHOLD(默认为页的50%),InnoDB会开始寻找最靠近的页(前或后)看看是否可以将两个页合并以优化空间使用
在这里插入图片描述
在这里插入图片描述

删除数据,并将页合并之后,再次插入新的数据21,则直接插入3#页
在这里插入图片描述

这个里面所发生的合并页的这个现象,就称之为 “页合并”。

知识小贴士:

  • MERGE_THRESHOLD:合并页的阈值,可以自己设置,在创建表或者创建索引时指定。

4.1.4 主键索引设计原则

  • 满足业务需求的情况下,尽量降低主键的长度。
    • 一张表的聚集索引只有一个,二级索引可以有多个,二级索引叶子下面挂的就是主键,所以如果主键长度比较长二级索引比较多,那么将会占用大量的磁盘空间,在搜索的时候占用大量的磁盘io。
  • 插入数据时,尽量选择顺序插入,选择使用AUTO_INCREMENT自增主键。
    • 主键顺序插入性能较高,主键乱序插入有可能出现页分裂现象,导致性能降低。
  • 尽量不要使用UUID做主键或者是其他自然主键,如身份证号。
    • uuid生成的主键值是无序的,这样插入数据时就是乱序插入,就可能出现页分裂现象。并且uuid生成的数据长度比较长,在检索的时候也会耗费大量的磁盘io。
  • 业务操作时,避免对主键的修改。
    • 修改主键,还要动对应的数据结构,这个代价还是比较大的。

在这里插入图片描述

4.2 EXISTS 和 IN 的区分

问题:

不太理解哪种情况下应该使用EXISTS,哪种情况应该用IN。选择的标准是看能否使用表的索引吗?

回答:

索引是个前提,其实选择与否还是要看表的大小。你可以将选择的标准理解为小表驱动大表。在这种方式下效率是最高的。

比如下面这样:

#普通子查询
SELECT * FROM A WHERE cc IN (SELECT cc FROM B)

#改造为相关子查询
SELECT * FROM A WHERE EXISTS (SELECT cc FROM B WHERE B.cc=A.cc)

当A小于B时,用EXISTS。因为EXISTS的实现,相当于外表循环,实现的逻辑类似于:

for i in A
	for j in B
		if j.cc == i.cc then ...

当B小于A时用IN,因为实现的逻辑类似于:

for i in B
	for j in A
		if j.cc == i.cc then ...

哪个表小就用哪个表来驱动,A表小就用EXISTS,B表小就用IN。

4.3 count优化

4.3.1 概述

select count(*) from tb_user ;

在之前的测试中,我们发现,如果数据量很大,在执行count操作时,是非常耗时的,它是由存储引擎决定的。

  • MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个数,效率很高; 但是如果是带条件的count,MyISAM也慢。
  • InnoDB 引擎就麻烦了,它执行 count(*) 的时候,需要把数据一行一行地从引擎里面读出来,然后累积计数。

如果说要大幅度提升InnoDB表的count效率,主要的优化思路:自己计数(可以借助于redis这样的数据库进行,但是如果是带条件的count又比较麻烦了)。

  • 自己计数:比如我们可以借助一些像key value形式的内存级别的数据库像Redis,当我们执行插入数据时直接把某一个计数加1,当我们往某一张表中去删除一条数据时把这个计数减1,自己去维护这个计数,比较繁琐。

4.3.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(*)。

4.4 关于SELECT(*)

在表查询中,建议明确字段,不要使用 * 作为查询的字段列表,推荐使用SELECT <字段列表> 查询。

原因:

  1. MySQL 在解析的过程中,会通过 查询数据字典"*"按序转换成所有列名,这会大大的耗费资源和时
    间。
  2. 无法使用覆盖索引

4.5 LIMIT 1 对优化的影响

针对的是会扫描全表的 SQL 语句,如果你可以确定结果集只有一条,那么加上LIMIT 1的时候,当找到一条结果的时候就不会继续扫描了,这样会加快查询速度。

如果数据表已经对字段建立了唯一索引,那么可以通过索引进行查询,不会全表扫描的话,就不需要加上LIMIT 1了。

4.6 多使用COMMIT

只要有可能,在程序中尽量多使用 COMMIT,这样程序的性能得到提高,需求也会因为 COMMIT 所释放的资源而减少。

COMMIT 所释放的资源:

  • 回滚段上用于恢复数据的信息
  • 被程序语句获得的锁
  • redo / undo log buffer 中的空间
  • 管理上述 3 种资源中的内部花费

4.7 插入数据

4.7.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插入数据,每次insert都要与数据库建立连接进行网络传输,这个性能是比较低的,所以我们建议批量插入。即:如果一次性你要插入多条数据可以通过一条sql语句来完成。
  • 如果批量插入,一次性插入的数据也不建议超过1000条,500~1000比较合适。如果要插入几万条数据可以将其分割为多条insert语句进行插入。
Insert into tb_test values(1,'Tom'),(2,'Cat'),(3,'Jerry');

2).优化方案二:手动控制事务

  • mysql中的事务提交方式默认是自动提交,那也就意味着当你执行完一条insert语句之后,它就自动提交了。再次执行一条insert,执行之前开启事务,执行完毕后自动提交事务,这个时候就会涉及到频繁的事务开启与提交,所以建议手动控制事务提交。
#在执行insert语句之前开启事务
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');

#在多条insert语句执行完之后,统一提交事务
commit;

3).优化方案三:主键顺序插入,性能要高于乱序插入。

  • 这个取决于mysql的数据组织结构,在sql的主键优化中讲解。
主键乱序插入 : 8 1 9 21 88 2 4 15 89 5 7 3

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

4.7.2 大批量插入数据

如果一次性需要插入大批量数据(比如: 几百万的记录)使用insert语句插入性能较低,此时可以使用MySQL数据库提供的load指令进行插入。操作如下:

  • 注意:这个数据脚本文件中的数据格式,本地文件中写的并不是sql语句而是一个符合一定规则的文件。
    • 每一个字段使用逗号隔开,这个规则里面不一定是逗号还可以是冒号,可以是任意其他的符号。

在这里插入图片描述

可以执行如下指令,一次性将数据脚本文件中的数据加载到表结构中:

#客户端连接服务端时,加上参数 --local-infile
#表示当前客户端连接服务端的时候,需要去加载本地的文件。
mysql --local-infile -u root -p

#设置全局参数local_infile为1,开启从本地加载文件导入数据的开关
#开启这个开关之后,我们才可以在本地去加载文件到数据库的表结构当中。
set global local_infile = 1;

#执行load指令将准备好的数据,加载到表结构中
#要加载本地的哪个磁盘文件(/root/sql1.log),往哪一张表中(tb_user )加载数据,
#  每一个字段之间使用什么分隔(逗号),每一行数据使用什么分隔(换行)
load data local infile '/root/sql1.log' into table tb_user fields
terminated by ',' lines terminated by '\n' ;

主键顺序插入性能高于乱序插入

示例演示:

A:连接数据库、显示所有的数据库、创建一个新的数据库

-- 客户端连接服务端时,加上参数 --local-infile
mysql --local-infile -uroot -p1234

#显示所有的数据库
show databases;

#创建数据库
create database itheima;

在这里插入图片描述

B:查看默认开关是否打开,如果没有打开进行开启

#查看开关状态
select @@local_infile;

-- 设置全局参数local_infile为1,开启从本地加载文件导入数据的开关
set global local_infile = 1;

在这里插入图片描述

C:创建表结构

#切换数据库
use 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 ;


show tables;

在这里插入图片描述

D:把准备好的数据脚本上传到服务器中

在这里插入图片描述

E:load加载数据

load data local infile '/root/load_user_100w_sort.sql' into table tb_user
fields terminated by ',' lines terminated by '\n' ;

select count(*) from tb_user;

在这里插入图片描述

我们看到,插入100w的记录,14.25s就完成了,性能很好。之前测试的如果是insert语句插入需要十几分钟,所以这个性能提升很明显。

在使用load指令时,我们也要按照主键顺序插入,主键顺序插入性能高于乱序插入。

4.8 update优化

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

4.8.1 准备数据

create table course(
    id int auto_increment comment '主键ID' primary key,
    name varchar(10) null comment '课程名称'
)comment '课程表';


INSERT INTO course (name) VALUES ('Java');
INSERT INTO course (name) VALUES ('PHP');
INSERT INTO course (name) VALUES ('MySQL');
INSERT INTO course (name) VALUES ('Hadoop');

select * from course;

在这里插入图片描述

4.8.2 正常情况

A:开启事务模拟一个事务操作,执行一条更新语句

回忆 InnoDB引擎的3大特性:事务、外键、行级锁。也就是说在InnoDB引擎中那么我们当前默认的这种事务隔离级别里面,它默认使用的是行锁。

#开启事务模拟一个事务操作。
begin;

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

对于InnoDB引擎当前默认的事务隔离级别,我们在进行操作的时候执行一条update语句,那么当前他会把id为1的这一行数据会锁住。只要你的事务没有提交,这一行的行锁就不会释放。

在这里插入图片描述

B:开启一个新的会话窗口,开启事务模拟事务操作,执行一条更新语句。

mysql -uroot -p1234

use itcast;

begin;

update course set name = 'Kafka' where id = 4;

第一个窗口使用行级锁锁的是id为1的这行数据,第二个窗口要操作的是id为4的这行数据,如果是行锁可以执行成功。

在这里插入图片描述

C:2个会话窗口进行事务提交:都成功没有任何问题。

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

4.8.3 异常情况

A:当前表的数据情况
在这里插入图片描述

B:第一个窗口模拟事务操作,执行更新语句。

#开启事务模拟一个事务操作。
begin;

update course set name = 'Springboot' where name= "PHP";

当我们执行这条sql语句时,它锁住的是第二行记录
在这里插入图片描述

C:第二个窗口模拟事务操作,执行更新语句。

#开启事务模拟一个事务操作。
begin;

update course set name = 'Kafka2' where id = 4;

第一个窗口锁住的是第二行数据,第二个窗口操作的是第4行数据,那为什么没有更新呢????

答:此时你在执行updata语句时,name这个字段没有索引,此时加的不再是行锁了而是表锁,所以他会把整张表都锁住了,第二个窗口在执行时就阻塞住了。

在这里插入图片描述

D:第一个窗口提交事务,此时表锁释放,第二个窗口在才可以更新成功。

这就是我们在执行update语句时需要规避的问题,在更新数据时一定要根据索引字段进行更新。

在这里插入图片描述

第2个窗口再次执行更新操作,更新成功,之后提交事务。

在这里插入图片描述

E:给name字段建立索引,再次执行相同的sql效果:更新成功

create index idx_course_name on course(name);

表数据效果:

在这里插入图片描述

第一个窗口:

begin;

update course set name = 'spring' where name= "Springboot";

锁的是第4行记录
在这里插入图片描述
第二个窗口:

begin;

update course set name = 'cloud' where name= "Kafka2";

操作的是第2行记录,由于第一个窗口使用的是行级锁,所以更新成功。
在这里插入图片描述

2个窗口commit提交事务。

4.8.4 更新优化说明

  • 以上演示就是update语句在执行的时候需要规避的问题:在执行update语句的时候,我们一定要根据索引字段进行更新,否则就会出现行锁升级为表锁,锁住整张表,这样会降低并发性能。
  • InnoDB引擎的行锁是针对索引加的锁,不是针对记录加的锁 ,所以在进行更新的时候更新的条件一定要有索引,如果没有索引就会出现行锁升级为表锁。并且这个索引不能失效,否则行锁也会升级为表锁 ,一旦升级为表锁那么并发性能就会降低。

4.9 总结

在这里插入图片描述

5.视图/存储过程/存储函数/触发器

5.1 视图

5.1.1介绍

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

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

5.1.2 语法:创建 查询 删除 修改

1).创建

  • [ ]中括号的内容可以省略。
  • 关键字CREATE VIEW
  • OR REPLACE:如果我们要替换某一个视图可以加上OR REPLACE
  • 视图名称:自己定义
  • AS SELECT语句:指定视图所封装的数据
  • 视图是一张虚拟表,最终的数据是来源于后面的select语句,select语句当中我们要指定要从哪一张表中去查询数据。
  • select语句当中所查询的表就是这个视图所关联的基表,也叫基础表。
  • [ WITH [CASCADED | LOCAL ] CHECK OPTION ]:检查选项
CREATE [OR REPLACE] VIEW 视图名称[(列名列表)] AS SELECT语句 [ WITH [
CASCADED | LOCAL ] CHECK OPTION ]

2).查询

  • 查看视图数据:视图可以当成一张表去查,表怎么查视图就怎么查询。后面有条件写条件。
  • 因为视图是一张虚拟存在的表,所以我们可以像操作表一样操作视图。
查看创建视图语句:SHOW CREATE VIEW 视图名称;

查看视图数据:SELECT * FROM 视图名称 ...... ;

3).修改

  • 方式一:和创建视图的语法基本一致,只不过创建视图的OR REPLACE替换关键字可以不加,但是修改视图一定要加上OR REPLACE关键字,表示把对应的视图内容替换掉。
方式一:CREATE [OR REPLACE] VIEW 视图名称[(列名列表)] AS SELECT语句 [ WITH
[ CASCADED | LOCAL ] CHECK OPTION ]

方式二:ALTER VIEW 视图名称[(列名列表)] AS SELECT语句 [ WITH [ CASCADED |
LOCAL ] CHECK OPTION ]

4).删除

  • 可选项IF EXISTS:如果视图存在,在执行删除操作。
DROP VIEW [IF EXISTS] 视图名称 [,视图名称] ...

5.1.3 演示:创建 查询 删除 修改视图

准备数据:

create table student(
    id   int auto_increment comment '主键ID' primary key,
    name varchar(10) null comment '姓名',
    no   varchar(10) null comment '学号'
)comment '学生表';

INSERT INTO student (name, no) VALUES ('黛绮丝', '2000100101');
INSERT INTO student (name, no) VALUES ('谢逊', '2000100102');
INSERT INTO student (name, no) VALUES ('殷天正', '2000100103');
INSERT INTO student (name, no) VALUES ('韦一笑', '2000100104');

select * from student;

在这里插入图片描述

创建视图:

-- 创建视图
#创建或者替换视图 指定视图名称 数据来自于(展示student表中的2个字段id name)
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;

显示的创建视图所执行的sql语句和我们自己编写的有些差异,这个地方所查询出来的sql语句包含了在创建视图的时候我们所没有指定的一些默认参数,在这一块并没有指定但是在查询的时候是查询到了。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

修改视图:

#方式一   原先查询2个字段,现在查询3个字段
create or replace view stu_v_1 as select id,name,no from student where id <= 10;

#查看视图发现已经修改,由显示的2个变为3个
select * from stu_v_1;

#方式二
alter view stu_v_1 as select id,name from student where id <= 10;

#查看视图发现已经修改,由显示的3个变为2个
select * from stu_v_1;

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

删除视图:

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');

在这里插入图片描述
在这里插入图片描述
执行没有报错,2条数据都可以插入成功,视图当中并不存储数据,具体的数据都是在基表当中存在的,也就是说现在我们插入的数据是在这个视图对应的基表 student 表当中。

执行上述的SQL,我们会发现,id为6和17的数据都是可以成功插入的。 但是我们执行查询视图,查询出来的数据,却没有id为17的记录。

在这里插入图片描述

因为:我们在创建视图的时候,指定的条件为 id<=10, id为17的数据,是不符合条件的,所以没有查询出来,但是这条数据确实是已经成功的插入到了基表中

如果我们定义视图时,如果指定了条件,然后我们在插入、修改、删除数据时,是否可以做到必须满足条件才能操作,否则不能够操作呢? 答案是可以的,这就需要借助于视图的检查选项了

给创建视图的sql添加检查选项的语句,之后再次执行不符合条件的sql,如果你插入的这条数据和我们创建视图时的条件是相违背的,这个 CHECK OPTION选项就会阻止我们插入。

create or replace view stu_v_1 as select id,name from student where id <= 10 WITH CASCADED CHECK OPTION;

insert into stu_v_1 values(19,'Tom36');

在这里插入图片描述

5.1.4 检查选项

当使用WITH CHECK OPTION子句创建视图时,MySQL会通过视图检查正在更改的每个行,例如 插入,更新,删除,以使其符合视图定义时的条件。 MySQL允许基于另一个视图创建视图,它还会检查依赖视图中的规则以保持一致性。为了确定检查的范围,mysql提供了两个选项: CASCADED 和 LOCAL,默认值为 CASCADED

1).CASCADED

级联

比如,v2视图是基于v1视图的,如果在v2视图创建的时候指定了检查选项为 cascaded,但是v1视图创建时未指定检查选项。 则在执行检查时,不仅会检查是否满足v2的条件,还会级联检查是否满足v1的条件,相当于在v1里面也加上了WITH CASCADED CHECK OPTION

总结:基于v1视图创建v2,v1没有写检查选项,v2写了检查选项。此时在往v2中插入数据的时候不仅要检查v2还要检查v1。相当于v1也写了检查选项。

在这里插入图片描述

测试:
恢复student表中的数据:
在这里插入图片描述

# CASCADED

#创建v1视图,不指定检查选项
create or replace view stu_v_1 as select id,name from student where id <= 20;

#插入成功:id为5<20
insert into stu_v_1 values(5,'Tom');

#插入成功:虽然id为25不满足v1视图的查询条件,但是没有指定检查选项,所以可以插入。
INSERT INTO stu_v_1 VALUES(25,'Tom');




#基于v1视图创建v2视图,指定检查选项
CREATE OR REPLACE VIEW stu_v_2 AS SELECT id,NAME FROM stu_v_1 WHERE id >= 10 WITH CASCADED CHECK OPTION;

#插入失败:由于v2视图添加了检查选项,7不满足v2视图的条件
INSERT INTO stu_v_2 VALUES(7,'Tom');

#插入失败:因为v2视图使用的是CASCADED关键字,它会检查当前视图所依赖的底层所有视图,v2视图的条件满足,v1视图的条件不满足,所以插入失败。
INSERT INTO stu_v_2 VALUES(26,'Tom');

#插入成功:id=15满足v2的条件,也满足v1视图的条件。
INSERT INTO stu_v_2 VALUES(15,'Tom');




#基于v2视图创建v3视图,没有检查选项
CREATE OR REPLACE VIEW stu_v_3 AS SELECT id,NAME FROM stu_v_2 WHERE id <= 15 ;

#插入成功:id满足v3 v2 v1视图的查询条件,所以插入成功。
INSERT INTO stu_v_3 VALUES(11,'Tom');

#插入成功:id=17虽然不满足v3视图的条件,但是v3视图没有写检查选项。之后17又满足v2 v3视图的条件,所以插入成功。
INSERT INTO stu_v_3 VALUES(17,'Tom');

#插入失败:id=28不满足v3视图,但是由于v3视图没有写检查选项所以即使不满足他也不会进行检查。之后检查v2视图满足,检查v1视图不满足,所以插入失败。
INSERT INTO stu_v_3 VALUES(28,'Tom');

2).LOCAL

本地

比如,v2视图是基于v1视图的,如果在v2视图创建的时候指定了检查选项为 local ,但是v1视图创建时未指定检查选项。 则在执行检查时,只会检查v2,不会检查v2的关联视图v1。

总结:对于local视图检查选项,当我们在操作视图的时候它会递归的去找当前视图所依赖的视图,如果当前视图以及所依赖的视图都定义了WITH LOCAL CHECK OPTION选项,将会去判定我们所操作的数据是否满足这个视图的条件,如果在递归的过程当中找到某一个视图在定义的时候,没有增加这样的检查选项,此时我们在操作这块数据的时候将不对这个条件做检查。

CASCADED和LOCAL的区别:

  • CASCADED:基于v1视图创建v2视图,v1没有检查选项v2有,在往v2插入数据时会检查v2和v1,v1即使没有写检查选项也相当于写了
    • 如果v2有检查选项,级联的v1一定有检查选项,所以一定会检查。
  • LOCAL:基于v1视图创建v2视图,v2有检查选项,在往v2插入数据时会检查v2和v1,此时如果v1有检查选项则会检查,如果v1没有检查选项则不会检查
    • v1有没有检查选项看是不是自己写了,有就检查没有就不检查。而上面那个级联是即使不写也相当于写了检查选项,一定会检查

在这里插入图片描述
测试:

恢复student表数据
在这里插入图片描述

# local

#创建v4视图,不指定检查选项
CREATE OR REPLACE VIEW stu_v_4 AS SELECT id,NAME FROM student WHERE id <= 20;

#插入成功:id为5满足v4条件
INSERT INTO stu_v_4 VALUES(5,'Tom');

#插入成功:虽然id不满足v4视图的查询条件,但是v4没有写检查选项,所以不会进行检查。
INSERT INTO stu_v_4 VALUES(16,'Tom');




#基于v4视图创建v5视图,指定检查选项
CREATE OR REPLACE VIEW stu_v_5 AS SELECT id,NAME FROM stu_v_4 WHERE id >= 10 WITH LOCAL CHECK OPTION;

#插入成功:13满足v5视图,之后它还会递归的去找所依赖的视图v4,如果v4视图没有写检查选项,则不检查。所以插入成功。
INSERT INTO stu_v_5 VALUES(13,'Tom');

#插入成功:13满足v5视图,之后它还会递归的去找所依赖的视图v4,如果v4视图没有写检查选项,则不检查。所以插入成功。
INSERT INTO stu_v_5 VALUES(17,'Tom');




#基于v5视图创建v6视图,没有检查选项
CREATE OR REPLACE VIEW stu_v_6 AS SELECT id,NAME FROM stu_v_5 WHERE id <= 15 ;

#插入成功:v6没有定义检查选项所以不做检查,之后递归到v5 v5定义的检查选项所以会进行检查 14满足条件,之后递归到v4 v4没有检查选项 所以不做检查。
INSERT INTO stu_v_6 VALUES(14,'Tom');

5.1.5 视图的更新

mysql中的视图并不是任何的视图都可以进行增删改。所以我们要了解一下视图的更新需要什么条件。

要使视图可更新,视图中的行与基础表中的行之间必须存在一对一的关系如果视图包含以下任何一 项,则该视图不可更新

  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);

在这里插入图片描述

5.1.6 视图作用

1).简单
视图不仅可以简化用户对数据的理解,也可以简化他们的操作。那些被经常使用的查询可以被定义为视图,从而使得用户不必为以后的操作每次指定全部的条件。

即:如果不使用视图多个插入数据每次都要写条件,如果使用视图只需要在视图中写条件,之后在写插入数据的时候就不需要写条件了。

2).安全
数据库可以授权,但不能授权到数据库特定行和特定的列上。通过视图用户只能查询和修改他们所能见到的数据

即:数据库中的操作我们可以进行用户授权,通过授权来决定每个用户登录mysql后,能够看到操作那些数据库,能够看到操作那些表。但是对于mysql中的数据库授权只能操作到表,不能控制到表中的字段,而视图就可以。

eg:只想要某一个用户只看到学生表当中的id name2个字段,不想让他看到学号,此时就可以创建视图,这个视图在查询时只包含id 和name,这样这个用户在操作视图的时候只能看到id name2个字段并不能看到学号,保证了一些敏感数据的安全性。

3).数据独立
视图可帮助用户屏蔽真实表结构变化带来的影响。

即:基础表的name字段变为 student_name,此时我们只需要对视图进行一个更新操作就可以了。把创建视图的student_name字段起个别名为name,这样就屏蔽了基础表的变化对业务的影响。

#基础表原始数据 name
CREATE OR REPLACE VIEW stu_v_4 AS SELECT id,NAME FROM student WHERE id <= 20;

#基础表数据name字段变为student_name
CREATE OR REPLACE VIEW stu_v_4 AS SELECT id,student_name FROM student WHERE id <= 20;

#使用别名屏蔽基础表的差异 student_name as name
CREATE OR REPLACE VIEW stu_v_4 AS SELECT id,student_name as name FROM student WHERE id <= 20;

5.1.7 案例

1).为了保证数据库表的安全性,开发人员在操作tb_user表时,只能看到的用户的基本字段,屏蔽 手机号和邮箱两个字段。

#创建视图的时候返回查询的数据不包含这2个字段
create view tb_user_view as select id,name,profession,age,gender,status,createtime
from tb_user;

#查询视图就看不到这2个字段了
select * from tb_user_view;

2).查询每个学生所选修的课程(三张表联查),这个功能在很多的业务中都有使用到,为了简化操作不必每次都把3表联查的sql都写出来,可以把这个三表联查的sql封装到视图当中,在后续的业务课程当中要想查询每个学生所选修的课程,只需要查询该视图就可以了。

学生表、课程表、由于是多对多关系所以还会有一个中间表。

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.2 存储过程

5.2.1 介绍

存储过程是事先经过编译并存储在数据库中的一段SQL语句的集合,调用存储过程可以简化应用开发人员的很多工作,减少数据在数据库和应用服务器之间的传输,对于提高数据处理的效率是有好处的。

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

重用:业务1想要用到这个p1的sql集合直接去调用这个存储过程就行了,下一次在遇到相同的业务的时候只需要调用对应的存储过程即可。

场景:

  • 问题:一个业务当中它可能需要操作多次数据库,eg 第一次先要去查询数据库表当中的数据,之后根据查询的结果要不要更新,如 更新第一张表的数据之后更新第二张表的数据。一个逻辑当中需要操作多次数据库,因意味着多次网络请求。
  • 优化:在数据库层面将多条sq语句封装到一个集合当中,之后只需要调用这个p1这个sql集合就可以了。

在这里插入图片描述

特点:

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

5.2.2 基本语法

1).创建

  • ([ 参数列表 ]):存储过程在定义的时候可以指定它输入的参数,以及返回的参数信息的。当然参数是可有可无的。
  • BEGIN,END是固定格式,里面写的是这个存储过程封装的sql语句,可以是一条也可以是是多条。
  • 所有存储过程的逻辑都是在BEGIN和END之间定义的。

# 这种创建方式有问题,失败
CREATE PROCEDURE 存储过程名称([ 参数列表 ])

BEGIN

	-- SQL语句
	
END ;

-----------------------------------------------

#这种才可以常创建成功
#表示现在sql语句在结束的时候,是以2个$为结束符号的。
delimiter $$

CREATE PROCEDURE 存储过程名称([ 参数列表 ])

BEGIN

	-- SQL语句
	
END $$


-------------------------
之后不想要在命令行操作时可以改回来 分号形式


  • 问题:如果在命令行中定义存储过程可能会报错。
    在这里插入图片描述
  • 原因:在命令行当中执行sql语句的时候,一旦见到分号它就认为当前的sql语句已经结束了,而实际上创建存储过程的语法是到end之后才会结束。
  • 解决:在命令行中,执行创建存储过程的SQL时,需要通过关键字 delimiter 指定SQL语句的结束符。默认mysql语句的结束符是分号(;),走到封装的select语句时因为有分号所以就会结束,所以我们需要重新定义它的结束符号,此时走到select是就不会结束了。

2).调用

CALL 名称([ 参数 ]);

#此时只能使用$$作为结束符了。

3).查看

#形式1:数据库自带的ROUTINES这张表中去查询,当前数据库服务器里面所有的存储过程的信息  参数:数据库的名字
SELECT * FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = 'xxx'; -- 查询指定数据库的存储过程及状态信息

#形式2:查看指定的存储过程在创建的时候,它的sql语句是什么样的。
SHOW CREATE PROCEDURE 存储过程名称 ; -- 查询某个存储过程的定义

4).删除

DROP PROCEDURE [ IF EXISTS ] 存储过程名称 ;

演示示例:

-- 存储过程基本语法
-- 创建:暂时定义无参的,参数具体的格式类型在后面中学习。
create procedure p1()

begin
	select count(*) from student;
end;

-- 调用:会执行存储过程中所封装的逻辑
call p1();

-- 查看
# 形式1
select * from information_schema.ROUTINES where ROUTINE_SCHEMA = 'itcast';

# 形式2
show create procedure p1;

-- 删除
drop procedure if exists p1;

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
sql会多了个DEFINER=root@% ,创建的用户是那个(root用户),创建的时候没写默认会加上这个参数

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

5.2.3 变量

说明:上面只是演示的是存储过程中定义一个简单的select语句,并没有任何的逻辑,在一些复杂的业务逻辑当中想要去编写一个复杂的存储过程,里面就会涉及到很多的语法结构。所以接下来呢我们就需要针对于存储过程当中,所涉及到的语法结构来进行详细的讲解首先要学习的第一块就是变量

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

1)系统变量

系统变量 是MySQL服务器提供,不是用户定义的,属于服务器层面。分为全局变量(GLOBAL)、会话变量(SESSION)。

  • 全局变量:对所有的会话有效

    • eg:创建多个查询控制台,一个查询窗口就是一个会话
      在这里插入图片描述
  • 会话变量:在当前会话中有效

1).查看系统变量

  • 如果写了SESSION :表示查看所有的会话变量
  • 如果写了GLOBAL :表示查看所有的全局系统变量
  • SESSION | GLOBAL不指定默认是SESSION 会话级别。
SHOW [ SESSION | GLOBAL ] VARIABLES ; -- 查看所有系统变量

#如果知道查看系统变量的某几个字符,就可以查询部分系统变量
SHOW [ SESSION | GLOBAL ] VARIABLES LIKE '......'; -- 可以通过LIKE模糊匹配方式查找变量

#如果准确的知道了某一个系统变量名,查找具体的系统变量
SELECT @@[SESSION | GLOBAL] 系统变量名; -- 查看指定变量的值

2).设置系统变量

#方式一
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 = 0;

#查看是否修改成功
select @@session.autocommit;

#插入一条数据:执行成功
insert into course(id, name) VALUES (6, 'ES');

#查询course表发现没有插入数据,原因是事务自动提交关闭了,意味着要手动事务提交
#注意如果是直接刷新表是看不到数据的,但如果是select还是能够看到数据的。
select * from course;

#手动提交事务,再次查询发现
commit;
select * from course;


#开启自动提交
set session autocommit = 1;

#再次查询系统变量
select @@session.autocommit;

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

2)用户定义变量

用户定义变量 是用户根据需要自己定义的变量,用户变量不用提前声明,在用的时候直接用 “@变量名” 使用就可以。其作用域为当前连接(当前会话)。

  • @@:2个表示系统变量
  • @:1个表示用户自定义变量

1).赋值
方式一:

#形式一
SET @var_name = expr [, @var_name = expr] ... ;

#形式二
SET @var_name := expr [, @var_name := expr] ... ;

赋值时,可以使用 = ,也可以使用 := 。
推荐使用 := ,因为在mysql当中的比较运算符也是=,它是没有==,所以mysql中=既可以作为赋值运算符又可以作为比较运算符,为了区分推荐在赋值的时候使用:=

方式二:

#形式一
SELECT @var_name := expr [, @var_name := expr] ... ;

#形式二:把查询的某个结果赋值给某个变量
SELECT 字段名 INTO @var_name FROM 表名;

2).查询:查看变量是否赋值成功

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;

3)局部变量

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

1).声明

  • [DEFAULT … ]:可选的参数DEFAULT ,如果这个变量有默认值,可以通过default这个关键字给它指定默认值。
DECLARE 变量名 变量类型 [DEFAULT ... ] ;

变量类型就是数据库字段类型:INT、BIGINT、CHAR、VARCHAR、DATE、TIME等。

2).赋值

SET 变量名 =;
SET 变量名 :=;
SELECT 字段名 INTO 变量名 FROM 表名 ... ;

演示示例:


-- 定义存储过程
create procedure p2()

begin
    #定义局部变量
	declare stu_count int default 0;
	#赋值
	select count(*) into stu_count from student;
	#查询这个变量
	select stu_count;
end;

#调用存储过程
call p2();

5.2.4 if

1).介绍

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

  • 如果条件1成立执行then之后的sql逻辑,否则就要去判断条件2的逻辑是否成立,如果成立则执行then之后的sql逻辑,如果还有ELSEIF则继续判断…
  • ELSE :默认的逻辑
  • 最后通过END IF语法结束。
IF 条件1 THEN
	.....
ELSEIF 条件2 THEN -- 可选
	.....
ELSE -- 可选
	.....
END IF;

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

2).案例

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

  • score >= 85分,等级为优秀。
  • score >= 60分 且 score < 85分,等级为及格。
  • 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.2.5 参数

1).介绍
参数的类型,主要分为以下三种:IN、OUT、INOUT。 具体的含义如下:

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

用法:

#在创建存储过程的时候声明参数
CREATE PROCEDURE 存储过程名称 ([ IN/OUT/INOUT 参数名 参数类型 ])

BEGIN
	-- SQL语句
	
END ;

在这里插入图片描述

2).案例一
根据传入(in)参数score,判定当前分数对应的分数等级,并返回(out)

  • score >= 85分,等级为优秀。
  • score >= 60分 且 score < 85分,等级为及格。
  • 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;

end;


#查询:存储过程
#第一个参数是传入的数据,第二个参数是将执行的返回值返回,通过用户自定义变量进行接收
#定义用户变量 @result来接收返回的数据, 用户变量可以不用声明
call p4(18, @result);

#查看这个用户自定义变量
select @result;

在这里插入图片描述

3).案例二

传入的200分制的分数,进行换算,换算成百分制,然后 返回分数。

分数既是传入参数又是传出参数------INOUT

#创建存储过程
create procedure p5(inout score double)

begin
	set score := score * 0.5;
end;

#调用存储过程:如果直接传递198就没办法拿到返回值了,所以需要定义一个用户自定义变量@score
#  这个变量没有值所以要先进行赋值,然后在去调用这个存储过程,调用完毕之后他会将计算完毕后的结果,
#  再次赋值给这个用户自定义变量

set @score = 198;
call p5(@score);

#查询这个用户自定义变量封装的返回结果
select @score;

在这里插入图片描述

5.2.6 case

1).介绍
case结构及作用,和我们在基础篇中所讲解的流程控制函数很类似。有两种语法格式:

语法1:

-- 含义: 当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; #结束语法

语法2:

-- 含义: 当条件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;

2).案例
根据传入的月份,判定月份所属的季节(要求采用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 ;
	
	#查询局部变量:借助concat函数拼接字符串
	select concat('您输入的月份为: ',month, ', 所属的季度为: ',result);

end;


#调用存储过程
call p6(16);

在这里插入图片描述

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

5.2.7 while循环

1).介绍
while 循环是有条件的循环控制语句满足条件后,再执行循环体中的SQL语句。具体语法为:

  • 特点:满足条件才循环
-- 先判定条件,如果条件为true,则执行逻辑,否则,不执行逻辑
WHILE 条件 DO

	SQL逻辑...
	
END WHILE; #结束语句

2).案例

计算从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.2.8 repeat循环

1).介绍

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

  • 特点:满足条件退出循环
-- 先执行一次逻辑,然后判定UNTIL条件是否满足,如果满足,则退出。如果不满足,则继续下一次循环
REPEAT
	SQL逻辑...
	UNTIL 条件
END REPEAT;

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

-- A. 定义局部变量, 记录累加之后的值;
-- B. 每循环一次, 就会对n进行-1 , 如果n减到0, 则退出循环
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;

call p8(10);
call p8(100);

在这里插入图片描述

5.2.9 loop循环

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

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

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

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

2).案例一
计算从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);

在这里插入图片描述

3).案例二
计算从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 = 1 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(100);

在这里插入图片描述

5.2.10 游标

1)场景分析:

这个存储过程的sql是把SELECT执行结果 count(*)总记录数,赋值给stu_count这个变量。
即:单个字段的值 赋值给一个变量---------成功。

delimiter $$


-- 定义存储过程
create procedure p2()

begin
    #定义局部变量
	declare stu_count int default 0;
	#赋值:把SELECT执行结果赋值给stu_count这个变量
	select count(*) into stu_count from student;
	#查询这个变量
	select stu_count;
end$$

#调用存储过程
call p2();

在这里插入图片描述
count(*)修改为*,此时变为把查询的所有结果赋值给一个变量--------失败。

DELIMITER $$


-- 定义存储过程
CREATE PROCEDURE p2()

BEGIN
    #定义局部变量
	DECLARE stu_count INT DEFAULT 0;
	#赋值:把SELECT执行结果赋值给stu_count这个变量
	SELECT COUNT * INTO stu_count FROM student;
	#查询这个变量
	SELECT stu_count;
END$$

#调用存储过程
CALL p2();

在这里插入图片描述

原因:

  • count(*)返回单列数据,所以可以赋值给一个变量
  • 而*返回的数据有很多列,所以不能赋值给一个变量
  • 说明:局部变量只能接受一些单行单列类型的数据,如果接收的是一张表或者是一个结果集该如何接收呢???
    • 使用游标。
2)介绍

游标(CURSOR)是用来存储查询结果集的数据类型 , 在存储过程和函数中可以使用游标对结果集进行循环的处理。游标的使用包括游标的声明、OPEN、FETCH 和 CLOSE,其语法分别如下。

A.声明游标:类似于声明局部变量

#把sql语句的查询结果封装到这个游标当中。
DECLARE 游标名称 CURSOR FOR 查询语句 ;

B.打开游标:使用游标之前一定要打开游标

OPEN 游标名称 ;

C.获取游标记录

#把游标中的记录赋值给一个个的变量
FETCH 游标名称 INTO 变量 [, 变量 ] ;

D.关闭游标

#当我们把游标当中的记录便利获取完毕之后,就可以关闭游标了
CLOSE 游标名称 ; 
3)案例

根据传入的参数uage,来查询用户表tb_user中,所有的用户年龄小于等于uage的用户姓名(name)和专业(profession),并将用户的姓名和专业插入到所创建的一张新表(id,name,profession)中。

-- 逻辑:
-- A. 声明游标, 存储查询结果集
-- B. 准备: 创建表结构
-- C. 开启游标
-- D. 获取游标中的记录
-- E. 插入数据到新表中
-- F. 关闭游标

DELIMITER $$

CREATE PROCEDURE p11(IN uage INT)

#begin和end之间可以编写任何的sql语句
BEGIN

    #声明2个局部变量接收遍历游标时的2个参数(注意顺序:要先声明局部变量再声明游标,否则执行时会报错)
	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
		#获取游标的记录并赋值给2个字段
		FETCH u_cursor INTO uname,upro;
		#之后把这2个字段插入到新创建的表中
		INSERT INTO tb_user_pro VALUES (NULL, uname, upro);
	END WHILE;
	#关闭游标
	CLOSE u_cursor;

END$$

CALL p11(30);


在这里插入图片描述

上述的存储过程,最终我们在调用的过程中,会报错,之所以报错是因为上面的while循环中,并没有退出条件使用的是死循环。当游标的数据集获取完毕之后,再次获取数据,就会报错,从而终止了程序的执行。

在这里插入图片描述

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

在这里插入图片描述

上述的功能,虽然我们实现了,但是逻辑并不完善,而且程序执行完毕,获取不到数据,数据库还报错。 接下来,我们就需要来完成这个存储过程,并且解决这个问题。

要想解决这个问题,也就是说一旦发现游标当中没有数据了 此时就要退出循环关闭游标,就需要通过MySQL中提供的条件处理程序 Handler来解决。

5.2.11 条件处理程序

1)介绍

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

即:我们定义了一个条件处理程序,可以去指定什么时候这个条件处理程序被触发,触发了之后执行什么样的动作。

具体语法为:

# 想要使用条件处理程序,我们就需要声明条件处理程序,再声明的时候还需要指定当前条件处理程序的类型(2个)
DECLARE handler_action HANDLER FOR condition_value [, condition_value] ... statement;
  • handler_action:当前条件处理程序的类型

    • CONTINUE: 继续执行当前程序
    • EXIT: 终止执行当前程序
  • condition_value:满足什么样的条件才会执行handler这个动作,执行这个动作之后还可以执行具体的sql逻辑(statement)

    • SQLSTATE sqlstate_value: 状态码,如 02000

    • SQLWARNING: 所有以01开头的SQLSTATE代码的简写(警告)

    • NOT FOUND: 所有以02开头的SQLSTATE代码的简写(找不到数据)

    • SQLEXCEPTION: 所有没有被SQLWARNING 或 NOT FOUND捕获的SQLSTATE代码的简写

具体的错误状态码,可以参考官方文档:

https://dev.mysql.com/doc/refman/8.0/en/declare-handler.html

https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html

2)案例

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

根据传入的参数uage,来查询用户表tb_user中,所有的用户年龄小于等于uage的用户姓名(name)和专业(profession),并将用户的姓名和专业插入到所创建的一张新表(id,name,profession)中。

A.通过SQLSTATE指定具体的状态码

-- 逻辑:
-- A. 声明游标, 存储查询结果集
-- B. 准备: 创建表结构
-- C. 开启游标
-- D. 获取游标中的记录
-- E. 插入数据到新表中
-- F. 关闭游标

DELIMITER $$

create procedure p11(in uage int)

#begin和end之间可以编写任何的sql语句
begin

    #声明2个局部变量接收遍历游标时的2个参数(注意顺序:要先声明局部变量再声明游标,否则执行时会报错)
	DECLARE uname VARCHAR(100);
	DECLARE upro VARCHAR(100);
	#声明游标,存储查询的结果集
	declare u_cursor cursor for select name,profession from tb_user where age <=uage;
	
	#声明条件处理程序:满足这个状态码就执行退出操作,退出的时候还需要关闭游标
	#执行流程:即使下面的while true死循环,循环走到最后抓取不到数据会报错,报错之后
	#   它就会进入到我们的条件处理程序,在条件处理程序中它会关闭游标,然后执行退出操作。
	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
		#获取游标的记录并赋值给2个字段
		fetch u_cursor into uname,upro;
		#之后把这2个字段插入到新创建的表中
		insert into tb_user_pro values (null, uname, upro);
	end while;
	#关闭游标
	close u_cursor;

end$$

call p11(30);


再次执行发现不会在报错了,查询新创建的表发现数据插入成功

在这里插入图片描述

在这里插入图片描述

B.通过SQLSTATE的代码简写方式 NOT FOUND

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

DELIMITER $$

create procedure p12(in uage int)

#begin和end之间可以编写任何的sql语句
begin

        #声明2个局部变量接收遍历游标时的2个参数(注意顺序:要先声明局部变量再声明游标,否则执行时会报错)
	DECLARE uname VARCHAR(100);
	DECLARE upro VARCHAR(100);
	#声明游标,存储查询的结果集
	declare u_cursor cursor for select name,profession from tb_user where age <=uage;
	
	#声明条件处理程序:满足这个状态码就执行退出操作,退出的时候还需要关闭游标
	#执行流程:即使下面的while true死循环,循环走到最后抓取不到数据会报错,报错之后
	#   它就会进入到我们的条件处理程序,在条件处理程序中它会关闭游标,然后执行退出操作。
	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
		#获取游标的记录并赋值给2个字段
		fetch u_cursor into uname,upro;
		#之后把这2个字段插入到新创建的表中
		insert into tb_user_pro values (null, uname, upro);
	end while;
	#关闭游标
	close u_cursor;

end$$

call p12(30);


同样:再次执行发现不会在报错了,查询新创建的表发现数据插入成功
在这里插入图片描述

效果:
在这里插入图片描述

5.3 存储函数

5.3.1 介绍

存储函数是有返回值的存储过程,存储函数的参数只能是IN类型的。
也就是说:定义一个存储函数必须要指定返回值,并且它的参数列表当中所有的参数类型必须是输入类型in类型。

说明:存储函数和存储过程的整体语法结构和流程控制基本都是一致的,只是里面的基础语法结构略有不同。

具体语法如下:

  • RETURNS type :指定返回时的类型
  • 可选参数characteristic :当前存储参数的特性
CREATE FUNCTION 存储函数名称 ([ 参数列表 ])

RETURNS type [characteristic ...]

BEGIN
	-- SQL语句
	-- 必须有返回值
	RETURN ...;
	
END ;

characteristic说明:

  • DETERMINISTIC:相同的输入参数总是产生相同的结果
    • 即:传入的参数一样,返回值也是一样的。
  • NO SQL :不包含 SQL 语句。
    • 当前的存储函数中不包含 SQL 语句,就可以通过这个描述符来描述当前存储函数。
  • READS SQL DATA:只包含读取数据的语句,但不包含写入数据的语句。

5.3.2 案例

计算从1累加到n的值,n为传入的参数值。(通过存储函数来实现

#默认就是in
create function fun1(n int)

returns int

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(50);

会报错:

在这里插入图片描述

在这里插入图片描述

原因:在mysql8.0版本中binlog默认是开启的,一旦开启了,mysql就要求在定义存储过程时,需要指定characteristic特性,否则就会报如下错误:

解决:

DELIMITER $$

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(50);

在这里插入图片描述

5.3.3 缺点

存储函数呢相对来说用的比较少一些,因为存储函数能做的事情那么存储过程也能做到,而且存储函数还有一个弊端他必须要用返回值。假如我要去执行一段逻辑而这段逻辑有返回值,我可不可以使用存储过程来实现???

可以:因为存储过程我们可以指定参数的类型为out,就可以接收我存储过程执行的结果,所以可以使用存储函数的地方都可以使用存储过程来代替。

5.4 触发器

5.4.1 介绍

触发器是与表有关的数据库对象,指在insert/update/delete之前(BEFORE)或之后(AFTER)触发,一旦触发器触发了它就会自动的去执行触发器中定义的SQL语句集合。触发器的这种特性可以协助应用在数据库端确保数据的完整性, 日志记录 , 数据校验等操作

使用别名OLD(引用原来的记录内容)NEW(引用新的记录内容)来引用触发器中发生变化的记录内容,这与其他的数据库是相似的。现在触发器还只支持行级触发,不支持语句级触发

  • 行级触发器:比如执行一条update语句,他影响了5行,那么这个时候这个触发器会被触发5次。
  • 语句级触发器:比如执行一条update语句,不管这条update影响了多少行,只触发一次。
触发器类型NEW 和 OLD
INSERT 型触发器NEW 表示将要或者已经新增的数据(old没有用,因为old代表原来的数据)
UPDATE 型触发器OLD 表示修改之前的数据 , NEW 表示将要或已经修改后的数据
DELETE 型触发器OLD 表示将要或者已经删除的数据(没有new,因为数据已经删除了,是没有新的数据的)

5.4.2 语法

1).创建

#创建触发器并指定名称
CREATE TRIGGER trigger_name

#指定触发的时机 指定触发器的类型
BEFORE/AFTER INSERT/UPDATE/DELETE

# on 表的名字:哪一张表的数据进行增删改之后触发
#FOR EACH ROW:指定行级触发器,mysql目前只支持行级触发器。
ON tbl_name FOR EACH ROW 

#编写触发器的业务逻辑
BEGIN

	trigger_stmt ;

END;

2).查看

#查看当前数据库中所有的触发器
SHOW TRIGGERS ;

3).删除
schema_name:数据库名
trigger_name:触发器名

# 删除指定数据库下的触发器
# 如果没有指定 schema_name数据库名字,默认为当前数据库 。
DROP TRIGGER [schema_name.]trigger_name ; --

5.4.3 案例

通过触发器记录 tb_user 表的数据变更日志,将变更日志插入到日志表user_logs中, 包含增加, 修改 , 删除 ;

也就是说:只要tb_user 表进行了增删改就会触发触发器,然后把变更日志记录在日志表user_logs中。

日志表结构准备:

字段说明:

  • id:主键
  • operation 操作类型:当前操作是插入还是更新还是删除
  • operate_time 操作时间:触发器执行的当前时间
  • operate_id 操作的ID:操作的原始记录的id是多少,你操作的是tb_user 表当中的那一条数据。
  • operate_params 操作的参数:因为记录的是日志就需要知道我当前插入数据,数据里面的每一个参数值是什么样的。更新数据,更新之前 更新之后是什么样的。删除数据,删除的是哪一块数据。
-- 准备工作 : 日志表 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;

在这里插入图片描述

A.插入数据触发器

# 插入触发器
DELIMITER $$

create trigger tb_user_insert_trigger
	after insert on tb_user for each row
begin

	#operate_id:操作的原始记录的id,通过之前学习的别名new拿到插入到数据库的id(new.id)
	#operate_params:插入时传递了哪些参数,通过字符串拼接函数指定
	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 ;

-- 插入数据到tb_user:验证在插入数据后会不会往日志表当中保存日志
insert into tb_user(id, name, phone, email, profession, age, gender, status,
createtime) VALUES (26,'三皇子','18809091212','erhuangzi@163.com','软件工
程',23,'1','1',now());

-- 查看日志表当中有没有数据
SELECT * FROM user_logs;

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

B.修改数据触发器

# 修改触发器
DELIMITER $$

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; -- 因为是行级触发器,所以会触发5次


-- 查看日志表当中有没有数据
SELECT * FROM user_logs;

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

在这里插入图片描述

C.删除数据触发器

# 删除触发器
DELIMITER $$

CREATE TRIGGER tb_user_delete_trigger
	AFTER DELETE ON tb_user FOR EACH ROW
BEGIN
	#operate_id原始数据的id:删除之前有数据删除之后没有数据,所以只能使用old拿到删除之前的id是多少。
	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;

-- 查看日志表当中有没有数据
SELECT * FROM user_logs;

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

在这里插入图片描述

5.5 总结

在这里插入图片描述

6. 淘宝数据库,主键如何设计的?

聊一个实际问题:淘宝的数据库,主键是如何设计的?

某些错的离谱的答案还在网上年复一年的流传着,甚至还成为了所谓的MySQL军规。其中,一个最明显的错误就是关于MySQL的主键设计。

大部分人的回答如此自信:用8字节的 BIGINT 做主键,而不要用INT。 错 !

这样的回答,只站在了数据库这一层,而没有 从业务的角度 思考主键。主键就是一个自增ID吗?站在2022年的新年档口,用自增做主键,架构设计上可能 连及格都拿不到

6.1 自增ID的问题

自增ID做主键,简单易懂,几乎所有数据库都支持自增类型,只是实现上各自有所不同而已。自增ID除了简单,其他都是缺点,总体来看存在以下几方面的问题:

1. 可靠性不高

存在自增ID回溯的问题,这个问题直到最新版本的MySQL 8.0才修复。

2. 安全性不高

对外暴露的接口可以非常容易猜测对应的信息。比如:/User/1/这样的接口,可以非常容易猜测用户ID的值为多少,总用户数量有多少,也可以非常容易地通过接口进行数据的爬取。

3. 性能差

自增ID的性能较差,需要在数据库服务器端生成。

4. 交互多

业务还需要额外执行一次类似 last_insert_id() 的函数才能知道刚才插入的自增值,这需要多一次的网络交互。在海量并发的系统中,多1条SQL,就多一次性能上的开销。

5. 局部唯一性

最重要的一点,自增ID是局部唯一,只在当前数据库实例中唯一,而不是全局唯一,在任意服务器间都是唯一的。对于目前分布式系统来说,这简直就是噩梦。

6.2 业务字段做主键

为了能够唯一地标识一个会员的信息,需要为 会员信息表 设置一个主键。那么,怎么为这个表设置主键,才能达到我们理想的目标呢? 这里我们考虑业务字段做主键。

表数据如下:
在这里插入图片描述
在这个表里,哪个字段比较合适呢?

  • 选择卡号(cardno)

会员卡号(cardno)看起来比较合适,因为会员卡号不能为空,而且有唯一性,可以用来 标识一条会员记录

mysql> CREATE TABLE demo.membermaster
-> (
-> cardno CHAR(8) PRIMARY KEY, -- 会员卡号为主键
-> membername TEXT,
-> memberphone TEXT,
-> memberpid TEXT,
-> memberaddress TEXT,
-> sex TEXT,
-> birthday DATETIME
-> );
Query OK, 0 rows affected (0.06 sec)

不同的会员卡号对应不同的会员,字段“cardno”唯一地标识某一个会员。如果都是这样,会员卡号与会员一一对应,系统是可以正常运行的。

但实际情况是, 会员卡号可能存在重复使用 的情况。比如,张三因为工作变动搬离了原来的地址,不再到商家的门店消费了 (退还了会员卡),于是张三就不再是这个商家门店的会员了。但是,商家不想让这个会 员卡空着,就把卡号是“10000001”的会员卡发给了王五。

从系统设计的角度看,这个变化只是修改了会员信息表中的卡号是“10000001”这个会员 信息,并不会影响到数据一致性。也就是说,修改会员卡号是“10000001”的会员信息, 系统的各个模块,都会获取到修改后的会员信息,不会出现“有的模块获取到修改之前的会员信息,有的模块获取到修改后的会员信息,而导致系统内部数据不一致”的情况。因此,从信息系统层面上看是没问题的。

但是从使用 系统的业务层面 来看,就有很大的问题 了,会对商家造成影响。

比如,我们有一个销售流水表(trans),记录了所有的销售流水明细。2020 年 12 月 01 日,张三在门店购买了一本书,消费了 89 元。那么,系统中就有了张三买书的流水记录,如下所示:
在这里插入图片描述
接着,我们查询一下 2020 年 12 月 01 日的会员销售记录:

mysql> SELECT b.membername,c.goodsname,a.quantity,a.salesvalue,a.transdate
-> FROM demo.trans AS a
-> JOIN demo.membermaster AS b
-> JOIN demo.goodsmaster AS c
-> ON (a.cardno = b.cardno AND a.itemnumber=c.itemnumber);
+------------+-----------+----------+------------+---------------------+
| membername | goodsname | quantity | salesvalue | transdate           |
+------------+-----------+----------+------------+---------------------+
| 张三        || 1.000    | 89.00      | 2020-12-01 00:00:00 |
+------------+-----------+----------+------------+---------------------+
1 row in set (0.00 sec)

如果会员卡“10000001”又发给了王五,我们会更改会员信息表。导致查询时:

mysql> SELECT b.membername,c.goodsname,a.quantity,a.salesvalue,a.transdate
-> FROM demo.trans AS a
-> JOIN demo.membermaster AS b
-> JOIN demo.goodsmaster AS c
-> ON (a.cardno = b.cardno AND a.itemnumber=c.itemnumber);
+------------+-----------+----------+------------+---------------------+
| membername | goodsname | quantity | salesvalue | transdate |
+------------+-----------+----------+------------+---------------------+
| 王五        || 1.000    | 89.00      | 2020-12-01 00:00:00 |
+------------+-----------+----------+------------+---------------------+
1 row in set (0.01 sec)

这次得到的结果是:王五在 2020 年 12 月 01 日,买了一本书,消费 89 元。显然是错误的!结论:千万不能把会员卡号当做主键。

  • 选择会员电话 或 身份证号

会员电话可以做主键吗?不行的。在实际操作中,手机号也存在被运营商收回,重新发给别人用的情况。

那身份证号行不行呢?好像可以。因为身份证决不会重复,身份证号与一个人存在一一对 应的关系。可问题是,身份证号属于 个人隐私 ,顾客不一定愿意给你。要是强制要求会员必须登记身份证号,会把很多客人赶跑的。其实,客户电话也有这个问题,这也是我们在设计会员信息表的时候,允许身份证号和电话都为空的原因。

所以,建议尽量不要用跟业务有关的字段做主键。毕竟,作为项目设计的技术人员,我们谁也无法预测在项目的整个生命周期中,哪个业务字段会因为项目的业务需求而有重复,或者重用之类的情况出现。

经验:

  • 刚开始使用 MySQL 时,很多人都很容易犯的错误是喜欢用业务字段做主键,想当然地认为了解业务需求,但实际情况往往出乎意料,而更改主键设置的成本非常高。

6.3 淘宝的主键设计

在淘宝的电商业务中,订单服务是一个核心业务。请问, 订单表的主键 淘宝是如何设计的呢?是自增ID吗?

打开淘宝,看一下订单信息:
在这里插入图片描述
从上图可以发现,订单号不是自增ID!我们详细看下上述4个订单号:

1550672064762308113
1481195847180308113
1431156171142308113
1431146631521308113

订单号是19位的长度,且订单的最后5位都是一样的,都是08113。且订单号的前面14位部分是单调递增的。

大胆猜测,淘宝的订单ID设计应该是:

订单ID = 时间 + 去重字段 + 用户ID后6位尾号

这样的设计能做到全局唯一,且对分布式系统查询及其友好。

6.4 推荐的主键设计

非核心业务 :对应表的主键自增ID,如警告、日志、监控等信息。

核心业务主键设计至少应该是全局唯一且是单调递增。全局唯一保证在各系统之间都是唯一的,单调递增是希望插入时不影响数据库性能防止出现页分裂。

这里推荐最简单的一种主键设计:UUID。

UUID的特点:

全局唯一,占用36字节,数据无序,插入性能差。

认识UUID:

  • 为什么UUID是全局唯一的?
  • 为什么UUID占用36个字节?
  • 为什么UUID是无序的?

MySQL数据库的UUID组成如下所示:

UUID = 时间+UUID版本(16字节)- 时钟序列(4字节) - MAC地址(12字节)

我们以UUID值e0ea12d4-6473-11eb-943c-00155dbaa39d举例:

在这里插入图片描述
为什么UUID是全局唯一的?

在UUID中时间部分占用60位,存储的类似TIMESTAMP的时间戳,但表示的是从1582-10-15 00:00:00.00到现在的100ns的计数。可以看到UUID存储的时间精度比TIMESTAMPE更高,时间维度发生重复的概率降低到1/100ns。

时钟序列是为了避免时钟被回拨导致产生时间重复的可能性。MAC地址用于全局唯一。

为什么UUID占用36个字节?

UUID根据字符串进行存储,设计时还带有无用"-"字符串,因此总共需要36个字节。

为什么UUID是随机无序的呢?

因为UUID的设计中,将时间低位放在最前面(eg:时分秒在前,时分秒是随时发生变化的),而这部分的数据是一直在变化的,并且是无序。

改造UUID

若将时间高低位互换(eg:把年月日放在前面,年月日是递增的),则时间就是单调递增的了,也就变得单调递增了。MySQL 8.0可以更换时间低位和时间高位的存储方式,这样UUID就是有序的UUID了。

MySQL 8.0还解决了UUID存在的空间占用的问题,除去了UUID字符串中无意义的"-"字符串,并且将字符串用二进制类型保存,这样存储空间降低为了16字节。

可以通过MySQL8.0提供的uuid_to_bin函数实现上述功能,同样的,MySQL也提供了bin_to_uuid函数进行转化:

SET @uuid = UUID();

SELECT @uuid,uuid_to_bin(@uuid),uuid_to_bin(@uuid,TRUE);

在这里插入图片描述

通过函数uuid_to_bin(@uuid,true)将UUID转化为有序UUID了。全局唯一 + 单调递增,这不就是我们想要的主键!

有序UUID性能测试

16字节的有序UUID,相比之前8字节的自增ID,性能和存储空间对比究竟如何呢?

我们来做一个测试,插入1亿条数据,每条数据占用500字节,含有3个二级索引,最终的结果如下所示:

在这里插入图片描述
从上图可以看到插入1亿条数据有序UUID是最快的,而且在实际业务使用中有序UUID在业务端就可以生成。还可以进一步减少SQL的交互次数。

另外,虽然有序UUID相比自增ID多了8个字节,但实际只增大了3G的存储空间,还可以接受。

  • 在当今的互联网环境中,非常不推荐自增ID作为主键的数据库设计。更推荐类似有序UUID的全局唯一的实现。
  • 另外在真实的业务系统中,主键还可以加入业务和系统属性,如用户的尾号,机房的信息等。这样的主键设计就更为考验架构师的水平了。

如果不是MySQL8.0 肿么办?

手动赋值字段做主键!

比如,设计各个分店的会员表的主键,因为如果每台机器各自产生的数据需要合并,就可能会出现主键重复的问题。

可以在总部 MySQL 数据库中,有一个管理信息表,在这个表中添加一个字段,专门用来记录当前会员编号的最大值。

门店在添加会员的时候,先到总部 MySQL 数据库中获取这个最大值,在这个基础上加 1,然后用这个值作为新会员的“id”,同时,更新总部 MySQL 数据库管理信息表中的当前会员编号的最大值。

这样一来,各个门店添加会员的时候,都对同一个总部 MySQL 数据库中的数据表字段进行操作,就解决了各门店添加会员时会员编号冲突的问题。

7. 事务基础知识

7.1 数据库事务概述

事务是数据库区别于文件系统的重要特性之一,当我们有了事务就会让数据库始终保持一致性,同时我们还能通过事务的机制恢复到某个时间点,这样可以保证已提交到数据库的修改不会因为系统崩溃而丢失。

7.1.1 存储引擎支持情况

SHOW ENGINES命令来查看当前 MySQL 支持的存储引擎都有哪些,以及这些存储引擎是否支持事务。
在这里插入图片描述

能看出在 MySQL 中,只有InnoDB 是支持事务的。

7.1.2 基本概念

事务:一组逻辑操作单元,使数据从一种状态变换到另一种状态。

事务处理的原则:保证所有事务都作为一个工作单元来执行,即使出现了故障,都不能改变这种执行方式。当在一个事务中执行多个操作时,要么所有的事务都被提交( commit ),那么这些修改就 永久 地保存下来;要么数据库管理系统将 放弃 所作的所有 修改 ,整个事务回滚( rollback )到最初状态。

#案例:AA用户给BB用户转账100
update account set money = money - 100 where name = 'AA';

#服务器宕机
update account set money = money + 100 where name = 'BB';

7.1.3 事务的ACID特性

  • 原子性(atomicity):

    • 原子性是指事务是一个不可分割的工作单位,要么全部提交,要么全部失败回滚。即要么转账成功,要么转账失败,是不存在中间的状态。如果无法保证原子性会怎么样?就会出现数据不一致的情形,A账户减去100元,而B账户增加100元操作失败,系统将无故丢失100元。
  • 一致性(consistency):

    • (国内很多网站上对一致性的阐述有误,具体你可以参考 Wikipedia 对Consistency 的阐述)
    • 根据定义,一致性是指事务执行前后,数据从一个 合法性状态 变换到另外一个 合法性状态 。这种状态是 语义上 的而不是语法上的,跟具体的业务有关。
    • 那什么是合法的数据状态呢?满足 预定的约束 的状态就叫做合法的状态。通俗一点,这状态是由你自己来定义的(比如满足现实世界中的约束)。满足这个状态,数据就是一致的,不满足这个状态,数据就是不一致的!如果事务中的某个操作失败了,系统就会自动撤销当前正在执行的事务,返回到事务操作之前的状态。
    • 举例1:A账户有200元,转账300元出去,此时A账户余额为-100元。你自然就发现了此时数据是不一致的,为什么呢? 因为你定义了一个状态,余额这列必须>=0。
    • 举例2:A账户200元,转账50元给B账户,A账户的钱扣了,但是B账户因为各种意外,余额并没有增加。你也知道此时数据是不一致的,为什么呢? 因为你定义了一个状态,要求A+B的总余额必须不变。
    • 举例3:在教据表中我们将姓名字段设置为唯一性约束,这时当事务进行提交或者事务发生回滚的时候,如果数据表中的姓名不唯一,就破坏了事务的一致性要求。
  • 隔离型(isolation):

    • 事务的隔离性是指一个事务的执行 不能被其他事务干扰 ,即一个事务内部的操作及使用的数据对 并发 的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
    • 如果无法保证隔离性会怎么样?假设A账户有200元,B账户0元。A账户往B账户转账两次,每次金额为50 元,分别在两个事务中执行。如果无法保证隔离性,会出现下面的情形:
UPDATE accounts SET money = money - 50 WHERE NAME = 'AA';

UPDATE accounts SET money = money + 50 WHERE NAME = 'BB';

在这里插入图片描述

  • 持久性(durability)
    • 持久性是指一个事务一旦被提交,它对数据库中数据的改变就是 永久性的 ,接下来的其他操作和数据库故障不应该对其有任何影响。
    • 持久性是通过事务日志来保证的。日志包括了重做日志回滚日志 。当我们通过事务对数据进行修改的时候,首先会将数据库的变化信息记录到重做日志中,然后再对数据库中对应的行进行修改。这样做的好处是,即使数据库系统崩溃,数据库重启后也能找到没有更新到数据库系统中的重做日志,重新执行,从而使事务具有持久性。
    • 总结
      • ACID是事务的四大特性,在这四个特性中,原子性是基础,隔离性是手段,一致性是约束条件,而持久性是我们的目的。
      • 数据库事务,其实就是数据库设计者为了方便起见,把需要保证原子性、隔离性、一致性持久性的一个或多个数据库操作称为一个事务。

7.1.4 事务的状态

我们现在知道 事务 是一个抽象的概念,它其实对应着一个或多个数据库操作,MySQL根据这些操作所执行的不同阶段把 事务 大致划分成几个状态:

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

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

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

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

举例:

UPDATE accounts SET money = money - 50 WHERE NAME = 'AA';

UPDATE accounts SET money = money + 50 WHERE NAME = 'BB';
  • 提交的(committed)
    当一个处在 部分提交的 状态的事务将修改过的数据都 同步到磁盘 上之后,我们就可以说该事务处在了提交的状态。

一个基本的状态转换图如下所示:

在这里插入图片描述

图中可见,只有当事务处于提交的或者中止的状态时,一个事务的生命周期才算是结束了。对于已经提交的事务来说,该事务对数据库所做的修改将永久生效,对于处于中止状态的事务,该事务对数据库所做的所有修改都会被回滚到没执行该事务之前的状态。

7.2 如何使用事务

使用事务有两种方式,分别为显式事务隐式事务

7.2.1 显式事务

步骤1 START TRANSACTION或者BEGIN,作用是显式开启一个事务。

mysql> BEGIN;
#或者
mysql> START TRANSACTION;

START TRANSACTION 语句相较于BEGIN特别之处在于,后边能跟随几个修饰符

READ ONLY :标识当前事务是一个 只读事务 ,也就是属于该事务的数据库操作只能读取数据,而不能修改数据。

补充:只读事务中只是不允许修改那些其他事务也能访问到的表中的数据,对于临时表来说(我们使用CREATE TMEPORARY TABLE创建的表),由于它们只能在当前会话中可见,所以只读事务其实也是可以对临时表进行增、删、改操作的。

READ WRITE :标识当前事务是一个 读写事务 ,也就是属于该事务的数据库操作既可以读取数据, 也可以修改数据。(默认)

WITH CONSISTENT SNAPSHOT:启动一致性读。

要么是①和③,要么是②和③,不能是①和②(读和读写)搭配这样就没有意义了。

比如:

START TRANSACTION READ ONLY; #开启一个只读事务

START TRANSACTION READ ONLY, WITH CONSISTENT SNAPSHOT; #开启只读事务和一致性读

START TRANSACTION READ WRITE,WITH CONSISTENT SNAPSHOT; #开启读写事务和一致性读

注意:

  • READ ONLYREAD WRITE是用来设置所谓的事务访问模式的,就是以只读还是读写的方式来访问数据库中的数据,一个事务的访问模式不能同时既设置为只读的也设置为读写的,所以不能同时把READ ONLYREAD WRITE 放到START TRANSACTION语句后边。
  • 如果我们不显式指定事务的访问模式,那么该事务的访问模式就是读写模式。

步骤2:一系列事务中的操作(主要是DML,不含DDL)

步骤3:提交事务 或 中止事务(即回滚事务)

# 提交事务。当提交事务后,对数据库的修改是永久性的。
mysql> COMMIT;

# 回滚事务。即撤销正在进行的所有没有提交的修改
mysql> ROLLBACK;

# 将事务回滚到某个保存点。
mysql> ROLLBACK TO [SAVEPOINT]

其中关于SAVEPOINT相关操作有:

#在事务中创建保存点,方便后续针对保存点进行回滚。一个事务中可以存在多个保存点。
SAVEPOINT 保存点名称;

#删除某个保存点。
RELEASE SAVEPOINT 保存点名称;

保存点: 如果开启一个事务,已经写了很多语句,突然发现执行完上一语句时发现语句写错了。你只好使用rollback语句让数据库恢复到事务开启之前的状态。以前写完的语句也需要重新执行。所以mysql数据库的作者提出了保存点(savepoint)的概念。开启事务后在执行语句后面打几个点,我们调用rollback语句就会回滚到指定的点。而不是回到事务执行之前的样子。

7.2.2 隐式事务

MySQL中有一个系统变量 autocommit

mysql> SHOW VARIABLES LIKE 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.01 sec)

默认情况下,如果我们不显式的使用START TRANSACTION或者BEGIN语句开启一个事务,那么每一条语句都算是一个独立的事务,这种特性称之为事务的自动提交。也就是说,不以START TRANSACTION或者BEGIN语句显式的开启一个事务,那么下边这两条语句就相当于放到两个独立的事务中去执行:

UPDATE account SET balance = balance - 10 WHERE id = 1;

UPDATE account SET balance = balance + 10 WHERE id = 2;

当然,如果我们想关闭这种自动提交的功能,可以使用下边两种方法之一:

  • 显式的的使用 START TRANSACTION 或者 BEGIN 语句开启一个事务。这样在本次事务提交或者回滚前会暂时关闭掉自动提交的功能。
  • 把系统变量autocommit的值设置为 OFF,就像这样:
    • 针对于DML操作是有效的,对DDL操作是无效的。
SET autocommit = OFF;
#或
SET autocommit = 0;

这样的话,我们写入的多条语句就算是属于同一个事务了,直到我们显式的写出 CONMIT语句来把这个事务提交掉,或者显式的写出ROLLBACK语句来把这个事务回滚掉。

补充: Oracle默认不自动提交,需要手写COMMIT命令,而 MySQL默认自动提交。

7.2.3 隐式提交数据的情况

一下操作都会自动提交事务,不受autocommit 属性的控制。

  • 数据定义语言(Data definition language,缩写为:DDL)

数据库对象,指的就是数据库、表、视图、存储过程等结构。当我们使用CREATE、ALTER、DROP等语句去修改数据库对象时,就会隐式的提交前边语句所属于的事务。即:

BEGIN;

SELECT ...   #事务中的一条语句
UPDATE ...   #事务中的一条语句
...          #事务中的其它语句

CREATE TABLE ...  #此语句会隐式的提交前边语句所属于的事务

  • 隐式使用或修改mysql数据库中的表

当我们使用ALTER USER、CREATE USER、DROP USER、GRANT、RENAME USER、 REVOKE、SET PASSWORD等语句时也会隐式的提交前边语句所属于的事务。

  • 事务控制或关于锁定的语句

① 当我们在一个事务还没提交或者回滚时就又使用 START TRANSACTION 或者 BEGIN 语句开启了另一个事务时,会隐式的提交一个事务。即:

BEGIN;

SELECT ...   #事务中的一条语句
UPDATE ...   #事务中的一条语句
...          #事务中的其它语句

BEGIN;       #此语句会隐式的提交前边语句所属于的事务

② 当前的 autocommit 系统变量的值为OFF,我们手动把它调为ON时,也会隐式的提交前边语句所属的事务。

③ 使用 LOCK TABLES 、 UNLOCK TABLES 等关于锁定的语句也会 隐式的提交 前边语句所属的事务。

  • 加载数据的语句

使用LOAD DATA语句来批量往数据库中导入数据时,也会隐式的提交前边语句所属的事务。

  • 关于MySQL复制的一些语句

使用START SLAVE、STOP SLAVE、RESET SLAVE、CHANGE MASTER TO等语句时会隐式的提交前边语句所属的事务。

  • 其它的一些语句

使用ANALYZE TABLE、CACHE INDEX、CHECK TABLE、FLUSH、LOAD INDEX INTO CACHE ,OPTIMIZE TABLE、REPAIR TABLE、RESET等语句也会隐式的提交前边语句所属的事务。

7.2.4 使用举例1:提交与回滚

我们看下在 MySQL 的默认状态下,下面这个事务最后的处理结果是什么。

autocommit 事务自动提交开关,此时是打开状态。

情况1:

CREATE TABLE user(
	name varchar(20), 
	PRIMARY KEY (name)
) ENGINE=InnoDB;

BEGIN;
INSERT INTO user SELECT '张三'; #此时不会自动提交数据
COMMIT;

BEGIN; #开启一个新的事务
INSERT INTO user SELECT '李四'; #此时不会自动提交数据
INSERT INTO user SELECT '李四'; #受主键的影响,不能添加成功
ROLLBACK;

SELECT * FROM user;

运行结果(1 行数据):

mysql> commit;
Query OK, 0 rows affected (0.00)

mysql> BEGIN;
Query OK, 0 rows affected (0.00)

mysql> INSERT INTO user SELECT '李四';
Query OK, 1 rows affected (0.00)

mysql> INSERT INTO user SELECT '李四';
Duplicate entry '李四' for key 'user.PRIMARY'

mysql> ROLLBACK;
Query OK, 0 rows affected (0.01)

mysql> select * from user;
+--------+
| name   |
+--------+
| 张三    |
+--------+
1 行于数据集 (0.01)

情况2:

#DDL操作会自动提交数据,不受autocommit变量的影响。
CREATE TABLE user (
	name varchar(20), 
	PRIMARY KEY (name)
) ENGINE=InnoDB;

BEGIN;
INSERT INTO user SELECT '张三';#此时不会自动提交数据
COMMIT;

#没有写BEGIN
INSERT INTO user SELECT '李四';# 默认情况下(即autocommit为true),DML操作也会自动提交数据。
INSERT INTO user SELECT '李四';#事务的失败的状态
#回滚到最近的一次commit之后,所以此时有2条数据
ROLLBACK;

运行结果(2 行数据):

mysql> SELECT * FROM user;
+--------+
| name   |
+--------+
| 张三    |
| 李四    |
+--------+
2 行于数据集 (0.01)

情况3:

CREATE TABLE user(
	name varchar(255),
	PRIMARY KEY (name)
) ENGINE=InnoDB;

#开启链式,默认没有开启
SET @@completion_type = 1;
BEGIN;
INSERT INTO user SELECT '张三';
COMMIT;

INSERT INTO user SELECT '李四';
INSERT INTO user SELECT '李四';
ROLLBACK;

#此时只有一个数据
SELECT * FROM user;

运行结果(1 行数据):

mysql> SELECT * FROM user;
+--------+
| name   |
+--------+
| 张三    |
+--------+
1 行于数据集 (0.01)

你能看到相同的SQL代码,只是在事务开始之前设置了SET @@completion_type = 1;,结果就和我们第一次处理的一样,只有一个“张三”。这是为什么呢?

这里我讲解下MySQL 中completion_type参数的作用,实际上这个参数有3种可能:

  1. completion=0,这是默认情况。当我们执行COMMIT的时候会提交事务,在执行下一个事务时,还需要使用START TRANSACTION 或者 BEGIN 来开启。
  2. completion=1,这种情况下,当我们提交事务后,相当于执行了COMMIT AND CHAIN,也就是开启一个链式事务,即当我们提交事务之后会开启一个相同隔离级别的事务。
  3. completion=2,这种情况下CONNIT=COMMIT AND RELEASE,也就是当我们提交后,会自动与服务器断开连接。
  • 当我们设置 autocommit=0 时,不论是否采用 START TRANSACTION 或者 BEGIN 的方式来开启事务,都需要用 COMMIT 进行提交,让事务生效,使用 ROLLBACK 对事务进行回滚。
  • 当我们设置 autocommit=1 时,每条 SQL 语句都会自动进行提交。
  • 不过这时,如果你采用 STARTTRANSACTION 或者 BEGIN 的方式来显式地开启事务,那么这个事务只有在 COMMIT 时才会生效,在 ROLLBACK 时才会回滚。

7.2.5 使用举例2:体会INNODB 和 MyISAM

CREATE TABLE test1(i INT) ENGINE = INNODB;

#针对于innodb表
BEGIN
INSERT INTO test1 VALUES (1);
ROLLBACK;

#回滚之后没有数据
SELECT * FROM test1;

在这里插入图片描述

CREATE TABLE test2(i INT) ENGINE = MYISAM;

#针对于myisam表:不支持事务
BEGIN
INSERT INTO test2 VALUES (1);
ROLLBACK;

#仍然有数据
SELECT * FROM test2;

在这里插入图片描述

7.2.6 使用举例3:体会SAVEPOINT

CREATE TABLE user3(NAME VARCHAR(15),balance DECIMAL(10,2));

BEGIN
INSERT INTO user3(NAME,balance) VALUES('张三',1000);
COMMIT;

SELECT * FROM user3;


BEGIN;
UPDATE user3 SET balance = balance - 100 WHERE NAME = '张三';#900

UPDATE user3 SET balance = balance - 100 WHERE NAME = '张三';#800

SAVEPOINT s1;#设置保存点

#本来是想要在减去100,此时一不小心写错了写成加1了,可以在上面设置保存点
UPDATE user3 SET balance = balance + 1 WHERE NAME = '张三';#801

ROLLBACK TO s1; #回滚到保存点 800

#显示800,但是回滚到保存点并不是事务的终止状态
#事务终止状态:提交或者回滚
SELECT * FROM user3;

ROLLBACK; #回滚操作 1000

#显示1000
SELECT * FROM user3;

7.3 事务隔离级别

MySQL是一个客户端/服务器架构的软件,对于同一个服务器来说,可以有若干个客户端与之连接,每个客户端与服务器连接上之后,就可以称为一个会话(Session)。每个客户端都可以在自己的会话中向服务器发出请求语句,一个请求语句可能是某个事务的一部分,也就是对于服务器来说可能同时处理 多个事务。事务有 隔离性 的特性,理论上在某个事务 对某个数据进行访问时,其他事务应该进排队 ,当该事务提交之后,其他事务才可以继续访问这个数据。但是这样对 性能影响太大,我们既想保持事务的隔离性,又想让服务器在处理访问同一数据的多个事务时性能尽量高些,那就看二者如何权衡取舍了。

7.3.1 数据准备

我们需要创建一个表:

CREATE TABLE student (
	studentno INT,
	name VARCHAR(20),
	class varchar(20),
	PRIMARY KEY (studentno)
) Engine=InnoDB CHARSET=utf8;

然后向这个表里插入一条数据:

INSERT INTO student VALUES(1, '小谷', '1班');

现在表里的数据就是这样的:

mysql> select * from student;
+-----------+--------+-------+
| studentno | name | class   |
+-----------+--------+-------+
| 1         | 小谷 |  1|
+-----------+--------+-------+
1 row in set (0.00 sec)

7.3.2 数据并发问题

针对事务的隔离性和并发性,我们怎么做取舍呢?先看一下访问相同数据的事务在 不保证串行执行(也就是执行完一个再执行另一个)的情况下可能会出现哪些问题:

1. 脏写( Dirty Write )

对于两个事务 Session A、Session B,如果事务Session A修改了另一个 未提交事务Session B 修改过的数据,那就意味着发生了脏写
在这里插入图片描述
Session A和Session B各开启了一个事务,Session B中的事务先将studentno列为1的记录的name列更新为’李四’,然后Session A中的事务接着又把这条studentno列为1的记录的name列更新为’张三’。如果之后Session B中的事务进行了回滚,那么Session A中的更新也将不复存在,这种现象就称之为脏写。这时Session A中的事务就没有效果了,明明把数据更新了,最后也提交事务了,最后看到的数据什么变化也没有。这里大家对事务的隔离级比较了解的话,会发现默认隔离级别下,上面SessionA中的更新语句会处于等待状态,这里只是跟大家说明一下会出现这样现象。

2. 脏读( Dirty Read )

对于两个事务 Session A、Session B,Session A读取了已经被 Session B 更新 但还 没有被提交 的字段。之后若 Session B 回滚 ,Session A 读取 的内容就是 临时且无效 的。
在这里插入图片描述
Session A和Session B各开启了一个事务,Session B中的事务先将studentno列为1的记录的name列更新为’张三’,然后Session A中的事务再去查询这条studentno为1的记录,如果读到列name的值为’张三’,而Session B中的事务稍后进行了回滚,那么Session A中的事务相当于读到了一个不存在的数据,这种现象就称之为脏读

3. 不可重复读( Non-Repeatable Read )

对于两个事务Session A、Session B,Session A 读取了一个字段,然后 Session B 更新了该字段。 之后Session A 再次读取 同一个字段,值就不同了。那就意味着发生了不可重复读。
在这里插入图片描述

我们在Session B中提交了几个 隐式事务 (注意是隐式事务,意味着语句结束事务就提交了),这些事务都修改了studentno列为1的记录的列name的值,每次事务提交之后,如果Session A中的事务都可以查看到最新的值,这种现象也被称之为 不可重复读

4. 幻读( Phantom )

对于两个事务Session A、Session B, Session A 从一个表中 读取 了一个字段, 然后 Session B 在该表中 插入 了一些新的行。 之后, 如果 Session A 再次读取 同一个表, 就会多出几行。那就意味着发生了幻读。
在这里插入图片描述

Session A中的事务先根据条件 studentno > 0这个条件查询表student,得到了name列值为’张三’的记录; 之后Session B中提交了一个 隐式事务 ,该事务向表student中插入了一条新记录;之后Session A中的事务再根据相同的条件 studentno > 0查询表student,得到的结果集中包含Session B中的事务新插入的那条记录,这种现象也被称之为 幻读 。我们把新插入的那些记录称之为 幻影记录

注意1:

有的同学会有疑问,那如果Session B中删除了一些符合studentno > 0的记录而不是插入新记录,那Session A之后再根据studentno > 0的条件读取的记录变少了,这种现象算不算幻读呢? 这种现象不属于幻读,幻读强调的是一个事务按照某个相同条件多次读取记录时,后读取时读到了之前没有读到的记录

注意2:

那对于先前已经读到的记录,之后又读取不到这种情况,算啥呢? 这相当于对每一条记录都发生了不可重复读的现象。幻读只是重点强调了读取到了之前读取没有获取到的记录。

7.3.3 SQL中的四种隔离级别

上面介绍了几种并发事务执行过程中可能遇到的一些问题,这些问题有轻重缓急之分,我们给这些问题 按照严重性来排一下序:

脏写 > 脏读 > 不可重复读 > 幻读

我们愿意舍弃一部分隔离性来换取一部分性能在这里就体现在:设立一些隔离级别,隔离级别越低,并 发问题发生的就越多。 SQL标准 中设立了4个 隔离级别

  • READ UNCOMMITTED读未提交,在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。不能避免脏读、不可重复读、幻读。
  • READ COMMITTED 读已提交,它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。可以避免脏读,但不可 重复读、幻读问题仍然存在。
  • REPEATABLE READ可重复读,事务A在读到一条数据之后,此时事务B对该数据进行了修改并提交,那么事务A再读该数据,读到的还是原来的内容。可以避免脏读、不可重复读,但幻读问题仍 然存在。这是MySQL的默认隔离级别。
  • SERIALIZABLE可串行化,确保事务可以从一个表中读取相同的行。在这个事务持续期间,禁止其他事务对该表执行插入、更新和删除操作。所有的并发问题都可以避免,但性能十分低下。能避 免脏读、不可重复读和幻读。

SQL标准 中规定,针对不同的隔离级别,并发事务可以发生不同严重程度的问题,具体情况如下:

在这里插入图片描述

脏写怎么没涉及到?因为脏写这个问题太严重了,不论是哪种隔离级别,都不允许脏写的情况发生。

不同的隔离级别有不同的现象,并有不同的锁和并发机制,隔离级别越高,数据库的并发性能就越差,4 种事务隔离级别与并发性能的关系如下:

在这里插入图片描述

7.3.4 MySQL支持的四种隔离级别

不同的数据库厂商对sQL标准中规定的四种隔离级别支持不一样。比如,Oracle就只支持READ COMMITTED(默认隔离级别SERIALIZABLE隔离级别。MySQL虽然支持4种隔离级别,但与SQL标准中所规定的各级隔离级别允许发生的问题却有些出入,MySQL在REPEATABLE READ隔离级别下,是可以禁止幻读问题的发生的,禁止幻读的原因我们在第16章讲解。

MySQL的默认隔离级别为REPEATABLE READ,我们可以手动修改一下事务的隔离级别。

# 查看隔离级别,MySQL 5.7.20的版本之前:
mysql> SHOW VARIABLES LIKE 'tx_isolation';
+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| tx_isolation  | REPEATABLE-READ |
+---------------+-----------------+
1 row in set (0.00 sec)

# MySQL 5.7.20版本之后,引入transaction_isolation来替换tx_isolation

# 查看隔离级别,MySQL 5.7.20的版本及之后:
mysql> SHOW VARIABLES LIKE 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name         |    Value        |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set (0.02 sec)

#或者不同MySQL版本中都可以使用的:
SELECT @@transaction_isolation;

7.3.5 如何设置事务的隔离级别

通过下面的语句修改事务的隔离级别:

SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL 隔离级别;

#其中,隔离级别格式:
> READ UNCOMMITTED
> READ COMMITTED
> REPEATABLE READ
> SERIALIZABLE

或者:

SET [GLOBAL|SESSION] TRANSACTION_ISOLATION = '隔离级别'

#其中,隔离级别格式:
> READ-UNCOMMITTED
> READ-COMMITTED
> REPEATABLE-READ
> SERIALIZABLE

关于设置时使用GLOBAL或SESSION的影响:

  • 使用GLOBAL关键字(在全局范围影响):
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
#或
SET GLOBAL TRANSACTION_ISOLATION = 'SERIALIZABLE';
  • 则:

    • 当前已经存在的会话无效

    • 只对执行完该语句之后产生的会话起作用

  • 使用 SESSION 关键字(在会话范围影响):

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
#或
SET SESSION TRANSACTION_ISOLATION = 'SERIALIZABLE';
  • 则:
    • 对当前会话的所有后续的事务有效
    • 如果在事务之间执行,则对后续的事务有效
    • 该语句可以在已经开启的事务中间执行,但不会影响当前正在执行的事务

注意:

  • 以上这些修改都是针对内存层面的,如果重启mysql又会恢复到默认的隔离级别。
  • 如果在服务器启动时想改变事务的默认隔离级别,可以在配置文件中修改启动参数transaction_isolation的值。比如,在启动服务器时指定了transaction_isolation=SERIALIZABLE,那么事务的默认隔离级别就从原来的
    REPEATABLE-READ变成了SERIALIZABLE

小结:

  • 数据库规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。

7.3.6 不同隔离级别举例

初始化数据:

CREATE TABLE account(
  id INT PRIMARY KEY AUTO_INCREMENT,
  NAME VARCHAR(15),
  balance DECIMAL(10,2)
);

INSERT INTO account VALUES (1,'张三','108'),(2,'李四','0');

表中的数据如下:

select * from account ;

在这里插入图片描述

演示1:读未提交之脏读

设置隔离级别为未提交读:
在这里插入图片描述
脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。

再举一个严重的例子,证明一下危害。表中的数据如下:

mysql> select * from account;
+----+--------+----------+
| id |  name  | balance  |
+----+--------+----------+
| 1  |  张三   | 100      |
| 2  |  李四   | 0        |
+----+--------+----------+
2 行于数据集(0.01 秒)

事务1和事务2的执行流程如下:

在这里插入图片描述
执行完成,数据库中的数据如下:

mysql> select * from account;
+----+--------+----------+
| id |  name  | balance  |
+----+--------+----------+
| 1  |  张三   | 100      |
| 2  |  李四   | -100     |
+----+--------+----------+
2 行于数据集(0.01 秒)

这样的情况对于实际业务来说是极其不合理的。

演示2:读已提交

表中的数据如下:

mysql> select * from account;
+----+--------+----------+
| id |  name  | balance  |
+----+--------+----------+
| 1  |  张三   | 100      |
| 2  |  李四   |  0       |
+----+--------+----------+
2 行于数据集(0.01 秒)

设置隔离级别为读已提交,事务的执行流程如下:

在这里插入图片描述
不可重复读是指在事务1内,读取了一个数据,事务1还没有结束时,事务2也访问了这个数据,修改了这个数据,并提交。紧接着,事务1又读这个数据。由于事务2的修改,那么事务1两次读到的的数据可能是不一样的,因此称为是不可重复读。

可以看到在T2时间段事务2修改完id=2的账户余额但没有commit的时候,在事务1查询id=2的账户余额,发现账户余额为0,可以证明提交读这个隔离级别不会发生脏读。

演示3:可重复读

表中的数据如下:

mysql> select * from account;
+----+--------+----------+
| id |  name  | balance  |
+----+--------+----------+
| 1  |  张三   | 100      |
| 2  |  李四   |  0       |
+----+--------+----------+
2 行于数据集(0.01 秒)

设置隔离级别为可重复读,事务的执行流程如下:
在这里插入图片描述
当我们将当前会话的隔离级别设置为可重复读的时候,当前会话可以重复读,就是每次读取的结果集都相同,而不管其他事务有没有提交。但是在可重复读的隔离级别上,会产生幻读的问题。

演示4:幻读

表中的数据如下:

mysql> select * from account;
+----+--------+----------+
| id |  name  | balance  |
+----+--------+----------+
| 1  |  张三   | 100      |
| 2  |  李四   |  0       |
+----+--------+----------+
2 行于数据集(0.01 秒)

在这里插入图片描述
这里要灵活的理解读取的意思,第一次select是读取,第二次的insert其实也属于隐式的读取,只不过是在mysql的机制中读取的,插入数据也是要先读取一下有没有主键冲突才能决定是否执行插入。

幻读,并不是说两次读取获取的结果集不同,幻读侧重的方面是某一次的select 操作得到的结果所表征的数据状态无法支撑后续的业务操作。更为具体一些: select某记录是否存在,不存在,准备插入此记录,但执行insert时发现此记录已存在,无法插入,此时就发生了幻读。

在RR隔离级别下,step1、step2是会正常执行的,step3则会报错主键冲突,对于事务1的业务来说是执行失败的,这里事务1就是发生了幻读,因为事务1在step1中读取的数据状态并不能支撑后续的业务操作,事务1:“见鬼了,我刚才读到的结果应该可以支持我这样操作才对啊,为什么现在不可以”。事务1不敢相信的又执行了step4,发现和setp1读取的结果是一样的 (RR下的MVCC机制)。此时,幻读无疑已经发生,事务1无论读取多少次,都查不到id=3的记录,但它的确无法插入这条他通过读取来认定不存在的记录(此数据已被事务2插入),对于事务1来说,它幻读了。

其实RR也是可以避免幻读的,通过对select 操作手动加行X锁(独占锁) (SELECT…FOR UPDATE这也正是
SERIALIZABLE隔离级别下会隐式为你做的事情)。同时,即便当前记录不存在,比如id =3是不存在的,当前事务也会获得一把记录锁 (因为InnoDB的行锁锁定的是索引,故记录实体存在与否没关系,存在就加行X锁,不存在就加间隙锁),其他事务则无法插入此索引的记录,故杜绝了幻读。

SERIALIZABLE隔离级别下,step1执行时是会隐式的添加行(X)锁/gap(X)锁的,从而step2会被阻塞,step3 会正常执行,待事务1提交后,事务2才能继续执行(主键冲突执行失败),对于事务1来说业务是正确的,成功的阻塞扼杀了扰乱业务的事务2,对于事务1来说他前期读取的结果是可以支撑其后续业务的。

所以MysQL的幻读并非什么读取两次返回结果集不同,而是事务在插入事先检测不存在的记录时,惊奇的发现这些数据已经存在了,之前的检测读获取到的数据如同鬼影一般。

7.4 事务的常见分类

从事务理论的角度来看,可以把事务分为以下几种类型:

  • 扁平事务(Flat Transactions)
  • 带有保存点的扁平事务(Flat Transactions with Savepoints)
  • 链事务(Chained Transactions)
  • 嵌套事务(Nested Transactions)
  • 分布式事务(Distributed Transactions)

下面分别介绍这几种类型:

1)扁平事务是事务类型中最简单的一种,但是在实际生产环境中,这可能是使用最频繁的事务,在扁平事务中,所有操作都处于同一层次,其由BEGIN WORK开始,由COMMIT WORK或ROLLBACK WORK结束,其间的操作是原子的,要么都执行,要么都回滚,因此,扁平事务是应用程序成为原子操作的基本组成模块。扁平事务虽然简单,但是在实际环境中使用最为频繁,也正因为其简单,使用频繁,故每个数据库系统都实现了对扁平事务的支持。扁平事务的主要限制是不能提交或者回滚事务的某一部分,或分几个步骤提交。

扁平事务一般有三种不同的结果:①事务成功完成。在平常应用中约占所有事务的96%。②应用程序要求停止事务。比如应用程序在捕获到异常时会回滚事务,约占事务的3%。③外界因素强制终止事务。如连接超时或连接断开,约占所有事务的1%。

2)带有保存点的扁平事务除了支持扁平事务支持的操作外,还允许在事务执行过程中回滚到同一事务中较早的一个状态。这是因为某些事务可能在执行过程中出现的错误并不会导致所有的操作都无效,放弃整个事务不合乎要求,开销太大。

3)链事务是指一个事务由多个子事务链式组成,它可以被视为保存点模式的一个变种。带有保存点的扁平事务,当发生系统崩溃时,所有的保存点都将消失,这意味着当进行恢复时,事务需要从开始处重新执行,而不能从最近的一个保存点继续执行。链事务的思想是:在提交一个事务时,释放不需要的数据对象,将必要的处理上下文隐式地传给下一个要开始的事务,前一个子事务的提交操作和下一个子事务的开始操作合并成一个原子操作,这意味着下一个事务将看到上一个事务的结果,就好像在一个事务中进行一样。这样,在提交子事务时就可以释放不需要的数据对象,而不必等到整个事务完成后才释放。其工作方式如下:
在这里插入图片描述
链事务与带有保存点的扁平事务的不同之处体现在:

①带有保存点的扁平事务能回滚到任意正确的保存点,而链事务中的回滚仅限于当前事务,即只能恢复到最近的一个保存点。

②对于锁的处理,两者也不相同,链事务在执行COMMIT后即释放了当前所持有的锁,而带有保存点的扁平事务不影响迄今为止所持有的锁。

4)嵌套事务是一个层次结构框架,由一个顶层事务(Top-Level Transaction)控制着各个层次的事务,顶层事务之下嵌套的事务被称为子事务(Subtransaction),其控制着每一个局部的变换,子事务本身也可以是嵌套事务。因此,嵌套事务的层次结构可以看成是一棵树。

5)分布式事务通常是在一个分布式环境下运行的扁平事务,因此,需要根据数据所在位置访问网络中不同节点的数据库资源。例如,一个银行用户从招商银行的账户向工商银行的账户转账1000元,这里需要用到分布式事务,因为不能仅调用某一家银行的数据库就完成任务。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值