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
数据字典信息中就包含该表的表结构。
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 区别及特点
特点 | InnoDB | MyISAM | Memory |
---|---|---|---|
存储限制 | 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中所支持的所有的索引结构,接下来,我们再来看看不同的存储引擎对于索引结构的支持情况。
索引 | InnoDB | MyISAM | Memory |
---|---|---|---|
B+tree索引 | 支持 | 支持 | 支持 |
Hash 索引 | 不支持 | 不支持 | 支持 |
R-tree 索引 | 不支持 | 支持 | 不支持 |
Full-text | 5.6版本之后支持 | 支持 | 不支持 |
注意:
我们平常所说的索引,如果没有特别指明,都是指B+树结构组织的索引
。
2.2.2 二叉树
假如说MySQL的索引结构采用二叉树的数据结构,比较理想的结构如下:
特点:
-
每个结点最多有两颗子树,所以二叉树中不存在度大于2的结点。
-
左子树和右子树是有顺序的,次序不能任意颠倒。
-
即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。譬如:
如果主键是顺序插入的,则会形成一个单向链表,结构如下:
所以,如果选择二叉树作为索引结构,会存在以下缺点:
- 顺序插入时,会形成一个链表,查询性能大大降低。
- 大数据量情况下,层级较深,检索速度慢。
此时大家可能会想到,我们可以选择红黑树,红黑树是一颗自平衡二叉树,那这样即使是顺序插入数据,最终形成的数据结构也是一颗平衡的二叉树,结构如下:
使用红黑树解决第一个问题
但是,即使如此,由于红黑树也是一颗二叉树,所以也会存在一个缺点:
- 大数据量情况下,层级较深,检索速度慢。
所以,在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 个指针:
知识小贴士:
树的度数指的是一个节点的子节点个数
。
我们可以通过一个数据结构可视化的网站来简单演示一下。 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 索引分类
在MySQL数据库,将索引的具体类型主要分为以下几类:主键索引、唯一索引、常规索引、全文索引。
- 主键索引: 如果一张表中指定了主键,那么在建表的时候会针对主键自动的创建一个索引。
分类 | 含义 | 特点 | 关键字 |
---|---|---|---|
主键索引 | 针对于表中主键创建的索引 | 默认自动创建, 只能有一个 | PRIMARY |
唯一索引 | 避免同一个表中某数据列中的值重复 | 可以有多个 | UNIQUE |
常规索引 | 快速定位特定数据 | 可以有多个 | |
全文索引 | 全文索引查找的是文本中的关键词,而不是比较索引中的值 | 可以有多个 | FULLTEXT |
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 创建索引
- 关键字:CREATE INDEX
- 可选项
- UNIQUE :创建的是一个唯一索引,要求该字段不能重复出现数据。
- FULLTEXT :创建的是一个全文索引
- 如果没有写可选项,代表创建的是一个常规索引。
...
:表示一个索引可以关联多个字段- 如果一个索引只关联一个字段,那么称这个索引为单列索引
- 如果一个索引关联了多个字段,那么称这个索引为联合索引或者组合索引
#表示为每一张表中的每一个字段来创建索引
CREATE [可选项] INDEX 索引名称 ON 表名(表中的字段名,... ) ;CREATE [ UNIQUE | FULLTEXT ] INDEX index_name ON table_name (index_col_name,... ) ;
2.4.2 查看索引
#查看指定表中的所有索引
SHOW INDEX FROM table_name ;
2.4.3 删除索引
#删除哪一张表中的哪一个索引
DROP INDEX index_name ON table_name ;
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 执行计划中各个字段的含义:
字段 | 含义 |
---|---|
id | select查询的序列号,表示查询中执行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 | 表示索引中使用的字节数, 该值为索引字段最大可能长度,并非实际使用长度,在不损失精确性的前提下, 长度越短越好 。 |
rows | MySQL认为必须要执行查询的行数,在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会把每一列转化为一行进行展示。
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 最左前缀法则
主要针对联合索引
如果一个索引关联了多个字段(联合索引),要遵守最左前缀法则。最左前缀法则指的是查询从索引的最左列开始, 并且不跳过索引中的列。如果跳跃某一列,索引将会部分失效(后面的字段索引失效)
。
以 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.6.3 范围查询
主要针对联合索引
联合索引中,出现范围查询(>,<),范围查询右侧的列索引失效
。
explain select * from tb_user where profession = '软件工程' and age > 30 and status = '0';
走的是联合索引
当范围查询使用> 或 < 时,走联合索引了,但是索引的长度为49,就说明范围查询右边的status字段是没有走索引的。
explain select * from tb_user where profession = '软件工程' and age >= 30 and
status = '0';
当范围查询使用>= 或 <= 时,走联合索引了,但是索引的长度为54,就说明所有的字段都是走索引的。
所以,在业务允许的情况下,尽可能的使用类似于 >= 或 <= 这类的范围查询,而避免使用 > 或 <。
2.6.4 索引失效情况
2.6.4.1 索引列运算
不要在索引列上进行运算操作,索引将失效
。
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';
2.6.4.2 字符串不加引号
字符串类型字段使用时,不加引号,索引将失效
。
接下来,我们通过两组示例,来看看对于字符串类型的字段,加单引号与不加单引号的区别:
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;
不走唯一索引。
经过上面两组示例,我们会明显的发现,如果字符串不加单引号,对于查询结果,没什么影响,但是数据库存在隐式类型转换,索引将失效。
2.6.4.3 模糊查询
如果仅仅是尾部模糊匹配,索引不会失效。如果是头部模糊匹配,索引失效。
接下来,我们来看一下这三条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模糊查询中,在关键字后面加%,索引可以生效。而如果在关键字 前面加了%,索引将会失效。
2.6.4.4 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连接的条件,左右两侧字段都有索引时,索引才会生效。
2.6.4.5 数据分布影响
如果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是否走索引,得具体情况具体分析,并不是固定的。
2.6.5 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内部还会再次进行评估)。
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.6 覆盖索引(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.7 前缀索引
当字段类型为字符串(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.8 单列索引与联合索引的选择
单列索引:即一个索引只包含单个列。
联合索引:即一个索引包含了多个列。
我们先来看看 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必须存在,所以在创建联合索引的时候我们需要考虑字段的顺序。
- 有影响,根据之前学习的最左前缀法则,每一次查询想要使用联合索引那么最左边的列必须存在。如果是(phone,name)表示最左边的phone必须存在,如果是(name,phone)表示最左边的name必须存在,所以在创建联合索引的时候我们需要考虑字段的顺序。
2.7 索引设计原则
那假如说在具体的业务当中我要对某一张表的某些字段来建立索引,提高sql语句的查询效率,那我应该针对于什么样的表什么样的字段建立什么样的索引呢????
- 针对于数据量较大,且查询比较频繁的表建立索引。
- 一般一张表中的数据超过100多万条,此时就可以考虑建立索引了,如果是几千几万条即使不建立索引,那么它的查询性能也很高。
- 如果一张表直接往里面插入数据很少去查询,那么不用建立索引,因为索引是为了提高检索效率的。
- 针对于常作为查询条件(where)、排序(order by)、分组(group by)操作的字段建立索引。
- 查询条件、排序条件、分组条件这些都会用到索引,尽量使用联合索引。
- 尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高。
- 比如说一张用户表,每一个用的身份证号都是不同的,这就叫做区分度高,进来使用这些列建立索引区分度越高索引的效率也就越高。
- 对于某一些字段它的区分度很低,即使建立了索引效率也不高,例如 用户的性别、表中的状态字段。
- 如果是字符串类型的字段,字段的长度较长,可以针对于字段的特点,建立前缀索引。
- 字符串长度比较长甚至大文本字段,这种字段建立索引建议使用前缀索引,要考虑它的前缀区分度。
- 尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间, 避免回表,提高查询效率。
- 注意使用联合索引要遵循最左前缀法则。
- 要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价也就越大,会影响增删改的效率,占用磁盘空间。
- 只建立有必要的索引,没有必要的索引尽量不要去创建。
- 如果索引列不能存储NULL值(也就是说对应的这一列不允许存储null值
),请在创建表时使用NOT NULL约束它。当优化器知道每列是否包含NULL值时,它可以更好地确定使用哪个索引最有效地用于查询。
2.8 总结
3.SQL优化
3.1 插入数据
3.1.1 insert
如果我们需要一次性往数据库表中插入多条记录,可以从以下三个方面进行优化。
insert into tb_test values(1,'tom');insert into tb_test values(2,'cat');insert into tb_test values(3,'jerry');
.....
1).优化方案一:批量插入数据
- 如果是一条一条的insert插入数据,每次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
3.1.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指令时,我们也要按照主键顺序插入,主键顺序插入性能高于乱序插入。
3.2 主键优化
在上一小节,我们提到,主键顺序插入的性能是要高于乱序插入的。 这一小节,就来介绍一下具体的原因,然后再分析一下主键又该如何设计。
3.2.1 数据组织方式
在InnoDB存储引擎中,表数据都是根据主键顺序组织存放的,这种存储方式的表称为索引组织表(index organized table IOT)。
表数据都是根据主键顺序组织存放的:InnoDB存储引擎中按照存储方式分为聚集索引和二级索引,聚集索引的叶子节点下面挂的是这一行的行数据。一张表默认主键索引就是聚集索引,所以最终我们表中的数据在存放的时候,他就是根据主键进行顺序存放的。比如:6下面存放的就是6这个主键对应的这一行数据,12下面存放的就是12这个主键对应的数据。所以在InnoDB存储引擎中表数据都是根据主键顺序存放的。
行数据,都是存储在聚集索引的叶子节点上的。而我们之前也讲解过InnoDB的逻辑结构图:
在InnoDB引擎中,数据行是记录在逻辑结构 page 页中的,而每一个页的大小是固定的,默认16K。那也就意味着, 一个页中所存储的行也是有限的,如果插入的数据行row在该页存储不小,将会存储到下一个页中,页与页之间会通过指针连接。
3.2.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#。 所以,此时,需要重新设置链表指针。
上述的这种现象,称之为 “页分裂”,是比较耗费性能的操作。
3.2.3 页合并
目前表中已有数据的索引结构(叶子节点)如下:
当我们对已有数据进行删除时,具体的效果如下:
当删除一行记录时,实际上记录并没有被物理删除(并不会直接从磁盘当中,将数据页当中的这一行数据干掉),只是记录被标记(flaged)为删除并且它的空间变得允许被其他记录声明使用。
当我们继续删除2#的数据记录
当页中删除的记录达到 MERGE_THRESHOLD(默认为页的50%),InnoDB会开始寻找最靠近的页(前或后)看看是否可以将两个页合并以优化空间使用。
删除数据,并将页合并之后,再次插入新的数据21,则直接插入3#页
这个里面所发生的合并页的这个现象,就称之为 “页合并”。
知识小贴士:
MERGE_THRESHOLD:合并页的阈值,可以自己设置,在创建表或者创建索引时指定。
3.2.4 主键索引设计原则
- 满足业务需求的情况下,尽量降低主键的长度。
- 一张表的聚集索引只有一个,二级索引可以有多个,二级索引叶子下面挂的就是主键,所以如果主键长度比较长二级索引比较多,那么将会占用大量的磁盘空间,在搜索的时候占用大量的磁盘io。
- 插入数据时,尽量选择顺序插入,选择使用AUTO_INCREMENT自增主键。
- 主键顺序插入性能较高,主键乱序插入有可能出现页分裂现象,导致性能降低。
- 尽量不要使用UUID做主键或者是其他自然主键,如身份证号。
- uuid生成的主键值是无序的,这样插入数据时就是乱序插入,就可能出现页分裂现象。并且uuid生成的数据长度比较长,在检索的时候也会耗费大量的磁盘io。
- 业务操作时,避免对主键的修改。
- 修改主键,还要动对应的数据结构,这个代价还是比较大的。
3.3 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.4 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.5 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.6 count优化
3.6.1 概述
select count(*) from tb_user ;
在之前的测试中,我们发现,如果数据量很大,在执行count操作时,是非常耗时的,它是由存储引擎决定的。
- MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个数,效率很高; 但是如果是带条件的count,MyISAM也慢。
- InnoDB 引擎就麻烦了,它执行 count(*) 的时候,需要把数据一行一行地从引擎里面读出来,然后累积计数。
如果说要大幅度提升InnoDB表的count效率,主要的优化思路:自己计数(可以借助于redis这样的数据库进行,但是如果是带条件的count又比较麻烦了)。
- 自己计数:比如我们可以借助一些像key value形式的内存级别的数据库像Redis,当我们执行插入数据时直接把某一个计数加1,当我们往某一张表中去删除一条数据时把这个计数减1,自己去维护这个计数,比较繁琐。
3.6.2 count用法
count() 是一个聚合函数,对于返回的结果集,一行行地判断,如果 count 函数的参数不是NULL,累计值就加 1,否则不加,最后返回累计值。
用法:count(*)、count(主键)、count(字段)、count(数字)
count用法 | 含义 |
---|---|
count(主键) | InnoDB 引擎会遍历整张表,把每一行的 主键id 值都取出来,返回给服务层。服务层拿到主键后,直接按行进行累加(主键不可能为null) |
count(字段) | 没有not null 约束 : InnoDB 引擎会遍历整张表把每一行的字段值都取出来,返回给服务层,服务层判断是否为null,不为null,计数累加。有not null 约束:InnoDB 引擎会遍历整张表把每一行的字段值都取出来,返回给服务层,直接按行进行累加。 |
count(数字) | InnoDB 引擎遍历整张表,但不取值。服务层对于返回的每一行,放一个数字“1” 进去,直接按行进行累加。 |
count(*) | InnoDB引擎并不会把全部字段取出来,而是专门做了优化,不取值,服务层直接按行进行累加。 |
按照效率排序的话,count(字段) < count(主键 id) < count(1) ≈ count(*),所以尽量使用 count(*)。
3.7 update优化
我们主要需要注意一下update语句执行时的注意事项。
3.7.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;
3.7.2 正常情况
A:开启事务模拟一个事务操作,执行一条更新语句
回忆 InnoDB引擎的3大特性:事务、外键、行级锁。也就是说在InnoDB引擎中那么我们当前默认的这种事务隔离级别里面,它默认使用的是行锁。
#开启事务模拟一个事务操作。
begin;update course set name = 'javaEE' where id = 1 ;
对于InnoDB引擎当前默认的事务隔离级别,我们在进行操作的时候执行一条update语句,那么当前他会把id为1的这一行数据会锁住。只要你的事务没有提交,这一行的行锁就不会释放。
B:开启一个新的会话窗口,开启事务模拟事务操作,执行一条更新语句。
mysql -uroot -p1234use itcast;begin;update course set name = 'Kafka' where id = 4;
第一个窗口使用行级锁锁的是id为1的这行数据,第二个窗口要操作的是id为4的这行数据,如果是行锁可以执行成功。
C:2个会话窗口进行事务提交:都成功没有任何问题。
3.7.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提交事务。
3.7.4 更新优化说明
- 以上演示就是update语句在执行的时候需要规避的问题:在执行update语句的时候,我们一定要根据索引字段进行更新,否则就会出现行锁升级为表锁,锁住整张表,这样会降低并发性能。
- InnoDB引擎的行锁是针对索引加的锁,不是针对记录加的锁 ,所以在进行更新的时候更新的条件一定要有索引,如果没有索引就会出现行锁升级为表锁。并且这个索引不能失效,否则行锁也会升级为表锁 ,一旦升级为表锁那么并发性能就会降低。
3.8 总结
4.视图/存储过程/存储函数/触发器
4.1 视图
4.1.1介绍
视图(View)是一种虚拟存在的表。视图中的数据并不在数据库中实际存在,行和列数据来自定义视图的查询中使用的表(基表),并且是在使用视图时动态生成的。
通俗的讲,视图只保存了查询的SQL逻辑,不保存查询结果。所以我们在创建视图的时候,主要的工作就落在创建这条SQL查询语句上。
4.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] 视图名称 [,视图名称] ...
4.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');
4.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视图满足,检查v3视图不满足,所以插入失败。
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');
4.1.5 视图的更新
mysql中的视图并不是任何的视图都可以进行增删改。所以我们要了解一下视图的更新需要什么条件。
要使视图可更新,视图中的行与基础表中的行之间必须存在一对一的关系。如果视图包含以下任何一 项,则该视图不可更新:
-
聚合函数或窗口函数(SUM()、 MIN()、 MAX()、 COUNT()等)
-
DISTINCT
-
GROUP BY
-
HAVING
-
UNION 或者 UNION ALL
示例演示:
#创建视图时使用了聚合函数
create view stu_v_count as select count(*) from student;
上述的视图中由于使用了聚合函数,导致视图和基础表不能一 一对应,如果我们对这个视图进行更新或插入的,将会报错。
insert into stu_v_count values(10);
4.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;
4.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;
4.2 存储过程
4.2.1 介绍
存储过程是事先经过编译并存储在数据库中的一段SQL语句的集合,调用存储过程可以简化应用开发人员的很多工作,减少数据在数据库和应用服务器之间的传输,对于提高数据处理的效率是有好处的。
存储过程思想上很简单,就是数据库 SQL 语言层面的代码封装
与重用
。
重用:业务1想要用到这个p1的sql集合直接去调用这个存储过程就行了,下一次在遇到相同的业务的时候只需要调用对应的存储过程即可。
场景:
- 问题:一个业务当中它可能需要操作多次数据库,eg 第一次先要去查询数据库表当中的数据,之后根据查询的结果要不要更新,如 更新第一张表的数据之后更新第二张表的数据。一个逻辑当中需要操作多次数据库,因意味着多次网络请求。
- 优化:在数据库层面将多条sq语句l封装到一个集合当中,之后只需要调用这个p1这个sql集合就可以了。
特点:
- 封装,复用 -----------------------> 可以把某一业务SQL封装在存储过程中,需要用到的时候直接调用即可。
- 可以接收参数,也可以返回数据 --------> 在存储过程中,可以传递参数,也可以接收返回值。
- 减少网络交互,效率提升 -------------> 如果涉及到多条SQL,每执行一次都是一次网络传输。 而如果封装在存储过程中,我们只需要网络交互一次可能就可以了。
4.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()beginselect 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用户),创建的时候没写默认会加上这个参数
4.2.3 变量
说明:上面只是演示的是存储过程中定义一个简单的select语句,并没有任何的逻辑,在一些复杂的业务逻辑当中想要去编写一个复杂的存储过程,里面就会涉及到很多的语法结构。所以接下来呢我们就需要针对于存储过程当中,所涉及到的语法结构来进行详细的讲解,首先要学习的第一块就是变量。
在MySQL中变量分为三种类型: 系统变量、用户定义变量、局部变量。
4.2.3.1 系统变量
系统变量
是MySQL服务器提供,不是用户定义的,属于服务器层面。分为全局变量(GLOBAL
)、会话变量(SESSION
)。
-
全局变量:对所有的会话有效
- eg:创建多个查询控制台,一个查询窗口就是一个会话
- 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;
4.2.3.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;
4.2.3.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();
4.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 thenset result := '优秀';elseif score >= 60 thenset result := '及格';elseset result := '不及格';end if;#查看局部变量select result;end;#调用存储过程
call p3();
-
问题:上述的需求我们虽然已经实现了,但是也存在一些问题,比如:score 分数我们是在存储过程中定义死的,而且最终计算出来的分数等级,我们也仅仅是最终查询展示出来而已。
-
解决:那么我们能不能,把score分数动态的传递进来,计算出来的分数等级是否可以作为返回值返回呢? 答案是肯定的,我们可以通过接下来所讲解的
参数
来解决上述的问题。
4.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))beginif score >= 85 thenset result := '优秀';elseif score >= 60 thenset result := '及格';elseset result := '不及格';end if;end;#查询:存储过程
#第一个参数是传入的数据,第二个参数是将执行的返回值返回,通过用户自定义变量进行接收
#定义用户变量 @result来接收返回的数据, 用户变量可以不用声明
call p4(18, @result);#查看这个用户自定义变量
select @result;
3).案例二
将传入
的200分制的分数,进行换算,换算成百分制,然后 返回
分数。
分数既是传入参数又是传出参数------INOUT
#创建存储过程
create procedure p5(inout score double)beginset score := score * 0.5;
end;#调用存储过程:如果直接传递198就没办法拿到返回值了,所以需要定义一个用户自定义变量@score
# 这个变量没有值所以要先进行赋值,然后在去调用这个存储过程,调用完毕之后他会将计算完毕后的结果,
# 再次赋值给这个用户自定义变量set @score = 198;
call p5(@score);#查询这个用户自定义变量封装的返回结果
select @score;
4.2.6 case
1).介绍
case结构及作用,和我们在基础篇中所讲解的流程控制函数很类似。有两种语法格式:
语法1:
-- 含义: 当case_value的值为 when_value1时,执行statement_list1,当值为 when_value2时,执行statement_list2, 否则就执行 statement_list
CASE case_valueWHEN 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
CASEWHEN 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);casewhen month >= 1 and month <= 3 thenset result := '第一季度';when month >= 4 and month <= 6 thenset result := '第二季度';when month >= 7 and month <= 9 thenset result := '第三季度';when month >= 10 and month <= 12 thenset result := '第四季度';elseset result := '非法参数';end case ;#查询局部变量:借助concat函数拼接字符串select concat('您输入的月份为: ',month, ', 所属的季度为: ',result);end;#调用存储过程
call p6(16);
注意:如果判定条件有多个,多个条件之间,可以使用 and 或 or 进行连接。
4.2.7 while循环
1).介绍
while 循环是有条件的循环控制语句。满足条件后,再执行循环体中的SQL语句。具体语法为:
- 特点:满足条件才循环
-- 先判定条件,如果条件为true,则执行逻辑,否则,不执行逻辑
WHILE 条件 DOSQL逻辑...END WHILE; #结束语句
2).案例
计算从1累加到n的值,n为传入的参数值。
-- A. 定义局部变量, 记录累加之后的值;
-- B. 每循环一次, 就会对n进行减1 , 如果n减到0, 则退出循环
create procedure p7(in n int)begindeclare total int default 0;while n>0 doset total := total + n;set n := n - 1;end while;select total;end;call p7(100);
4.2.8 repeat循环
1).介绍
repeat是有条件的循环控制语句, 当满足until声明的条件的时候,则退出循环 。具体语法为:
- 特点:满足条件退出循环
-- 先执行一次逻辑,然后判定UNTIL条件是否满足,如果满足,则退出。如果不满足,则继续下一次循环
REPEATSQL逻辑...UNTIL 条件
END REPEAT;
2).案例
计算从1累加到n的值,n为传入的参数值。(使用repeat实现)
-- A. 定义局部变量, 记录累加之后的值;
-- B. 每循环一次, 就会对n进行-1 , 如果n减到0, 则退出循环
create procedure p8(in n int)begindeclare total int default 0;repeatset total := total + n;set n := n - 1;until n <= 0end repeat;select total;
end;call p8(10);
call p8(100);
4.2.9 loop循环
1).介绍
LOOP 实现简单的循环,如果不在SQL逻辑中增加退出循环的条件,可以用其来实现简单的死循环。
LOOP可以配合一下两个语句使用:
- LEAVE :配合循环使用,退出循环。
- ITERATE:必须用在循环中,作用是跳过当前循环剩下的语句,直接进入下一次循环。
#[begin_label:] :指定标记
[begin_label:] LOOPSQL逻辑...
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)begindeclare total int default 0;sum:loopif n<=0 thenleave 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)begindeclare total int default 0;sum:loopif n<=0 thenleave sum;end if;if n%2 = 1 thenset n := n - 1;iterate sum;end if;set total := total + n;set n := n - 1;end loop sum;select total;end;call p10(100);
4.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
来解决。
4.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);
同样:再次执行发现不会在报错了,查询新创建的表发现数据插入成功
效果:
4.3 存储函数
4.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:只包含读取数据的语句,但不包含写入数据的语句。
4.3.2 案例
计算从1累加到n的值,n为传入的参数值。(通过存储函数来实现
)
#默认就是in
create function fun1(n int)returns intbegindeclare total int default 0;while n>0 doset 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 deterministicbegindeclare total int default 0;while n>0 doset total := total + n;set n := n - 1;end while;return total;
end$$select fun1(50);
4.3.3 缺点
存储函数呢相对来说用的比较少一些,因为存储函数能做的事情那么存储过程也能做到,而且存储函数还有一个弊端他必须要用返回值。假如我要去执行一段逻辑而这段逻辑有返回值,我可不可以使用存储过程来实现???
可以:因为存储过程我们可以指定参数的类型为out,就可以接收我存储过程执行的结果,所以可以使用存储函数的地方都可以使用存储过程来代替。
4.4 触发器
4.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,因为数据已经删除了,是没有新的数据的) |
4.4.2 语法
1).创建
#创建触发器并指定名称
CREATE TRIGGER trigger_name#指定触发的时机 指定触发器的类型
BEFORE/AFTER INSERT/UPDATE/DELETE# on 表的名字:哪一张表的数据进行增删改之后触发
#FOR EACH ROW:指定行级触发器,mysql目前只支持行级触发器。
ON tbl_name FOR EACH ROW #编写触发器的业务逻辑
BEGINtrigger_stmt ;END;
2).查看
#查看当前数据库中所有的触发器
SHOW TRIGGERS ;
3).删除
schema_name:数据库名
trigger_name:触发器名
# 删除指定数据库下的触发器
# 如果没有指定 schema_name数据库名字,默认为当前数据库 。
DROP TRIGGER [schema_name.]trigger_name ; --
4.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_triggerafter 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_triggerafter update on tb_user for each row
begininsert 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_triggerAFTER 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;
测试完毕之后,检查日志表中的数据是否可以正常插入,以及插入数据的正确性。