基础篇
在cmd中使用MYSQL的相关指令:
net start mysql // 启动mysql服务
net stop mysql // 停止mysql服务
mysql -uroot -p1234//登录MYSQL(-u为用户名-p为密码)
//登录参数
mysql -u用户名 -p密码 -h要连接的mysql服务器的ip地址(默认127.0.0.1) -P端口号(默认3306)
exit //退出mysql
quit //退出mysql
SQL通用语法:
- SQL 语句可以单行或多行书写,以分号结尾。
· 2.MySQL 数据库的 SQL 语句不区分大小写,关键字建议使用大写。
3.单行注释: -- 注释内容 或 #注释内容(MySQL 特有)
使用-- 添加单行注释时,--后面一定要加空格,而#没有要求。
多行注释: /* 注释 */
SQL分类:
DDL:
操作数据库,表等。
操作数据库:
1.查询
show databases;
2.创建数据库
create database 数据库名称;
创建数据库(如果不存在则创建)
create database if not exists 数据库名称;
3.删除数据库
drop database 数据库名称;
删除数据库(如果存在则删除)
drop database if exists 数据库名称;
4.使用数据库
查看当前数据库
select database();
使用数据库
use 数据库名称;
操作表:
查询表:
查询当前数据库下的所有表
show tables;
查询表结构:
desc 表名;
创建表:
create table 表名(
字段名1 数据类型1,
字段名2 数据类型2,
字段名3 数据类型3,
字段名4 数据类型4
);
删除表:
1.删除表
drop table 表名;
2.判断表是否存在,再删除表
drop table if not exists 表名;
修改表:
1.修改表名
alter table 表名 rename to 新表名;
2.增加一列
alter table 表名 add 列名 数据类型;
3.修改数据类型
alter table 表名 modify 列名 新的数据类型;
4.修改列名和数据类型
alter table 表名 change 列名 新列名 数据类型;
5.删除列
alter table 表名 drop 列名;
DML:
添加数据:
1.给指定列添加数据
insert into 表名(列名1,列名2,...) values(值1,值2,...);
2.给全部列添加数据
insert into 表名 values(值1,值2,...);
3.批量添加数据
insert into 表名 (列名1,列名2,...)values(值1,值2,...),(值1,值2,...),....;
insert into 表名 values(值1,值2,...),(值1,值2,...),...;
修改数据:
1.修改表数据
update 表名 set 列名1=值1,列名2=值2,...[where 条件]
2.删除数据
delete from 表名 [where条件];
DQL:
执行顺序:
查询语法:
select [distinct//去除重复记录]
字段列表(age,math) (可以起别名如:stu AS学生)
from
表名列表
where
条件列表(age>=20 && age<=30;
age>=20 and age<=30;
age between 20 and 30;
age=20 || age=22 ||age=30;
age=20 or age=22 or age=30;
age in (18,22,30);)
//NULL 的比较不能用=或!= 需要使用is null/is not null
// _为站位符代表单个字符 %为任意多个字符
//模糊查询
SELECT * FROM tb_brand WHERE brand_name LIKE '%三%';
//分组之后,查询的字段一般为聚合函数和分组字段
group by
分组字段
//查询男同学和女同学各自的数学平均分
// select sex,avg(math) form stu group by sex;
//查询男同学和女同学各自的数学平均分以及人数
// select count(*),sex,avg(math) form stu group by sex;
//查询男同学和女同学各自的数学平均分,且要求数学低于70不参与分组
// select sex,avg(math)
form stu where math>70 group by sex;
//查询男同学和女同学各自的数学平均分,且要求数学低于70不参与分组,且分组之后人数大于2
// select sex,avg(math)
form stu where math>70 group by sex
having count(*)> 2 ;
having
分组后条件
//查询男同学和女同学各自的数学平均分,且要求数学低于70不参与分组,且分组之后人数大于2
// select sex,avg(math)
form stu where math>70 group by sex
having count(*)> 2 ;
order by
排序字段(math desc;//按数学降序排序
english asc;//按英语升序排序
math desc ,english asc;
//按数学降序排序,如果数学成绩一样再按英语升序排序 )
limit
分页限定
//语法:select 字段列表 from 表名 limit起始索引,查询条目数;
起始索引:从0开始
计算公式:起始索引=(当前页码-1)*每页条目数
聚合函数:
count(列名)统计数量
max(列名)最大值
min(列名)最小值
sum(列名)求和
avg(列名)平均值0
语法:
select 聚合函数(列名) from 表;
//null不参与聚合函数运算。
DCL:
权限控制:
函数:
字符串函数
数值函数
日期函数
流程函数
约束:
约束是作用于表中列的规则,用于限制加入表的数据。
非空约束:保证列中数据不为空 not null
唯一约束:保证列中数据各不相同 unique
主键约束:主键是一行数据的唯一标识,要求非空且唯一primary key
检查约束:检查列中数据是否满足要求。Check(MYSQL不支持)
默认约束:保存数据未指定值,则采用默认值。default
外键约束:外键用来让两个表的数据建立连接,保证数据的唯一性和完整性。foreign key
不指定值时自动增长: auto_increment
语法:
1.添加约束:
create table 表名(
列名 数据类型 not null,
...
);
2.建完表后添加非空约束
alter table 表名 modify 列名 数据类型 not null;
3.删除约束
alter table modify 字段名 数据类型;
4.添加外键约束
create table 表名(
列名 数据类型 not null,
...
[constraint] [外键名称] foreign key (外键列名) references 主表名称(主表列名)
);
5.建完表后添加外键约束
alter table 表名 add constraint 外键名称 foreign key (外键列名) references 主表名称(主表列名);
6.删除外键约束
alter table 表名 drop foreign key 外键名称;
多表查询:
1.内连接(查询两个表的交集)
隐式内连接:
select 字段列表 from 表1,表2... where 条件;
显示内连接:
select 字段列表 from 表1 join 表2 on 条件
2.外连接
左外连接(表1所有数据+两表交集):
select 字段列表 from 表1 left join 表2 on 条件
右外连接(表2所有数据+两表交集):
select 字段列表 from 表1 right join 表2 on 条件
补充:
联合查询
union 合并后去重,union all 直接合并
使用条件,查询返回的列必须保持一致。
3.子查询(嵌套查询)
单行单例(标量子查询)
作为条件值,使用=,!=,<,>进行条件判断
select 字段列表 from 表名 where 字段名= (子查询);
例:select name form stu where math >
(select math from stu where name=’小马’ );
多行单列(列子查询)
作为条件值,使用in进行条件判断
select 字段列表 from 表名 where 字段名in(子查询);
例:select name form stu where stu_id in
(select did from dept where dname=’数学’ );
一行多列(行子查询)
select salary,managerid from emp where name ='张无忌'select * from emp where (salary,managerid) = (select salary,managerid from emp where name ='张无忌')
工资和领导与张无忌相同
多行多列(表子查询)
作为虚拟表进行查询
select 字段列表 from (子查询) where 条件;
例:
select * from
(select * from emp where join_date >’2021-1-1’ ) t1,dept where t1.dep_id = dept.did;
作为查询条件:
例:
select * from emp where (job,salary) in (select job, salary from emp where name='张三'or name=‘李四’)
工资和职位要么与张三相同要么与李四相同
事务:
数据库的事务是一个不可分割的数据单元,包含一组数据库操作,要么同时失败,要么同时成功。
语法:
开始事务:start transaction; 或begin;
提交事务:commit;
回滚事务:rollback;
事务的四大特性ACID:
原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。
隔离性(lsolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。
并发事务问题:
事务的隔离级别:
进阶篇
存储引擎
MySQL 体系结构
1). 连接层
最上层是一些客户端和链接服务,包含本地 sock 通信和大多数基于客户端 / 服务端工具实现的类似于
TCP/IP 的通信。主要完成一些类似于连接处理、授权认证、及相关的安全方案。在该层上引入了线程
池的概念,为通过认证安全接入的客户端提供线程。同样在该层上可以实现基于 SSL 的安全链接。服务
器也会为安全接入的每个客户端验证它所具有的操作权限。
2). 服务层
第二层架构主要完成大多数的核心服务功能,如 SQL 接口,并完成缓存的查询, SQL 的分析和优化,部
分内置函数的执行。所有跨存储引擎的功能也在这一层实现,如过程、函数等。在该层,服务器会解
析查询并创建相应的内部解析树,并对其完成相应的优化如确定表的查询的顺序,是否利用索引等,
最后生成相应的执行操作。如果是 select 语句,服务器还会查询内部的缓存,如果缓存空间足够大,
这样在解决大量读操作的环境中能够很好的提升系统的性能。
3). 引擎层
存储引擎层,存储引擎真正的负责了 MySQL 中数据的存储和提取,服务器通过 API 和存储引擎进行通
信。不同的存储引擎具有不同的功能,这样我们可以根据自己的需要,来选取合适的存储引擎。数据库
中的索引是在存储引擎层实现的。
4). 存储层
数据存储层,主要是将数据 ( 如 : redolog 、 undolog 、数据、索引、二进制日志、错误日志、查询
日志、慢查询日志等 ) 存储在文件系统之上,并完成与存储引擎的交互。
和其他数据库相比, MySQL 有点与众不同,它的架构可以在多种不同场景中应用并发挥良好作用。主要体现在存储引擎上,插件式的存储引擎架构,将查询处理和其他的系统任务以及数据的存储提取分离。
这种架构可以根据业务的需求和实际需要选择合适的存储引擎。
存储引擎介绍
存储引擎,也是一样,他是 mysql 数据库的核心,我们也需要在合适的场景选择合适的存储引
擎。接下来就来介绍一下存储引擎。
存储引擎就是存储数据、建立索引、更新 / 查询数据等技术的实现方式。存储引擎是基于表的,而不是基于库的,所以存储引擎也可被称为表类型。我们可以在创建表的时候,来指定选择的存储引擎,如果没有指定将自动选择默认的存储引擎。
2). 查询当前数据库支持的存储引擎
show engines;
3).查询建表语句
show create table account;
我们可以看到,创建表时,即使我们没有指定存储疫情,数据库也会自动选择默认的存储引擎。
创建表my_myisam , 并指定MyISAM存储引擎
创建表my_memory , 指定Memory存储引擎
存储引擎特点
上面我们介绍了什么是存储引擎,以及如何在建表时如何指定存储引擎,接下来我们就来介绍下来上面重点提到的三种存储引擎InnoDB、 MyISAM 、 Memory 的特点。
介绍
InnoDB 是一种兼顾高可靠性和高性能的通用存储引擎,在 MySQL 5.5 之后, InnoDB 是默认的
MySQL 存储引擎。
特点
1.DML 操作遵循 ACID 模型,支持事务;
2.行级锁,提高并发访问性能;
3.支持外键 FOREIGN KEY 约束,保证数据的完整性和正确性;
文件
xxx.ibd : xxx 代表的是表名, innoDB 引擎的每张表都会对应这样一个表空间文件,存储该表的表结
构( frm- 早期的、 sdi- 新版的)、数据和索引。
参数: innodb_file_per_table
如果该参数开启,代表对于 InnoDB 引擎的表,每一张表都对应一个 ibd 文件。我们直接打开 MySQL 的数据存放目录:C:\ProgramData\MySQL\MySQL Server 8.0\Data ,这个目录下有很多文件
夹,不同的文件夹代表不同的数据库,我们直接打开 itcast 文件夹。
可以看到里面有很多的 ibd 文件,每一个 ibd 文件就对应一张表,比如:我们有一张表 account ,就
有这样的一个 account.ibd 文件,而在这个 ibd 文件中不仅存放表结构、数据,还会存放该表对应的
索引信息。而该文件是基于二进制存储的,不能直接基于记事本打开,我们可以使用 mysql 提供的一个指令ibd2sdi ,通过该指令就可以从 ibd 文件中提取 sdi 信息,而 sdi数据字典信息中就包含该表的表结构
逻辑存储结构
表空间 : InnoDB 存储引擎逻辑结构的最高层, ibd 文件其实就是表空间文件,在表空间中可以
包含多个 Segment 段。
段 : 表空间是由各个段组成的,常见的段有数据段、索引段、回滚段等。 InnoDB 中对于段的管
理,都是引擎自身完成,不需要人为对其控制,一个段中包含多个区。
区 : 区是表空间的单元结构,每个区的大小为 1M 。默认情况下, InnoDB 存储引擎页大小为
16K ,即一个区中一共有 64 个连续的页。
页 : 页是组成区的最小单元, 页也是 InnoDB 存储引擎磁盘管理的最小单元 ,每个页的大小默
认为 16KB 。为了保证页的连续性, InnoDB 存储引擎每次从磁盘申请 4-5 个区。
行 : InnoDB 存储引擎是面向行的,也就是说数据是按行进行存放的,在每一行中除了定义表时
所指定的字段以外,还包含两个隐藏字段 ( 后面会详细介绍 ) 。
MyISAM
介绍
MyISAM 是 MySQL 早期的默认存储引擎。
特点
不支持事务,不支持外键
支持表锁,不支持行锁
访问速度快
文件
xxx.sdi :存储表结构信息
xxx.MYD: 存储数据
xxx.MYI: 存储索引
Memory
介绍
Memory 引擎的表数据时存储在内存中的,由于受到硬件问题、或断电问题的影响,只能将这些表作为临时表或缓存使用。
InnoDB 引擎与 MyISAM 引擎的区别 ?
① . InnoDB 引擎 , 支持事务 , 而 MyISAM 不支持。
② . InnoDB 引擎 , 支持行锁和表锁 , 而 MyISAM 仅支持表锁 , 不支持行锁。
③ . InnoDB 引擎 , 支持外键 , 而 MyISAM 是不支持的。
存储引擎选择
在选择存储引擎时,应该根据应用系统的特点选择合适的存储引擎。对于复杂的应用系统,还可以根据实际情况选择多种存储引擎进行组合。
InnoDB: 是 Mysql 的默认存储引擎,支持事务、外键。如果应用对事务的完整性有比较高的要 求,在并发条件下要求数据的一致性,数据操作除了插入和查询之外,还包含很多的更新、删除操作,那么InnoDB 存储引擎是比较合适的选择。
MyISAM :如果应用是以读操作和插入操作为主,只有很少的更新和删除操作,并且对事务的完
整性、并发性要求不是很高,那么选择这个存储引擎是非常合适的。
MEMORY :将所有数据保存在内存中,访问速度快,通常用于临时表及缓存。 MEMORY 的缺陷就是对表的大小有限制,太大的表无法缓存在内存中,而且无法保障数据的安全性。
索引
索引概述
索引( index )是帮助 MySQL 高效获取数据的数据结构 ( 有序 ) 。在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。
无索引情况
有索引情况
备注:这里我们只是假设索引的结构是二叉树,介绍一下索引的大概原理,只是一个示意图,并
不是索引的真实结构,索引的真实结构,后面会详细介绍。
索引结构
上述是 MySQL 中所支持的所有的索引结构,接下来,我们再来看看不同的存储引擎对于索引结构的支持情况。
B-Tree
B-Tree , B 树是一种多叉路衡查找树,相对于二叉树, B 树每个节点可以有多个分支,即多叉。
以一颗最大度数( max-degree )为 5(5 阶 ) 的 b-tree 为例,那这个 B 树每个节点最多存储 4 个 key , 5
个指针:
知识小贴士: 树的度数指的是一个节点的子节点个数。
我们可以通过一个数据结构可视化的网站来简单演示一下。 https://www.cs.usfca.edu/~gall es/visualization/BTree.html
特点:
5阶的 B 树,每一个节点最多存储 4 个 key ,对应 5 个指针。
一旦节点存储的key 数量到达 5 ,就会裂变,中间元素向上分裂。
在B 树中,非叶子节点和叶子节点都会存放数据。
B+Tree
B+Tree 是 B-Tree 的变种,我们以一颗最大度数( max-degree )为 4 ( 4 阶)的 b+tree 为例,来看一
下其结构示意图:
我们可以看到,两部分:
绿色框框起来的部分,是索引部分,仅仅起到索引数据的作用,不存储数据。
红色框框起来的部分,是数据存储部分,在其叶子节点中要存储具体的数据。
我们可以通过一个数据结构可视化的网站来简单演示一下。 https://www.cs.usfca.edu/~gall es/visualization/BTree.html
B+Tree 与 B-Tree 相比,主要有以下三点区别:
1.所有的数据都会出现在叶子节点。
2.叶子节点形成一个单向链表。
3.非叶子节点仅仅起到索引数据作用,具体的数据都是在叶子节点存放的。
上述我们所看到的结构是标准的 B+Tree 的数据结构,接下来,我们再来看看 MySQL 中优化之后的
B+Tree 。
MySQL 索引数据结构对经典的 B+Tree 进行了优化。在原 B+Tree 的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的B+Tree,提高区间访问的性能,利于排序。
Hash
MySQL 中除了支持 B+Tree 索引,还支持一种索引类型 ---Hash 索引。
结构
哈希索引就是采用一定的 hash 算法,将键值换算成新的 hash 值,映射到对应的槽位上,然后存储在hash表中。
如果两个 ( 或多个 ) 键值,映射到一个相同的槽位上,他们就产生了 hash 冲突(也称为 hash 碰撞),可以通过链表来解决。
特点
A. Hash 索引只能用于对等比较 (= , in) ,不支持范围查询( between , > , < , ... )
B. 无法利用索引完成排序操作
C. 查询效率高,通常 ( 不存在 hash 冲突的情况 ) 只需要一次检索就可以了,效率通常要高于 B+tree 索
引
存储引擎支持
在 MySQL 中,支持 hash 索引的是 Memory 存储引擎。而 InnoDB 中具有自适应 hash 功能, hash 索引是InnoDB存储引擎根据 B+Tree 索引在指定条件下自动构建的。
为什么 InnoDB 存储引擎选择使用 B+tree 索引结构 ?
A. 相对于二叉树,层级更少,搜索效率高;
B. 对于 B-tree ,无论是叶子节点还是非叶子节点,都会保存数据,这样导致一页中存储
的键值减少,指针跟着减少,要同样保存大量数据,只能增加树的高度,导致性能降低;
C. 相对 Hash 索引, B+tree 支持范围匹配及排序操作;
索引分类
在MySQL数据库,将索引的具体类型主要分为以下几类:主键索引、唯一索引、常规索引、全文索引。
聚集索引&二级索引
而在在InnoDB存储引擎中,根据索引的存储形式,又可以分为以下两种:
聚集索引选取规则 :
如果存在主键,主键索引就是聚集索引。 如果不存在主键,将使用第一个唯一( UNIQUE )索引作为聚集索引。
如果表没有主键,或没有合适的唯一索引,则 InnoDB 会自动生成一个 rowid 作为隐藏的聚集索
引。
聚集索引和二级索引的具体结构如下:
1.聚集索引的叶子节点下挂的是这一行的数据 。
2.二级索引的叶子节点下挂的是该字段值对应的主键值。
接下来,我们来分析一下,当我们执行如下的 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 字段的二级索引,然
后再查询聚集索引,也就是需要进行回表查询。
索引语法
创建索引
CREATE [ UNIQUE | FULLTEXT ] INDEX index_name ON table_name (index_col_name,... ) ;
查看索引
SHOW INDEX FROM table_name ;
删除索引
DROP INDEX index_name ON table_name ;
SQL性能分析
SQL执行频率
MySQL 客户端连接成功后,通过
show [session|global] status
可以提供服务器状态信 息。通过如下指令,可以查看当前数据库的INSERT、UPDATE、DELETE、SELECT的访问频次:
-- session 是查看当前会话 ;
-- global 是查询全局数据 ;
//7个_
SHOW GLOBAL STATUS LIKE 'Com_______';
Com_delete: 删除次数
Com_insert: 插入次数
Com_select: 查询次数
Com_update: 更新次数
通过上述指令,我们可以查看到当前数据库到底是以查询为主,还是以增删改为主,从而为数据
库优化提供参考依据。 如果是以增删改为主,我们可以考虑不对其进行索引的优化。 如果是以
查询为主,那么就要考虑对数据库的索引进行优化了。
那么通过查询 SQL 的执行频次,我们就能够知道当前数据库到底是增删改为主,还是查询为主。 那假 如说是以查询为主,我们又该如何定位针对于那些查询语句进行优化呢? 次数我们可以借助于慢查询日志。
接下来,我们就来介绍一下 MySQL 中的慢查询日志。
慢查询日志
慢查询日志记录了所有执行时间超过指定参数( long_query_time ,单位:秒,默认 10 秒)的所有
SQL 语句的日志。
MySQL 的慢查询日志默认没有开启,我们可以查看一下系统变量 slow_query_log
show variables like 'slow_query_log'
如果要开启慢查询日志,需要在MySQL的配置文件(/etc/my.cnf)中配置如下信息:
# 开启MySQL慢日志查询开关slow_query_log=1# 设置慢日志的时间为2秒,SQL语句执行时间超过2秒,就会视为慢查询,记录慢查询日志long_query_time=2
配置完毕之后,重新启动 MySQL 服务器进行测试,查看慢日志文件中记录的信息
/var/lib/mysql/localhost-slow.log
测试:
执行如下SQL 语句 :
select * from tb_user; -- 这条SQL执行效率比较高, 执行耗时 0.00secselect count(*) from tb_sku; -- 由于tb_sku表中, 预先存入了1000w的记录, count一次,耗时
13.35sec
检查慢查询日志 :
最终我们发现,在慢查询日志中,只会记录执行时间超多我们预设时间( 2s )的 SQL ,执行较快的 SQL 是不会记录的。
那这样,通过慢查询日志,就可以定位出执行效率比较低的SQL,从而有针对性的进行优化。
profile详情 (查看耗时)
show profiles 能够在做 SQL 优化时帮助我们了解时间都耗费到哪里去了。通过 have_profiling
参数,能够看到当前 MySQL 是否支持 profile 操作:
SELECT @@have_profiling ;
可以看到,当前 MySQL 是支持 profile 操作的,但是开关是关闭的。可以通过 set 语句在
session/global 级别开启 profiling :
SET profiling = 1;
开关已经打开了,接下来,我们所执行的 SQL 语句,都会被 MySQL 记录,并记录执行时间消耗到哪儿去 了。 我们直接执行如下的SQL 语句
select * from tb_user;
select * from tb_user where id = 1;
select * from tb_user where name = '白起';
select count(*) from tb_sku;
执行一系列的业务SQL的操作,然后通过如下指令查看指令的执行耗时:
-- 查看每一条SQL的耗时基本情况show profiles;-- 查看指定query_id的SQL语句各个阶段的耗时情况
show profile for query query_id;-- 查看指定query_id的SQL语句CPU的使用情况
show profile cpu for query query_id;
explain (查看执行计划)
EXPLAIN 或者 DESC 命令获取 MySQL 如何执行 SELECT 语句的信息,包括在 SELECT 语句执行 过程中表如何连接和连接的顺序。
语法:
-- 直接在select语句之前加上关键字 explain / desc
EXPLAIN SELECT 字段列表 FROM 表名 WHERE 条件 ;
Explain 执行计划中各个字段的含义:
索引使用
验证索引效率
在讲解索引的使用原则之前,先通过一个简单的案例,来验证一下索引,看看是否能够通过索引来提升
数据查询性能。在演示的时候,我们还是使用之前准备的一张表 tb_sku , 在这张表中准备了 1000w
的记录
这张表中 id 为主键,有主键索引,而其他字段是没有建立索引的。 我们先来查询其中的一条记录,看 看里面的字段情况,执行如下SQL :
select * from tb_sku where id = 1\G;
可以看到即使有 1000w 的数据 , 根据 id 进行数据查询 , 性能依然很快,因为主键 id 是有索引的。 那么接 下来,我们再来根据 sn 字段进行查询,执行如下 SQL :
SELECT * FROM tb_sku WHERE sn = '100000003145001';
我们可以看到根据 sn 字段进行查询,查询返回了一条数据,结果耗时 20.78sec ,就是因为 sn 没有索 引,而造成查询效率很低。
那么我们可以针对于 sn 字段,建立一个索引,建立了索引之后,我们再次根据 sn 进行查询,再来看一 下查询耗时情况。
创建索引:
create index idx_sku_sn on tb_sku(sn) ; 1
然后再次执行相同的SQL语句,再次查看SQL的耗时
SELECT * FROM tb_sku WHERE sn = '100000003145001';
我们明显会看到, sn 字段建立了索引之后,查询性能大大提升。建立索引前后,查询耗时都不是一个数量级的。
最左前缀法则
如果索引了多列(联合索引),要遵守最左前缀法则。最左前缀法则指的是查询从索引的最左列开始, 并且不跳过索引中的列。如果跳跃某一列,索引将会部分失效(后面的字段索引失效)。
以 tb_user 表为例,我们先来查看一下之前 tb_user 表所创建的索引。
在 tb_user 表中,有一个联合索引,这个联合索引涉及到三个字段,顺序分别为: profession ,
age , status 。
对于最左前缀法则指的是,查询时,最左变的列,也就是 profession 必须存在,否则索引全部失效。 而且中间不能跳过某一列,否则该列后面的字段索引将失效。 接下来,我们来演示几组案例,看一下 具体的执行计划
1.
explain select * from tb_user where profession = '软件工程' and age = 31 and status
= '0';
2.
explain select * from tb_user where profession = '软件工程' and age = 31;
3.
explain select * from tb_user where profession = '软件工程';
以上的这三组测试中,我们发现只要联合索引最左边的字段 profession 存在,索引就会生效,只不
过索引的长度不同。 而且由以上三组测试,我们也可以推测出 profession 字段索引长度为 47 、 age
字段索引长度为 2 、 status 字段索引长度为 5 。
1.
explain select * from tb_user where age = 31 and status = '0';
2.
explain select * from tb_user where status = '0';
而通过上面的这两组测试,我们也可以看到索引并未生效,原因是因为不满足最左前缀法则,联合索引最左边的列profession 不存在。
explain select * from tb_user where profession = '软件工程' and status = '0';
上述的 SQL 查询时,存在 profession 字段,最左边的列是存在的,索引满足最左前缀法则的基本条
件。但是查询时,跳过了 age 这个列,所以后面的列索引是不会使用的,也就是索引部分生效,所以索 引的长度就是47 。
思考题:
当执行 SQL 语句 : explain select * from tb_user where age = 31 and status = '0' and profession = '软件工程 ' ; 时,是否满足最左前缀法则,走不走上述的联合索引,索引长度?
可以看到,是完全满足最左前缀法则的,索引长度 54 ,联合索引是生效的。
注意 : 最左前缀法则中指的最左边的列,是指在查询时,联合索引的最左边的字段 ( 即是 第一个字段) 必须存在,与我们编写 SQL 时,条件编写的先后顺序无关。
范围查询
联合索引中,出现范围查询 (>,<) ,范围查询右侧的列索引失效
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 ,就说明所有的字段都是走索引的。
所以,在业务允许的情况下,尽可能的使用类似于 >= 或 <= 这类的范围查询,而避免使用 > 或 <
索引失效情况
索引列运算
不要在索引列上进行运算操作, 索引将失效。
在 tb_user 表中,除了前面介绍的联合索引之外,还有一个索引,是 phone 字段的单列索引
当根据phone字段进行等值匹配查询时, 索引生效
explain select * from tb_user where phone = '17799990015';
当根据phone字段进行函数运算操作之后,索引失效
explain select * from tb_user where substring(phone,10,2) = '15';
字符串不加引号
字符串类型字段使用时,不加引号,索引将失效。
接下来,我们通过两组示例,来看看对于字符串类型的字段,加单引号与不加单引号的区别:
explain select * from tb_user where profession = '软件工程' and age = 31 and status
= '0';
explain select * from tb_user where profession = '软件工程' and age = 31 and status
= 0;
explain select * from tb_user where phone = '17799990015';
explain select * from tb_user where phone = 17799990015;
经过上面两组示例,我们会明显的发现,如果字符串不加单引号,对于查询结果,没什么影响,但是数 据库存在隐式类型转换,索引将失效。
模糊查询
如果仅仅是尾部模糊匹配,索引不会失效。如果是头部模糊匹配,索引失效。
接下来,我们来看一下这三条 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 模糊查询中,在关键字后面加 % ,索引可以生效。而如果在关键字前面加了% ,索引将会失效。
or连接条件
用 or 分割开的条件, 如果 or 前的条件中的列有索引,而后面的列中没有索引,那么涉及的索引都不会被用到。
explain select * from tb_user where id = 10 or age = 23;explain select * from tb_user where phone = '17799990017' or age = 23;
由于age没有索引,所以即使id、phone有索引,索引也会失效。所以需要针对于age也要建立索引。
create index idx_user_age on tb_user(age);
建立了索引之后,我们再次执行上述的SQL语句,看看前后执行计划的变化。
最终,我们发现,当or连接的条件,左右两侧字段都有索引时,索引才会生效。
数据分布影响
如果MySQL评估使用索引比全表更慢,则不使用索引。
select * from tb_user where phone >= '17799990005';
select * from tb_user where phone >= '17799990015';
经过测试我们发现,相同的 SQL 语句,只是传入的字段值不同,最终的执行计划也完全不一样,这是为 什么呢?
就是因为 MySQL 在查询时,会评估使用索引的效率与走全表扫描的效率,如果走全表扫描更快,则放弃 索引,走全表扫描。 因为索引是用来索引少量数据的,如果通过索引查询返回大批量的数据,则还不 如走全表扫描来的快,此时索引就会失效
接下来,我们再来看看 is null 与 is not null 操作是否走索引。
执行如下两条语句 :
explain select * from tb_user where profession is null;
explain select * from tb_user where profession is not null;
接下来,我们做一个操作将profession字段值全部更新为null。
然后,再次执行上述的两条SQL,查看SQL语句的执行计划
最终我们看到,一模一样的 SQL 语句,先后执行了两次,结果查询计划是不一样的,为什么会出现这种现象,这是和数据库的数据分布有关系。查询时MySQL 会评估,走索引快,还是全表扫描快,如果全表扫描更快,则放弃索引走全表扫描。 因此,is null 、 is not null 是否走索引,得具体情况具体分析,并不是固定的。
SQL提示
目前 tb_user 表的数据情况如下 :
索引情况如下:
把上述的 idx_user_age, idx_email 这两个之前测试使用过的索引直接删除。
drop index idx_user_age on tb_user;
drop index idx_email on tb_user;
执行SQL :
explain select * from tb_user where profession = '软件工程';
执行 SQL ,创建 profession 的单列索引:
create index idx_user_pro on tb_user (profession);
创建单列索引后,再次执行 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 = '软件工
程';
覆盖索引
尽量使用覆盖索引,减少 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 。
因为,在 tb_user 表中有一个联合索引 idx_user_pro_age_sta ,该索引关联了三个字段profession、 age 、 status ,而这个索引也是一个二级索引,所以叶子节点下面挂的是这一行的主 键id 。 所以当我们查询返回的数据在 id 、 profession 、 age 、 status 之中,则直接走二级索引直接返回数据了。 如果超出这个范围,就需要拿到主键id,再去扫描聚集索引,再获取额外的数据 了,这个过程就是回表。 而我们如果一直使用 select * 查询返回所有字段值,很容易就会造成回表 查询(除非是根据主键查询,此时只会扫描聚集索引)。
为了大家更清楚的理解,什么是覆盖索引,什么是回表查询,我们一起再来看下面的这组 SQL 的执行过程。
id是主键,是一个聚集索引。 name字段建立了普通索引,是一个二级索引(辅助索引)。
执行SQL : select * from tb_user where id = 2;
根据id查询,直接走聚集索引查询,一次索引扫描,直接返回数据,性能高。
执行SQL:selet id,name from tb_user where name = 'Arm';
虽然是根据 name 字段查询,查询二级索引,但是由于查询返回在字段为 id , name ,在 name 的二级索引中,这两个值都是可以直接获取到的,因为覆盖索引,所以不需要回表查询,性能高
执行 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语句,在查询的过程中,出现回表查询
前缀索引
当字段类型为字符串( varchar , text , longtext 等)时,有时候需要索引很长的字符串,这会让
索引变得很大,查询时,浪费大量的磁盘 IO , 影响查询效率。此时可以只将字符串的一部分前缀,建立索引,这样可以大大节约索引空间,从而提高索引效率。
语法:
create index idx_xxxx on table_name(column(n)) ;
示例 :
为 tb_user 表的 email 字段,建立长度为 5 的前缀索引。
create index idx_email_5 on tb_user(email(5));
前缀长度
可以根据索引的选择性来决定,而选择性是指不重复的索引值(基数)和数据表的记录总数的比值,索引选择性越高则查询效率越高, 唯一索引的选择性是1 ,这是最好的索引选择性,性能也是最好的。
select count(distinct email) / count(*) from tb_user ;
select count(distinct substring(email,1,5)) / count(*) from tb_user ;
前缀索引的查询流程
单列索引与联合索引
单列索引:即一个索引只包含单个列。
联合索引:即一个索引包含了多个列。
我们先来看看 tb_user 表中目前的索引情况:
在查询出来的索引中,既有单列索引,又有联合索引。
接下来,我们来执行一条 SQL 语句,看看其执行计划:
通过上述执行计划我们可以看出来,在 and 连接的两个字段 phone 、 name 上都是有单列索引的,但是最终mysql 只会选择一个索引,也就是说,只能走一个字段的索引,此时是会回表查询的。
紧接着,我们再来创建一个 phone 和 name 字段的联合索引来查询一下执行计划。
create unique index idx_user_phone_name on tb_user(phone,name);
此时,查询时,就走了联合索引,而在联合索引中包含 phone 、 name 的信息,在叶子节点下挂的是对应的主键id ,所以查询是无需回表查询的。
在业务场景中,如果存在多个查询条件,考虑针对于查询字段建立索引时,建议建立联合索引,
而非单列索引。
如果查询使用的是联合索引,具体的结构示意图如下:
索引设计原则
1). 针对于数据量较大,且查询比较频繁的表建立索引。
2). 针对于常作为查询条件( where )、排序( order by )、分组( group by )操作的字段建立索
引。
3). 尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高。
4). 如果是字符串类型的字段,字段的长度较长,可以针对于字段的特点,建立前缀索引。
5). 尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间避免回表,提高查询效率。
6). 要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价也就越大,会影响增删改的效率。
7). 如果索引列不能存储 NULL 值,请在创建表时使用 NOT NULL 约束它。当优化器知道每列是否包含NULL值时,它可以更好地确定哪个索引最有效地用于查询。
SQL优化
插入数据
insert
如果我们需要一次性往数据库表中插入多条记录,可以从以下三个方面进行优化。
insert into tb_test values(1,'tom');
insert into tb_test values(2,'cat');
insert into tb_test values(3,'jerry');
优化方案一
批量插入数据
Insert into tb_test values(1,'Tom'),(2,'Cat'),(3,'Jerry');
优化方案二
手动控制事务
start transaction;insert into tb_test values(1,'Tom'),(2,'Cat'),(3,'Jerry');insert into tb_test values(4,'Tom'),(5,'Cat'),(6,'Jerry');insert into tb_test values(7,'Tom'),(8,'Cat'),(9,'Jerry');commit;
优化方案三
主键顺序插入,性能要高于乱序插入。
主键乱序插入 : 8 1 9 21 88 2 4 15 89 5 7 3
主键顺序插入 : 1 2 3 4 5 7 8 9 15 21 88 89
大批量插入数据
如果一次性需要插入大批量数据 ( 比如 : 几百万的记录 ) ,使用 insert 语句插入性能较低,此时可以使
用 MySQL 数据库提供的 load 指令进行插入。操作如下:
可以执行如下指令,将数据脚本文件中的数据加载到表结构中:
-- 客户端连接服务端时,加上参数 -–local-infilemysql –-local-infile -u root -p-- 设置全局参数local_infile为1,开启从本地加载文件导入数据的开关set global local_infile = 1;-- 执行load指令将准备好的数据,加载到表结构中load data local infile '/root/sql1.log' into table tb_user fields
terminated by ',' lines terminated by '\n' ;
主键顺序插入性能高于乱序插入
示例演示 :
A. 创建表结构
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 ;
B. 设置参数
-- 客户端连接服务端时,加上参数 -–local-infile
mysql –-local-infile -u root -p
-- 设置全局参数local_infile为1,开启从本地加载文件导入数据的开关
set global local_infile = 1;
C. load 加载数据
load data local infile '/root/load_user_100w_sort.sql' into table tb_user
fields terminated by ',' lines terminated by '\n' ;
我们看到,插入 100w 的记录, 17s 就完成了,性能很好。
在 load 时,主键顺序插入性能高于乱序插入
主键优化
在上一小节,我们提到,主键顺序插入的性能是要高于乱序插入的。 这一小节,就来介绍一下具体的原因,然后再分析一下主键又该如何设计
1). 数据组织方式
在 InnoDB 存储引擎中,表数据都是根据主键顺序组织存放的,这种存储方式的表称为索引组织表
(index organized table IOT) 。
行数据,都是存储在聚集索引的叶子节点上的。而我们之前也讲解过InnoDB的逻辑结构图:
在 InnoDB 引擎中,数据行是记录在逻辑结构 page 页中的,而每一个页的大小是固定的,默认 16K 。
那也就意味着, 一个页中所存储的行也是有限的,如果插入的数据行 row 在该页存储不小,将会存储到下一个页中,页与页之间会通过指针连接。
2). 页分裂
页可以为空,也可以填充一半,也可以填充 100% 。每个页包含了 2-N 行数据 ( 如果一行数据过大,会行 溢出) ,根据主键排列。
A. 主键顺序插入效果
① . 从磁盘中申请页, 主键顺序插入
②. 第一个页没有满,继续往第一页插入
③. 当第一个也写满之后,再写入第二个页,页与页之间会通过指针连接
④. 当第二页写满了,再往第三页写入
B. 主键乱序插入效果
① . 加入 1#,2# 页都已经写满了,存放了如图所示的数据
② . 此时再插入 id 为 50 的记录,我们来看看会发生什么现象
会再次开启一个页,写入新的页中吗?
不会。因为,索引结构的叶子节点是有顺序的。按照顺序,应该存储在47之后。
但是47所在的1#页,已经写满了,存储不了50对应的数据了。 那么此时会开辟一个新的页 3#。
但是并不会直接将50存入3#页,而是会将1#页后一半的数据,移动到3#页,然后在3#页,插入50。
移动数据,并插入 id 为 50 的数据之后,那么此时,这三个页之间的数据顺序是有问题的。 1# 的下一个页,应该是3# , 3# 的下一个页是 2# 。 所以,此时,需要重新设置链表指针。
上述的这种现象,称之为 " 页分裂 " ,是比较耗费性能的操作。
3). 页合并
目前表中已有数据的索引结构 ( 叶子节点 ) 如下:
当我们对已有数据进行删除时,具体的效果如下 :
当删除一行记录时,实际上记录并没有被物理删除,只是记录被标记( flaged )为删除并且它的空间 变得允许被其他记录声明使用。
当我们继续删除2#的数据记录
当页中删除的记录达到 MERGE_THRESHOLD (默认为页的 50% ), InnoDB 会开始寻找最靠近的页(前或后)看看是否可以将两个页合并以优化空间使用。
删除数据,并将页合并之后,再次插入新的数据21,则直接插入3#页
知识小贴士:
MERGE_THRESHOLD :合并页的阈值,可以自己设置,在创建表或者创建索引时指定。
4). 索引设计原则
1.满足业务需求的情况下,尽量降低主键的长度。
2.插入数据时,尽量选择顺序插入,选择使用 AUTO_INCREMENT 自增主键。
3.尽量不要使用 UUID 做主键或者是其他自然主键,如身份证号。
4.业务操作时,避免对主键的修改。
order by优化
MySQL 的排序,有两种方式:
Using filesort : 通过表的索引或全表扫描,读取满足条件的数据行,然后在排序缓冲区 sort
buffer 中完成排序操作,所有不是通过索引直接返回排序结果的排序都叫 FileSort 排序。
Using index : 通过有序索引顺序扫描直接返回有序数据,这种情况即为 using index ,不需要
额外排序,操作效率高。
对于以上的两种排序方式, Using index 的性能高,而 Using filesort 的性能低,我们在优化排序
操作时,尽量要优化为 Using index 。
接下来,我们来做一个测试:
A. 数据准备
把之前测试时,为 tb_user 表所建立的部分索引直接删除掉
drop index idx_user_phone on tb_user;
drop index idx_user_phone_name on tb_user;
drop index idx_user_name on tb_user;
执行排序 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;
排序时 , 也需要满足最左前缀法则 , 否则也会出现 filesort 。因为在创建索引的时候, age 是第一个
字段, phone 是第二个字段,所以排序时,也就该按照这个顺序来,否则就会出现 Using
filesort 。
F. 根据 age, phone 进行降序一个升序,一个降序
explain select id,age,phone from tb_user order by age asc , phone desc ;
因为创建索引时,如果未指定顺序,默认都是按照升序排序的,而查询时,一个升序,一个降序,此时就会出现Using filesort 。
为了解决上述的问题,我们可以创建一个索引,这个联合索引中 age 升序排序,phone 倒序排序。
G. 创建联合索引(age 升序排序,phone 倒序排序)
create index idx_user_age_phone_ad on tb_user(age asc ,phone desc);
H. 然后再次执行如下SQL
explain select id,age,phone from tb_user order by age asc , phone desc ;
升序/降序联合索引结构图示:
由上述的测试 , 我们得出 order by 优化原则 :
A. 根据排序字段建立合适的索引,多字段排序时,也遵循最左前缀法则。
B. 尽量使用覆盖索引。
C. 多字段排序 , 一个升序一个降序,此时需要注意联合索引在创建时的规则( ASC/DESC )。
D. 如果不可避免的出现 filesort ,大数据量排序时,可以适当增大排序缓冲区大小
sort_buffer_size( 默认 256k) 。
group by优化
分组操作,我们主要来看看索引对于分组操作的影响。
首先我们先将 tb_user 表的索引全部删除掉 。
drop index idx_user_pro_age_sta on tb_user;
drop index idx_email_5 on tb_user;
drop index idx_user_age_phone_aa on tb_user;
drop index idx_user_age_phone_ad on tb_user;
接下来,在没有索引的情况下,执行如下 SQL ,查询执行计划:
explain select profession , count(*) from tb_user group by profession ;
然后,我们在针对于 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,查看执行计划
我们发现,如果仅仅根据 age 分组,就会出现 Using temporary ;而如果是根据profession,age两个字段同时分组,则不会出现 Using temporary 。原因是因为对于分组操作,在联合索引中,也是符合最左前缀法则的。
所以,在分组操作中,我们需要通过以下两点进行优化,以提升性能:
A. 在分组操作时,可以通过索引来提高效率。
B. 分组操作时,索引的使用也是满足最左前缀法则的。
limit优化
在数据量比较大时,如果进行 limit 分页查询,在查询时,越往后,分页查询效率越低。
我们一起来看看执行 limit 分页查询耗时对比:
通过测试我们会看到,越往后,分页查询效率越低,这就是分页查询的问题所在。
因为,当在进行分页查询时,如果执行 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;
count优化
概述
select count(*) from tb_user ;
在之前的测试中,我们发现,如果数据量很大,在执行 count 操作时,是非常耗时的。
MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个
数,效率很高; 但是如果是带条件的 count , MyISAM 也慢。
InnoDB 引擎就麻烦了,它执行 count(*) 的时候,需要把数据一行一行地从引擎里面读出
来,然后累积计数。
如果说要大幅度提升 InnoDB 表的 count 效率,主要的优化思路:自己计数 ( 可以借助于 redis 这样的数据库进行, 但是如果是带条件的 count 又比较麻烦了 ) 。
count 用法
count() 是一个聚合函数,对于返回的结果集,一行行地判断,如果 count 函数的参数不是NULL,累计值就加 1 ,否则不加,最后返回累计值。
用法: count ( * )、 count (主键)、 count (字段)、 count (数字)
按照效率排序的话, count( 字段 ) < count( 主键 id) < count(1) ≈ count(*) ,所以尽量使用 count(*) 。
update优化
我们主要需要注意一下 update 语句执行时的注意事项。
update course set name = 'javaEE' where id = 1 ;
当我们在执行删除的 SQL 语句时,会锁定 id 为 1 这一行的数据,然后事务提交之后,行锁释放。
但是当我们在执行如下SQL时。
update course set name = 'SpringBoot' where name = 'PHP' ;
当我们开启多个事务,在执行上述的 SQL 时,我们发现行锁升级为了表锁。 导致该 update 语句的性能大大降低
InnoDB 的行锁是针对索引加的锁,不是针对记录加的锁 , 并且该索引不能失效,否则会从行锁
升级为表锁 。
视图
介绍
视图( View )是一种虚拟存在的表。视图中的数据并不在数据库中实际存在,行和列数据来自定义视图的查询中使用的表,并且是在使用视图时动态生成的。
通俗的讲,视图只保存了查询的 SQL 逻辑,不保存查询结果。所以我们在创建视图的时候,主要的工作就落在创建这条SQL 查询语句上。
语法
创建
CREATE [OR REPLACE] VIEW 视图名称[(列名列表)] AS SELECT语句 [ WITH [CASCADED | LOCAL ] CHECK OPTION ]
查询
查看创建视图语句:SHOW CREATE VIEW 视图名称;查看视图数据:SELECT * FROM 视图名称 ...... ;
修改
方式一:
CREATE [OR REPLACE] VIEW 视图名称[(列名列表)] AS SELECT语句 [ WITH [ CASCADED | LOCAL ] CHECK OPTION ]方式二:
ALTER VIEW 视图名称[(列名列表)] AS SELECT语句 [ WITH [ CASCADED |LOCAL ] CHECK OPTION ]
删除
DROP VIEW [IF EXISTS] 视图名称 [,视图名称] ...
演示示例:
-- 创建视图create or replace view stu_v_1 as select id,name from student where id <= 10;-- 查询视图show create view stu_v_1;
select * from stu_v_1;
select * from stu_v_1 where id < 3;-- 修改视图create or replace view stu_v_1 as select id,name,no from student where id <= 10;
alter view stu_v_1 as select id,name from student where id <= 10;-- 删除视图drop view if exists stu_v_1;
上述我们演示了,视图应该如何创建、查询、修改、删除,那么我们能不能通过视图来插入、更新数据呢? 接下来,做一个测试
create or replace view stu_v_1 as select id,name from student where id <= 10 ;select * from stu_v_1;insert into stu_v_1 values(6,'Tom');insert into stu_v_1 values(17,'Tom22');
执行上述的 SQL ,我们会发现, id 为 6 和 17 的数据都是可以成功插入的。 但是我们执行查询,查询出来的数据,却没有id 为 17 的记录
因为我们在创建视图的时候,指定的条件为 id<=10, id 为 17 的数据,是不符合条件的,所以没有查
询出来,但是这条数据确实是已经成功的插入到了基表中。
如果我们定义视图时,如果指定了条件,然后我们在插入、修改、删除数据时,是否可以做到必须满足 条件才能操作,否则不能够操作呢? 答案是可以的,这就需要借助于视图的检查选项了。
检查选项
当使用 WITH CHECK OPTION 子句创建视图时, MySQL 会通过视图检查正在更改的每个行,例如 插 入,更新,删除,以使其符合视图的定义。 MySQL 允许基于另一个视图创建视图,它还会检查依赖视 图中的规则以保持一致性。为了确定检查的范围,mysql 提供了两个选项: CASCADED 和 LOCAL ,默认值为 CASCADED 。
1). CASCADED
级联。
比如, v2 视图是基于 v1 视图的,如果在 v2 视图创建的时候指定了检查选项为 cascaded ,但是 v1 视图创建时未指定检查选项。 则在执行检查时,不仅会检查v2 ,还会级联检查 v2 的关联视图 v1 。
2). LOCAL
本地。
比如, v2 视图是基于 v1 视图的,如果在 v2 视图创建的时候指定了检查选项为 local ,但是 v1 视图创
建时未指定检查选项。 则在执行检查时,知会检查 v2 ,不会检查 v2 的关联视图 v1 。
视图的更新
要使视图可更新,视图中的行与基础表中的行之间必须存在一对一的关系。如果视图包含以下任何一项,则该视图不可更新:
A. 聚合函数或窗口函数( SUM() 、 MIN() 、 MAX() 、 COUNT() 等)
B. DISTINCT
C. GROUP BY
D. HAVING
E. UNION 或者 UNION ALL
视图作用
1). 简单
视图不仅可以简化用户对数据的理解,也可以简化他们的操作。那些被经常使用的查询可以被定义为视图,从而使得用户不必为以后的操作每次指定全部的条件。
2). 安全
数据库可以授权,但不能授权到数据库特定行和特定的列上。通过视图用户只能查询和修改他们所能见到的数据
3). 数据独立
视图可帮助用户屏蔽真实表结构变化带来的影响。
案例
1). 为了保证数据库表的安全性,开发人员在操作 tb_user 表时,只能看到的用户的基本字段,屏蔽
手机号和邮箱两个字段。
create view tb_user_view as select id,name,profession,age,gender,status,createtime
from tb_user;
select * from tb_user_view;
2). 查询每个学生所选修的课程(三张表联查),这个功能在很多的业务中都有使用到,为了简化操作,定义一个视图
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;
存储过程
介绍
存储过程是事先经过编译并存储在数据库中的一段 SQL 语句的集合,调用存储过程可以简化应用开发人员的很多工作,减少数据在数据库和应用服务器之间的传输,对于提高数据处理的效率是有好处的。
存储过程思想上很简单,就是数据库 SQL 语言层面的代码封装与重用。
特点:
1.封装,复用 -----------------------> 可以把某一业务 SQL 封装在存储过程中,需要用到
的时候直接调用即可。
2.可以接收参数,也可以返回数据 --------> 再存储过程中,可以传递参数,也可以接收返回
值。
3.减少网络交互,效率提升 -------------> 如果涉及到多条 SQL ,每执行一次都是一次网络传
输。 而如果封装在存储过程中,我们只需要网络交互一次可能就可以了。
基本语法
创建
CREATE PROCEDURE 存储过程名称 ([ 参数列表 ])BEGIN-- SQL语句END ;
调用
CALL 名称 ([ 参数 ]);
查看
-- 查询指定数据库的存储过程及状态信息SELECT * FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = 'xxx'; -- 查询某个存储过程的定义SHOW CREATE PROCEDURE 存储过程名称 ;
删除
DROP PROCEDURE [ IF EXISTS ] 存储过程名称 ;
注意 :
在命令行中,执行创建存储过程的 SQL 时,需要通过关键字 delimiter 指定 SQL 语句的结束符
演示示例 :
-- 存储过程基本语法
-- 创建create procedure p1()
begin
select count(*) from student;
end;-- 调用call p1();-- 查看select * from information_schema.ROUTINES where ROUTINE_SCHEMA = 'itcast';show create procedure p1;-- 删除drop procedure if exists p1;
变量
在 MySQL 中变量分为三种类型 : 系统变量、用户定义变量、局部变量。
系统变量
系统变量 是 MySQL 服务器提供,不是用户定义的,属于服务器层面。分为全局变量( GLOBAL )、会话变量(SESSION )。
1). 查看系统变量
SHOW [ SESSION | GLOBAL ] VARIABLES ; -- 查看所有系统变量SHOW [ SESSION | GLOBAL ] VARIABLES LIKE '......'; -- 可以通过LIKE模糊匹配方式查找变量SELECT @@[SESSION | GLOBAL] 系统变量名; -- 查看指定变量的值
2). 设置系统变量
SET [ SESSION | GLOBAL ] 系统变量名 = 值 ;SET @@[SESSION | GLOBAL]系统变量名 = 值 ;
注意 :
如果没有指定 SESSION/GLOBAL ,默认是 SESSION ,会话变量。
A. 全局变量 (GLOBAL): 全局变量针对于所有的会话。
B. 会话变量 (SESSION): 会话变量针对于单个会话,在另外一个会话窗口就不生效了。
mysql 服务重新启动之后,所设置的全局参数会失效,要想不失效,可以在 /etc/my.cnf 中配置。
演示示例 :
-- 查看系统变量show session variables ;show session variables like 'auto%';show global variables like 'auto%';select @@global.autocommit;select @@session.autocommit;-- 设置系统变量set session autocommit = 1;insert into course(id, name) VALUES (6, 'ES');set global autocommit = 0;select @@global.autocommit;
用户定义变量
用户定义变量 是用户根据需要自己定义的变量,用户变量不用提前声明,在用的时候直接用 "@ 变量名" 使用就可以。其作用域为当前连接。
1). 赋值
方式一 :
SET @var_name = expr [, @var_name = expr] ... ;SET @var_name := expr [, @var_name := expr] ... ;
赋值时,可以使用 = ,也可以使用 := 。
方式二 :
SELECT @var_name := expr [, @var_name := expr] ... ;SELECT 字段名 INTO @var_name FROM 表名;
2). 使用
SELECT @var_name ;
注意 : 用户定义的变量无需对其进行声明或初始化,只不过获取到的值为 NULL 。
局部变量
局部变量 是根据需要定义的在局部生效的变量,访问之前,需要 DECLARE 声明。可用作存储过程内的局部变量和输入参数,局部变量的范围是在其内声明的BEGIN ... END 块。
1). 声明
DECLARE 变量名 变量类型 [DEFAULT ... ] ;
变量类型就是数据库字段类型: INT 、 BIGINT 、 CHAR 、 VARCHAR 、 DATE 、 TIME 等。
2). 赋值
SET 变量名 = 值 ;SET 变量名 := 值 ;SELECT 字段名 INTO 变量名 FROM 表名 ... ;
演示示例 :
-- 声明局部变量 - declare
-- 赋值create procedure p2()begindeclare stu_count int default 0;select count(*) into stu_count from student;select stu_count;end;call p2();
if
1). 介绍
if 用于做条件判断,具体的语法结构为:
IF 条件1 THEN.....
ELSEIF 条件2 THEN -- 可选.....
ELSE -- 可选.....END IF;
在 if 条件判断的结构中, ELSE IF 结构可以有多个,也可以没有。 ELSE 结构可以有,也可以没有。
参数
1). 介绍
参数的类型,主要分为以下三种: IN 、 OUT 、 INOUT 。 具体的含义如下:
用法:
CREATE PROCEDURE 存储过程名称 ([ IN/OUT/INOUT 参数名 参数类型 ])BEGIN-- SQL语句
END ;
2). 案例
根据传入参数 score ,判定当前分数对应的分数等级,并返回。
score >= 85 分,等级为优秀。
score >= 60 分 且 score < 85 分,等级为及格。
score < 60 分,等级为不及格。
create procedure p4(in score int, out result varchar(10))
beginif score >= 85 thenset result := '优秀';else if score >= 60 then set result := '及格';elseset result := '不及格';end if;
end;-- 定义用户变量 @result来接收返回的数据, 用户变量可以不用声明call p4(18, @result);select @result;
3). 案例二
将 传入 的 200 分制的分数,进行换算,换算成百分制,然后 返回 。
create procedure p5(inout score double)
beginset score := score * 0.5;end;set @score = 198;call p5(@score);select @score;
case
1). 介绍
case 结构及作用,和我们在基础篇中所讲解的流程控制函数很类似。有两种语法格式:
语法 1 :
-- 含义: 当case_value的值为 when_value1时,执行statement_list1,当值为 when_value2时,
执行statement_list2, 否则就执行 statement_listCASE 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_listCASEWHEN 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 ;select concat('您输入的月份为: ',month, ', 所属的季度为: ',result);
end;call p6(16);
注意:如果判定条件有多个,多个条件之间,可以使用 and 或 or 进行连接。
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);
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);
loop
1). 介绍
LOOP 实现简单的循环,如果不在 SQL 逻辑中增加退出循环的条件,可以用其来实现简单的死循环。
LOOP 可以配合一下两个语句使用:
LEAVE :配合循环使用,退出循环。
ITERATE:必须用在循环中,作用是跳过当前循环剩下的语句,直接进入下一次循环。
[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 xxcreate 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 xxcreate 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);
游标
1). 介绍
游标( CURSOR )是用来存储查询结果集的数据类型 , 在存储过程和函数中可以使用游标对结果集进行循环的处理。游标的使用包括游标的声明、OPEN 、 FETCH 和 CLOSE ,其语法分别如下。
A. 声明游标
DECLARE 游标名称 CURSOR FOR 查询语句 ;
B. 打开游标
OPEN 游标名称 ;
C. 获取游标记录
FETCH 游标名称 INTO 变量 [, 变量 ] ;
D. 关闭游标
CLOSE 游标名称 ;
2). 案例
根据传入的参数 uage ,来查询用户表 tb_user 中,所有的用户年龄小于等于 uage 的用户姓名
( name )和专业( profession ),并将用户的姓名和专业插入到所创建的一张新表
(id,name,profession) 中。
-- 逻辑:
-- A. 声明游标, 存储查询结果集
-- B. 准备: 创建表结构
-- C. 开启游标
-- D. 获取游标中的记录
-- E. 插入数据到新
-- F. 关闭游标create procedure p11(in uage int)
begindeclare 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 dofetch u_cursor into uname,upro;insert into tb_user_pro values (null, uname, upro);end while;close u_cursor;
end;call p11(30);表中
上述的存储过程,最终我们在调用的过程中,会报错,之所以报错是因为上面的 while 循环中,并没有退出条件。当游标的数据集获取完毕之后,再次获取数据,就会报错,从而终止了程序的执行。
但是此时, tb_user_pro 表结构及其数据都已经插入成功了,我们可以直接刷新表结构,检查表结构中的数据。
上述的功能,虽然我们实现了,但是逻辑并不完善,而且程序执行完毕,获取不到数据,数据库还报错。 接下来,我们就需要来完成这个存储过程,并且解决这个问题。
要想解决这个问题,就需要通过 MySQL 中提供的 条件处理程序 Handler 来解决。
条件处理程序
1). 介绍
条件处理程序( Handler )可以用来定义在流程控制结构执行过程中遇到问题时相应的处理步骤。具体语法为:
DECLARE handler_action HANDLER FOR condition_value [, condition_value]
... statement ;handler_action 的取值:CONTINUE: 继续执行当前程序EXIT: 终止执行当前程序condition_value 的取值:SQLSTATE sqlstate_value: 状态码,如 02000SQLWARNING: 所有以01开头的SQLSTATE代码的简写NOT FOUND: 所有以02开头的SQLSTATE代码的简写SQLEXCEPTION: 所有没有被SQLWARNING 或 NOT FOUND捕获的SQLSTATE代码的简写
案例
我们继续来完成在上一小节提出的这个需求,并解决其中的问题。
根据传入的参数 uage ,来查询用户表 tb_user 中,所有的用户年龄小于等于 uage 的用户姓名
( name )和专业( profession ),并将用户的姓名和专业插入到所创建的一张新表
(id,name,profession) 中。
A. 通过 SQLSTATE 指定具体的状态码
-- 逻辑:
-- A. 声明游标, 存储查询结果集
-- B. 准备: 创建表结构
-- C. 开启游标
-- D. 获取游标中的记录
-- E. 插入数据到新表中
-- F. 关闭游标
create procedure p11(in uage int)
begindeclare uname varchar(100);declare upro varchar(100);declare u_cursor cursor for select name,profession from tb_user where age <=uage;
-- 声明条件处理程序 : 当SQL语句执行抛出的状态码为02000时,将关闭游标u_cursor,并退出declare exit handler for SQLSTATE '02000' close u_cursor;drop table if exists tb_user_pro;create table if not exists tb_user_pro(id int primary key auto_increment,name varchar(100),profession varchar(100));open u_cursor;while true dofetch u_cursor into uname,upro;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
create procedure p12(in uage int)
begindeclare uname varchar(100);declare upro varchar(100);declare u_cursor cursor for select name,profession from tb_user where age <=uage;-- 声明条件处理程序 : 当SQL语句执行抛出的状态码为02开头时,将关闭游标u_cursor,并退出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 dofetch u_cursor into uname,upro;insert into tb_user_pro values (null, uname, upro);end while;close u_cursor;
end;
call p12(30);
具体的错误状态码,可以参考官方文档:
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
存储函数
1). 介绍
存储函数是有返回值的存储过程,存储函数的参数只能是 IN 类型的。具体语法如下:
CREATE FUNCTION 存储函数名称 ([ 参数列表 ])
RETURNS type [characteristic ...]
BEGIN-- SQL语句RETURN ...;
END ;
characteristic 说明:
DETERMINISTIC:相同的输入参数总是产生相同的结果
NO SQL :不包含 SQL 语句。
READS SQL DATA:包含读取数据的语句,但不包含写入数据的语句。
2). 案例
计算从 1 累加到 n 的值, n 为传入的参数值。
create function fun1(n int)
returns int deterministic
begindeclare 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 特性,否则就会报如下错误:
触发器
介绍
触发器是与表有关的数据库对象,指在 insert/update/delete 之前 (BEFORE) 或之后 (AFTER) ,触
发并执行触发器中定义的 SQL 语句集合。触发器的这种特性可以协助应用在数据库端确保数据的完整性 , 日志记录 , 数据校验等操作 。
使用别名 OLD 和 NEW 来引用触发器中发生变化的记录内容,这与其他的数据库是相似的。现在触发器还只支持行级触发,不支持语句级触发。
语法
1). 创建
CREATE TRIGGER trigger_nameBEFORE/AFTER INSERT/UPDATE/DELETEON tbl_name FOR EACH ROW -- 行级触发器BEGINtrigger_stmt ;
END;
2). 查看
SHOW TRIGGERS ;
3). 删除
DROP TRIGGER [schema_name.]trigger_name ; -- 如果没有指定 schema_name,默认为当前数
据库 。
案例
通过触发器记录 tb_user 表的数据变更日志,将变更日志插入到日志表 user_logs 中 , 包含增加 ,
修改 , 删除 ;
表结构准备 :
-- 准备工作 : 日志表 user_logs
create table user_logs(id int(11) not null auto_increment,operation varchar(20) not null comment '操作类型, insert/update/delete',operate_time datetime not null comment '操作时间',operate_id int(11) not null comment '操作的ID',operate_params varchar(500) comment '操作参数',primary key(`id`)
)engine=innodb default charset=utf8;
A. 插入数据触发器
create trigger tb_user_insert_trigger
after insert on tb_user for each row
begininsert 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());
测试完毕之后,检查日志表中的数据是否可以正常插入,以及插入数据的正确性。
B. 修改数据触发器
create trigger tb_user_update_trigger
after 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;
C. 删除数据触发器
create trigger tb_user_delete_trigger
after delete on tb_user for each row
begininsert 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;
锁
概述
锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中,除传统的计算资源( CPU RAM、 I/O )的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。
MySQL 中的锁,按照锁的粒度分,分为以下三类:
全局锁:锁定数据库中的所有表。
表级锁:每次操作锁住整张表。
行级锁:每次操作锁住对应的行数据。
全局锁
介绍
全局锁就是对整个数据库实例加锁,加锁后整个实例就处于只读状态,后续的 DML 的写语句, DDL 语句,已经更新操作的事务提交语句都将被阻塞。
其典型的使用场景是做全库的逻辑备份,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性。
为什么全库逻辑备份,就需要加全就锁呢?
A. 我们一起先来分析一下不加全局锁,可能存在的问题。
假设在数据库中存在这样三张表 : tb_stock 库存表, tb_order 订单表, tb_orderlog 订单日
志表
在进行数据备份时,先备份了 tb_stock 库存表。
然后接下来,在业务系统中,执行了下单操作,扣减库存,生成订单(更新 tb_stock 表,插入
tb_order 表)。
然后再执行备份 tb_order 表的逻辑。
业务中执行插入订单日志操作。
最后,又备份了 tb_orderlog 表。
此时备份出来的数据,是存在问题的。因为备份出来的数据, tb_stock 表与 tb_order 表的数据不一
致 ( 有最新操作的订单信息 , 但是库存数没减 ) 。
那如何来规避这种问题呢 ? 此时就可以借助于 MySQL 的全局锁来解决。
对数据库进行进行逻辑备份之前,先对整个数据库加上全局锁,一旦加了全局锁之后,其他的 DDL, DML全部都处于阻塞状态,但是可以执行 DQL 语句,也就是处于只读状态,而数据备份就是查询操作。
那么数据在进行逻辑备份的过程中,数据库中的数据就是不会发生变化的,这样就保证了数据的一致性和完整性
语法
1.加全局锁
flush tables with read lock ;
2). 数据备份
mysqldump -uroot –p1234 itcast > itcast.sql
这个命令是在命令行中执行的
数据备份的相关指令 , 在后面 MySQL 管理章节 , 还会详细讲解 .
3.释放锁
特点
数据库中加全局锁,是一个比较重的操作,存在以下问题:
如果在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆。
如果在从库上备份,那么在备份期间从库不能执行主库同步过来的二进制日志( binlog ),会导
致主从延迟
在 InnoDB 引擎中,我们可以在备份时加上参数 --single-transaction 参数来完成不加锁的一致
性数据备份。
mysqldump --single-transaction -uroot –p123456 itcast > itcast.sql
表级锁
介绍
表级锁,每次操作锁住整张表。锁定粒度大,发生锁冲突的概率最高,并发度最低。应用在 MyISAM, InnoDB、 BDB 等存储引擎中。
对于表级锁,主要分为以下三类:
1.表锁
2.元数据锁( meta data lock , MDL )
3.意向锁
表锁
对于表锁,分为两类:
1.表共享读锁( read lock )
2.表独占写锁( write lock )
语法:
加锁: lock tables 表名 ... read/write 。
释放锁: unlock tables / 客户端断开连接 。
左侧为客户端一,对指定表加了读锁,不会影响右侧客户端二的读,但是会阻塞右侧客户端的写。
测试 :
B. 写锁
左侧为客户端一,对指定表加了写锁,会阻塞右侧客户端的读和写。
测试
结论 : 读锁不会阻塞其他客户端的读,但是会阻塞写。写锁既会阻塞其他客户端的读,又会阻塞
其他客户端的写。
元数据锁
meta data lock , 元数据锁,简写 MDL 。
MDL 加锁过程是系统自动控制,无需显式使用,在访问一张表的时候会自动加上。 MDL 锁主要作用是维护表元数据的数据一致性,在表上有活动事务的时候,不可以对元数据进行写入操作。为了避免 DML 与 DDL 冲突,保证读写的正确性。
这里的元数据,大家可以简单理解为就是一张表的表结构。 也就是说,某一张表涉及到未提交的事务时,是不能够修改这张表的表结构的。
在 MySQL5.5 中引入了 MDL ,当对一张表进行增删改查的时候,加 MDL 读锁 ( 共享 ) ;当对表结构进行变更操作的时候,加MDL 写锁 ( 排他 ) 。
常见的 SQL 操作时,所添加的元数据锁:
演示:
当执行 SELECT 、 INSERT 、 UPDATE 、 DELETE 等语句时,添加的是元数据共享锁( SHARED_READ / SHARED_WRITE),之间是兼容的。
当执行 SELECT 语句时,添加的是元数据共享锁( SHARED_READ ),会阻塞元数据排他锁
( EXCLUSIVE ),之间是互斥的。
我们可以通过下面的SQL,来查看数据库中的元数据锁的情况:
select object_type,object_schema,object_name,lock_type,lock_duration from
performance_schema.metadata_locks ;
我们在操作过程中,可以通过上述的 SQL 语句,来查看元数据锁的加锁情况。
意向锁
1). 介绍
为了避免 DML 在执行时,加的行锁与表锁的冲突,在 InnoDB 中引入了意向锁,使得表锁不用检查每行数据是否加锁,使用意向锁来减少表锁的检查。
假如没有意向锁,客户端一对表加了行锁后,客户端二如何给表加表锁呢,来通过示意图简单分析一下:
首先客户端一,开启一个事务,然后执行 DML 操作,在执行 DML 语句时,会对涉及到的行加锁。
当客户端二,想对这张表加表锁时,会检查当前表是否有对应的行锁,如果没有,则添加表锁,此时就会从第一行数据,检查到最后一行数据,效率较低。
有了意向锁之后 :
客户端一,在执行 DML 操作时,会对涉及的行加行锁,同时也会对该表加上意向锁。
而其他客户端,在对这张表加表锁的时候,会根据该表上所加的意向锁来判定是否可以成功加表锁,而不用逐行判断行锁情况了。
2). 分类
意向共享锁 (IS): 由语句 select ... lock in share mode 添加 。 与 表锁共享锁 (read)兼容,与表锁排他锁 (write) 互斥。
意向排他锁 (IX): 由 insert 、 update 、 delete 、 select...for update 添加 。与表锁共享锁(read) 及排他锁 (write) 都互斥,意向锁之间不会互斥。
一旦事务提交了,意向共享锁、意向排他锁,都会自动释放。
可以通过以下 SQL ,查看意向锁及行锁的加锁情况:
select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from
performance_schema.data_locks;
演示:
A. 意向共享锁与表读锁是兼容的
B. 意向排他锁与表读锁、写锁都是互斥的
行级锁
介绍
行级锁,每次操作锁住对应的行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。应用在
InnoDB 存储引擎中。
InnoDB 的数据是基于索引组织的,行锁是通过对索引上的索引项加锁来实现的,而不是对记录加的锁。对于行级锁,主要分为以下三类:
行锁( Record Lock ):锁定单个行记录的锁,防止其他事务对此行进行 update 和 delete 。在
RC 、 RR 隔离级别下都支持。
间隙锁( Gap Lock ):锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事
务在这个间隙进行 insert ,产生幻读。在 RR 隔离级别下都支持。
临键锁( Next-Key Lock ):行锁和间隙锁组合,同时锁住数据,并锁住数据前面的间隙 Gap 。
在 RR 隔离级别下支持。
行锁
1). 介绍
InnoDB 实现了以下两种类型的行锁:
共享锁( S ):允许一个事务去读一行,阻止其他事务获得相同数据集的排它锁。
排他锁( X ):允许获取排他锁的事务更新数据,阻止其他事务获得相同数据集的共享锁和排他
锁。
两种行锁的兼容情况如下:
常见的SQL语句,在执行时,所加的行锁如下:
2). 演示
默认情况下, InnoDB 在 REPEATABLE READ 事务隔离级别运行, InnoDB 使用 next-key 锁进行搜
索和索引扫描,以防止幻读。
针对唯一索引进行检索时,对已存在的记录进行等值匹配时,将会自动优化为行锁。
InnoDB 的行锁是针对于索引加的锁,不通过索引条件检索数据,那么 InnoDB 将对表中的所有记
录加锁,此时 就会升级为表锁。
可以通过以下 SQL ,查看意向锁及行锁的加锁情况:
select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from
performance_schema.data_locks;
示例演示
数据准备 :
CREATE TABLE `stu` (
`id` int NOT NULL PRIMARY KEY AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`age` int NOT NULL
) ENGINE = InnoDB CHARACTER SET = utf8mb4;
INSERT INTO `stu` VALUES (1, 'tom', 1);
INSERT INTO `stu` VALUES (3, 'cat', 3);
INSERT INTO `stu` VALUES (8, 'rose', 8);
INSERT INTO `stu` VALUES (11, 'jetty', 11);
INSERT INTO `stu` VALUES (19, 'lily', 19);
INSERT INTO `stu` VALUES (25, 'luci', 25);
演示行锁的时候,我们就通过上面这张表来演示一下。
A. 普通的 select 语句,执行时,不会加锁。
B. select...lock in share mode,加共享锁,共享锁与共享锁之间兼容。
共享锁与排他锁之间互斥。
客户端一获取的是 id 为 1 这行的共享锁,客户端二是可以获取 id 为 3 这行的排它锁的,因为不是同一行数据。 而如果客户端二想获取id 为 1 这行的排他锁,会处于阻塞状态,以为共享锁与排他锁之间互斥。
当客户端一,执行 update 语句,会为 id 为 1 的记录加排他锁; 客户端二,如果也执行 update 语句更
新 id 为 1 的数据,也要为 id 为 1 的数据加排他锁,但是客户端二会处于阻塞状态,因为排他锁之间是互斥的。 直到客户端一,把事务提交了,才会把这一行的行锁释放,此时客户端二,解除阻塞。
D. 无索引行锁升级为表锁
stu 表中数据如下
我们在两个客户端中执行如下操作:
在客户端一中,开启事务,并执行 update 语句,更新 name 为 Lily 的数据,也就是 id 为 19 的记录 。
然后在客户端二中更新 id 为 3 的记录,却不能直接执行,会处于阻塞状态,为什么呢?
原因就是因为此时,客户端一,根据 name 字段进行更新时, name 字段是没有索引的,如果没有索引,此时行锁会升级为表锁( 因为行锁是对索引项加的锁,而 name 没有索引 ) 。
接下来,我们再针对 name 字段建立索引,索引建立之后,再次做一个测试:
此时我们可以看到,客户端一,开启事务,然后依然是根据 name 进行更新。而客户端二,在更新 id 为 3的数据时,更新成功,并未进入阻塞状态。 这样就说明,我们根据索引字段进行更新操作,就可以避 免行锁升级为表锁的情况。
间隙锁 & 临键锁
默认情况下, InnoDB 在 REPEATABLE READ 事务隔离级别运行, InnoDB 使用 next-key 锁进行搜
索和索引扫描,以防止幻读。
索引上的等值查询 ( 唯一索引 ) ,给不存在的记录加锁时 , 优化为间隙锁 。
索引上的等值查询 ( 非唯一普通索引 ) ,向右遍历时最后一个值不满足查询需求时, next-key
lock 退化为间隙锁。
索引上的范围查询 ( 唯一索引 )-- 会访问到不满足条件的第一个值为止
注意:间隙锁唯一目的是防止其他事务插入间隙。间隙锁可以共存,一个事务采用的间隙锁不会
阻止另一个事务在同一间隙上采用间隙锁。
示例演示
A. 索引上的等值查询 ( 唯一索引 ) ,给不存在的记录加锁时 , 优化为间隙锁 。
B. 索引上的等值查询 ( 非唯一普通索引 ) ,向右遍历时最后一个值不满足查询需求时, next-key
lock 退化为间隙锁。
介绍分析一下:
我们知道 InnoDB 的 B+ 树索引,叶子节点是有序的双向链表。 假如,我们要根据这个二级索引查询值为18 的数据,并加上共享锁,我们是只锁定 18 这一行就可以了吗? 并不是,因为是非唯一索引,这个结构中可能有多个18 的存在,所以,在加锁时会继续往后找,找到一个不满足条件的值(当前案例中也就是29 )。此时会对 18 加临键锁,并对 29 之前的间隙加锁。
C. 索引上的范围查询(唯一索引)--会访问到不满足条件的第一个值为止。
查询的条件为 id>=19 ,并添加共享锁。 此时我们可以根据数据库表中现有的数据,将数据分为三个部
分:
[19]
(19,25]
(25,+∞]
所以数据库数据在加锁是,就是将 19 加了行锁, 25 的临键锁(包含 25 及 25 之前的间隙),正无穷的临键锁( 正无穷及之前的间隙 ) 。
InnoDB引擎
逻辑存储结构
InnoDB 的逻辑存储结构如下图所示 :
1). 表空间
表空间是InnoDB存储引擎逻辑结构的最高层, 如果用户启用了参数 innodb_file_per_table(在 8.0版本中默认开启) ,则每张表都会有一个表空间(xxx.ibd),一个mysql实例可以对应多个表空 间,用于存储记录、索引等数据。
2). 段
段,分为数据段( Leaf node segment )、索引段( Non-leaf node segment )、回滚段
( Rollback segment ), InnoDB 是索引组织表,数据段就是 B+ 树的叶子节点, 索引段即为 B+ 树的
非叶子节点。段用来管理多个 Extent (区)。
3). 区
区,表空间的单元结构,每个区的大小为 1M 。 默认情况下, InnoDB 存储引擎页大小为 16K , 即一 个区中一共有64 个连续的页。
4). 页
页,是 InnoDB 存储引擎磁盘管理的最小单元,每个页的大小默认为 16KB 。为了保证页的连续性,InnoDB 存储引擎每次从磁盘申请 4-5 个区。
5). 行
行, InnoDB 存储引擎数据是按行进行存放的。
在行中,默认有两个隐藏字段:
Trx_id :每次对某条记录进行改动时,都会把对应的事务 id 赋值给 trx_id 隐藏列。
Roll_pointer :每次对某条引记录进行改动时,都会把旧的版本写入到 undo 日志中,然后这个
隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
架构
概述
MySQL5.5 版本开始,默认使用 InnoDB 存储引擎,它擅长事务处理,具有崩溃恢复特性,在日常开发中使用非常广泛。下面是InnoDB 架构图,左侧为内存结构,右侧为磁盘结构。
内存结构
在左侧的内存结构中,主要分为这么四大块儿: Buffer Pool 、 Change Buffer 、 Adaptive
Hash Index 、 Log Buffer 。 接下来介绍一下这四个部分。
1). Buffer Pool
InnoDB 存储引擎基于磁盘文件存储,访问物理硬盘和在内存中进行访问,速度相差很大,为了尽可能弥补这两者之间的I/O 效率的差值,就需要把经常使用的数据加载到缓冲池中,避免每次访问都进行磁盘I/O 。
在 InnoDB 的缓冲池中不仅缓存了索引页和数据页,还包含了 undo 页、插入缓存、自适应哈希索引以及InnoDB的锁信息等等。
缓冲池 Buffer Pool ,是主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增删改查操作时,先操作缓冲池中的数据(若缓冲池没有数据,则从磁盘加载并缓存),然后再以一定频率刷新到磁盘,从而减少磁盘IO ,加快处理速度。
缓冲池以 Page 页为单位,底层采用链表数据结构管理 Page 。根据状态,将 Page 分为三种类型:
• free page :空闲 page ,未被使用。
• clean page :被使用 page ,数据没有被修改过。
• dirty page :脏页,被使用 page ,数据被修改过,也中数据与磁盘的数据产生了不一致。
在专用服务器上,通常将多达 80 %的物理内存分配给缓冲池 。参数设置: show variables
like 'innodb_buffer_pool_size';
2). Change Buffer
Change Buffer ,更改缓冲区(针对于非唯一二级索引页),在执行 DML 语句时,如果这些数据 Page 没有在Buffer Pool中,不会直接操作磁盘,而会将数据变更存在更改缓冲区 Change Buffer
中,在未来数据被读取时,再将数据合并恢复到 Buffer Pool 中,再将合并后的数据刷新到磁盘中。
Change Buffer 的意义是什么呢 ?
先来看一幅图,这个是二级索引的结构图:
与聚集索引不同,二级索引通常是非唯一的,并且以相对随机的顺序插入二级索引。同样,删除和更新可能会影响索引树中不相邻的二级索引页,如果每一次都操作磁盘,会造成大量的磁盘IO 。有了ChangeBuffer之后,我们可以在缓冲池中进行合并处理,减少磁盘 IO 。
3). Adaptive Hash Index
自适应 hash 索引,用于优化对 Buffer Pool 数据的查询。 MySQL 的 innoDB 引擎中虽然没有直接支持
hash 索引,但是给我们提供了一个功能就是这个自适应 hash 索引。因为前面我们讲到过, hash 索引在进行等值匹配时,一般性能是要高于B+ 树的,因为 hash 索引一般只需要一次 IO 即可,而 B+ 树,可能需要几次匹配,所以hash 索引的效率要高,但是 hash 索引又不适合做范围查询、模糊匹配等。
InnoDB 存储引擎会监控对表上各索引页的查询,如果观察到在特定的条件下 hash 索引可以提升速度,则建立hash 索引,称之为自适应 hash 索引。
自适应哈希索引,无需人工干预,是系统根据情况自动完成。
参数: adaptive_hash_index
4.Log Buffer
Log Buffer :日志缓冲区,用来保存要写入到磁盘中的 log 日志数据( redo log 、 undo log ),
默认大小为 16MB ,日志缓冲区的日志会定期刷新到磁盘中。如果需要更新、插入或删除许多行的事务,增加日志缓冲区的大小可以节省磁盘 I/O 。
参数 :
innodb_log_buffer_size :缓冲区大小
innodb_flush_log_at_trx_commit :日志刷新到磁盘时机,取值主要包含以下三个:
1: 日志在每次事务提交时写入并刷新到磁盘,默认值。
0: 每秒将日志写入并刷新到磁盘一次。
2: 日志在每次事务提交后写入,并每秒刷新到磁盘一次。
磁盘结构
接下来,再来看看InnoDB体系结构的右边部分,也就是磁盘结构:
1). System Tablespace
系统表空间是更改缓冲区的存储区域。如果表是在系统表空间而不是每个表文件或通用表空间中创建的,它也可能包含表和索引数据。( 在 MySQL5.x 版本中还包含 InnoDB 数据字典、 undolog 等 )
参数: innodb_data_file_path
系统表空间,默认的文件名叫 ibdata1 。
2). File-Per-Table Tablespaces
如果开启了 innodb_file_per_table 开关 ,则每个表的文件表空间包含单个 InnoDB 表的数据和索
引 ,并存储在文件系统上的单个数据文件中。
开关参数: innodb_file_per_table ,该参数默认开启。
那也就是说,我们没创建一个表,都会产生一个表空间文件,如图:
3). General Tablespaces
通用表空间,需要通过 CREATE TABLESPACE 语法创建通用表空间,在创建表时,可以指定该表空间。
A. 创建表空间
CREATE TABLESPACE ts_name ADD DATAFILE 'file_name' ENGINE = engine_name;
B. 创建表时指定表空间
CREATE TABLE xxx ... TABLESPACE ts_name;
4). Undo Tablespaces
撤销表空间, MySQL 实例在初始化时会自动创建两个默认的 undo 表空间(初始大小 16M ),用于存储undolog日志。
5). Temporary Tablespaces
InnoDB 使用会话临时表空间和全局临时表空间。存储用户创建的临时表等数据。
6). Doublewrite Buffer Files
双写缓冲区, innoDB 引擎将数据页从 Buffer Pool 刷新到磁盘前,先将数据页写入双写缓冲区文件
中,便于系统异常时恢复数据。
7). Redo Log
重做日志,是用来实现事务的持久性。该日志文件由两部分组成:重做日志缓冲( redo log
buffer )以及重做日志文件( redo log ) , 前者是在内存中,后者在磁盘中。当事务提交之后会把所
有修改信息都会存到该日志中 , 用于在刷新脏页到磁盘时 , 发生错误时 , 进行数据恢复使用。
以循环方式写入重做日志文件,涉及两个文件:
前面我们介绍了 InnoDB 的内存结构,以及磁盘结构,那么内存中我们所更新的数据,又是如何到磁盘中的呢? 此时,就涉及到一组后台线程,接下来,就来介绍一些InnoDB 中涉及到的后台线程。
后台线程
在 InnoDB 的后台线程中,分为 4 类,分别是: Master Thread 、 IO Thread 、 Purge Thread 、
Page Cleaner Thread 。
1). Master Thread
核心后台线程,负责调度其他线程,还负责将缓冲池中的数据异步刷新到磁盘中 , 保持数据的一致性,还包括脏页的刷新、合并插入缓存、undo 页的回收 。
2). IO Thread
在 InnoDB 存储引擎中大量使用了 AIO 来处理 IO 请求 , 这样可以极大地提高数据库的性能,而 IO
Thread 主要负责这些 IO 请求的回调。
我们可以通过以下的这条指令,查看到InnoDB的状态信息,其中就包含IO Thread信息。
show engine innodb status \G;
3). Purge Thread
主要用于回收事务已经提交了的 undo log ,在事务提交之后, undo log 可能不用了,就用它来回
收。
4). Page Cleaner Thread
协助 Master Thread 刷新脏页到磁盘的线程,它可以减轻 Master Thread 的工作压力,减少阻
塞。
事务基础
1). 事务
事务 是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。
2). 特性
• 原子性( Atomicity ):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
• 一致性( Consistency ):事务完成时,必须使所有的数据都保持一致状态。
• 隔离性( Isolation ):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环
境下运行。
• 持久性( Durability ):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。
那实际上,我们研究事务的原理,就是研究 MySQL 的 InnoDB 引擎是如何保证事务的这四大特性的。
而对于这四大特性,实际上分为两个部分。 其中的原子性、一致性、持久化,实际上是由 InnoDB 中的两份日志来保证的,一份是redo log 日志,一份是 undo log 日志。 而持久性是通过数据库的锁,加上MVCC 来保证的。
我们在讲解事务原理的时候,主要就是来研究一下redolog,undolog以及MVCC。
redo log
重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性。
该日志文件由两部分组成:重做日志缓冲( redo log buffer )以及重做日志文件( redo log
file ) , 前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都存到该日志文件中 , 用
于在刷新脏页到磁盘 , 发生错误时 , 进行数据恢复使用。
如果没有 redolog ,可能会存在什么问题的? 我们一起来分析一下。
我们知道,在 InnoDB 引擎中的内存结构中,主要的内存区域就是缓冲池,在缓冲池中缓存了很多的数据页。 当我们在一个事务中,执行多个增删改的操作时,InnoDB 引擎会先操作缓冲池中的数据,如果 缓冲区没有对应的数据,会通过后台线程将磁盘中的数据加载出来,存放在缓冲区中,然后将缓冲池中的数据修改,修改后的数据页我们称为脏页。 而脏页则会在一定的时机,通过后台线程刷新到磁盘 中,从而保证缓冲区与磁盘的数据一致。 而缓冲区的脏页数据并不是实时刷新的,而是一段时间之后将缓冲区的数据刷新到磁盘中,假如刷新到磁盘的过程出错了,而提示给用户事务提交成功,而数据却没有持久化下来,这就出现问题了,没有保证事务的持久性。
那么,如何解决上述的问题呢? 在 InnoDB 中提供了一份日志 redo log ,接下来我们再来分析一
下,通过 redolog 如何解决这个问题。
有了 redolog 之后,当对缓冲区的数据进行增删改之后,会首先将操作的数据页的变化,记录在 redolog buffer中。在事务提交时,会将 redolog buffer 中的数据刷新到 redo log 磁盘文件中。
过一段时间之后,如果刷新缓冲区的脏页到磁盘时,发生错误,此时就可以借助于 redolog 进行数据恢复,这样就保证了事务的持久性。 而如果脏页成功刷新到磁盘 或 或者涉及到的数据已经落盘,此时redo log 就没有作用了,就可以删除了,所以存在的两个 redo log 文件是循环写的。
那为什么每一次提交事务,要刷新 redo log 到磁盘中呢,而不是直接将 buffer pool 中的脏页刷新
到磁盘呢 ?
因为在业务操作中,我们操作数据一般都是随机读写磁盘的,而不是顺序读写磁盘。 而 redo log 在
往磁盘文件中写入数据,由于是日志文件,所以都是顺序写的。顺序写的效率,要远大于随机写。 这种先写日志的方式,称之为 WAL ( Write-Ahead Logging )。
undo log
回滚日志,用于记录数据被修改前的信息 , 作用包含两个 : 提供回滚 ( 保证事务的原子性 ) 和
MVCC( 多版本并发控制 ) 。
undo log 和 redo log 记录物理日志不一样,它是逻辑日志。可以认为当 delete 一条记录时, undo
log 中会记录一条对应的 insert 记录,反之亦然,当 update 一条记录时,它记录一条对应相反的
update 记录。当执行 rollback 时,就可以从 undo log 中的逻辑记录读取到相应的内容并进行回滚。
Undo log 销毁: undo log 在事务执行时产生,事务提交时,并不会立即删除 undo log ,因为这些
日志可能还用于 MVCC 。
Undo log 存储: undo log 采用段的方式进行管理和记录,存放在前面介绍的 rollback segment
回滚段中,内部包含 1024 个 undo log segment 。
MVCC
基本概念
1). 当前读
读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:
select ... lock in share mode( 共享锁 ) ,
select ... for update、update 、 insert 、 delete( 排他锁 ) 都是一种当前读。
测试:
在测试中我们可以看到,即使是在默认的 RR 隔离级别下,事务 A 中依然可以读取到事务 B 最新提交的内容,因为在查询语句后面加上了 lock in share mode 共享锁,此时是当前读操作。当然,当我们加排他锁的时候,也是当前读操作。
2). 快照读
简单的 select (不加锁)就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据不加锁,是非阻塞读。
• Read Committed :每次 select ,都生成一个快照读。
• Repeatable Read :开启事务后第一个 select 语句才是快照读的地方。
• Serializable :快照读会退化为当前读。
测试 :
在测试中 , 我们看到即使事务 B 提交了数据 , 事务 A 中也查询不到。 原因就是因为普通的 select 是快照
读,而在当前默认的 RR 隔离级别下,开启事务后第一个 select 语句才是快照读的地方,后面执行相同的select 语句都是从快照中获取数据,可能不是当前的最新数据,这样也就保证了可重复读。
3). MVCC
全称 Multi-Version Concurrency Control ,多版本并发控制。指维护一个数据的多个版本,
使得读写操作没有冲突,快照读为 MySQL 实现 MVCC 提供了一个非阻塞读功能。 MVCC 的具体实现,还需要依赖于数据库记录中的三个隐式字段、undo log 日志、 readView 。
接下来,我们再来介绍一下 InnoDB 引擎的表中涉及到的隐藏字段 、 undolog 以及 readview ,从
而来介绍一下 MVCC 的原理。
隐藏字段
介绍
当我们创建了上面的这张表,我们在查看表结构的时候,就可以显式的看到这三个字段。 实际上除了这三个字段以外,InnoDB 还会自动的给我们添加三个隐藏字段及其含义分别是:
而上述的前两个字段是肯定会添加的, 是否添加最后一个字段 DB_ROW_ID ,得看当前表有没有主键,如果有主键,则不会添加该隐藏字段。
测试
1). 查看有主键的表 stu
进入服务器中的 /var/lib/mysql/itcast/ , 查看 stu 的表结构信息 , 通过如下指令 :
ibd2sdi stu.ibd
查看到的表结构信息中,有一栏 columns ,在其中我们会看到处理我们建表时指定的字段以外,还有额外的两个字段 分别是:DB_TRX_ID 、 DB_ROLL_PTR ,因为该表有主键,所以没有 DB_ROW_ID 隐藏字段
2). 查看没有主键的表 employee
建表语句:
create table employee (id int , name varchar(10));
此时,我们再通过以下指令来查看表结构及其其中的字段信息:
ibd2sdi employee.ibd
查看到的表结构信息中,有一栏 columns ,在其中我们会看到处理我们建表时指定的字段以外,还有额外的三个字段 分别是:DB_TRX_ID 、 DB_ROLL_PTR 、 DB_ROW_ID ,因为 employee 表是没有指定主键的
undolog
介绍
回滚日志,在 insert 、 update 、 delete 的时候产生的便于数据回滚的日志。
当 insert 的时候,产生的 undo log 日志只在回滚时需要,在事务提交后,可被立即删除。
而 update 、 delete 的时候,产生的 undo log 日志不仅在回滚时需要,在快照读时也需要,不会立即
被删除。
DB_TRX_ID : 代表最近修改事务 ID ,记录插入这条记录或最后一次修改该记录的事务 ID ,是
自增的。
DB_ROLL_PTR : 由于这条数据是才插入的,没有被更新过,所以该字段值为 null 。
然后,有四个并发事务同时在访问这张表。
A. 第一步
当事务 2 执行第一条修改语句时,会记录 undo log 日志,记录数据变更之前的样子 ; 然后更新记录,
并且记录本次操作的事务 ID ,回滚指针,回滚指针用来指定如果发生回滚,回滚到哪一个版本。
B.第二步
当事务 3 执行第一条修改语句时,也会记录 undo log 日志,记录数据变更之前的样子 ; 然后更新记
录,并且记录本次操作的事务 ID ,回滚指针,回滚指针用来指定如果发生回滚,回滚到哪一个版本。
C. 第三步
当事务 4 执行第一条修改语句时,也会记录 undo log 日志,记录数据变更之前的样子 ; 然后更新记
录,并且记录本次操作的事务 ID ,回滚指针,回滚指针用来指定如果发生回滚,回滚到哪一个版本。
最终我们发现,不同事务或相同事务对同一条记录进行修改,会导致该记录的 undolog 生成一条
记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录。
readview
ReadView (读视图)是 快照读 SQL 执行时 MVCC 提取数据的依据,记录并维护系统当前活跃的事务 (未提交的)id 。
而在 readview 中就规定了版本链数据的访问规则:
trx_id 代表当前 undolog 版本链对应事务 ID 。
不同的隔离级别,生成 ReadView 的时机不同:
READ COMMITTED :在事务中每一次执行快照读时生成 ReadView 。
REPEATABLE READ :仅在事务中第一次执行快照读时生成 ReadView ,后续复用该 ReadView 。
原理分析
RC隔离级别
RC隔离级别下,在事务中每一次执行快照读时生成ReadView。
我们就来分析事务 5 中,两次快照读读取数据,是如何获取数据的 ?
在事务 5 中,查询了两次 id 为 30 的记录,由于隔离级别为 Read Committed ,所以每一次进行快照读
都会生成一个 ReadView ,那么两次生成的 ReadView 如下
那么这两次快照读在获取数据时,就需要根据所生成的 ReadView 以及 ReadView 的版本链访问规则,到undolog 版本链中匹配数据,最终决定此次快照读返回的数据。
A. 先来看第一次快照读具体的读取过程
在进行匹配时,会从undo log的版本链,从上到下进行挨个匹配:
B. 再来看第二次快照读具体的读取过程:
在进行匹配时,会从undo log的版本链,从上到下进行挨个匹配
RR 隔离级别
RR 隔离级别下,仅在事务中第一次执行快照读时生成 ReadView ,后续复用该 ReadView 。 而 RR 是可重复读,在一个事务中,执行两次相同的select 语句,查询到的结果是一样的。
那 MySQL 是如何做到可重复读的呢 ? 我们简单分析一下就知道了
我们看到,在 RR 隔离级别下,只是在事务中第一次快照读时生成 ReadView ,后续都是复用该
ReadView ,那么既然 ReadView 都一样, ReadView 的版本链匹配规则也一样, 那么最终快照读返 回的结果也是一样的。
所以呢, MVCC 的实现原理就是通过 InnoDB 表的隐藏字段、 UndoLog 版本链、 ReadView 来实现的。
而 MVCC + 锁,则实现了事务的隔离性。 而一致性则是由 redolog 与 undolog 保证。
MySQL管理
系统数据库
Mysql 数据库安装完成后,自带了一下四个数据库,具体作用如下:
常用工具
mysql
该 mysql 不是指 mysql 服务,而是指 mysql 的客户端工具
语法 :
mysql [options] [database]
选项 :
-u, --user=name #指定用户名
-p, --password[=name] #指定密码
-h, --host=name #指定服务器IP或域名
-P, --port=port #指定连接端口
-e, --execute=name #执行SQL语句并退出
-e 选项可以在 Mysql 客户端执行 SQL 语句,而不用连接到 MySQL 数据库再执行,对于一些批处理脚本,这种方式尤其方便
示例:
mysql -uroot –p123456 db01 -e "select * from stu";
mysqladmin
mysqladmin 是一个执行管理操作的客户端程序。可以用它来检查服务器的配置和当前状态、创建并删除数据库等。
通过帮助文档查看选项:
mysqladmin --help
语法:
mysqladmin [options] command ...
选项:
-u, --user=name #指定用户名
-p, --password[=name] #指定密码
-h, --host=name #指定服务器IP或域名
-P, --port=port #指定连接端口
示例:
mysqladmin -uroot –p1234 drop 'test01';
mysqladmin -uroot –p1234 version;
mysqlbinlog
由于服务器生成的二进制日志文件以二进制格式保存,所以如果想要检查这些文本的文本格式,就会使用到mysqlbinlog 日志管理工具。
语法 :
mysqlbinlog [options] log-files1 log-files2 ...
选项 :
-d, --database=name 指定数据库名称,只列出指定的数据库相关操作。
-o, --offset=# 忽略掉日志中的前n行命令。
-r,--result-file=name 将输出的文本格式日志输出到指定文件。
-s, --short-form 显示简单格式, 省略掉一些信息。
--start-datatime=date1 --stop-datetime=date2 指定日期间隔内的所有日志。
--start-position=pos1 --stop-position=pos2 指定位置间隔内的所有日志。
示例 :
A. 查看 binlog.000008 这个二进制文件中的数据信息
上述查看到的二进制日志文件数据信息量太多了,不方便查询。 我们可以加上一个参数 -s 来显示简单格式。
mysqlshow
mysqlshow 客户端对象查找工具,用来很快地查找存在哪些数据库、数据库中的表、表中的列或者索引。
语法 :
mysqlshow [options] [db_name [table_name [col_name]]]
选项 :
--count 显示数据库及表的统计信息(数据库,表 均可以不指定)
-i 显示指定数据库或者指定表的状态信息
示例:
#查询test库中每个表中的字段书,及行数
mysqlshow -uroot -p2143 test --count
#查询test库中book表的详细情况
mysqlshow -uroot -p2143 test book --count
mysqldump
mysqldump 客户端工具用来备份数据库或在不同数据库之间进行数据迁移。备份内容包含创建表,及插入表的SQL 语句
语法 :
mysqldump [options] db_name [tables]
mysqldump [options] --database/-B db1 [db2 db3...]
mysqldump [options] --all-databases/-A
连接选项 :
-u, --user=name 指定用户名
-p, --password[=name] 指定密码
-h, --host=name 指定服务器ip或域名
-P, --port=# 指定连接端口
输出选项:
--add-drop-database 在每个数据库创建语句前加上 drop database 语句
--add-drop-table 在每个表创建语句前加上 drop table 语句 , 默认开启 ; 不
开启 (--skip-add-drop-table)
-n, --no-create-db 不包含数据库的创建语句
-t, --no-create-info 不包含数据表的创建语句
-d --no-data 不包含数据
-T, --tab=name 自动生成两个文件:一个.sql文件,创建表结构的语句;一
个.txt文件,数据文件
mysqlimport/source
1). mysqlimport
mysqlimport 是客户端数据导入工具,用来导入 mysqldump 加 -T 参数后导出的文本文件。
语法 :
mysqlimport [options] db_name textfile1 [textfile2...]
示例 :
mysqlimport -uroot -p2143 test /tmp/city.txt
2). source
如果需要导入 sql 文件 , 可以使用 mysql 中的 source 指令 :
语法 :
source /root/xxxxx.sql
运维篇
日志
错误日志
错误日志是 MySQL 中最重要的日志之一,它记录了当 mysqld 启动和停止时,以及服务器在运行过程中发生任何严重错误时的相关信息。当数据库出现任何故障导致无法正常使用时,建议首先查看此日志。
该日志是默认开启的,默认存放目录 /var/log/ ,默认的日志文件名为 mysqld.log 。查看日志
位置:
show variables like '%log_error%';
二进制日志
介绍
二进制日志( BINLOG )记录了所有的 DDL (数据定义语言)语句和 DML (数据操纵语言)语句,但不包括数据查询(SELECT 、 SHOW )语句。
作用:① . 灾难时的数据恢复;② . MySQL 的主从复制。在 MySQL8 版本中,默认二进制日志是开启着的,涉及到的参数如下:
show variables like '%log_bin%';
参数说明:
log_bin_basename :当前数据库服务器的 binlog 日志的基础名称 ( 前缀 ) ,具体的 binlog 文
件名需要再该 basename 的基础上加上编号 ( 编号从 000001 开始 ) 。
log_bin_index : binlog 的索引文件,里面记录了当前服务器关联的 binlog 文件有哪些。
格式
MySQL服务器中提供了多种格式来记录二进制日志,具体格式及特点如下:
show variables like '%binlog_format%';
如果我们需要配置二进制日志的格式,只需要在 /etc/my.cnf 中配置 binlog_format 参数即
可。
查看
由于日志是以二进制方式存储的,不能直接读取,需要通过二进制日志查询工具 mysqlbinlog 来查
看,具体语法:
mysqlbinlog [ 参数选项 ] logfilename
参数选项:
-d 指定数据库名称,只列出指定的数据库相关操作。
-o 忽略掉日志中的前n行命令。
-v 将行事件(数据变更)重构为SQL语句
-vv 将行事件(数据变更)重构为SQL语句,并输出注释信息
删除
对于比较繁忙的业务系统,每天生成的 binlog 数据巨大,如果长时间不清除,将会占用大量磁盘空
间。可以通过以下几种方式清理日志:
show variables like '%binlog_expire_logs_seconds%';
也可以在mysql的配置文件中配置二进制日志的过期时间,设置了之后,二进制日志过期会自动删除。
show variables like '%binlog_expire_logs_seconds%';
查询日志
查询日志中记录了客户端的所有操作语句,而二进制日志不包含查询数据的 SQL 语句。默认情况下查询日志是未开启的。
如果需要开启查询日志,可以修改MySQL的配置文件 /etc/my.cnf 文件,添加如下内容:
#该选项用来开启查询日志 , 可选值 : 0 或者 1 ; 0 代表关闭, 1 代表开启
general_log=1
#设置日志的文件名 , 如果没有指定, 默认的文件名为 host_name.log
general_log_file=mysql_query.log
开启了查询日志之后,在 MySQL 的数据存放目录,也就是 /var/lib/mysql/ 目录下就会出现
mysql_query.log 文件。之后所有的客户端的增删改查操作都会记录在该日志文件之中,长时间运
行后,该日志文件将会非常大。
慢查询日志
慢查询日志记录了所有执行时间超过参数 long_query_time 设置值并且扫描记录数不小于
min_examined_row_limit 的所有的 SQL 语句的日志,默认未开启。 long_query_time 默认为
10 秒,最小为 0 , 精度可以到微秒。
如果需要开启慢查询日志,需要在 MySQL 的配置文件 /etc/my.cnf 中配置如下参数:
#慢查询日志
slow_query_log=1
#执行时间参数
long_query_time=2
默认情况下,不会记录管理语句,也不会记录不使用索引进行查找的查询。可以使用
log_slow_admin_statements 和 更改此行为 log_queries_not_using_indexes ,如下所
述。
#记录执行较慢的管理语句
log_slow_admin_statements =1
#记录执行较慢的未使用索引的语句
log_queries_not_using_indexes = 1
上述所有的参数配置完成之后,都需要重新启动 MySQL 服务器才可以生效。
主从复制
主从复制是指将主数据库的 DDL 和 DML 操作通过二进制日志传到从库服务器中,然后在从库上对这些日志重新执行(也叫重做),从而使得从库和主库的数据保持同步。
MySQL 支持一台主库同时向多台从库进行复制, 从库同时也可以作为其他从服务器的主库,实现链状复制。
MySQL 复制的优点主要包含以下三个方面:
1.主库出现问题,可以快速切换到从库提供服务。
2.实现读写分离,降低主库的访问压力。
3.可以在从库中执行备份,以避免备份期间影响主库服务。
原理
MySQL 主从复制的核心就是 二进制日志,具体的过程如下:
从上图来看,复制分成三步:
1. Master 主库在事务提交时,会把数据变更记录在二进制日志文件 Binlog 中。
2. 从库读取主库的二进制日志文件 Binlog ,写入到从库的中继日志 Relay Log 。
3. slave 重做中继日志中的事件,将改变反映它自己的数据。
搭建
准备
准备好两台服务器之后,在上述的两台服务器中分别安装好 MySQL ,并完成基础的初始化准备 ( 安装,密码配置等操作) 工作。 其中:
192.168.200.200 作为主服务器 master
192.168.200.201 作为从服务器 slave
主库配置
1. 修改配置文件 /etc/my.cnf
#mysql 服务ID,保证整个集群环境中唯一,取值范围:1 – 232-1,默认为1
server-id=1
#是否只读,1 代表只读, 0 代表读写
read-only=0
#忽略的数据, 指不需要同步的数据库
#binlog-ignore-db=mysql
#指定同步的数据库
#binlog-do-db=db01
2. 重启 MySQL 服务器
systemctl restart mysqld
3. 登录 mysql ,创建远程连接的账号,并授予主从复制权限
#创建itcast用户,并设置密码,该用户可在任意主机连接该MySQL服务
CREATE USER 'itcast'@'%' IDENTIFIED WITH mysql_native_password BY 'Root@123456';#为 'itcast'@'%' 用户分配主从复制权限
GRANT REPLICATION SLAVE ON *.* TO 'itcast'@'%';
4. 通过指令,查看二进制日志坐标
show master status ;
字段含义说明:
file : 从哪个日志文件开始推送日志文件
position : 从哪个位置开始推送日志
binlog_ignore_db : 指定不需要同步的数据库
从库配置
1. 修改配置文件 /etc/my.cnf
#mysql 服务ID,保证整个集群环境中唯一,取值范围:1 – 2^32-1,和主库不一样即可
server-id=2
#是否只读,1 代表只读, 0 代表读写
read-only=1
2. 重新启动MySQL服务
systemctl restart mysqld
3. 登录 mysql ,设置主库配置
CHANGE REPLICATION SOURCE TO SOURCE_HOST='192.168.200.200', SOURCE_USER='itcast',
SOURCE_PASSWORD='Root@123456', SOURCE_LOG_FILE='binlog.000004',
SOURCE_LOG_POS=663;
上述是8.0.23中的语法。如果mysql是 8.0.23 之前的版本,执行如下SQL:
CHANGE MASTER TO MASTER_HOST='192.168.200.200', MASTER_USER='itcast',
MASTER_PASSWORD='Root@123456', MASTER_LOG_FILE='binlog.000004',
MASTER_LOG_POS=663;
4. 开启同步操作
start replica ; #8.0.22之后
start slave ; #8.0.22之前
5. 查看主从同步状态
show replica status ; #8.0.22之后
show slave status ; #8.0.22之前
测试
1. 在主库 192.168.200.200 上创建数据库、表,并插入数据
create database db01;
use db01;
create table tb_user(id int(11) primary key not null auto_increment,name varchar(50) not null,sex varchar(1)
)engine=innodb default charset=utf8mb4;
insert into tb_user(id,name,sex) values(null,'Tom', '1'),(null,'Trigger','0'),
(null,'Dawn','1');
2. 在从库 192.168.200.201 中查询数据,验证主从是否同步
分库分表
介绍
随着互联网及移动互联网的发展,应用系统的数据量也是成指数式增长,若采用单数据库进行数据存储,存在以下性能瓶颈:
1. IO 瓶颈:热点数据太多,数据库缓存不足,产生大量磁盘 IO ,效率较低。 请求数据太多,带宽
不够,网络 IO 瓶颈。
2. CPU 瓶颈:排序、分组、连接查询、聚合统计等 SQL 会耗费大量的 CPU 资源,请求数太多, CPU 出现瓶颈。
为了解决上述问题,我们需要对数据库进行分库分表处理。
分库分表的中心思想都是将数据分散存储,使得单一数据库 / 表的数据量变小来缓解单一数据库的性能问题,从而达到提升数据库性能的目的。
拆分策略
分库分表的形式,主要是两种:垂直拆分和水平拆分。而拆分的粒度,一般又分为分库和分表,所以组成的拆分策略最终如下:
垂直拆分
1. 垂直分库
垂直分库:以表为依据,根据业务将不同表拆分到不同库中。
特点:
1.每个库的表结构都不一样。
2.每个库的数据也不一样。
3.所有库的并集是全量数据。
2. 垂直分表
垂直分表:以字段为依据,根据字段属性将不同字段拆分到不同表中。
特点:
1.每个表的结构都不一样。
2.每个表的数据也不一样,一般通过一列(主键 / 外键)关联。
3.所有表的并集是全量数据。
水平拆分
1. 水平分库
水平分库:以字段为依据,按照一定策略,将一个库的数据拆分到多个库中。
特点:
1.每个库的表结构都一样。
2.每个库的数据都不一样。
3.所有库的并集是全量数据。
2. 水平分表
水平分表:以字段为依据,按照一定策略,将一个表的数据拆分到多个表中。
特点:
每个表的表结构都一样。
每个表的数据都不一样。
所有表的并集是全量数据。
在业务系统中,为了缓解磁盘 IO 及 CPU 的性能瓶颈,到底是垂直拆分,还是水平拆分;具体是分
库,还是分表,都需要根据具体的业务需求具体分析。
实现技术
shardingJDBC :基于 AOP 原理,在应用程序中对本地执行的 SQL 进行拦截,解析、改写、路由处
理。需要自行编码配置实现,只支持 java 语言,性能较高。
MyCat :数据库分库分表中间件,不用调整代码即可实现分库分表,支持多种语言,性能不及前
者。
我们选择了是MyCat数据库中间件,通过MyCat中间件来完成分库分表操作。
MyCat概述
介绍
Mycat 是开源的、活跃的、基于 Java 语言编写的 MySQL 数据库中间件。可以像使用 mysql 一样来使用mycat,对于开发人员来说根本感觉不到 mycat 的存在。
开发人员只需要连接 MyCat 即可,而具体底层用到几台数据库,每一台数据库服务器里面存储了什么数据,都无需关心。 具体的分库分表的策略,只需要在MyCat 中配置即可。
优势:
1.性能可靠稳定
2.强大的技术团队
3.体系完善
4.社区活跃
下载
下载地址: http://dl.mycat.org.cn/
安装
Mycat 是采用 java 语言开发的开源的数据库中间件,支持 Windows 和 Linux 运行环境,下面介绍
MyCat 的 Linux 中的环境搭建。我们需要在准备好的服务器中安装如下软件。
MySQL
JDK
Mycat