MySQL
- 六、MySQL索引视图
-
- [6.1 索引底层原理](#6.1 索引底层原理)
-
- [6.1.1 索引hash算法](#6.1.1 索引hash算法)
- [6.1.2 索引二叉树算法](#6.1.2 索引二叉树算法)
- [6.1.3 索引平衡二叉树算法](#6.1.3 索引平衡二叉树算法)
- [6.1.4 索引BTREE树算法](#6.1.4 索引BTREE树算法)
- [6.1.5 普通SQL全表扫描过程](#6.1.5 普通SQL全表扫描过程)
- [6.2 索引分类](#6.2 索引分类)
-
- [6.2.1 按数据结构层次分类](#6.2.1 按数据结构层次分类)
- [6.2.2 按字段数量层次分类](#6.2.2 按字段数量层次分类)
- [6.2.3 按功能逻辑层次分类(面试题)](#6.2.3 按功能逻辑层次分类(面试题))
- [6.2.4 按存储方式层次分类](#6.2.4 按存储方式层次分类)
- [6.3 索引的设计原则](#6.3 索引的设计原则)
- [6.4 创建索引](#6.4 创建索引)
- [6.5 查看索引](#6.5 查看索引)
- [6.6 删除索引](#6.6 删除索引)
- [6.7 索引的优劣分析](#6.7 索引的优劣分析)
-
- [6.7.1 引入索引带来的优势](#6.7.1 引入索引带来的优势)
- [6.7.2 引入索引带来的弊端](#6.7.2 引入索引带来的弊端)
- [6.8 视图概念](#6.8 视图概念)
- [6.9 创建视图](#6.9 创建视图)
- [6.10 更新视图](#6.10 更新视图)
- [6.11 修改视图](#6.11 修改视图)
- [6.12 删除视图](#6.12 删除视图)
- [6.13 视图使用规则](#6.13 视图使用规则)
- 七、SQL编程
-
- [7.1 存储过程](#7.1 存储过程)
- [7.2 流程控制](#7.2 流程控制)
-
- [7.2.1 if判断](#7.2.1 if判断)
- [7.2.2 case判断](#7.2.2 case判断)
- [7.2.3 循环](#7.2.3 循环)
- [7.3 触发器](#7.3 触发器)
-
- [7.3.1 创建触发器](#7.3.1 创建触发器)
- [7.3.2 NEW与OLD](#7.3.2 NEW与OLD)
- [7.4 存储函数](#7.4 存储函数)
-
- [7.4.1 存储函数创建和调用(create function)](#7.4.1 存储函数创建和调用(create function))
- [7.4.2 删除存储函数(drop function)](#7.4.2 删除存储函数(drop function))
- [八 MySQL事务](#八 MySQL事务)
-
- [8.1 数据库事务处理原则](#8.1 数据库事务处理原则)
- [8.2 事务的特性(面试)-ACID](#8.2 事务的特性(面试)-ACID)
-
- [8.2.1 原子性(Atomicity)](#8.2.1 原子性(Atomicity))
- [8.2.2 一致性(Consistency)](#8.2.2 一致性(Consistency))
- [8.2.3 隔离性(Isolation)](#8.2.3 隔离性(Isolation))
- [8.2.4 持久性(Durability)](#8.2.4 持久性(Durability))
- [8.3 MySQL使用事务](#8.3 MySQL使用事务)
-
- [8.3.1 MySQL的事务操作主要有以下三种:](#8.3.1 MySQL的事务操作主要有以下三种:)
- [8.3.2 使用事务的方法](#8.3.2 使用事务的方法)
- [8.3.3 自动提交策略](#8.3.3 自动提交策略)
- [8.3.4 设置回滚点](#8.3.4 设置回滚点)
- [8.3.5 事务的隐式提交](#8.3.5 事务的隐式提交)
- [8.4 InnoDB 事务的ACID如何保证](#8.4 InnoDB 事务的ACID如何保证)
-
- [8.4.1 WAL技术](#8.4.1 WAL技术)
- [8.4.2 redo log相关参数](#8.4.2 redo log相关参数)
- [8.4.3 MySQL CSR---前滚](#8.4.3 MySQL CSR—前滚)
- [8.4.4 undo 回滚日志](#8.4.4 undo 回滚日志)
- [8.5 事务的隔离级别](#8.5 事务的隔离级别)
-
- [8.5.1 脏读](#8.5.1 脏读)
- [8.5.2 不可重复读](#8.5.2 不可重复读)
- [8.5.3 幻读](#8.5.3 幻读)
- [8.5.4 事务的隔离级别](#8.5.4 事务的隔离级别)
- [8.5.5 查看隔离级别](#8.5.5 查看隔离级别)
- [8.5.6 设置隔离级别](#8.5.6 设置隔离级别)
- [8.5.7 示例](#8.5.7 示例)
- [8.5.8 总结](#8.5.8 总结)
- 九、SQL优化
-
- [9.1 SQL优化技巧](#9.1 SQL优化技巧)
-
- [9.1.1 查询时尽量不要使用 *](#9.1.1 查询时尽量不要使用 *)
- [9.1.2 连表查询时尽量不要关联太多表](#9.1.2 连表查询时尽量不要关联太多表)
- [9.1.3 多表查询时一定要以小驱大(小表放前)](#9.1.3 多表查询时一定要以小驱大(小表放前))
- [9.1.4 不要使用like左模糊和全模糊查询](#9.1.4 不要使用like左模糊和全模糊查询)
- [9.1.5 查询时尽量不要对字段做空值判断](#9.1.5 查询时尽量不要对字段做空值判断)
- [9.1.6 不要在条件查询 = 前对字段做任何运算](#9.1.6 不要在条件查询 = 前对字段做任何运算)
- [9.1.7 !=、!<>、not in、not like、or...要慎用](#9.1.7 !=、!<>、not in、not like、or...要慎用)
- [9.1.8 避免频繁创建、销毁临时表](#9.1.8 避免频繁创建、销毁临时表)
- [9.1.9 从业务设计层面减少大量数据返回的情况](#9.1.9 从业务设计层面减少大量数据返回的情况)
- [9.1.10 尽量避免深分页的情况出现](#9.1.10 尽量避免深分页的情况出现)
- [9.1.11 SQL务必要写完整,不要使用缩写法](#9.1.11 SQL务必要写完整,不要使用缩写法)
- [9.1.12 明确仅返回一条数据的语句可以使用limit 1](#9.1.12 明确仅返回一条数据的语句可以使用limit 1)
- [9.1.13 客户端的一些操作可以批量化完成](#9.1.13 客户端的一些操作可以批量化完成)
- [9.2 查看SQL执行频率](#9.2 查看SQL执行频率)
- [9.3 定位低效率执行SQL](#9.3 定位低效率执行SQL)
- [9.4 explain分析执行计划](#9.4 explain分析执行计划)
- [9.5 show profile 分析SQL](#9.5 show profile 分析SQL)
- [9.6 trace分析优化器执行计划](#9.6 trace分析优化器执行计划)
- [9.7 使用索引优化](#9.7 使用索引优化)
- [9.8 架构优化](#9.8 架构优化)
六、MySQL索引视图
mysql表建立索引的本质是为了避免查询时走全表扫描,减少时间开销
新建表,插入大量数据,通过无索引查询及有索引查询来对比性能
sql
mysql> create database mydb13_indexdb;
mysql> use mydb13_indexdb;
mysql> create table student(id int, name varchar(64), age int(2));
mysql> desc student;
+-------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id | int | YES | | NULL | |
| name | varchar(64) | YES | | NULL | |
| age | int | YES | | NULL | |
+-------+-------------+------+-----+---------+-------+
mysql> insert into student values(1,'das',20),(2,'dasdas',19),(3,'dsfsfsd',18),(4,'bbggbbg',22),(5,'eeeee',19);
mysql> insert into student select * from student;
mysql> insert into student select * from student;
......
# 多次执行上述的自我复制,将数据量增加到百万级......
......
mysql> insert into student select * from student;
Query OK, 1310720 rows affected (13.59 sec)
Records: 1310720 Duplicates: 0 Warnings: 0
# 插入一条新数据
mysql> insert into student values(666,'andy',40);
# 查询刚插入的新数据
mysql> select * from student where id=666;
+------+------+------+
| id | name | age |
+------+------+------+
| 666 | andy | 40 |
+------+------+------+
1 row in set (2.75 sec) # 耗费2.75秒
# 查看文件容量,student表占用120M
C:\ProgramData\MySQL\MySQL Server 8.0\Data\mydb13_indexdb
mysql> create index id_index on student(id); # 给student表新建索引
Query OK, 0 rows affected (12.13 sec) # 耗时较长
Records: 0 Duplicates: 0 Warnings: 0
mysql> select * from student where id=666; # 在有索引的情况下再次查询
+------+------+------+
| id | name | age |
+------+------+------+
| 666 | andy | 40 |
+------+------+------+
1 row in set (0.00 sec) # 有索引查询花费的时间
mysql> select 2.75/0.000001; # 性能提升了
+----------------+
| 2.75/0.000001 |
+----------------+
| 2750000.000000 |
+----------------+
# 再次查看文件容量,新建索引前为120m,新建索引后为164m,表容量增大
C:\ProgramData\MySQL\MySQL Server 8.0\Data\mydb13_indexdb
# 总结:索引的本质就是以空间换时间,同时把随机的事件变成顺序的事件,日常项目开发中,读写比例在10:1左右,查询使用频率比较大,虽然损失点存储空间但两害取其轻,还是需要对关键字段新建索引提高查询性能,最后需要删除上述student表
mysql> drop table student;
mysql> show tables;
6.1 索引底层原理
一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。
6.1.1 索引hash算法
- 优点:通过字段的值 计算的hash值,定位数据非常快 。
- 缺点:不能进行范围查找,因为散列表中的值是无序的,无法进行大小的比较。

6.1.2 索引二叉树算法
- 特性:分为左子树、右子树和根节点,左子树比根节点值要小,右子树比根节点值要大
- 缺点:有可能产生不平衡 类似于链表的结构 。

6.1.3 索引平衡二叉树算法
- 优点
- 它的左子树和右子树都是平衡二叉树
- 左子树比中间小,右子树比中间值
- 左子树和右子树的深度之差的绝对值不超过1
- 缺点:
- 插入操作需要旋转
- 支持范围查询,但回旋查询效率较低,比如要查找大于8的,会回旋到父节点7、10。
- 如果存放几百条数据的情况下,树高度越高,查询效率会越慢
6.1.4 索引BTREE树算法
- 目前大部分数据库系统及文件系统都采用B-Tree或其变种B+Tree作为索引结构,Btree结构可以有效的解决之前的相关算法遇到的问题。

- 注意:InnoDB引擎使用B+Tree,InnoDB的叶节点的data域存放的是数据,相比MyISAM效率要高一些,但是比较占硬盘内存大小。

6.1.5 普通SQL全表扫描过程
-
以下列表为例
-
首先假设表中不存在任何索引,此时来执行下述这条SQL:
sql
mysql> select * from student where sname='陈晓晓';
- 因为表中不具备索引,所以这里会走全表扫描的形式检索数据,但不管是走全表亦或索引,本质上由于数据都存储在磁盘中,因此首先都会触发磁盘IO,磁盘结构如下:
- 当走全表扫描时,会发生磁盘IO,但是磁盘寻道是需要有一个地址的,这个地址最开始就是本地表数据文件中的起始地址,也就是从表中的第一行数据开始读,读到数据后会载入内存,然后MySQL-Server会根据SQL条件对读到的数据做判断,如果不符合条件则继续发生磁盘IO读取其他数据(如果表比较大,这里不会以顺序IO的形式走全表检索,而是会触发随机的磁盘IO)。
- 上面student表中,【陈晓晓】 这条数据位于表的第五行,那这里会发生五次磁盘IO吗?不会,因为OS即MYSQL中都有一个优化措施,叫做局部性读取原理:
局部性原理的思想:如目前有三块内存页x、y、z是相连的,CPU此刻在操作x页中的数据,那按照计算机的特性,一般同一个数据都会放入到物理相连的内存地址上存储,也就是当前在操作x页的数据,那么对于y,z这两页内存的数据也很有可能在接下来的时间内被操作,因此对于y,z这两页数据则会提前将其载入到高速缓冲区(L1/L2/L3),这个过程叫做利用局部性原理"预读"数据。
作用:一般用于减少存储数据的磁盘和内存之间的性能差异MySQL的InnoDB引擎默认一次磁盘IO会读取16KB数据到内存。
- 继续全表扫描,由于MySQL中会使用局部性原理的思想,可能只需发生一次磁盘IO就能将前五条数据全部读到内存,然后会在内存中对本次读取的数据逐条判断,看一下每条数据的姓名字段是否为【陈晓晓】:
- 如果发现不符合SQL条件的行数据,则会将当前这条数据放弃,同时在本次SQL执行过程中会排除掉这条数据,不会对其进行二次读取。
- 如果发现当前的数据符合SQL条件要求,则会将当前数据写入到结果集中,然后继续判断其他数据。
- 当本次磁盘IO读取到的所有数据全部筛选完成后,紧接着会看一下表中是否还有其他数据,如果还有则继续触发磁盘
IO
检索数据,如果没有则将内存中的结果集返回。
- 全表扫描结束,会产生问题:当表的数据量变为百万级别、千万级别,假设表中一条数据大小为512Byte,一次磁盘IO也只能读32条,假设表中有320w条数据,一次全表就有可能会触发10W次磁盘IO,每次都需要在硬件上让那个盘面转啊转,其过程的开销可想而知...
因此建立索引的原因就在于此处,为了避免查询时走全表扫描。
6.2 索引分类
6.2.1 按数据结构层次分类
- 索引建立后也会在磁盘生成索引文件,每个索引节点在本地文件中的存储形式是由索引的数据结构来决定的。
- MySQL索引支持的数据结构如下:
B+Tree
类型:MySQL中最常用的索引结构也是 默认索引物理存储结构 ,有序。Hash
类型:大部分存储引擎都支持,字段值不重复的情况下查询最快 ,无序。R-Tree
类型:MyISAM
引擎支持,也就是空间索引的默认结构类型。T-Tree
类型:NDB-Cluster
引擎支持,主要用于MySQL-Cluster
服务中。


6.2.2 按字段数量层次分类
- 从表字段的层次来看,索引又可以分为单列索引和多列索引
- 单列 索引:
- 唯一索引:指索引中的索引节点值不允许重复,一般配合唯一约束使用。
- 主键索引:主键索引是一种特殊的唯一索引,和普通唯一索引的区别在于不允许有空值。
- 普通索引:通过
KEY、INDEX
关键字创建的索引就是这个类型,没啥限制,单纯的可以让查询快一点。 - ...还有很多很多,只要是基于单个字段建立的索引都可以被称为单列索引。
- 多列索引:组合索引、联合索引、复合索引、多值索引...,即由多个字段组合建立的索引
6.2.3 按功能逻辑层次分类(面试题)
- 面试题:请回答一下你知道的
MySQL
索引类型。 - 其上主要就是指
MySQL
索引从逻辑上可以分为那些类型,以功能逻辑划分索引类型,这也是最常见的划分方式,从这个维度来看主要可划分为五种:- 普通索引:加速查找,最常用的索引,允许重复
- 唯一索引:加速查找+约束(不能重复)
- 主键索引:加速查找+约束(不为空、不能重复)
- 全文索引:仅可用于 MyISAM 表,建立于char字段
- 空间索引:使用不多,基于GIS(地理信息系统)的空间数据相关字段创建
6.2.4 按存储方式层次分类
- 分为两大类
- 聚簇索引:也被称为聚集索引、簇类索引
- 非聚簇索引:也叫非聚集索引、非簇类索引、二级索引、辅助索引、次级索引
- 区别
- 聚簇索引:逻辑上连续且物理空间连续,索引数据和表数据在磁盘中的位置是一起的,一张表中只能存在一个聚簇索引,聚簇索引要求索引必须是非空唯一索引,一般适合采用带有自增性的顺序值
- 非聚簇索引:逻辑上的连续,物理空间上不连续,索引数据和表数据在磁盘中分开存储,用物理地址的方式维护两者的联系
6.3 索引的设计原则
- 选择惟一性索引
- 为经常需要排序、分组和联合操作的字段建立索引
- 为常作为查询条件的字段建立索引
- 限制索引的数目
- 尽量使用数据量少的索引
- 尽量使用字段前缀来索引,即限制索引长度
- 索引长度:对表中特定字段的前N个字符创建索引
- 通常用于减少索引的大小,并且可以提高查询性能
- 删除不再使用或者很少使用的索引
6.4 创建索引
- 方法1:使用create语句在已经存在的表上创建索引
sql
# 创建普通索引
create index indexname on tablename (columnName(length) [asc|desc]);
# indexName:当前创建的索引,创建成功后叫啥名字。
# tableName:要在哪张表上创建一个索引,这里指定表名。
# columnName:要为表中的哪个字段创建索引,这里指定字段名。
# length:如果字段存储的值过长,选用值的前多少个字符创建索引。
# asc|desc:指定索引的排序方式,asc是升序,desc是降序,默认asc。
sql
# 创建唯一索引
create unique index indexname on tablename (columnName(length) [asc|desc]);
- 方法2:使用alter table语句来添加索引
sql
# 添加普通索引
alter table tableName add index indexname(columnname(length) [asc|desc]);
# 添加唯一索引
alter table tableName add unique index indexname(columnname(length) [asc|desc]);
- 方式3:创建表的时候创建索引
sql
# 创建普通索引
create table 表名 (
字段名1 数据类型 [完整性约束条件...],
字段名2 数据类型 [完整性约束条件...],
[unique | fulltext | spatial] index | key
[索引名] (字段名[(长度)] [asc | desc])
);
sql
# 创建唯一索引
create table 表名 (
字段名1 数据类型 [完整性约束条件...],
字段名2 数据类型 [完整性约束条件...],
unique index
[索引名] (字段名[(长度)] [asc | desc])
);
sql
# 创建主索引
create table 表名 (
字段名1 数据类型 [完整性约束条件...],
字段名2 数据类型 [完整性约束条件...],
primary key
[索引名] (字段名[(长度)] [asc | desc])
);
# 注意
# 创建主键索引时,必须要将索引字段先设为主键,否则会抛1068错误码。
# 主索引不同使用create语句创建,否则会提示1064语法错误。
# 创建索引时,关键字要换成key,并非index,否则会提示语法错误。
- 示例
sql
# 创建表的同时创建普通索引
mysql> create table index1_tb( id int, name varchar(20), sex boolean, index(id));
# 创建表的同时创建唯一索引
mysql> create table index2_tb( id int unique, name varchar(20), unique index index2(id asc) );
# 创建单列索引 (即普通的单列索引)
mysql> create table index3_tb( id int, subject varchar(30), index index3(subject(10)) );
# 创建多列索引 (即普通的多列索引)
# 注意:使用多列索引时一定要特别注意,只有使用了索引中的第一个字段时才会触发索引。
mysql> create table index4_tb( id int, name varchar(20), sex char(4), index index4(name,sex) );
sql
# 在创建完表后为其添加索引
mysql> create unique index un_index on index1_tb(name);
mysql> alter table index3_tb add primary key(id);
6.5 查看索引
- 格式:
sql
# 查询索引
show create table 表名 \G
# 查询某张表中索引情况
show index from table_name;
# 使用计划查询SQL使用索引情况
explain select * from 表名 where id=1 \G
# 使用系统表查看所有索引
select * from mysql.`innodb_index_stats` a where a.`database_name` = '数据库名';
# 使用系统表查看单张表的所有索引
select * from mysql.`innodb_index_stats` a where a.`database_name` = '数据库名' and a.table_name like '%表名%';
- 示例
sql
mysql> show create table index1_tb \G
sql
mysql> show index from index3_tb\G
*************************** 1. row ***************************
Table: index3_tb
Non_unique: 0
Key_name: PRIMARY
Seq_in_index: 1
Column_name: id
Collation: A
Cardinality: 0
Sub_part: NULL
Packed: NULL
Null:
Index_type: BTREE
Comment:
Index_comment:
Visible: YES
Expression: NULL
*************************** 2. row ***************************
Table: index3_tb
Non_unique: 1
Key_name: index3
Seq_in_index: 1
Column_name: subject
Collation: A
Cardinality: 0
Sub_part: 10
Packed: NULL
Null: YES
Index_type: BTREE
Comment:
Index_comment:
Visible: YES
Expression: NULL
# 分析
• Table:当前索引属于那张表。
• Non_unique:目前索引是否属于唯一索引,0代表是的,1代表不是。
• Key_name:当前索引的名字。
• Seq_in_index:如果当前是联合索引,目前字段在联合索引中排第几个。
• Column_name:当前索引是位于哪个字段上建立的。
• Collation:字段值以什么方式存储在索引中,A表示有序存储,NULL表无序。
• Cardinality:当前索引的散列程度,也就是索引中存储了多少个不同的值。
• Sub_part:当前索引使用了字段值的多少个字符建立,NULL表示全部。
• Packed:表示索引在存储字段值时,以什么方式压缩,NULL表示未压缩,
• Null:当前作为索引字段的值中,是否存在NULL值,YES表示存在。
• Index_type:当前索引的结构(BTREE, FULLTEXT, HASH, RTREE)。
• Comment:创建索引时,是否对索引有备注信息。
........................
sql
mysql> explain select * from index1_tb where id=1 \G
# 注意possible_keys和key 这两个属性,possible_keys:MySQL在搜索数据记录时可以选用的各个索引,key:实际选用的索引
6.6 删除索引
- 语法
sql
drop index 索引名 on 表名
- 注意:一些不再使用的索引会降低表的更新速度,影响数据库的性能,对于这样的索引,应该将其删除
6.7 索引的优劣分析
6.7.1 引入索引带来的优势
- 整个数据库中,数据表的查询速度直线提升,数据量越大时效果越明显。
- 通过创建唯一索引,可以确保数据表中的数据唯一性,无需额外建立唯一约束。
- 在使用分组和排序时,同样可以显著减少SQL查询的分组和排序的时间。
- 连表查询时,基于主外键字段上建立索引,可以带来十分明显的性能提升。
- 索引默认是B+Tree有序结构,基于索引字段做范围查询时,效率会明显提高。
- 从MySQL整体架构而言,减少了查询SQL的执行时间,提高了数据库整体吞吐量。
6.7.2 引入索引带来的弊端
-
建立索引会生成本地磁盘文件,需要额外的空间存储索引数据,磁盘占用率会变高。
-
写入数据时,需要额外维护索引结构,增、删、改数据时,都需要额外操作索引。
-
写入数据时维护索引需要额外的时间开销,执行写SQL时效率会降低,性能会下降。
6.8 视图概念
MySQL中的视图(view)是一种虚拟表,其内容由查询定义,视图本身并不包含数据,是一种数据库对象,其内没有存储任何数据,它只是对表的一个查询
-
作用:
- 控制安全
- 保存查询数据
-
优点:
- 简化操作:通过视图可以使用户将注意力集中在他所关心的数据上。使用视图的用户完全不需要关心后面对应的表的结构、关联条件和筛选条件。
- 提高数据的安全性:在设计数据库时可以针对不同的用户定义不同的视图,使用视图的用户只能访问他们被允许查询的结果集。
- 数据独立:视图的结构定义好之后,如果增加新的关系或对原有的关系增加新的字段对用户访问的数据都不会造成影响。
6.9 创建视图
- 语法
sql
create [or replace] [algorithm = {undefined | merge | temptable}]
view view_name [(column_list)]
as select_statement
[with [cascaded | local] check option]
# 说明:
1、or replace:如果要创建的视图名称已存在,则替换已有视图。
2、algorithm:可选参数,表示视图选择的算法,默认算法是 undefined
(1)undefined:未定义指定算法
(2)merge:更新视图表数据的同时会更新真实表的数据
(3)temptable:只能查询不能更新
3、view_name:新建的视图名称。
4、column_list:可选,表示视图的字段列表。若省略,则使用 select 语句中的字段列表。
5、as select_statement:创建视图的 select 语句。
6、with check option:表示更新视图时要保证该视图的 where 子句为真。
比如定义视图:create view v1 as select * from salary > 5000;
如果要更新视图,则必须保证 salary 字段的值在 5000 以上,否则报错。
(1)cascaded:必须满足所有针对该视图的条件才可以更新
(2)local:只需满足本视图的条件就可以更新
- 示例1:创建来源1张表的视图
sql
mysql> show databases;
mysql> use mydb9_stusys;
mysql> create view v_student as select sno,sname,ssex,year(now())-year(birth) as age from student;
mysql> select * from v_student;
mysql> show tables;
- 示例2:创建多表连接的视图
sql
mysql> create view v_score as select student.*, score from student join sc on student.sno = sc.sno;
Query OK, 0 rows affected (0.02 sec)
mysql> select * from v_score;
mysql> show tables;
mysql> show create view v_score;
- 示例3:创建视图,字段起别名
sql
mysql> create or replace view v_avg(sex,avg_score) as select ssex , round(avg(score),2) from student inner join sc on student.sno=sc.sno group by ssex;
Query OK, 0 rows affected (0.01 sec)
mysql> select * from v_avg;
+------+-----------+
| sex | avg_score |
+------+-----------+
| 女 | 73.52 |
| 男 | 78.51 |
+------+-----------+
6.10 更新视图
-
更新视图中的数据,实际上是更新创建视图时用到的基本表中的数据
-
以下视图不可更新
- 包含以下关键字的 SQL 语句:聚合函数、distinct、group by 、having、union 或 uinon all
- select 中包含子查询
- from 一个不可更新的试图
- where 子句的子查询引用了 from 子句中的表。
-
示例4:创建视图,限制更新
sql
mysql> create or replace view v_age as select sno,sname,ssex,sage from student where sage>20 with check option; # 增加限制更新参数
Query OK, 0 rows affected (0.00 sec)
mysql> select * from v_age;
mysql> update v_age set sage = 24 where sno = 's011'; # 更新视图,24符合条件
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from student; # 查看视图的基本表,数据已经变更
# 若更新时条件不符合where字句则限制更新
mysql> update v_age set sage = 18 where sno = 's011'; # 18岁不符合where子句
ERROR 1369 (HY000): CHECK OPTION failed 'mydb9_stusys.v_age'
- 示例5:视图中聚合函数不可更新
sql
mysql> select * from v_student;
mysql> update v_student set age=30 where sno="s001";
ERROR 1348 (HY000): Column 'age' is not updatable
- 示例6:对分组和having字段不可更新
sql
mysql> select * from v_avg;
+------+-----------+
| sex | avg_score |
+------+-----------+
| 女 | 73.52 |
| 男 | 78.51 |
+------+-----------+
2 rows in set (0.00 sec)
mysql> update v_avg set avg_score=80 where sex="女";
ERROR 1288 (HY000): The target table v_avg of the UPDATE is not updatable
6.11 修改视图
- 通过 create or replace view 命令修改视图
sql
mysql> desc v_student; # 查看结构
mysql> create or replace view v_student as select sno, sname, ssex, sage from student; # 将age直接读取
- 通过 alter view 命令修改视图
sql
mysql> alter view v_student as select sno, sname, ssex, sage from student where ssex="女";
Query OK, 0 rows affected (0.02 sec)
mysql> select * from v_student;
6.12 删除视图
- 格式
sql
drop view [if exists] view_name;
- 示例7:
sql
mysql> show tables;
mysql> drop view v_age;
mysql> drop view v_avg;
mysql> drop view v_score;
mysql> drop view v_student;
6.13 视图使用规则
-
视图必须有唯一命名
-
在mysql中视图的数量没有限制
-
创建视图必须从管理员那里获得必要的权限
-
视图支持嵌套,也就是说可以利用其他视图检索出来的数据创建新的视图
-
在视图中可以使用order by,但是如果视图内已经使用该排序子句,则视图的order by将覆盖前面的order by。
-
视图不能索引,也不能关联触发器或默认值
-
视图可以和表同时使用
七、SQL编程
7.1 存储过程
7.1.1 存储过程概念
存储过程(Stored Procedure): 是一组为了完成特定功能的SQL语句集合,将常用且复杂的SQL语句预先写好,然后用一个指定名称存储起来,该存储过程会经过MySQL编译解析、执行优化后存储在数据库中,因此称为存储过程。当以后需要使用这个过程时,只需调用根据名称调用即可
- 存储过程和函数的区别:
- 函数必须有返回值,而存储过程没有。
- 存储过程的参数可以是IN、OUT、INOUT类型,函数的参数只能是IN
优点:
- 复用性:存储过程被创建后,可以在程序中被反复调用,不必重新编写该存储过程的SQL语句,同时库表结构发生更改时,只需要修改数据库中的存储过程,无需修改业务代码,也就意味着不会影响到调用它的应用程序源代码。
- 灵活性:普通的SQL语句很难像写代码那么自由,而存储过程可以用流程控制语句编写,也支持在其中定义变量,有很强的灵活性,可以完成复杂的条件查询和较繁琐的运算。
- 省资源:普通的SQL一般都会存储在客户端,如Java中的dao/mapper层,每次执行SQL需要通过网络将SQL语句发送给数据库执行,而存储过程是保存在MySQL中的,因此当客户端调用存储过程时,只需要通过网络传送存储过程的调用语句和参数,无需将一条大SQL通过网络传输,从而可降低网络负载
- 高性能:存储过程执行多次后,会将SQL语句编译成机器码驻留在线程缓冲区,在以后的调用中,只需要从缓冲区中执行机器码即可,无需再次编译执行,从而提高了系统的效率和性能。
- 安全性:对于不同的存储过程,可根据权限设置执行的用户,因此对于一些特殊的SQL,例如清空表这类操作,可以设定root、admin用户才可执行。同时由于存储过程编写好之后,对于客户端而言是黑盒的,因此减小了SQL被暴露的风险。
缺点:
- CPU开销大:如果一个存储过程中涉及大量逻辑运算工作,会导致MySQL所在的服务器CPU飙升,因而会影响正常业务的执行,有可能导致MySQL在线上出现抖动,毕竟MySQL在设计时更注重的是数据存储和检索,对于计算性的任务并不擅长。
- 内存占用高:为了尽可能的提升执行效率,因此当一个数据库连接反复调用某个存储过程后,MySQL会直接将该存储过程的机器码放入到连接的线程私有区中,当MySQL中的大量连接都在频繁调用存储过程时,这必然会导致内存占用率同样飙升。
- 维护性差:一方面是过于复杂的存储过程,普通的后端开发人员很难看懂,毕竟存储过程类似于一门新的语言,不同语言之间跨度较大。另一方面是很少有数据库的存储过程支持Debug调试,MySQL的存储过程就不支持,这也就意味着Bug出现时,无法像应用程序那样正常调试排查,必须得采取"人肉排查"模式,即一步步拆解存储过程并排查。
7.1.2 存储过程创建与调用
- 语法 :
sql
# 1.定义
delimiter 自定义结束符号
create procedure 储存名([IN | OUT | INOUT]参数名 类型...)
begin
SQL语句
end 自定义结束符号
delimiter ;
# 2.调用:
call 存储过程名(实参列表)
# 注1:自定义符号可以用除了分号的符号,一般用$$ 或 //
# 注2:存储过程的参数形式:
IN 输入参数
OUT 输出参数
INOUT 输入输出参数
- 在MySQL的存储过程中,delimiter的作用是改变执行语句的分隔符。
- 在MySQL中,delimiter是用于指定命令是否结束的符号,默认情况下是分号
;
。- 使用delimiter命令来更改默认的分隔符,例如将分隔符更改为
//
或$$
,这样MySQL就会将//
或$$
之后的分号视为代码块的结束,而不是单个语句的结束。- 这种做法在定义存储过程、函数或其他数据库对象时特别有用,因为它允许开发者在一个代码块中编写多个语句,而不需要为每个语句后都加上分号。当代码块编写完成后,可以通过将delimiter命令设置回默认的分号来恢复正常的语句执行方式。
- 示例
sql
mysql> use mydb2_stuinfo;
mysql> delimiter $$
mysql> mysql> create procedure proc01()
-> begin
-> select name,sex,age from student1;
-> end $$
mysql> delimiter ; # 注意空格
mysql> call proc01();
sql
# 使用存储过程,插如多条数据
mysql> create table passwd(id int,pwds varchar(50));
mysql> delimiter $$
mysql> create procedure proc02() begin declare i int default 1; while(i<10000) do insert into passwd values(i, md5(i)); set i=i+1; end while; end $$
mysql> delimiter ;
mysql> call proc02();
mysql> select * from passwd;
- 删除格式
sql
drop procedure 过程名;
# 示例
mysql> drop procedure proc01; # 不用加园括号
7.1.3 变量
局部变量
- 局部变量: 用户自定义,在begin/end块中有效,格式:
sql
# 声明变量
declare var_name type [default var_value];
# 举例
declare nickname varchar(32);
- 示例
sql
mysql> delimiter $$
# 定义局部变量
mysql> create procedure proc03() begin declare var_name01 varchar(20) default 'aaa'; set var_name01 = 'zhangsan'; select var_name01; end $$
mysql> delimiter ;
mysql> call proc03();
+------------+
| var_name01 |
+------------+
| zhangsan |
+------------+
- MySQL 中还可以使用 SELECT...INTO 语句为变量赋值。其基本语法如下:
sql
# 格式:
select col_name [...] into var_name[,...]
from table_name where condition
# 其中:
col_name 参数表示查询的字段名称;
var_name 参数是变量的名称;
table_name 参数指表的名称;
condition 参数指查询条件。
# 注意:当将查询结果赋值给变量时,该查询语句的返回结果只能是单行单列。
- 示例
sql
mysql> delimiter $$
mysql> create procedure proc04() begin declare var_name02 varchar(20) ; select name into var_name02 from student1 where id=1003; select var_name02; end $$
mysql> delimiter ;
mysql> call proc04();
+------------+
| var_name02 |
+------------+
| 李四 |
+------------+
用户变量
- 用户变量:用户自定义,当前会话(连接)有效。类比java的成员变量,格式
mysql
@var_name
# 不需要提前声明,使用即声明,即无declare子句
- 示例
sql
mysql> delimiter $$
mysql> create procedure proc05() begin set @var_name03 = 'openlab'; end $$
mysql> delimiter ;
mysql> call proc05() ;
mysql> select @var_name03 ;
+-------------+
| @var_name03
+-------------+
| openlab |
+-------------+
系统变量
- 系统变量:
-
系统变量又分为全局变量 与会话变量
-
在MYSQL启动的时候由服务器自动将它们初始化为默认值,这些默认值可以通过更改my.ini这个文件来更改。
-
会话变量在每次建立一个新的连接的时候,由MYSQL来初始化。MYSQL会将当前所有全局变量的值复制一份。来做为会话变量。
-
也就是说,如果在建立会话以后,没有手动更改过会话变量与全局变量的值,那所有这些变量的值都是一样的。
-
全局变量与会话变量的区别就在于,对全局变量的修改会影响到整个服务器 ,但是对会话变量的修改,只会影响到当前的会话(也就是当前的数据库连接)。
-
有些系统变量的值是可以利用语句来动态进行更改的,但是有些系统变量的值却是只读的,对于那些可以更改的系统变量,我们可以利用set语句进行更改。
-
系统变量-全局变量:由系统提供,在整个数据库有效
-
mysql
语法:
@@global.var_name
- 示例
sql
# 查看全局变量
show global variables;
# 查看某全局变量
select @@global.auto_increment_increment;
# 修改全局变量的值
set global sort_buffer_size = 40000;
set @@global.sort_buffer_size = 40000;
注意:
global 与 @@global 等价,都是系统全局变量
SET global 用于更改全局系统变量的值,影响所有客户端连接 SET
@@GLOBAL一般用于查看全局变量的当前值
- 系统变量-会话变量:由系统提供,当前会话(连接)有效,格式
mysql
# 语法:
@@session.var_name
- 示例
sql
# 查看会话变量
show session variables;
# 查看某会话变量
select @@session.auto_increment_increment;
# 修改会话变量的值
set session sort_buffer_size = 50000;
set @@session.sort_buffer_size = 50000 ;
- mysql变量种类总结:
- 用户变量:以"@"开始,形式为"@变量名"。用户变量跟mysql客户端是绑定的,设置的变量,只对当前用户使用的客户端生效
- 全局变量:定义时,以如下两种形式出现,set GLOBAL 变量名 或者 set @@global.变量名,对所有客户端生效。只有具有super权限才可以设置全局变量
- 会话变量:只对连接的客户端有效。
- 局部变量:作用范围在begin到end语句块之间。在该语句块里设置的变量。declare语句专门用于定义局部变量。
- 可以使用set语句是设置不同类型的变量,包括会话变量和全局变量
7.1.4 参数传递
- IN:表示传入 的参数, 可以传入数值或者变量,即使传入变量,并不会更改变量的值,可以内部更改,仅仅作用在函数范围内。
sql
# 接着passwd表的例子
mysql> delimiter $$
mysql> create procedure proc06(in a int) begin declare i int default 1; while(i<=a) do insert into passwd values(i,md5(i)); set i=i+1; end while; end $$
mysql> delimiter ;
mysql> set @num=100; # 定义用户变量
mysql> select @num; # 查看
+------+
| @num |
+------+
| 100 |
+------+
mysql> call proc06(@num); # 执行过程,传递变量值到过程中
mysql> select * from passwd;
- out:表示从存储过程内部传值给调用者,in的反向传递
sql
mysql> delimiter $$
mysql> create procedure proc07(out cnt1 int) begin select count(*) into cnt1 from passwd; end $$
mysql> delimiter ;
mysql> call proc07(@cnt2);
mysql> select @cnt2;
+-------+
| @cnt2 |
+-------+
| 10099 |
+-------+
sql
# in和out的综合使用
# 例1:统计指定部门的员工数
mysql> use mydb7_openlab;
mysql> select * from dept;
+-------+-----------+
| dept1 | dept_name |
+-------+-----------+
| 101 | 财务 |
| 102 | 销售 |
| 103 | 运维 |
| 104 | 行政 |
+-------+-----------+
4 rows in set (0.00 sec)
mysql> select * from emp_new;
+------+------+------+----------------+----------+-------+
| sid | name | age | worktime_start | incoming | dept2 |
+------+------+------+----------------+----------+-------+
| 1789 | 张三 | 35 | 1980-01-01 | 4000 | 101 |
| 1674 | 李四 | 32 | 1983-04-01 | 3500 | 101 |
| 1776 | 王五 | 24 | 1990-07-01 | 2000 | 101 |
| 1568 | 赵六 | 57 | 1970-10-11 | 7500 | 102 |
| 1564 | 荣七 | 64 | 1963-10-11 | 8500 | 102 |
| 1879 | 牛八 | 55 | 1971-10-20 | 7300 | 103 |
+------+------+------+----------------+----------+-------+
mysql> delimiter $$
mysql> create procedure proc08(in d_num int, out cnt1 int) begin select count(*) into cnt1 from emp_new where dept2=d_num; end$$
mysql> delimiter ;
mysql> call proc08(101,@cnt2);
mysql> select @cnt2;
# 例2:统计指定部门工资超过例如5000的总人数
mysql> delimiter $$
mysql> create procedure proc09(in d_num int, in wages int, out cnt1 int) begin select count(*) into cnt1 from emp_new where dept2=d_num and incoming>=wages; end$$
mysql> delimiter ;
mysql> call proc09(102,5000,@wa);
mysql> select @wa;
+------+
| @wa |
+------+
| 2 |
+------+
- inout:
sql
mysql> delimiter $$
mysql> create procedure proc10(inout p1 int) begin if (p1 is not null) then set p1=p1+1; else select 100 into p1; end if; end$$
mysql> delimiter ;
mysql> select @h;
+------+
| @h |
+------+
| NULL |
+------+
mysql> call proc10(@h);
mysql> select @h;
+------+
| @h |
+------+
| 100 |
+------+
- 总结:
- in :输入参数,意思说你的参数要传到存过过程的过程里面去,在存储过程中修改该参数的值不能被返回
- out :输出参数:该值可在存储过程内部被改变,并向外输出
- inout :输入输出参数,既能输入一个值又能传出来一个值
7.1.5 存储过程的管理
-
通过以下命令管理存储过程
SHOW PROCEDURE STATUS; # 查看当前数据库中的所有存储过程。
SHOW PROCEDURE STATUS WHERE db = '库名' AND NAME = '过程名'; # 查看指定库中的某个存储过程。
SHOW CREATE PROCEDURE 存储过程名; # 查看某个存储过程的源码。
ALTER PROCEDURE 存储过程名称 .... # 修改某个存储过程的特性。
DROP PROCEDURE 存储过程名; # 删除某个存储过程。
7.1.6 存储过程的应用场景
- 存储过程难以维护,同时拓展性和移植性都很差,因此大多数的开发规范中都会明令禁止使用,如存储过程在《阿里开发手册》中是强制禁止使用的,它是一把双刃剑,用的好其实能够带来不小的收益,推荐在以下场合使用:
- 插入测试数据时,一般为了测试项目,都会填充测试数据,往常是写Java-for跑数据,但现在可以用存储过程来批量插入,它的效率会比用for循环快上无数倍,毕竟从Java传递SQL需要时间,拿到SQL后还要经过解析、优化...一系列工作,而用存储过程则不会有这些问题。
- 对数据做批处理时,也可以用存储过程来跑,比如将一个表中的数据洗到另外一张表时,就可以利用存储过程来处理。
- 一条SQL无法完成的、需要应用程序介入处理的业务,尤其是组成起来SQL比较长时,也可以编写一个存储过程,然后客户端调用即可。
7.2 流程控制
7.2.1 if判断
-
IF语句包含多个条件判断,根据结果为TRUE、FALSE执行语句,与编程语言中的if、else if、else语法类似,其语法格式如下:
if 条件判断
then
-- 分支操作.....
elseif 条件判断
then
-- 分支操作.....
else
-- 分支操作.....
end if -
例1:定义了一个判断年龄的存储过程
sql
mysql> delimiter $$
mysql> create procedure if_user_age(in age int,out msg varchar(255))
begin
if age <18
then
set msg :='未成年';
elseif age =18
then
set msg :='刚成年';
else
set msg :='已成年';
end if;
end $$
mysql> delimiter ;
mysql> set @msg:="Not Data";
mysql> call if_user_age(16,@msg);
mysql> select @msg;
+--------+
| @msg |
+--------+
| 未成年 |
+--------+
- 例2:输入学生的成绩,来判断成绩的级别
sql
/*
score < 60 :不及格
score >= 60 , score <80 :及格
score >= 80 , score < 90 :良好
score >= 90 , score <= 100 :优秀
score > 100 : 成绩错误
*/
mysql> delimiter $$
mysql> create procedure proc11_if(in score int)
begin
if score < 60
then
select '不及格';
elseif score < 80
then
select '及格' ;
elseif score >= 80 and score < 90
then
select '良好';
elseif score >= 90 and score <= 100
then
select '优秀';
else
select '成绩错误';
end if;
end $$
mysql> delimiter ;
mysql> call proc11_if(120);
mysql> call proc11_if(86);
- 例3:输入员工的名字,判断工资的情况。
sql
mysql> use mydb7_openlab;
mysql> select * from emp_new;
+------+------+------+----------------+----------+-------+
| sid | name | age | worktime_start | incoming | dept2 |
+------+------+------+----------------+----------+-------+
| 1789 | 张三 | 35 | 1980-01-01 | 4000 | 101 |
| 1674 | 李四 | 32 | 1983-04-01 | 3500 | 101 |
| 1776 | 王五 | 24 | 1990-07-01 | 2000 | 101 |
| 1568 | 赵六 | 57 | 1970-10-11 | 7500 | 102 |
| 1564 | 荣七 | 64 | 1963-10-11 | 8500 | 102 |
| 1879 | 牛八 | 55 | 1971-10-20 | 7300 | 103 |
+------+------+------+----------------+----------+-------+
mysql> delimiter $$
mysql> create procedure proc12_if(in in_ename varchar(50))
begin
declare result varchar(20);
declare var_sal int;
select incoming into var_sal from emp_new where name = in_ename;
if var_sal < 3000
then set result = '试用薪资';
elseif var_sal<8000
then set result = '转正薪资';
else
set result = '元老薪资';
end if;
select result;
end$$
mysql> delimiter ;
mysql> call proc12_if('赵六');
mysql> call proc12_if('荣八');
7.2.2 case判断
-
case是另一个条件判断的语句,类似于C编程语言中的switch语法
-
格式:
语法1
case变量
when值1 then
-- 分支操作1....
when值2 then
-- 分支操作2....
.....
else
-- 分支操作n....
endcase;语法2
case
when 条件判断1 then
-- 分支操作1....
when 条件判断2 then
-- 分支操作2....
.....
else
-- 分支操作n....
endcase; -
例1
sql
mysql> delimiter $$
mysql> create procedure proc13_case(in pay_type int)
begin
case pay_type
when 1 then select '微信支付' ;
when 2 then select '支付宝支付' ;
when 3 then select '银行卡支付';
else select '其他方式支付';
end case ;
end $$
mysql> delimiter ;
mysql> call proc13_case(2);
mysql> call proc13_case(4);
- 例2
sql
mysql> delimiter $$
mysql> create procedure proc14_case(in score int)
begin
case
when score < 60 then select '不及格';
when score < 80 then select '及格' ;
when score >= 80 and score < 90 then select '良好';
when score >= 90 and score <= 100 then select '优秀';
else select '成绩错误';
end case;
end $$
mysql> delimiter ;
mysql> call proc14_case(88);
mysql> call proc14_case(120);
7.2.3 循环
- 循环分类:
- while
- repeat
- loop
- 循环控制:
- leave 类似于 break,跳出,结束当前所在的循环
- iterate 类似于 continue,继续,结束本次循环,继续下一次

while循环格式
[标签:]while 循环条件 do
循环体;
end while[标签];
sql
mysql> select database();
+---------------+
| database() |
+---------------+
| mydb7_openlab |
+---------------+
# 创建测试表
mysql> create table user (uid int primary key,username varchar(50),password varchar(50));
# 存储过程-while
mysql> delimiter $$
mysql> create procedure proc15_while1(in insertcount int)
begin
declare i int default 1;
label:while i<=insertcount do
insert into user(uid,username,password) values(i,concat('user-',i),'123456');
set i=i+1;
end while label;
end $$
mysql> delimiter ;
mysql> call proc15_while1(10);
mysql> select * from user;
+-----+----------+----------+
| uid | username | password |
+-----+----------+----------+
| 1 | user-1 | 123456 |
| 2 | user-2 | 123456 |
| 3 | user-3 | 123456 |
| 4 | user-4 | 123456 |
| 5 | user-5 | 123456 |
| 6 | user-6 | 123456 |
| 7 | user-7 | 123456 |
| 8 | user-8 | 123456 |
| 9 | user-9 | 123456 |
| 10 | user-10 | 123456 |
+-----+----------+----------+
# 存储过程-while + leave
mysql> truncate table user; # 清空表内容
mysql> delimiter $$
mysql> create procedure proc16_while2(in insertcount int)
begin
declare i int default 1;
label:while i<=insertcount do
insert into user(uid,username,`password`) values(i,concat('user-',i),'123456');
if i=5
then
leave label;
end if;
set i=i+1;
end while label;
end $$
mysql> delimiter ;
mysql> call proc16_while2(10);
mysql> select * from user;
+-----+----------+----------+
| uid | username | password |
+-----+----------+----------+
| 1 | user-1 | 123456 |
| 2 | user-2 | 123456 |
| 3 | user-3 | 123456 |
| 4 | user-4 | 123456 |
| 5 | user-5 | 123456 |
+-----+----------+----------+
# 存储过程-while+iterate
mysql> truncate table user;
mysql> delimiter $$
mysql> create procedure proc17_while3(in insertcount int)
begin
declare i int default 1;
label:while i<=insertcount do
set i=i+1;
if i=5
then
iterate label;
end if;
insert into user(uid,username,`password`) values(i,concat('user-',i),'123456');
end while label;
end $$
mysql> delimiter ;
mysql> call proc17_while3(10);
mysql> select * from user; # 没有第五条记录
+-----+----------+----------+
| uid | username | password |
+-----+----------+----------+
| 2 | user-2 | 123456 |
| 3 | user-3 | 123456 |
| 4 | user-4 | 123456 |
| 6 | user-6 | 123456 |
| 7 | user-7 | 123456 |
| 8 | user-8 | 123456 |
| 9 | user-9 | 123456 |
| 10 | user-10 | 123456 |
| 11 | user-11 | 123456 |
+-----+----------+----------+
repeat循环格式
[标签:]repeat
循环体;
until 条件表达式
end repeat [标签];
sql
# 存储过程-循环控制-repeat
mysql> truncate table user;
mysql> delimiter $$
mysql> create procedure proc18_repeat(in insertCount int)
begin
declare i int default 1;
label:repeat
insert into user(uid, username, password) values(i,concat('user-',i),'123456');
set i = i + 1;
until i > insertCount
end repeat label;
select '循环结束';
end $$
mysql> delimiter ;
mysql> call proc18_repeat(100);
loop循环格式
[标签:] loop
循环体;
if 条件表达式
then
leave [标签];
end if;
end loop;
sql
# 存储过程-循环控制-loop
mysql> truncate table user;
mysql> delimiter $$
mysql> create procedure proc19_loop(in insertCount int)
begin
declare i int default 1;
label:loop
insert into user(uid, username, password) values(i,concat('user-',i),'123456');
set i = i + 1;
if i > 5
then
leave label;
end if;
end loop label;
select '循环结束';
end $$
mysql> delimiter ;
mysql> call proc19_loop(10);
7.3 触发器
触发器(trigger)是一个特殊的存储过程 ,存储过程需要人为手动调用,而触发器是由事件
来触发
触发器经常用于加强数据的完整性约束和业务规则等
7.3.1 创建触发器
-
语法结构:
1、创建只有一个执行语句的触发器
create trigger 触发器名 before|after 触发事件
on 表名 for each row
执行语句;2、创建有多个执行语句的触发器
create trigger 触发器名 before|after 触发事件
on 表名 for each row
begin
执行语句列表
end;说明:
<触发器名称> :最多64个字符,它和MySQL中其他对象的命名方式一样 { before | after } :触发器时机 { insert | update | delete } :触发的事件 on <表名称> :标识建立触发器的表名,即在哪张表上建立触发器 for each row :触发器的执行间隔,通知触发器每隔一行执行一次动作,而不是对整个表执行一次 <触发器程序体> :要触发的SQL语句,可用顺序,判断,循环等语句实现一般程序需要的逻辑功能
注意:每个触发器创建后,必然是附着在一张表上 的,因为在创建触发器的时候必须要指定表名,它会监控这张表上发生的事件。
7.3.2 NEW与OLD
- MySQL 中定义了 NEW 和 OLD,用来表示触发器的所在表中,触发了触发器的那一行数据,来引用触发器中发生变化的记录内容,具体地:
触发器类型 | 触发器类型NEW 和OLD 的使用 |
---|---|
INSERT 型触发器 | NEW 表示将要 或者已经新增的数据 |
UPDATE 型触发器 | OLD 表示修改之前的数据 , NEW 表示将要或已经修改后的数据 |
DELETE 型触发器 | OLD 表示将要 或者已经删除的数据 |
使用方法: NEW.columnName (columnName为相应数据表某一列名)
- 示例
sql
1.创建表tab1
mysql> create table tab1(id int primary key auto_increment,name varchar(50),sex enum('m','f'),age int);
2.创建表tab2
mysql> create table tab2(id int primary key auto_increment,name varchar(50),salary double(10,2));
3.创建触发器
(1).创建触发器tab1_after_insert_trigger 作用:当tab1增加记录后,自动增加到tab2
mysql> create trigger tab1_after_insert_trigger after insert on tab1 for each row insert into tab2 values(id, new.name, 5000);
(3).创建触发器tab1_after_update_trigger ,作用:当tab1更新后,自动更新tab2
mysql> create trigger tab1_after_update_trigger after update on tab1 for each row update tab2 set name=new.name where name=old.name;
(2).创建tab1_after_delete_trigger触发器,作用:tab1表删除记录后,自动将tab2表中对应记录删除
mysql> create trigger tab1_after_delete_trigger after delete on tab1 for each row delete from tab2 where name=old.name;
(4).查看触发器
mysql> show triggers\G
(5)测试:
mysql> insert into tab1 values(1,"张三",'m',22);
Query OK, 1 row affected (0.01 sec)
mysql> select * from tab1;
+----+------+------+------+
| id | name | sex | age |
+----+------+------+------+
| 1 | 张三 | m | 22 |
+----+------+------+------+
1 row in set (0.00 sec)
mysql> select * from tab2;
+----+------+---------+
| id | name | salary |
+----+------+---------+
| 1 | 张三 | 5000.00 |
+----+------+---------+
1 row in set (0.00 sec)
mysql> update tab1 set name='李四' where name='张三';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from tab1;
+----+------+------+------+
| id | name | sex | age |
+----+------+------+------+
| 1 | 李四 | m | 22 |
+----+------+------+------+
1 row in set (0.00 sec)
mysql> select * from tab2;
+----+------+---------+
| id | name | salary |
+----+------+---------+
| 1 | 李四 | 5000.00 |
+----+------+---------+
1 row in set (0.00 sec)
mysql> delete from tab1 where name='李四';
Query OK, 1 row affected (0.01 sec)
mysql> select * from tab1;
Empty set (0.00 sec)
mysql> select * from tab2;
Empty set (0.00 sec)
注意:
- MYSQL中触发器中不能对本表进行 insert ,update ,delete 操作,以免递归循环触发
- 尽量少使用触发器,假设触发器触发每次执行1s,insert table 500条数据,那么就需要触发500次触发器,光是触发器执行的时间就花费了500s,而insert 500条数据一共是1s,那么这个insert的效率就非常低了。
- 触发器是针对每一行的;对增删改非常频繁的表上切记不要使用触发器,因为它会非常消耗资源。
7.4 存储函数
MySQL存储函数(自定义函数)一般用于计算和返回一个值,可以将经常需要使用的计算或功能写成一个函数。函数和存储过程类似
-
存储函数与存储过程的区别
- 存储函数有且只有一个返回值,而存储过程可以有多个返回值,也可以没有返回值。
- 存储函数只能有输入参数,而且不能带in, 而存储过程可以有多in,out,inout参数。
- 存储过程中的语句功能更强大,存储过程可以实现很复杂的业务逻辑,而函数有很多限制,如不能在函数中使用insert,update,delete,create等语句;
- 存储函数只完成查询 的工作,可接受输入参数并返回一个结果 ,也就是函数实现的功能针对性比较强。
- 存储过程可以调用存储函数。但函数不能调用存储过程。
- 存储过程一般是作为一个独立的部分来执行(call调用)。而函数可以作为查询语句的一个部分来调用。
7.4.1 存储函数创建和调用(create function)
创建存储函数,在MySQL中,创建存储函数使用 create function 关键字,其基本形式如下:
create function func_name ([param_name type[,...]])
returns type
[characteristic ...]
begin
routine_body
END;
# 参数说明:
(1)func_name :存储函数的名称。
(2)param_name type:可选项,指定存储函数的参数。type参数用于指定存储函数的参数类型,该类型可以是MySQL数据库中所有支持的类型。
(3)returns type:指定返回值的类型。
(4)characteristic:可选项,指定存储函数的特性。
(5)routine_body:SQL代码内容。
-
调用存储函数
-
在MySQL中,存储函数的使用方法与MySQL内部函数的使用方法基本相同
-
用户自定义的存储函数与MySQL内部函数性质相同。
-
区别在于,存储函数是用户自定义的。而内部函数由MySQL自带。其语法结构如下:
select func_name([parameter[,...]]);
-
-
例1 无参数存储函数
sql
mysql> set global log_bin_trust_function_creators=TRUE; # 信任子程序的创建者,否则可能报错,执行一次即可
# 创建存储函数-没有输输入参数
mysql> delimiter $$
mysql> create function myfunc1_emp()
returns int
begin
declare cnt int default 0;
select count(*) into cnt from emp_new;
return cnt;
end $$
mysql> delimiter ;
# 调用存储函数
mysql> select myfunc1_emp();
+---------------+
| myfunc1_emp() |
+---------------+
| 6 |
+---------------+
- 例2:有参数存储函数
sql
mysql> select * from emp_new;
+------+------+------+----------------+----------+-------+
| sid | name | age | worktime_start | incoming | dept2 |
+------+------+------+----------------+----------+-------+
| 1789 | 张三 | 35 | 1980-01-01 | 4000 | 101 |
| 1674 | 李四 | 32 | 1983-04-01 | 3500 | 101 |
| 1776 | 王五 | 24 | 1990-07-01 | 2000 | 101 |
| 1568 | 赵六 | 57 | 1970-10-11 | 7500 | 102 |
| 1564 | 荣七 | 64 | 1963-10-11 | 8500 | 102 |
| 1879 | 牛八 | 55 | 1971-10-20 | 7300 | 103 |
+------+------+------+----------------+----------+-------+
mysql> delimiter $$
mysql> create function myfunc2_emp(in_sid int)
returns varchar(50)
begin
declare out_name varchar(50);
select name into out_name from emp_new where sid = in_sid;
return out_name;
end $$
mysql> delimiter ;
mysql> select myfunc2_emp(1776);
+-------------------+
| myfunc2_emp(1776) |
+-------------------+
| 王五 |
+-------------------+
7.4.2 删除存储函数(drop function)
-
MySQL中使用 drop function 语句来删除存储函数。
-
示例:删除存储函数。
sql
mysql> drop function if exists myfunc1_emp;
mysql> drop function if exists myfunc2_emp;
八 MySQL事务
事务(Transaction):一个最小的不可再分的工作单元,一个事务对应一个完整的业务 ,一个完整的业务需要批量的DML(insert、update、delete)语句共同联合完成,事务只针对DML
语句(Data Manipulation Language(数据操作语言)eg.insert、update、delete)
- 数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位,事务由事务开始与事务结束之间执行的全部数据库操作组成

- mysql中,只有Innodb是支持事务
sql
mysql> show engines; # 查看Mysql的所有执行引擎可以到,默认的执行引擎是innoDB 支持事务,行级锁定和外键。

-
示例
- 以银行转账为例,账户转账是一个完整的业务,最小的单元,不可再分------也就是说银行账户转账是一个事务:

sql
update account set money = money - 200 where id = 1;
update account set money = money + 200 where id = 2;
- 分析
- 以上两个DML语句必须同时成功或者同时失败
- 最小单元不可再分,当第一条DML语句执行成功后,并不能将底层数据库中的第一个账户的数据修改,只是将操作记录了一下,这个记录是在内存中完成的
- 当第二条DML语句执行成功后,和底层数据库文件中的数据完成同步
- 若第二条DML语句执行失败,则清空所有的历史操作记录,要完成以上的功能必须借助事务
8.1 数据库事务处理原则
- 保证所有事务都作为一个工作单元来执行,即使出现了故障,都不能改变这种执行方式
- 当在一个事务中执行多个操作时,要么所有的事务都被提交(commit),那么这些修改就永久地保存下来
- 要么数据库管理系统将放弃所作的所有修改,整个事务回滚(rollback)到最初状态。
- 为确保数据库中数据的一致性 ,数据的操纵应当是离散的成组的逻辑单元:当它全部完成时,数据的一致性可以保持,而当这个单元中的一部分操作失败,整个事务应全部视为错误,所有从起始点以后的操作应全部回退到开始状态
8.2 事务的特性(面试)-ACID
8.2.1 原子性(Atomicity)
- 原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生,保证事务的整体性
例:zhangsan给lisi转帐200元钱的时候只执行了扣款语句,就提交了,此时如果突然断电,zhangsan账号已经发生了扣款,lisi账号却没收到加款,在生活中就会引起纠纷。这种情况就需要事务的原子性来保证事务要么都执行,要么就都不执行
8.2.2 一致性(Consistency)
- 一致性是指事务必须使数据库从一个一致性状态变换到另外一个一致性状态
- 分析
- 当事务完成时,数据必须处于一致状态
- 在事务开始前,数据库中存储的数据处于一致状态
- 在正在进行的事务中,数据可能处于不一致的状态
- 当事务成功完成时,数据必须再次回到一致状态
例:对银行转帐事务,不管事务成功还是失败,应该保证事务结束后表中zhangsan和lisi的存款总额跟事务执行前一致,如:zhangsan的账户有1000元,lisi的账户有1000元,现在zhangsan转账200元给lisi,不管事务成功还是失败,转账前后zhangsan和lisi的存款总额都应该是2000元
8.2.3 隔离性(Isolation)
- 隔离性是多个用户并发访问数据库 时,数据库为每一个用户开启的事务 ,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离,每个事务都有各自的完整数据空间
8.2.4 持久性(Durability)
- 持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响

- 注意:ACID面试常考
8.3 MySQL使用事务
8.3.1 MySQL的事务操作主要有以下三种:
- 1.开启事务:start transaction
- 任何一条DML语句(insert、update、delete)执行,标志事务的开启
- 命令:begin 或 start transaction
- 2.提交事务:commit transaction
- 成功的结束,将所有的DML语句操作历史记录和底层硬盘数据来一次同步
- 命令:commit
- 3.回滚事务:rollback transaction
- 失败的结束,将所有的DML语句操作历史记录全部清空
- 命令:rollback
8.3.2 使用事务的方法
方式1 | 方法2 | 方法3 | 方法4 |
---|---|---|---|
begin | start transaction | set autocommit=1 | set autocommit=0 |
sql语句 | sql语句 | sql语句 | sql语句 |
commit | rollback | commit | rollback | commit | rollback |
分析:
- 开始事务:begin 或 start transaction ,注意:mysql5.5 以上的版本,不需要手工书写begin,只要你执行的是一个DML,会自动在前面加一个begin命令,此方法又称为显示开启事务
- MySQL中用 autocommit参数设置事务是否自动提交
- commit : 手动提交事务,一旦事务提交成功 ,就说明具备ACID特性了
- rollback :回滚事务,将内存中,已执行过的操作,回滚回去
注意
- 事务是基于当前数据库连接而言的,而不是基于表,一个事务可以由操作不同表的多条SQL组成
下面画出了两个数据库连接,假设连接A
中开启了一个事务,那后续过来的所有SQL
都会被加入到一个事务中,也就是图中连接A
,后面的SQL②、SQL③、SQL④、SQL⑤
这四条都会被加入到一个事务中,只要在未曾收到commit/rollback
命令之前,这个连接来的所有SQL
都会加入到同一个事务中,因此对于这点要牢记,开启事务后一定要做提交或回滚处理。
8.3.3 自动提交策略
MySQL默认已经开启自动提交,可以通过对应的设置来开启或者关闭自动提交
- 查看自动提交设置
sql
mysql> select @@autocommit; # @@用于查看全局变量
mysql> show variables like 'autocommit'; # 或者
- 设置自动提交
sql
mysql> set autocommit=0; # 针对当前连接会话
mysql> set global autocommit=0; # 针对全局事务
# 可以在C:\ProgramData\MySQL\MySQL Server 8.0\my.ini配置文件中增加一行
autocommit=0
注意:一般在有事务需求的MySQL中,将其关闭,使用begin手动开启,所以不管有没有事务需求,都建议设置为0,可以很大程度上提高数据库性能
- 示例1
- 测试begin事务的一致性
sql
mysql> create database mydb17_transcation;
mysql> use mydb17_transcation;
mysql> create table account (id int(10) primary key not null, name varchar (40), money double);
mysql> insert into account values(1, '张三', 1000);
mysql> insert into account values(2, '李四', 2000);
mysql> begin;
mysql> update account set money=3000 where id=2;
mysql> select * from account;
+----+--------+-------+
| id | name | money |
+----+--------+-------+
| 1 | 张三 | 1000 |
| 2 | 李四 | 3000 |
+----+--------+-------+
mysql> exit; # 断开连接
Bye
PS C:\Users\Administrator> mysql -uroot -p123456
mysql> use mydb17_transcation;
mysql> show tables;
mysql> select * from account;
+----+--------+-------+
| id | name | money |
+----+--------+-------+
| 1 | 张三 | 1000 |
| 2 | 李四 | 2000 | # 发现未提交事务,数据没变
+----+--------+-------+
sql
# 手动提交事务
mysql> begin;
mysql> update account set money=3000 where id=2;
mysql> commit; # 提交事务
mysql> exit;
Bye
PS C:\Users\Administrator> mysql -uroot -p123456
mysql> use mydb17_transcation;
mysql> select * from account;
+----+--------+-------+
| id | name | money |
+----+--------+-------+
| 1 | 张三 | 1000 |
| 2 | 李四 | 3000 | # 内容改变
+----+--------+-------+
sql
# 回滚事务
mysql> select * from account;
+----+--------+-------+
| id | name | money |
+----+--------+-------+
| 1 | 张三 | 1000 |
| 2 | 李四 | 3000 |
+----+--------+-------+
2 rows in set (0.00 sec)
mysql> begin;
mysql> delete from account where id=1; # 删除记录
mysql> select * from account;
+----+--------+-------+
| id | name | money |
+----+--------+-------+
| 2 | 李四 | 3000 |
+----+--------+-------+
1 row in set (0.01 sec)
mysql> rollback; # 回滚事务
mysql> select * from account;
+----+--------+-------+
| id | name | money |
+----+--------+-------+
| 1 | 张三 | 1000 |
| 2 | 李四 | 3000 |
+----+--------+-------+
- 示例2
- 关闭自动提交
sql
mysql> select @@autocommit;
+--------------+
| @@autocommit |
+--------------+
| 1 |
+--------------+
mysql> set autocommit=0;
mysql> select @@autocommit;
+--------------+
| @@autocommit |
+--------------+
| 0 |
+--------------+
mysql> insert into account values(3, '王五', 3000); # 省略begin
mysql> commit; # 提交
mysql> select * from account;
+----+--------+-------+
| id | name | money |
+----+--------+-------+
| 1 | 张三 | 1000 |
| 2 | 李四 | 3000 |
| 3 | 王五 | 3000 |
+----+--------+-------+
8.3.4 设置回滚点
-
作用:可以创建一个或多个回滚点,将事务回滚到标记点
-
格式
savepoint 回滚点名
rollback to 回滚点
注意:手动设置回滚点,需要关闭事务自动提交,若自动提交已开启,则语句执行后代表事务已提交,所有回滚点都会删除,此时执行手动回滚会报错,即:
ERROR 1305 (42000): SAVEPOINT ** does not exist
-
示例
sql
mysql> exit
PS C:\Users\Administrator> mysql -uroot -p123456
mysql> use mydb17_transcation;
mysql> select @@autocommit;
+--------------+
| @@autocommit |
+--------------+
| 1 |
+--------------+
1 row in set (0.00 sec)
mysql> set autocommit=0; # 关闭自动提交
mysql> select * from account;
mysql> savepoint s1; # 创建1个回滚点
mysql> update account set money=money+1000 where id=1; # 金额+1000
mysql> select * from account;
mysql> rollback to s1; # 回滚到s1
mysql> select * from account;
8.3.5 事务的隐式提交
概念:因为某些特殊的语句而导致事务自动提交的情况称为隐式提交
- 那些事务不会自动提交
- 使用start transaction开启事务
- 使用begin开启事务
- 把系统变量autocommit的值设置为OFF | 0
- 常见隐式提交

- 示例
sql
mysql> exit
PS C:\Users\Administrator> mysql -uroot -p123456
mysql> use mydb17_transcation;
mysql> select @@autocommit;
+--------------+
| @@autocommit |
+--------------+
| 1 |
+--------------+
mysql> insert into account values(4, '刘七', 4000);
mysql> insert into account values(5, '孙鹏', 5000);
select * from account;
mysql> rollback; # 隐式提交后回滚
mysql> select * from account; # 未变化
- 注意
- 在MySQL中,为了避免隐式提交造成的隐患,建议使用显式提交操作,即通过手动执行COMMIT或ROLLBACK来明确事务的完成或回滚
- sql查询操作会在事务的上下文中执行,但是它并没有对数据进行写的操作,所以事务不会持有任何锁,在查询结束后会立即提交,因此,InnoDB所有的操作都是在事务中进行的
8.4 InnoDB 事务的ACID如何保证
8.4.1 WAL技术
-
redo log又叫"重做日志",是存储引擎层 (innoDB) 生成的日志,记录的是"物理级别"上的页修改操作,比如页号x,偏移量y写入了'z'数据,主要目的为了保证数据不丢失,当MySQL发生宕机的时候,可以利用redo log日志进行数据恢复,用于确保事务的持久性
-
一条SQL更新语句怎么运行:
- 当有一条记录要更新时,InnoDB 引擎就会先把记录写到 redo log磁盘文件中,此时更新就算结束了
- 在系统空闲的时候,将上述操作记录更新到磁盘文件中
eg.比如:小店做生意,有个粉板,有个账本,来客点餐了先写粉板,等不忙的时候再写账本
- 性能不够,缓存来凑,由于CPU的性能远远大于磁盘,为了消除这个鸿沟,引入了两个缓存
- Buffer Pool :缓冲池,包含了磁盘中部分数据页的映射,作为访问数据库的缓冲
- redo log buffer:redo log本质是磁盘上的日志文件,为提交读写增加了缓冲区来存放重做日志
- WAL:Write-Ahead Logging,预写日志系统 ,先写日志,再写磁盘,只有日志写入成功,才算事务提交成功的技术思想在MySQL叫做WAL技术,其基本过程如下:
- 先将原始数据从磁盘中读入到Buffer Pool中
- 修改Buffer Pool中的数据
- 生成一条重做日志并写入redo log buffer,记录数据修改后的值
- 当事务提交时,将redo log buffer中的内容追加磁盘中的redo log文件中
- 将磁盘日志文件redo log file 内容刷到数据库表中(也称为"脏刷")

- 为什么为什么要"多此一举"先写入到redo log磁盘文件中,然后再脏刷到数据库表中?而不直接落到数据库表中?
- 数据在MySQL中存储是以页为单位,事务中的数据可能遍布在不同的页中,如果直接写入到对应的页中,是随机IO写入
- 磁盘顺序IO性能远高于随机IO
- redo log是通过顺序IO"追加"的方式写入到文件末尾,只包含真正需要写入的,无效 IO 减少,但刷脏页以Page为单位,一个Page上的修改整页都要写且为随机IO
8.4.2 redo log相关参数
- redo log容量查看
sql
mysql> show variables like 'innodb_redo_log_capacity';
+--------------------------+-----------+
| Variable_name | Value |
+--------------------------+-----------+
| innodb_redo_log_capacity | 104857600 | # 默认100MB
+--------------------------+-----------+
-
存储位置:共生成32个 redo log 文件,每个文件的大小等于 1/32 * innodb_redo_log_capacity,redo log 有两种:正在使用的和未被使用的,分别使用 #ib_redoNN 和 #ib_redoNN_tmp其中NN是重做日志文件编号
C:\ProgramData\MySQL\MySQL Server 8.0\Data#innodb_redo
-
查看状态
sql
mysql> show global status like '%innodb%redo%';
+-------------------------------------+------------+
| Variable_name | Value |
+-------------------------------------+------------+
| Innodb_redo_log_read_only | OFF |# 当前redo不是处于RO状态
| Innodb_redo_log_uuid | 3173314963 |# 归档用
| Innodb_redo_log_checkpoint_lsn | 635392598 |# 最新的checkpoint
| Innodb_redo_log_current_lsn | 635392598 |# 当前LSN
| Innodb_redo_log_flushed_to_disk_lsn | 635392598 |# 已刷盘的LSN
| Innodb_redo_log_logical_size | 512 |# 当前活跃事务使用log大小
| Innodb_redo_log_physical_size | 6553600 |# 当前正在使用中的log大小
| Innodb_redo_log_capacity_resized | 104857600 |# 总大小
| Innodb_redo_log_resize_status | OK |# 状态
| Innodb_redo_log_enabled | ON |
+-------------------------------------+------------+
8.4.3 MySQL CSR---前滚
-
MySQL在启动时,必须保证redo日志文件和数据文件LSN必须一致, 如果不一致就会触发CSR,最终保证一致
-
LSN 称为日志的逻辑序列号(log sequence number)在InnoDB存储引擎中,LSN占8个字节,LSN的值会随着日志的写入而逐渐变大,内容包括:
- 数据页的版本信息。
- 写入的日志总量。通过LSN开始号码和结束号码可以计算出写入的日志量。
- 可知道检查点的位置。
- ......
-
前滚过程:
1.我们做了一个事务
begin;
update ....;
commit;
2. begin时会立即分配一个TXID=tx_01.
3. update时,会将需要修改的数据页(dp_01,LSN=101),加载到data buffer中
4. DBWR线程,会进行dp_01数据页修改更新,并更新LSN=102
5. LOGBWR日志写线程,会将dp_01数据页的变化+LSN+TXID存储到redobuffer
6. 执行commit时,LGWR日志写线程会将redobuffer信息写入redolog日志文件中,基于WAL原则,在日志完全写入磁盘后,commit命令才执行成功,(会将此日志打上commit标记)
7. 假如此时宕机,内存脏页没有来得及写入磁盘,内存数据全部丢失
8. MySQL再次重启时,要求redolog和磁盘数据页的LSN是一致的,但是磁盘的值为:LSN=101,而redolog中LSN=102
9.MySQL此时无法正常启动,MySQL触发CSR.在内存追平LSN号,触发ckpt,将内存数据页更新到磁盘,从而保证磁盘数据页和redolog LSN一致,此时MySQL正常启动
10.以上的工作过程,我们把它称之为基于REDO的"前滚操作"
8.4.4 undo 回滚日志
- undo(回滚)的作用是保证事务的原子性和隔离性 ,同时支持快照技术 和数据恢复
- 作用
- 原子性(Atomicity):通过undo,可以在事务回滚时将数据恢复到修改之前的状态,确保事务要么完全执行,要么完全回滚,避免了部分操作的影响。
- 隔离性(Isolation):并发事务可能同时修改同一个数据,使用undo可以提供事务的私有数据版本,保证每个事务独立地看到修改之前的数据,从而实现隔离性。
- 快照技术:undo提供了事务修改之前的数据状态的快照,这意味着在事务执行过程中,可以回退到之前的状态。这对于实现多版本并发控制(MVCC)非常重要,每个事务可以读取数据的一致版本,而不会受到其他事务的修改影响。
- 数据恢复:undo日志记录了事务的修改操作和之前的数据状态,当系统发生故障或崩溃时,可以使用undo日志来恢复未提交的事务或未完成的操作。这种数据恢复机制对于数据库的持久性和可靠性非常重要。
8.5 事务的隔离级别
隔离级别是指多个并发事务之间的隔离程度
多个线程 开启各自事务操作数据库中数据 时,数据库系统要负责隔离 操作,以 保证各个线程在获取数据时的准确性
8.5.1 脏读
- 脏读(Dirty Read)指:一个事务读取了另外一个事务未提交的数据,在这种情况下,如果另一个事务回滚,则第一个事务读取到的数据实际上是无效的或错误的
示例如下图:
分析:上图中,
DB
连接①/事务A
正在执行下单业务,目前扣减库存、增加订单两条SQL
已经完成了,恰巧此时DB
连接②/事务B
跑过来读取了一下库存剩余数量,就将事务A
已经扣减之后的库存数量读回去了。但好巧不巧,事务A
在添加物流信息时,执行异常导致事务A
全部回滚,也就是原本扣的库存又会增加回去,这种问题就是脏读,其会导致数据不一致,进而影响整个业务系统出现问题。
8.5.2 不可重复读
- 不可重复读(Non-repeatable Read):指在一个事务中,多次读取同一数据,先后读取到的数据不一致
示例如下:
分析:事务
A
执行下单业务时,因为添加物流信息的时候出错了,导致整个事务回滚,事务回滚完成后,事务A
就结束了。但事务B
却并未结束,在事务B
中,在事务A
执行时读取了一次剩余库存,然后在事务回滚后又读取了一次剩余库存,其中:B
事务第一次读到的剩余库存是扣减之后的,第二次读到的剩余库存则是扣减之前的(因为A
事务回滚又加回去了)。
- 上述案例中,同一个事务中读取同一数据,结果却并不一致,也就说明了该数据存在不可重复读问题。
- 要理解不可重复读和可重复读的意思是,可重复读:在同一事务中,不管读取多少次,读到的数据都是相同的。
8.5.3 幻读
- 示例:
- 假设此时平台要升级,用户表中的性别字段,原本是以「男、女」的形式保存数据,现在平台升级后要求改为「
0、1
」代替。 - 因此事务
A
开始更改表中所有数据的性别字段,当负责执行事务A
的线程正在更改最后一条表数据时,此时事务B
来了,正好向用户表中插入了一条「性别=男」的数据并提交了,然后事务A
改完原本的最后一条数据后,当再次去查询用户表时,结果会发现表中依旧还存在一条「性别=男」的数据,似乎跟产生了幻觉一样。
- 假设此时平台要升级,用户表中的性别字段,原本是以「男、女」的形式保存数据,现在平台升级后要求改为「
- 幻读问题的原因是在于:另外一个事务在第一个事务要处理的目标数据范围之内新增了数据,然后先于第一个事务提交造成的问题。
- 幻读和不可重复读的区别:
- 不可重复读:是同一条SQL查询的内容不同(被修改了)
- 幻读是:查询的结果条数不同(增加了、或者删除了记录)
8.5.4 事务的隔离级别
- 为了处理这些问题,SQL标准定义了以下几种事务隔离级别:
- READ-UNCOMMITTED(读取未提交 ) :
最低
的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。 - READ-COMMITTED(读取已提交) : 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
- REPEATABLE-READ(可重复读) : 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
- SERIALIZABLE(可串行化) : 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
- READ-UNCOMMITTED(读取未提交 ) :
- 总结如下表:
脏读 | 不可重复读 | 幻读 | |
---|---|---|---|
Read uncommitted(读取未提交) | √ | √ | √ |
Read committed(读取已提交) | × | √ | √ |
Repeatable read(可重复读) | × | × | √ |
Serializable(可串行化) | × | x | x |
8.5.5 查看隔离级别
- sql查询查看
sql
mysql> select @@GLOBAL.transaction_isolation, @@transaction_isolation;
+--------------------------------+-------------------------+
| @@GLOBAL.transaction_isolation | @@transaction_isolation |
+--------------------------------+-------------------------+
| REPEATABLE-READ | REPEATABLE-READ |
+--------------------------------+-------------------------+
- 查看变量
sql
mysql> show variables like "transaction_isolation";
+-----------------------+-----------------+
| Variable_name | Value |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
8.5.6 设置隔离级别
- 设置当前连接隔离:
sql
# 设置read uncommitted(读未提交)级别
mysql> set session transaction isolation level read uncommitted;
#设置read committed(读已提交)级别:
mysql> set session transaction isolation level read committed;
#设置repeatable read(可重复度)级别:
mysql> set session transaction isolation level repeatable read;
# 设置serializable(串行化)级别:
mysql> set session transaction isolation level serializable;
# 查看当前级别
mysql> show variables like "transaction_isolation";
- 设置全局隔离:
sql
mysql> set global transaction isolation level 隔离级别;
8.5.7 示例
- 准备工作:建立并发环境并建立2个mysql链接且登录数据库
sql
# 连接1:
mysql> use mydb17_transcation
mysql> create table ac (id int(2) primary key not null, name varchar (10), money double);
mysql> insert into ac values(1, '张三', 1000);
mysql> insert into ac values(2, '李四', 2000);
mysql> select * from ac;
mysql> show variables like "transaction_isolation"; # 查看当前隔离级别
mysql> set autocommit = 0; # 设置取消自动提交
sql
# 连接2:
mysql> use mydb17_transcation
mysql> select * from ac;
mysql> set autocommit = 0;
- read uncommitted(读未提交)级别
sql
# 连接1:
mysql> set session transaction isolation level read uncommitted;
mysql> select * from ac;
+----+--------+-------+
| id | name | money |
+----+--------+-------+
| 1 | 张三 | 1000 |
| 2 | 李四 | 2000 |
+----+--------+-------+
sql
# 连接2:
mysql> set session transaction isolation level read uncommitted;
mysql> begin; # 开始一个事务
mysql> update ac set money=5000 where name="张三"; # 更新数据
# 注意:此时连接2的事务未提交
sql
# 连接1:
mysql> select * from ac;
+----+--------+-------+
| id | name | money |
+----+--------+-------+
| 1 | 张三 | 5000 |
| 2 | 李四 | 2000 |
+----+--------+-------+
# 连接1中前后2次查询结果不同
# read uncommitted(读未提交)级别:不管对方事务是否提交,只要数据发生改变本连接就可以察觉到
sql
# 连接2:
mysql> commit; 提交事务
- read committed(读已提交)级别
sql
# 连接1:
mysql> exit # 退出重新登录
Bye
PS C:\Users\Administrator> mysql -uroot -p123456
mysql> use mydb17_transcation
mysql> set autocommit = 0;
mysql> set session transaction isolation level read committed; # 重新设置隔离
mysql> show variables like "transaction_isolation";
mysql> select * from ac;
+----+--------+-------+
| id | name | money |
+----+--------+-------+
| 1 | 张三 | 5000 |
| 2 | 李四 | 2000 |
+----+--------+-------+
sql
# 连接2:
mysql> exit # 退出重新登录
Bye
C:\Users\Administrator>mysql -uroot -p123456
mysql> use mydb17_transcation;
mysql> set autocommit = 0;
mysql> set session transaction isolation level read committed;
mysql> show variables like "transaction_isolation";
mysql> begin;
mysql> update ac set money=5000-1000 where name="张三"; # 转账
mysql> update ac set money=2000+1000 where name="李四"; # 转账
sql
# 连接1:
mysql> select * from ac;
+----+--------+-------+
| id | name | money |
+----+--------+-------+
| 1 | 张三 | 5000 |
| 2 | 李四 | 2000 |
+----+--------+-------+
# 此时查询由于连接2未提交,隔离策略为read committed(读已提交),所以在连接2未提交以前,连接1查询的结果不变
sql
# 连接2:
mysql> commit; # 提交事务
sql
# 连接1
mysql> select * from ac;
+----+--------+-------+
| id | name | money |
+----+--------+-------+
| 1 | 张三 | 4000 |
| 2 | 李四 | 3000 |
+----+--------+-------+
# 连接2事务提交后,连接1查询的数据发生变化
- repeatable read(可重复读)级别,该隔离级别为mysql的默认隔离级别,对某字段进行操作时,其他事务禁止操作该字段,所以总能保持读取数据的一致性
sql
# 连接1:
mysql> exit # 退出重新登录
Bye
PS C:\Users\Administrator> mysql -uroot -p123456
mysql> use mydb17_transcation
mysql> set autocommit = 0;
mysql> show variables like "transaction_isolation"; # 查看发现为默认级别
+-----------------------+-----------------+
| Variable_name | Value |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
sql
# 连接2
mysql> exit # 退出重新登录
Bye
C:\Users\Administrator> mysql -uroot -p123456
mysql> use mydb17_transcation
mysql> set autocommit = 0;
mysql> show variables like "transaction_isolation"; # 查看发现为默认级别
sql
# 连接1
mysql> select * from ac;
+----+--------+-------+
| id | name | money |
+----+--------+-------+
| 1 | 张三 | 4000 |
| 2 | 李四 | 3000 |
+----+--------+-------+
2 rows in set (0.00 sec)
mysql> begin; # 连接1开启事务
mysql> update ac set money=4000-1000 where name="张三"; # 模拟转账
mysql> update ac set money=3000+1000 where name="李四"; # 模拟转账
# 注意此时连接1的事务还未提交
sql
# 连接2
mysql> begin;
mysql> select * from ac;
+----+--------+-------+
| id | name | money |
+----+--------+-------+
| 1 | 张三 | 4000 |
| 2 | 李四 | 3000 |
+----+--------+-------+
2 rows in set (0.00 sec)
mysql> update ac set money=8000 where name="张三";
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
# 等待.....之后上例报错:超过锁定等待超时;尝试重新启动事务
# 由于连接1的update事务未提交,此时会给表增加update语句独占锁,此时其他事务对持有独占锁的记录进行修改时,就会被阻塞
sql
# 连接1
mysql> commit; # 提交事务
sql
# 连接2
mysql> update ac set money=8000 where name="张三"; # 执行成功
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
- serializable(串行化)级别,该隔离模式下执行的事务在对某表进行操作期间,禁止其他所有事务对该表进行任何操作,如果强行操作也会报错(和上述错误一致),因为serializable隔离级别并发性能太低,用的相对比较少
8.5.8 总结
- MySQL是一个客户端/服务器,对于同一个服务器来说,允许若干个客户端进行连接,每个客户端与服务器连接上后,就可以称为一个会话(Session)。
- 每个客户端都可以在自己的会话中向服务器发送请求语句,服务器可能同时处理多个事务
- 事务有隔离性的特性,理论上在某个事务对某个数据进行访问时,其他事务应该进行排队,当该事务提交之后,其他事务才可以继续访问这个数据。
- 但这样对性能影响太大,我们即想保持事务隔离性,又想让服务器在处理访问同一数据的多个事务性能尽量高些,那就看二者如何权衡取舍了。
- 四种隔离的并发性能如图:

九、SQL优化
SQL优化原则:减小查询的数据量、提升SQL的索引命中率
- MySQL的优化方式
- 从设计上优化
- 从查询上优化
- 从索引上优化
- 从存储上优化
9.1 SQL优化技巧
9.1.1 查询时尽量不要使用 *
- 一般在写SQL为了方便以通常会采用
*
来代替所有字段 ,毕竟用*
号只要按键盘一下,写字段则需要一个个字段名去写。 - 写
*
的确能让程序员更省力,但对机器就不太友好了,因此在写查询语句时一律不要使用*
代替所有字段 - 原因如下:
- 分析成本变高:增加了SQL解析器的额外解析成本
- 网络开销变大:返回数据太多无意义,网络数据包体积太大
- 内存占用变高:SQL查到的结果集更大、占用的内存也越大
- 维护性变差:增加维护业务量
9.1.2 连表查询时尽量不要关联太多表
- 关联太多的表,就会导致执行效率变慢 ,执行时间变长
- 交互型 的业务中,关联的表数量应当控制在5张表之内
- 后台型 的业务由于不考虑用户体验感,且业务复杂,可以增加多张表的关联,但按照《高性能MySQL》上的推荐,最好也要控制在16~18张表之内(阿里开发规范中要求控制在3张表以内)。
9.1.3 多表查询时一定要以小驱大(小表放前)
- 意义:先查小表,再用小表的结果去大表中检索数据
- 即写SQL时最好小表放前,大表放后
9.1.4 不要使用like左模糊和全模糊查询
- 原因:like关键字以 % 号开头会导致索引失效,从而导致SQL触发全表查询
- 因此需要使用模糊查询时,千万要避免 %xxx、%xxx% 这两种情况出现,实在需要使用这两类模糊查询时,可以适当建立全文索引 来代替,数据量较大时可以使用ES、Solr...这类搜索引擎来代替
9.1.5 查询时尽量不要对字段做空值判断
select * from xxx where xxx is null;
select * from xxx where xxx not is null;
- 由于判断 null 的时不会走索引,因此字段做空值判断,会导致索引失效
- 一般在设计字段结构的时候,请使用 not null 来定义字段,同时如果想为空的字段,可以设计一个
0、""
这类空字符代替,一方面要查询空值时可通过查询空字符的方式走索引检索
9.1.6 不要在条件查询 = 前对字段做任何运算
select * from student where id * 2 = 8;
select * from student where trim(name) = "张三"; # trim()函数用于清理串前后的空格
- 因为MySQL优化器在生成执行计划时,发现这些=前面涉及到了运算,因此就不会继续执行,会将具体的运算工作留到执行时完成,由于优化器没有继续执行,因此不会为运算完成后的字段选择索引,最终导致索引失效走全表查询。
9.1.7 !=、!<>、not in、not like、or...要慎用
- 原因:可能导致索引失效
sql
select name from student where id=1 or id=2;
# 可以替换成:
select name from student where id=1
union all
select name from student where id=2;
- 上述SQL虽然变长了,但查询效率会而更高,因为后面的SQL可以走索引
9.1.8 避免频繁创建、销毁临时表
- 临时表 是一种 数据缓存 ,对于一些常用的查询结果可以为其建立临时表,这样后续要查询时可以直接基于临时表来获取数据
- MySQL默认会在内存中开辟一块临时表数据的存放空间,所以走临时表查询数据是直接基于内存的,速度会比走磁盘检索快上很多倍。
- 但注意,只有对于经常查询的数据才对其建立临时表,不要盲目的去无限制创建,否则频繁的创建、销毁会对MySQL造成不小的负担。
9.1.9 从业务设计层面减少大量数据返回的情况
- 若查询时要求一次性将所有数据全部返回,而后在前端去做筛选展现,但如果一次性返回的数据量过于巨大时,就会引起网络阻塞、内存占用过高、资源开销过大的各类问题出现,因此如果项目中存在这类业务,一定要记住拆分掉它,比如分批返回给客户端。
9.1.10 尽量避免深分页的情况出现
- 有SQL如下:
sql
select xx,xx,xx from yyy limit 100000,10;
上述这条SQL相当于查询第1W页数据,在MySQL的实际执行过程中,首先会查询出100010条数据,然后丢弃掉前面的10W条数据,将最后的10条数据返回,这个过程无异极其浪费资源。
-
改进方法分为两种情况
-
情况1:如果查询出的结果集,存在递增且连续 的字段,可以基于有序字段来进一步做筛选后再获取分页数据,如下:
select xx,xx,xx from yyy where 有序字段 >= nnn limit 10;
-
情况2:搜索分页 ,该分页是无序的,因为搜索到的数据可以位于表中的任意行,所以搜索出的数据中,就算存在有序字段,也不会是连续的,这种情况下就只能在业务上限制深分页的情况 ,以百度为例,其搜索关键字MYSQL的结果有很多,甚至上亿条,为了杜绝分页问题到最后连分页按钮都不给用户提供接了,这时自然也就无法根据用户操作生成深分页的SQL,在搜索需求中,一般用户最多看前面
30
页,如果还未找到他需要的内容,基本上就会换个更精准的关键词重新搜索。
-
9.1.11 SQL务必要写完整,不要使用缩写法
sql
# 为字段取别名的简单写法
select name "姓名"from student;
# 为字段取别名的完整写法
select name as"姓名"from student; # as需要写上
# 内连接查询的简单写法
select * from 表1,表2... where 表1.字段=表2.字段...;
# 内连接查询的完整写法
select * from 表1 别名1 inner join 表2 别名2 on 别名1.字段=别名2.字段;
- 注意:所有隐式的这种写法,在底层都需要做一次转换,将其转换为完整的写法,因此简写的SQL会比完整的SQL多一步转化过程,如果需要极致程度的优化,切记将SQL写成完整语法。
9.1.12 明确仅返回一条数据的语句可以使用limit 1
select * from student where name = "李四";
select * from student where name = "李四" limit 1;
上述这两条SQL语句都是根据姓名查询一条数据,但后者大多数情况下会比前者好,因为加上 limit 1关键字后,当程序匹配到一条数据时就会停止扫描,可提升性能,如果不加的情况下会将所有数据都扫描一次。
9.1.13 客户端的一些操作可以批量化完成
sql
for (xxObject obj : xxObjs) {
xxbiao.insert(obj);
}
#
xxbiao.insert(obj)对应的SQL如下:
insert into tb_xxx values(......);
#
- 批量新增某些数据、批量修改某些数据的状态...,若使用上述接口的结构,即insert语句嵌入在for循环中,由于每次都需要往MySQL发送SQL语句,则会带来额外的网络开销以及耗时,应更改为如下:
sql
xxbiao.insertBatch(xxObjs);
#
xxbiao.insertBatch(xxObjs)对应的SQL如下:
insert into tb_xxx values(......),(......),(......),(......),.....;
#
上述修改,会 组合成一条SQL发送给MySQL执行 ,能够在很大程度上 节省网络资源的开销,提升批量操作的执行效率 。
9.2 查看SQL执行频率
- 作用:
- MySQL 客户端连接成功后,通过 show [session|global] status 命令可以查看服务器状态信息 。通过查看状态信息可以查看对当前数据库的主要操作类型。
- 格式
sql
mysql> show session status like 'Com_______'; # 7个下划线,查看当前会话统计结果
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Com_binlog | 0 |
| Com_commit | 1 |
| Com_delete | 0 |
| Com_import | 0 |
| Com_insert | 0 |
| Com_repair | 0 |
| Com_revoke | 0 |
| Com_select | 3 |
| Com_signal | 0 |
| Com_update | 2 |
| Com_xa_end | 0 |
+---------------+-------+
11 rows in set (0.00 sec)
mysql> show global status like 'Com_______'; # 查看自数据库上次启动至今统计结果
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Com_binlog | 0 |
| Com_commit | 5 |
| Com_delete | 1 |
| Com_import | 0 |
| Com_insert | 7 |
| Com_repair | 0 |
| Com_revoke | 0 |
| Com_select | 91 |
| Com_signal | 0 |
| Com_update | 16 |
| Com_xa_end | 0 |
+---------------+-------+
11 rows in set (0.00 sec)
mysql> show status like 'Innodb_rows_%'; # 查看针对Innodb引擎的统计结果
+----------------------+-------+
| Variable_name | Value |
+----------------------+-------+
| Innodb_rows_deleted | 1 |
| Innodb_rows_inserted | 7 |
| Innodb_rows_read | 120 |
| Innodb_rows_updated | 15 |
+----------------------+-------+
- 重要参数分析
9.3 定位低效率执行SQL
可以通过以下两种方式定位执行效率较低的 SQL 语句:
- 慢查询日志 : 通过慢查询日志定位那些执行效率较低的 SQL 语句。
- show processlist:该命令查看当前MySQL在进行的线程 ,包括线程的状态、是否锁表等,可以实时地查看 SQL 的执行情况,同时对一些锁表操作进行优化。
- 慢查询日志
sql
# 查看慢日志配置信息
show variables like '%slow_query_log%';
# 开启慢日志查询
set global slow_query_log=1;
# 查看慢日志记录SQL的最低阈值时间,单位:秒
show variables like 'long_query_time%';
# 修改慢日志记录SQL的最低阈值时间,需要重启mysql服务
set global long_query_time=4;
- SQL- show processlist
sql
mysql> show processlist;
+----+-----------------+-----------------+--------------------+---------+------+------------------------+------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+-----------------+-----------------+--------------------+---------+------+------------------------+------------------+
| 5 | event_scheduler | localhost | NULL | Daemon | 6637 | Waiting on empty queue | NULL |
| 26 | root | localhost:52507 | mydb17_transcation | Query | 0 | init | show processlist |
+----+-----------------+-----------------+--------------------+---------+------+------------------------+------------------+
sql
# 分析
(1) id列,用户登录mysql时,系统分配的"会话id",可以使用函数connection_id()查看
mysql> select connection_id();
+-----------------+
| connection_id() |
+-----------------+
| 26 |
+-----------------+
(2) user列,显示当前用户。如果不是root,这个命令就只显示用户权限范围的sql语句
(3) host列,显示这个语句是从哪个ip的哪个端口上发的,可以用来跟踪出现问题语句的用户
(4) db列,显示这个进程目前连接的是哪个数据库
(5) command列,显示当前连接的执行的命令,一般取值为休眠(sleep),查询(query),连接(connect)等
(6) time列,显示这个状态持续的时间,单位是秒
(7) state列,显示使用当前连接的sql语句的状态,很重要的列。state描述的是语句执行中的某一个状态。一个sql语句,以查询为例,可能需要经过copying to tmp table、sorting result、sending data等状态才可以完成
(8) info列,显示这个sql语句,是判断问题语句的一个重要依据
9.4 explain分析执行计划
作用: 通过以上步骤查询到效率低的 SQL 语句后,可以通过 EXPLAIN命令获取 MySQL如何执行 SELECT 语句的信息,包括在 SELECT 语句执行过程中表如何连接和连接的顺序。
格式:
explain sql
- 示例
sql
mysql> use mydb9_stusys;
mysql> select * from student;
+------+--------+------+------+---------+------------+
| sno | sname | ssex | sage | monitor | birth |
+------+--------+------+------+---------+------------+
| s001 | 张玲丽 | 女 | 20 | s010 | 2009-12-03 |
| s002 | 吴鹏 | 男 | 19 | s010 | 2009-10-11 |
| s003 | 李锐 | 男 | 19 | s003 | 2008-02-13 |
| s004 | 赵丁雯 | 女 | 21 | s003 | 2008-06-24 |
|........................
mysql> explain select * from student where sno = 003;

- 分析
- 参数使用
- key:如果该值为空,则表示未使用索引查询,此时需要调整
SQL
或建立索引。 - type:这个字段决定了查询的类型,如果为index、all就需要进行优化。
- rows:这个字段代表着查询时可能会扫描的数据行数,较大时也需要进行优化。
- filtered:这个字段代表着查询时,表中不会扫描的数据行占比,较小时需要进行优化。
- Extra:这个字段代表着查询时的具体情况,在某些情况下需要根据对应信息进行优化。
- key:如果该值为空,则表示未使用索引查询,此时需要调整
9.5 show profile 分析SQL
作用: show profiles 能够在做SQL优化时帮助我们了解时间都耗费到哪里去了。
- 查看是否开启
sql
mysql> select @@have_profiling; # 查看
+------------------+
| @@have_profiling |
+------------------+
| YES |
+------------------+
1 row in set, 1 warning (0.00 sec)
mysql> set profiling=1; # 设置开启
- 示例
sql
mysql> use mydb9_stusys;
mysql> set profiling=1;
mysql> select * from student where year(now())-year(birth)<20;
mysql> select * from student where year(now())-year(birth)<16;
mysql> select count(*) from student;
mysql> show profiles;

- 通过 show profile for query query_id 语句可以查看到该SQL执行过程中每个线程的状态和消耗的时间:
sql
mysql> show profile for query 3; # 第三个sql
+--------------------------------+----------+
| Status | Duration |
+--------------------------------+----------+
| starting | 0.000059 |
| Executing hook on transaction | 0.000002 |
| starting | 0.000018 |
| checking permissions | 0.000003 |
| Opening tables | 0.000025 |
| init | 0.000002 |
| System lock | 0.000005 |
| optimizing | 0.000002 |
| statistics | 0.000009 |
| preparing | 0.000009 |
| executing | 0.000977 |
| end | 0.000006 |
| query end | 0.000002 |
| waiting for handler commit | 0.000007 |
| closing tables | 0.000005 |
| freeing items | 0.000037 |
| cleaning up | 0.000006 |
+--------------------------------+----------+
17 rows in set, 1 warning (0.00 sec)
- 在获取到最消耗时间的线程状态后,MySQL支持进一步选择 all、cpu、block io 、context switch、page faults 等明细类型类查看MySQL在使用什么资源上耗费了过高的时间。例如,选择查看CPU的耗费时间 :
sql
mysql> show profile cpu for query 3;
+--------------------------------+----------+----------+------------+
| Status | Duration | CPU_user | CPU_system |
+--------------------------------+----------+----------+------------+
| starting | 0.000059 | 0.000000 | 0.000000 |
| Executing hook on transaction | 0.000002 | 0.000000 | 0.000000 |
| starting | 0.000018 | 0.000000 | 0.000000 |
| checking permissions | 0.000003 | 0.000000 | 0.000000 |
| Opening tables | 0.000025 | 0.000000 | 0.000000 |
| init | 0.000002 | 0.000000 | 0.000000 |
| System lock | 0.000005 | 0.000000 | 0.000000 |
| optimizing | 0.000002 | 0.000000 | 0.000000 |
| statistics | 0.000009 | 0.000000 | 0.000000 |
| preparing | 0.000009 | 0.000000 | 0.000000 |
| executing | 0.000977 | 0.000000 | 0.000000 |
| end | 0.000006 | 0.000000 | 0.000000 |
| query end | 0.000002 | 0.000000 | 0.000000 |
| waiting for handler commit | 0.000007 | 0.000000 | 0.000000 |
| closing tables | 0.000005 | 0.000000 | 0.000000 |
| freeing items | 0.000037 | 0.000000 | 0.000000 |
| cleaning up | 0.000006 | 0.000000 | 0.000000 |
+--------------------------------+----------+----------+------------+

9.6 trace分析优化器执行计划
作用: MySQL提供了对SQL的跟踪 trace,通过trace文件能够进一步了解为什么优化器选择A计划,而不是选择B计划

- 方法
- 打开trace , 设置格式为 JSON,并设置trace最大能够使用的内存大小,避免解析过程中因为默认内存过小而不能够完整展示。
sql
SET optimizer_trace="enabled=on",end_markers_in_json=on;
set optimizer_trace_max_mem_size=1000000;
- 示例
sql
mysql> select * from student where year(now())-year(birth)<16;
# 通过检查information_schema.optimizer_trace就可以知道MySQL是如何执行SQL的
mysql> select * from information_schema.optimizer_trace\G;
9.7 使用索引优化
作用: 索引是数据库优化最常用也是最重要的手段之一, 通过索引通常可以帮助用户解决大多数的MySQL的性能优化问题。
- 索引建立原则
- 一般针对数据分散的关键字进行建立索引,比如ID、QQ,像性别、状态值等建立索引没有意义
- 对大数据量表建立
聚集索引
,避免更新操作带来的碎片 - 尽量使用短索引,一般对int、char/varchar、date/time 等类型的字段建立索引
- 需要的时候建立联合索引,但是要注意查询SQL语句的编写
- 谨慎建立 unique 类型的索引(唯一索引)
- 大文本字段 不建立为索引,如果要对大文本字段进行检索,可以考虑全文索引
- 频繁更新的列 不适合建立索引
- order by 字句中的字段,where 子句中字段,最常用的sql语句中字段,应建立索引。
- 对于只是做查询 用的数据库索引 越多越好 ,但对于在线实时系统 建议 控制在5个以内 。
9.8 架构优化
- 业务拆分:搜索功能,like ,前后都有%,一般不用MySQL数据库
- 业务拆分:某些应用使用nosql持久化存储,例如memcahcedb、redis、ttserver 比如粉丝关注、好友关系等;
- 数据库前端必须要加cache,例如memcached,用户登录,商品查询
- 动态数据静态化。整个文件静态化,页面片段静态化
- 数据库集群与读写分离;
- 单表超过2000万,拆库拆表,人工或自动拆分(登录、商品、订单等)