InnoDB引擎是mysql 5.5 版本之后默认的存储引擎
存储引擎就是存储数据、建立索引、更新/查询数据等技术的实现方式。存储引擎是基于表而不是基于库的,所以存储引擎也可以被称为表引擎(表类型)。
-- 查询建表语句
show create table account;
-- 建表时指定存储引擎
CREATE TABLE 表名(
...
) ENGINE=INNODB;
-- 查看当前数据库支持的存储引擎
show engines;
-- 创建表,并指定MyISAM存储引擎
create table test_myisam(
id int,
name varchar(10)
)engine=MyISAM;
show create table test_myisam;
-- 创建表,并指定MEMORY存储引擎
create table test_memory(
id int,
name varchar(10)
)engine=memory;
InnoDB 是一种兼顾高可靠性和高性能的通用存储引擎,在 MySQL 5.5 之后,InnoDB 是默认的 MySQL 引擎。
特点:
事务
行级锁
,提高并发访问性能外键
约束,保证数据的完整性和正确性ACID: 原子性 隔离性 持久性 一致性
文件:
查看 Mysql 变量:
show variables like 'innodb_file_per_table';
从idb文件提取表结构数据:
(在cmd运行)
ibd2sdi xxx.ibd
Windows: C:\ProgramData\MySQL\MySQL Server 8.0\Data\my_db
linux: 数据库存放的数据 /var/lib/mysql/
"dd_object_type": "Table"
有一张表
"dd_object": { "name": "account",
名字叫account
"columns": [
字段
"is_auto_increment": true,
是否是自增的
页是innodb磁盘管理的最小单元,一个页的大小默认是16k,也就是说一个区当中它可以包含64个页。
MyISAM 是 MySQL 早期的默认存储引擎。
特点:
文件:
Memory 引擎的表数据是存储在内存中的,受硬件问题、断电问题的影响,只能将这些表作为临时表或缓存使用。
特点:
文件:
特点 | InnoDB | MyISAM | Memory |
---|---|---|---|
存储限制 | 64TB | 有 | 有 |
事务 安全 |
支持 | - | - |
锁机制 | 行锁 |
表锁 | 表锁 |
B+tree索引 | 支持 | 支持 | 支持 |
Hash索引 | - | - | 支持 |
全文索引 | 支持(5.6版本之后) | 支持 | - |
空间使用 | 高 | 低 | N/A |
内存使用 | 高 | 低 | 中等 |
批量插入速度 | 低 | 高 | 高 |
支持外键 |
支持 | - |
- |
在选择存储引擎时,应该根据应用系统的特点选择合适的存储引擎。对于复杂的应用系统,还可以根据实际情况选择多种存储引擎进行组合。
业务系统当中的日志相关的数据、电商当中的足迹和评论可以考虑使用 MyISAM 引擎,虽然它不支持事务,但是对于这类的数据实际上并不是业务系统的核心数据,偶尔丢那么一两条数据也没有问题。
缓存适合使用 Memory 引擎。
实际上绝大部分场景使用的是InnoDB。
在使用MyISAM存储引擎的场景当中现在都会被另外一个nosql系列的数据库代替了(比如mongodb数据库)。在使用Memory存储引擎的场景当中被另外一个nosql系列的数据库代替了(redis数据库)
索引是帮助 MySQL 高效获取数据的数据结构(有序)。在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查询算法,这种数据结构就是索引。
优缺点:
优点 | 缺点 |
---|---|
提高数据检索效率,降低数据库的IO成本 | 索引列也是要占用空间的 |
通过索引列对数据进行排序,降低数据排序的成本,降低CPU的消耗 | 索引大大提高了查询效率,但降低了更新的速度,比如 INSERT、UPDATE、DELETE |
MySQL的索引是在存储引擎层实现的,不同的存储引擎有不同的结构,主要包含以下几种
索引结构 | 描述 |
---|---|
B+Tree索引 |
最常见的索引类型,大部分引擎都支持B+树索引 |
Hash索引 | 底层数据结构是用哈希表实现,只有精确匹配索引列的查询才有效,不支持范围查询。性能很高但是它不支持范围查询 |
R-Tree(空间索引) | 空间索引是 MyISAM 引擎的一个特殊索引类型,主要用于地理空间数据类型,通常使用较少 |
Full-Text(全文索引) | 是一种通过建立倒排索引,快速匹配文档的方式,类似于 Lucene, Solr, ES |
索引 | InnoDB | MyISAM | Memory |
---|---|---|---|
B+Tree索引 | 支持 | 支持 | 支持 |
Hash索引 | 不支持 | 不支持 | 支持 |
R-Tree索引 | 不支持 | 支持 | 不支持 |
Full-text | 5.6版本后支持 | 支持 | 不支持 |
如果我们没有指定哪种存储引擎哪种索引结构,默认就是B+树索引
二叉树的缺点可以用红黑树来解决:
为了解决上述问题,可以使用 B-Tree 结构。
以一棵最大度数(max-degree,指一个节点的子节点个数)为5(5阶)的 b-tree 为例(每个节点最多存储4个key,5个指针)
演示地址:https://www.cs.usfca.edu/~galles/visualization/BTree.html
插入第五个值1024的时候,此时这个节点将会存储5个key,如果存储五个key将会有6个指针(指针会比key的个数多一个),6个指针就是6阶b-树,违背了5阶,此时树会发生裂变,中间元素向上分裂
,当1024插入进来之后,456变成了中间元素,它会向上分裂,456向上分裂之后接下来456下面就会有两个子节点。
1000进来之后1024变成中间元素中间元素会向上分裂,最终分裂的结果就是这样的:
Mysql的常用引擎,如InnoDB和MyISAM,都选择了B+树作为索引结构
在 B+树 当中,所有的数据都会出现在叶子节点,而且叶子节点形成了一个双向链表。
结构图:
上面的这部分(非叶子节点)主要起到索引的作用,叶子节点是用来存放数据的。
演示地址:https://www.cs.usfca.edu/~galles/visualization/BPlusTree.html
MySQL
索引数据结构对
经典的B+Tree
进行了优化
。在原 B+Tree 的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的 B+Tree,提高区间访问的性能。利于数据库数据的排序操作
memory存储引擎当中支持的索引结构,他就是一个哈希表,对于哈希索引来说,最大的特点就是它的检索性能很高,只需要计算出字段值的哈希值,然后直接定位到对应的数据,当然如果存在对应的哈希碰撞,那这个时候就需要沿着链表再去找到对应的数据,它的性能相对来说比较高。对于哈希索引来说,它还有个缺点,这个缺点指的就是它只能支持精确匹配,不支持范围查询及索引的排序。
哈希索引就是采用一定的hash算法,将键值换算成新的hash值,映射到对应的槽位上,然后存储在hash表中。
如果两个(或多个)键值,映射到一个相同的槽位上,他们就产生了hash冲突(也称为hash碰撞),可以通过链表来解决。
存储引擎支持:
在MySQL中,支持hash索引的是Memory引擎,而InnoDB 中具有自适应hash功能,hash索引是存储引擎根据 B+Tree 索引在指定条件下自动构建的。
提问:为什么 InnoDB 存储引擎选择使用 B+Tree 索引结构?
分类 | 含义 | 特点 | 关键字 |
---|---|---|---|
主键索引 | 针对于表中主键创建的索引 | 默认自动创建,只能有一个 | PRIMARY |
唯一索引 | 避免同一个表中某数据列中的值重复 | 可以有多个 | UNIQUE |
常规索引 | 快速定位特定数据 | 可以有多个 | |
全文索引 | 全文索引查找的是文本中的关键词,而不是比较索引中的值 | 可以有多个 | FULLTEXT |
在 InnoDB 存储引擎中,根据索引的存储形式,又可以分为以下两种:
分类 | 含义 | 特点 |
---|---|---|
聚集索引(Clustered Index) | 将数据存储与索引放一块,索引结构的叶子节点保存了行数据 | 必须有,而且只有一个 |
二级索引(Secondary Index) | 将数据与索引分开存储,索引结构的叶子节点关联的是对应的主键 | 可以存在多个 |
聚集索引选取规则:
如果存在主键,主键索引就是聚集索引
id是这张表的主键,如果一张表有主键,主键索引就是聚集索引。如果我们针对name字段建立的索引,name字段建的索引以及其他索引 都称为二级索引。
回表查询:先走二级索引找到对应的主键值id,再根据主键值再到聚集索引当中,拿到这一行的行数据。
提问1: 以下 SQL 语句,哪个执行效率高?为什么?
select * from user where id = 10;
select * from user where name = 'Arm';
-- 备注:id为主键,name字段创建的有索引
答:第一条语句,因为第二条需要回表查询,相当于两个步骤。
提问2: InnoDB 主键索引的 B+Tree 高度为多少?
答:假设一行数据大小为1k,一页中可以存储16行这样的数据。InnoDB 的指针占用6个字节的空间,主键假设为bigint,占用字节数为8.
可得公式:n * 8 + (n + 1) * 6 = 16 * 1024,其中 8 表示 bigint 占用的字节数,n 表示当前节点存储的key的数量,(n + 1) 表示指针数量(比key多一个)。算出n约为1170,指针有1171个。
如果树的高度为2,那么他能存储的数据量大概为:1171 * 16 = 18736;
如果树的高度为3,那么他能存储的数据量大概为:1171 * 1171 * 16 = 21939856。
另外,如果有成千上万的数据,那么就要考虑分表,涉及运维篇知识。
创建索引:
CREATE [ UNIQUE | FULLTEXT ] INDEX index_name ON table_name (index_col_name, ...);
如果不加 CREATE 后面不加索引类型参数,则创建的是常规索引。
一个索引是可以关联多个字段的,如果一个索引只关联一个字段,这种索引称为单列索引。如果一个索引关联了多个字段,那这种索引我们称之为联合索引(或者组合索引)。
注意:创建联合索引的时候,这个顺序是有讲究的。
查看索引:
SHOW INDEX FROM table_name;
删除索引:
DROP INDEX index_name ON table_name;
准备一张表
create table tb_student
(
student_id bigint auto_increment primary key,
student_name varchar(20) not null,
student_gender enum ('男', '女') null,
student_height tinyint unsigned null,
student_birthday timestamp null,
class_id char(5) null,
student_phone char(13) null
);
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101001, '曹杰宏', '男', 158, '2005-09-26 07:03:48', 'B4009', '141-5402-7823');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101002, '何宇宁', '女', 176, '2004-09-19 02:53:39', 'B4001', '769-842-5951');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101003, '潘嘉伦', '女', 173, '2005-11-10 13:54:47', 'B1015', '175-4460-0936');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101004, '谭睿', '女', 153, '2002-02-21 10:57:13', 'B1001', '28-7282-6419');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101005, '陆岚', '男', 174, '2005-02-15 23:25:25', 'B3003', '176-3255-2591');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101006, '朱子异', '女', 165, '2003-02-20 14:32:34', 'B5007', '163-7226-6158');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101007, '曹秀英', '女', 179, '2005-07-29 15:55:41', 'B5002', '137-6365-4834');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101008, '萧子韬', '女', 165, '2004-03-10 19:25:23', 'B4007', '28-0662-5680');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101009, '龚晓明', '男', 179, '2004-07-22 12:19:03', 'B4009', '177-8511-7488');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101010, '许云熙', '男', 179, '2003-12-14 13:19:13', 'B4008', '769-577-3239');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101011, '马子异', '男', 146, '2005-08-24 19:52:46', 'B4007', '132-7319-4472');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101012, '卢安琪', '男', 166, '2004-09-12 02:24:59', 'B4001', '21-008-8508');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101013, '常云熙', '男', 155, '2002-11-03 07:41:26', 'B2002', '21-155-7266');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101014, '徐震南', '女', 156, '2004-06-12 21:49:56', 'B4004', '184-6277-1788');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101015, '余詩涵', '男', 162, '2003-12-24 09:23:09', 'B1008', '134-5166-0350');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101016, '张宇宁', '女', 176, '2004-05-10 22:54:31', 'B4007', '183-5946-9849');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101017, '薛秀英', '男', 145, '2005-01-07 11:10:16', 'B5005', '171-0182-6782');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101018, '史晓明', '男', 158, '2003-08-24 23:13:14', 'B1003', '28-3114-9024');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101019, '邓云熙', '女', 152, '2005-08-17 22:49:26', 'B4007', '193-4509-2367');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101020, '姚杰宏', '女', 148, '2004-03-01 05:13:55', 'B1015', '755-0442-8195');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101021, '吴嘉伦', '男', 156, '2002-12-26 18:10:42', 'B4007', '760-659-9567');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101022, '龙震南', '女', 185, '2002-09-22 06:10:26', 'B1015', '180-4637-2160');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101023, '段睿', '女', 152, '2004-03-29 23:18:36', 'B1001', '130-3352-6121');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101024, '黄安琪', '女', 178, '2003-05-30 05:37:13', 'B1014', '20-8306-4749');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101025, '贾子韬', '男', 170, '2003-12-02 21:12:19', 'B4004', '134-8916-5452');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101026, '秦安琪', '女', 186, '2003-06-17 06:33:46', 'B3002', '181-6472-7121');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101027, '任岚', '女', 149, '2005-10-24 05:35:10', 'B4004', '146-1401-6887');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101028, '戴致远', '女', 161, '2002-06-18 08:25:20', 'B5003', '176-0100-5041');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101029, '董秀英', '男', 182, '2002-08-18 05:12:54', 'B4005', '141-1148-6261');
INSERT INTO tb_student (student_id, student_name, student_gender, student_height, student_birthday, class_id, student_phone) VALUES (20220101030, '唐嘉伦', '男', 180, '2003-07-28 16:17:29', 'B4005', '10-219-8570');
show index from tb_student;
show index from tb_student\G;# \G 看到的数据由原来的一列转换成一行,当然两种形式用哪种都可以
-- student_name字段为姓名字段,该字段的值可能会重复,为该字段创建索引
create index idx_student_name on tb_student(student_name);# 默认常规索引
show index from tb_student;
-- student_phone手机号字段的值非空,且唯一,为该字段创建唯一索引
create unique index idx_student_phone on tb_student(student_phone);
show index from tb_student;
-- 为student_gender, student_birthday, class_id创建联合索引
create index idx_student_gen_bir_classid on tb_student(student_gender, student_birthday, class_id);
show index from tb_student;
-- 为student_height建立合适的索引来提升查询效率
create index idx_student_height on tb_student(student_height);
show index from tb_student;
-- 删除索引
drop index idx_student_height on tb_student;
show index from tb_student;
MySQL客户端连接成功后,通过 show [session | global] status 命令可以提供服务器状态信息。通过如下指令,可以查看当前数据库的
INSERT、UPDATE、DELETE、SELECT的访问频次:
show global status like 'Com_______';
一个下划线_代表一个字符,7个
通过这个指令查看当前数据库增删改查它的频次从而来判定当前数据库到底是查询为主还是增删改为主。
查看开关(默认windows是开的,Linux是关的)
show variables like 'slow_query_log';
慢查询日志记录了所有执行时间超过指定参数(long_query_time,单位:秒,默认10秒)的所有SOL语句的日志。MySQL的慢查询日志默认没有开启,需要在MySQL的配置文件(/etc/my.cnf)中配置如下信息:
# 开启MySOL慢日志查询开关
slow_query_log=1
# 设置慢日志的时间为2秒,SOL语句执行时间超过2秒,就会视为慢查询,记录慢查询日志
long_query_time=2
配置完毕之后,通过指令重新启动MySQL服务器
进行测试,查看慢日志文件中记录的信息/var/lib/mysql/安装mysql机器名字-slow.log。
MacOS 在 /usr/local/etc 里面
ubuntu: vim etc/mysql/mysql.conf.d/mysqld.cnf
windows环境是修改C:\ProgramData\MySQL\MySQL Server 8.0的my.ini
net stop mysql80 net start mysql80
想要直到这个文件在哪里。在mysql中输入
show variables like 'slow_%';
右键属性
我们随便执行一条语句,直接select sleep(10);
隔一段时间之后我们来看这个慢查询日志,通过慢查询日志我就能够知道在这段时间当中 对执行哪些sql语句执行效率低,然后考虑对这类sql语句来进行优化
这些对于开发人员很少去关注,了解即可
show profiles 能够在做SQL优化时帮助我们了解时间都耗费到哪里去了。通过have_profiling参数,能够看到当前MySQL是否支持profile操作:
select @@have_profiling;
查看profiling开关
select @@profiling;
默认profilig是关闭的,可以通过set语句在session/global级别开启profiling:
set profiling =1;
执行一系列的业务SQL的操作,然后通过如下指令查看指令的执行耗时:
#查看每一条SQL的耗时基本情况
show profiles;
#查看指定query id的SQL语句各个阶段的耗时情况
show profile for query query_id;
#查看指定query_id的SQL语句CPU的使用情况
show profile cpu for query query_id;
select @@have_profiling;
select @@profiling;
set profiling =1;
select * from tb_student where student_id=20220101027;
select * from tb_student where student_name='任岚';
select count(*) from tb_student;
select sleep(10);
show profiles;
如果我们想看最后一条sql语句它的执行耗时都耗费在什么地方了,此时我们可以执行这样一条指令
show profile for query 3;
show profile cpu for query 3;
使用的多
前面已经讲解了三种sql语句性能分析的工具,分别用来查看sql语句的执行频次、查看慢查询日志、以及通过profiles查看sql语句的耗时以及时间耗费在哪里了,这些都是通过时间的层面来评判一条sql语句的性能。
执行时间短,那就说明sql语句的性能高; 执行时间长,那就说明sql语句的性能低。
实际上这种判定呢只是粗略地进行判定,并不能真正地评判一条sql语句的性能。我们要想去看一条sql语句的性能,还需要借助于第四种方法explain来查看sql语句的执行计划。explain在我们sql优化当中占据着非常重要的地位,我们经常会通过它来判定sql语句的性能。通过explain可以查看到sql语句的执行计划,它的执行过程当中,到底是否用到了索引、表的连接情况、表的连接顺序都可以看到。
EXPLAIN 或者 DESC命令获取 MySOL 如何执行 SELECT 语句的信息,包括在SELECT 语句执行过程中表如何连接和连接的顺序语法:
# 直接在select语句之前加上关键字 explain / desc
EXPLAIN SELECT 字段列表 FROM 表名 WHERE 条件
演示:
desc select * from tb_student where student_id=1;
explain select * from tb_student where student_id=1;
EXPLAIN 各字段 | 含义 |
---|---|
id | select 查询的序列号,表示查询中执行 select 子句或者操作表的顺序(id相同,执行顺序从上到下;id不同,值越大越先执行) |
select_type | 表示 SELECT 的类型,常见取值有 SIMPLE(简单表,即不适用表连接或者子查询)、PRIMARY(主查询,即外层的查询)、UNION(UNION中的第二个或者后面的查询语句)、SUBQUERY(SELECT/WHERE之后包含了子查询)等 |
type | 表示连接类型,性能由好到差的连接类型为 NULL、system、const、eq_ref、ref、range、index、all。在优化的时候尽量把type往前面优化,但是对于我们业务系统当中的sql来说,一般不太可能优化为NULL,那什么时候会出现NULL呢?当你查询的时候不访问任何表此时才会出现NULL。相当于访问系统表才会出现system。根据主键还有唯一索引进行访问,一般会出现const。如果我们使用非唯一性的索引进行查询时,这之后就会出现ref。我们在优化sql语句的时候,尽量把type往前优化,尽量不要出现all,如果出现all代表的是全表扫描,性能会比较低。如果出现index这个代表的是用了索引但是它会对索引进行扫描遍历整个索引数,性能也不是特别高。 |
possible_key | 可能应用在这张表上的索引,一个或多个 |
Key | 实际使用的索引,如果为 NULL,则没有使用索引 |
Key_len | 表示索引中使用的字节数,该值为索引字段最大可能长度,并非实际使用长度,在不损失精确性的前提下,长度越短越好 |
rows | MySQL认为必须要执行的行数,在InnoDB引擎的表中,是一个估计值,可能并不总是准确的 |
filtered | 表示返回结果的行数占需读取行数的百分比,filtered的值越大越好,100%就是性能最高的。 |
Extra | 额外的信息,在执行查询的过程当中,在前面这几个字段当中没有展示出来的值将会在Extra额外信息这一块进行展示。 NULL它要回表索引 using index condition:查找使用了索引,但是需要回表查询数据 using where; using index;:查找使用了索引,但是需要的数据都在索引列中能找到,所以不需要回表查询。 using index: 就是覆盖索引的意思 Using temporary: 用到了临时表,性能比较低。 |
前期准备
CREATE TABLE `tb_student`
(
`student_id` bigint,
`student_name` varchar(20),
`student_gender` enum ('男','女'),
`student_height` tinyint unsigned,
`student_birthday` timestamp,
`class_id` char(5),
`student_phone` char(13),
PRIMARY KEY (`student_id`) USING BTREE,
UNIQUE KEY `student_phone` (`student_phone`) USING BTREE,
KEY `fk_class_id1` (`class_id`) USING BTREE,
KEY `tb_student_name_index` (`student_name`)
) ENGINE = InnoDB
AUTO_INCREMENT = 20220101202;
INSERT INTO `tb_student`
VALUES (20220101001, '曹杰宏', '男', 158, '2005-09-25 23:03:48', 'B4009', '141-5402-7823'),
(20220101002, '何宇宁', '女', 176, '2004-09-18 18:53:39', 'B4001', '769-842-5951'),
(20220101003, '潘嘉伦', '女', 173, '2005-11-10 05:54:47', 'B1015', '175-4460-0936'),
(20220101004, '谭睿', '女', 153, '2002-02-21 02:57:13', 'B1001', '28-7282-6419'),
(20220101005, '陆岚', '男', 174, '2005-02-15 15:25:25', 'B3003', '176-3255-2591'),
(20220101006, '朱子异', '女', 165, '2003-02-20 06:32:34', 'B5007', '163-7226-6158'),
(20220101007, '曹秀英', '女', 179, '2005-07-29 07:55:41', 'B5002', '137-6365-4834'),
(20220101008, '萧子韬', '女', 165, '2004-03-10 11:25:23', 'B4007', '28-0662-5680'),
(20220101009, '龚晓明', '男', 179, '2004-07-22 04:19:03', 'B4009', '177-8511-7488'),
(20220101010, '许云熙', '男', 179, '2003-12-14 05:19:13', 'B4008', '769-577-3239'),
(20220101011, '马子异', '男', 146, '2005-08-24 11:52:46', 'B4007', '132-7319-4472'),
(20220101012, '卢安琪', '男', 166, '2004-09-11 18:24:59', 'B4001', '21-008-8508'),
(20220101013, '常云熙', '男', 155, '2002-11-02 23:41:26', 'B2002', '21-155-7266');
CREATE TABLE `tb_course`
(
`course_id` char(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '课程号',
`course_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '课程名称',
`course_type` enum ('公共必修课','公共选修课','专业基础课','专业选修课','集中实践课','拓展课') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '课程类型',
`course_credit` tinyint unsigned DEFAULT NULL COMMENT '课程学分',
`course_describe` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '课程描述',
PRIMARY KEY (`course_id`) USING BTREE,
UNIQUE KEY `union_cor_name` (`course_name`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_0900_ai_ci
ROW_FORMAT = DYNAMIC;
INSERT INTO `tb_course`
VALUES ('K1001', '数据库原理与应用', '专业基础课', 4,
'数据库原理与应用是本科计算机类专业的专业必修课,是一门理论与实践结合非常紧密的课程,是JAVA、Python、WEB开发技术等课程必不可少的一门前期必修课,该课程在计算机类专业的课程体系中起着承上启下的作用。本课程讲解数据库系统开发过程中所需的数据库、工作表、视图、索引、触发器以及对数据进行增删改操作,保障数据的安全性、统一性等知识点,以“综合性、实战型”的实践项目为教学目标,引入通关式的教学模式,教学内容和实践项目从易到难,一关一关地往下走,环环相扣,不断挑战和满足学生的学习成就感。在学期初设定一个一般难度的系统作为学习效果目标,在教学大纲中拆分课程知识点对应目标系统的子功能,在每一次教学活动中创设一个真实情境(目标系统的子功能)来不断激发学生的学习热情,让学生知道“我这节课认真听了,我就能做出系统的这个效果,我就能拿我做的项目参加学科竞赛和申报项目”,从而让学习变成有激励,有目标,有动力的主动学习。'),
('K1002', '面向对象程序设计', '专业基础课', 3,
'面向对象程序设计方法是尽可能模拟人类的思维方式,使得软件的开发方法与过程尽可能接近人类认识世界、解决现实问题的方法和过程,它是对计算机的结构化方法的深入、发展和补充,达到了软件工程的三个主要目标,即重用性、灵活性和扩展性,因此对程序设计感兴趣的同学,有必要学习掌握面向对象程序设计的方法与技术。'),
('K1003', '大学英语', '公共必修课', 3,
'《大学英语》是大学里的一门必修的公共基础课,为两学年的课程。本课程包括听、说、读、写、译等各方面的教学内容,属于基础阶段教学。'),
('K1004', '信息安全管理', '专业选修课', 4,
'《信息安全管理》课程是信息管理与信息系统专业高等教育的专业核心课程。《信息安全管理》以信息安 全为逻辑起点,以掌握一定信息管理及信息技术理论知识的学生为讲授对象,是集理论性与应用性为一体的学科。 通过本课程的学习,使学生掌握信息安全管理相关的内容、方法、原理及基础理论,并能了解结合实际如何运用这些 基本原理、方法,保障各实体的信息安全。'),
('K1005', '网页设计', '专业选修课', 3,
' 本课程以学生能独立完成静态网站开发和维护的实际工作能力为学习目标, 要求学生掌握HTML语言的文档结构和基本标签使用。能综合应用Dreamweaver 软件、HTML和CSS技术制作网页,掌握规划、开发、发布和管理静态网站的专 业知识和技能。'),
('K1006', '数据库原理与应用课程设计', '集中实践课', 1,
'学生通过本课程的学习具备开发一个给定功能的一般复杂程度的系统,如:教学管理系统、人事管理系统、社区人员管理系统等,同时具备在开发过程中找出错误和漏洞,并解决错误和漏洞的能力。'),
('K1007', '工作室教学', '拓展课', 3,
'工作室教学模式是集教学、研究、生产、实践于一体,注重学生专业应用能力的培养,将课程、教室 与生产实践融为一体,以课题研究及承接技术项目为主要任务,将生产与教学、理论与实践紧密结合的一种 教学模式。在教学内容上,注重知识的应用性;在教学形式上,以研究型教学为主;在教学方式上,以项目式、 开放式、互动式教学为主;在教学成果上,以论文、研究报告、作品为主。 '),
('K2001', '体育', '公共必修课', 2,
'体育课程是学生以身体练习为主要手段,通过合理的体育教育和科学的体育锻炼过程,达到增强体质、增进健康和提高体育素养为主要目标的公共必修课程;是学校课程体系的重要组成部分;是学校体育工作的中心环节。是我国实现素质教育和促进学生适应社会、培养学生完整个性的有效途径。'),
('K2002', '创新创业', '公共选修课', 4,
'《创新创业》课程是面向全校学生开展创新创业教育的核心课程。通过创新创业教育教学,促使学生树立创新、创业意识,提升创业精神,掌握创业的基础知识和基本理论,熟悉创业的基本流程和基本方法,了解创业的法律法规和相关政策,激发学生的创业意识,提高学生的社会责任感、创新创业精神、创新创业意识和创新创业能力,促进学生创业就业和全面发展。把创业教育融入人才培养体系,贯穿人才培养全过程,面向全体学生广泛、系统开展,让学生不断增强自身就业竞争能力和社会适应能力,成为适应创新型国家建设需要的高水平创新人才。'),
('K2003', '数字电子技术', '专业基础课', 4,
'数字电子技术课程是电气类、电子信息类、计算机类、自动化类及生医类等专业学生的技术基础课,也是高校相关专业研究生考试的必考课程、从事电子技术相关的工程技术人员和广大电子爱好者的参考课程。\r\n数字电子技术课程是电气类、电子信息类、计算机类、自动化类及生医类等专业学生的技术基础课,也是高校相关专业研究生考试的必考课程、从事电子技术相关的工程技术人员和广大电子爱好者的参考课程。\r\n数字电子技术课程是电气类、电子信息类、计算机类、自动化类及生医类等专业学生的技术基础课,也是高校相关专业研究生考试的必考课程、从事电子技术相关的工程技术人员和广大电子爱好者的参考课程。'),
('K2004', '数字信息处理', '专业基础课', 4,
'《数字信号处理》这门课介绍的是:将事物的运动变化转变为一串数字,并用计算的方法从中提取有用的信息,以满足我们实际应用的需求。\r\n《数字信号处理》这门课介绍的是:将事物的运动变化转变为一串数字,并用计算的方法从中提取有用的信息,以满足我们实际应用的需求。'),
('K2005', '高频电子线路', '公共选修课', 3,
'高频电子线路课程包括高频电路基础、高频小信号放大器、高频功率放大器、正弦波振荡器、振幅调制与解调及混频电路、角度调制与解调电路等内容。'),
('K2006', '电子技术综合设计', '集中实践课', 1,
'《电子技术综合设计》是大电子类各专业在第7 学期开设的一门专业必修实践课程,属于工程 实践类课程,内容综合了在大电子类专业的多 门理论课知识,融入工程设计理念,在实践教 学中有着重要地位。'),
('K2007', '考研类课程', '拓展课', 3,
'考研考四门课程,包括两门公共课、一门基础课(数学或专业基础)、一门专业课。两门公共课分别是政治、英语;一门基础课是数学或专业基础;一门专业课就是依据我们报考的专业所出具的专业课考试,主要分为13大类:哲学、经济学、法学、教育学、文学、历史学、理学、工学、农学、医学、军事学、管理学、艺术学等。'),
('K3001', '大学计算机基础', '公共必修课', 3,
'计算机基础知识 主要知识点包括计算机的工作原理、计算机的发展简史、计算机系统的硬件结构及软件结构、数据在计算机中的表示、微型计算机的硬件配置与软件配置、计算科学的学科分类和未来发展方向。'),
('K3002', '创新创业2', '公共选修课', 3,
'《创新创业》课程是面向全校学生开展创新创业教育的核心课程。通过创新创业教育教学,促使学生树立创新、创业意识,提升创业精神,掌握创业的基础知识和基本理论,熟悉创业的基本流程和基本方法,了解创业的法律法规和相关政策,激发学生的创业意识,提高学生的社会责任感、创新创业精神、创新创业意识和创新创业能力,促进学生创业就业和全面发展。把创业教育融入人才培养体系,贯穿人才培养全过程,面向全体学生广泛、系统开展,让学生不断增强自身就业竞争能力和社会适应能力,成为适应创新型国家建设需要的高水平创新人才。');
CREATE TABLE `tb_grade`
(
`student_id` bigint NOT NULL,
`course_id` char(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`grade_score` tinyint unsigned DEFAULT NULL,
`grade_level` enum ('优秀','良好','中等','及格','不及格') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
KEY `fk_student_id1` (`student_id`) USING BTREE,
KEY `fk_course_id1` (`course_id`) USING BTREE,
CONSTRAINT `fk_course_id1` FOREIGN KEY (`course_id`) REFERENCES `tb_course` (`course_id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `fk_student_id1` FOREIGN KEY (`student_id`) REFERENCES `tb_student` (`student_id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_0900_ai_ci
ROW_FORMAT = DYNAMIC;
INSERT INTO `tb_grade`
VALUES (20220101002, 'K2002', 49, NULL),
(20220101003, 'K1001', 69, NULL),
(20220101004, 'K2004', 43, NULL),
(20220101006, 'K1004', 69, NULL),
(20220101007, 'K1005', 42, NULL),
(20220101009, 'K1004', 67, NULL);
select s.*, c.*
from tb_student s,
tb_grade g,
tb_course c
where g.student_id = s.student_id
and g.course_id = c.course_id;
explain select s.*, c.*
from tb_student s,
tb_grade g,
tb_course c
where g.student_id = s.student_id
and g.course_id = c.course_id;
我们发现现在id三个都是1,我们发现这个id并不是自增的,即使有多条记录,它这个id有相同的也有可能出现不同的,如果id相同
则代表表结构的执行顺序是从上往下
,也就意味着先执行哪个表呢?g(tb_grade)中间表,紧接着再执行的是s(tb_student表),然后紧接着连接c表(tb_course),这是最终表结构的执行顺序。
为什么是这样去执行呢?
因为在这种多表关联关系当中,tb_student和tb_course之间没有直接关系,他们之间产生关系是通过中间表,所以先执行 tb_grade -> tb_student -> tb_course。
# select course_id from tb_course c where c.course_name = '信息安全管理';# K1004
# select student_id from tb_grade g where g.course_id = 'K1004';# 20220101006,20220101009
# select * from tb_student s where s.student_id in (20220101006, 20220101009);
select * from tb_student s
where s.student_id in (select student_id
from tb_grade g
where g.course_id = (select course_id from tb_course c where c.course_name = '信息安全管理'));
explain select * from tb_student s
where s.student_id in (select student_id
from tb_grade g
where g.course_id = (select course_id from tb_course c where c.course_name = '信息安全管理'));
如果id值不同
,值越大越先执行
,id=3的先执行tb_course,再执行id=2的tb_grade,然后id=1两个相同 从上到下执行,先执行上面的这张表subquery2,代表第二个子查询 select student_id from tb_grade g where g.course_id = (select course_id from tb_course c where c.course_name = ‘信息安全管理’) ,查询完了之后,最后才执行tb_student表。
explain select * from tb_student where student_name='何宇宁';
如果我们使用非唯一性的索引进行查询时,这之后就会出现ref。
索引是帮助 MySQL 高效获取数据的数据结构(有序)。在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查询算法,这种数据结构就是索引。
优点:
缺点:
如果索引了多列(联合索引),要遵守最左前缀法则。最左前缀法则指的是查询从索引的最左列(索引的第一个字段)开始,并且不跳过索引中的列。SQL语句里的字段 顺序无关,要求存在。
如果跳跃某一列,索引将部分失效(后面的字段索引失效)
。
联合索引中,出现范围查询(<, >),范围查询右侧的列索引失效。可以用>=或者<=来规避索引失效问题。
explain select * from tb_student where substring(student_phone, 10, 2) = '15';
explain select * from tb_student where student_phone= 17799990015;
,此处phone的值没有加引号explain select * from tb_student where student_name like '%宁';
,前后都有 % 也会失效。explain 得到的结果 type 是all
。explain 得到的结果 Key:到底走不走索引是取决于表中的数据分布的,而不是一个固定的
假如说一个字段在表中有跟其他字段 联合索引,又有单列索引
explain select * from tb_user where profession="软件工程";
最后它选择联合索引,是MySQL优化器自动选择的结果
假如说在某一些场景下,如果有多个索引,你(MySQL)按照我指定的索引来进行操作来进行查询,能不能做到呢? 可以的,这个就叫SQL提示。
SQL提示是优化数据库的一个重要手段,简单来说,就是在SQL语句中加入一些人为的提示来达到优化操作的目的。
例如,使用索引:use index
explain select * from tb_user use index(idx_user_pro) where profession="软件工程";
不使用哪个索引:ignore index
explain select * from tb_user ignore index(idx_user_pro) where profession="软件工程";
必须使用哪个索引:force index
explain select * from tb_user force index(idx_user_pro) where profession="软件工程";
PS: use 是建议,实际使用哪个索引 MySQL 还会自己权衡运行速度去更改,force就是无论如何都强制使用该索引。可别教优化器做事,优化器会选择成本最低的。
尽量使用覆盖索引(查询使用了索引,并且需要返回的列,在该索引中已经全部能找到),减少 select *。
explain
中extra
字段含义:
using index condition
:查找使用了索引,但是需要回表查询数据
using where; using index;
:查找使用了索引,但是需要的数据都在索引列中能找到,所以不需要回表查询
如果在聚集索引中直接能找到对应的行,则直接返回行数据,只需要一次查询,哪怕是select *;如果在辅助索引中找聚集索引,如select id, name from xxx where name=‘xxx’;,也只需要通过辅助索引(name)查找到对应的id,返回name和name索引对应的id即可,只需要一次查询;如果是通过辅助索引查找其他字段,则需要回表查询,如select id, name, gender from xxx where name=‘xxx’;
所以尽量不要用select *,容易出现回表查询,降低效率,除非有联合索引包含了所有字段
根据id查必然走上面这个索引,id=2和5一比,比5小走左侧,找到2,最终找到这行的数据,我语句要返回的是*,此时*不就代表这一行的所有字段吗?此时我直接拿到这一行的所有字段返回即可。(一次索引扫描),这就是为什么根据id查询性能会高,原因是因为我只需要遍历一次索引就可以了。
根据name字段进行查询,他要走的索引必然不走聚集索引,它是先走name字段对应的二级索引(辅助索引),此时name=‘Arm’和’Lee’相比,L在A的后面,这一块它会走左侧的这个分支找到’Arm’,找到’Arm’之后,接下来再来看我查询返回的字段就是id和name呀,此时name这个叶子节点这一块不就是这个name值吗,叶子节点下面挂的这个数据不就是id吗?此时只需要将Arm和id2直接返回即可。此时大家告诉我有没有涉及到回表?没有。因为我查询返回的字段在这个索引结构都已经找到了,这种就叫覆盖索引。
我先要扫描辅助索引,再去扫描聚集索引,那么这个过程呢我们就称这为回表查询。一旦出现回表查询,它的性能相对来说就要低一些了,虽然走了索引,但是扫描了两次。
面试题: 一张表,有四个字段(id, username, password, status),由于数据量大,需要对以下SQL语句进行优化,该如何进行才是最优方案: select id, username, password from tb_user where username=‘itcast’;
解: 给username和password字段建立联合索引,则不需要回表查询,直接覆盖索引。
当字段类型为字符串(varchar, text等)时,有时候需要索引很长的字符串,这会让索引变得很大,查询时,浪费大量的磁盘IO,影响查询效率,此时可以只降字符串的一部分前缀,建立索引,这样可以大大节约索引空间,从而提高索引效率。
语法:create index idx_xxxx on table_name(columnn(n));
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;
distinct 去重
show index 里面的sub_part可以看到接取的长度
‘lvbu6’跟’daqia’比,l在d之后,所以会走右侧的这个指针,然后接下来呢再往下找就找到了这个lvbu6,找到了这一点之后就可以拿到辅助索引对应的id1。拿到id1最终返回的是*,所以在这一块拿到id需要到聚集索引当中进行回表查询,然后根据id为1进行查询,和7比,比7小走左侧,和3比,比3小走左侧,最终拿到这一行的数据,拿到这一行的数据之后,不能直接把这行的数据返回,因为刚才在对比的时候我只是对比了前缀,拿到这一行的数据,我要从这一行的数据当中拿出email的值然后再去看这一行的数据email的值是不是我们所传递进来的email(‘[email protected]’),如果是 那么此时我就要将这一行的数据来查询到并且返回,然后再去(辅助索引)查询当前’lvbu6’再往下一个节点走(因为它是一个链表),我再去找下一个元素,看看下一个元素是不是’lvbu6’,如果不是我直接返回这一行的数据,如果是,那我接下来还需要再去查询下一行的数据,再把下一行的数据也拿到最终组装数据并返回。
解决了什么问题
用来解决一些长字符串或者是大文本字段,在整个字段进行索引的时候,索引体积过于庞大,而造成的浪费大量磁盘io的情况,那对于这种情况,我们可以使用前缀索引来降低索引的体积。
单列索引:即一个索引只包含单个列
联合索引:即一个索引包含了多个列
在业务场景中,如果存在多个查询条件,考虑针对于查询字段建立索引时,建议建立联合索引,而非单列索引。
单列索引情况:
explain select id, phone, name from tb_user where phone = '17799990010' and name = '韩信';
表中name和phone都有单列索引,但是explain这句发现只会用到phone索引字段,只走了一个字段的索引,另一个字段没有走索引。根据两个字段去查,只走了phone的索引, 这时候涉及到回表查询,它必然不包含name字段的值,所以它是会回表查询的。
再创建一个联合索引看看,这时候又有联合索引又有单列索引
create unique index idx_user_phone_name on tb_user(phone, name);
explain select id, phone, name from tb_user where phone = '17799990010' and name = '韩信';
发现实际上还是只用到了phone的单列索引,这是MySQL自己选择的结果(有单列索引的干扰)。
我们用 SQL提示use index
explain select id, phone, name from tb_user use index(idx_user_phone_name) where phone = '17799990010' and name = '韩信';
执行发现,用到了phone和name的联合索引了。
注意事项
多条件联合查询时,MySQL优化器会评估哪个字段的索引效率更高,会选择该索引完成本次查询,并不是所有的索引都会用到。
insert into tb_student values (20220101001, '小梦', '男', 180);
insert into tb_student values (20220101002, '小佳', '女', 176);
insert into tb_student values (20220101002, '小怡', '女', 166);
...
我们在往数据库表结构当中, 在插入数据的时候呢,那通常来说就是一条insert一条insert去执行,那么此时如果我们想去插入多条数据 ,那么我们可以从以下的三个方面来进行优化:
(1) 执行批量操作: 也就是说不要一条insert一条insert去执行了,因为每一次执行insert插入一条数据,再执行一次,再插入一条数据,每一次insert都需要与数据库进行建立连接、进行网络传输,那么这个性能相对来说是比较低的,所以我们建议批量插入。如果一次性你要插入多条数据,可以通过一条sgl语句来完成。
insert into tb_student values (20220101001, '小梦', '男', 180),
(20220101002, '小佳', '女', 176),
(20220101002, '小怡', '女', 166);
但是如果说你要批量插入,那么一次性所插入的数据,也不建议超过1000条,500~1000条是比较合适的。那如果我们要插入几万条数据怎么办呢?我们可以将其分割为多条insert语句进行插入。
(2) 手动提交事务:
start transaction ;
insert into tb_student values (20220101001, '小梦', '男', 180),(20220101002, '小佳', '女', 176),(20220101002, '小怡', '女', 166);
insert into tb_student values (20220101004, '小梦', '男', 180),(20220101005, '小佳', '女', 176),(20220101006, '小怡', '女', 166);
insert into tb_student values (20220101007, '小梦', '男', 180),(20220101008, '小佳', '女', 176),(20220101009, '小怡', '女', 166);
commit ;
(3) 主键顺序插入:
主键乱序插入: 8 1 9 21 88 2 4 15 89 5 7 3
主键顺序插入: 1 2 3 4 5 7 8 9 15 21 88 89
建议主建顺序插入,因为顺序插入的性能要高于乱序插入。这个呢是取决于mysql的数据组织结构的,这个我们在后面学到主键优化的时候会详细说。
# 客户端连接服务端时,加上参数 --local-infile(这一行在bash/cmd界面输入)
mysql --local-infile -u root -p
# 设置全局参数local_infile为1,开启从本地加载文件导入数据的开关
set global local_infile = 1;
select @@local_infile;# 零代表的是开关并未开启
# 执行load指令将准备好的数据,加载到表结构中
load data local infile '/root/sql1.log' into table 'tb_user' fields terminated by ',' lines terminated by '\n';
load data local infile '文件地址.sql' into table '表' fields terminated by '每一个字段分隔符' lines terminated by '每一行数据使用什么分隔';
数据组织方式:在InnoDB存储引擎中,表数据都是根据主键顺序组织存放的,这种存储方式的表称为索引组织表
(Index organized table, IOT
)
InnoDB
页分裂:页可以为空,也可以填充一般,也可以填充100%,每个页包含了2-N行数据(如果一行数据过大,会行溢出),根据主键排列。
每一个页当中呢至少包含两行数据,如果一个页只包含一行数据的话,就相当于是链表了。所以呢在innodb当中,规定每一个页当中至少包含两行数据,如果某一行数据啊它比较大,那么此时超出了一定的阈值之后,就会产生行溢出现象。
主键顺序插入
主键乱序插入
开辟一个新的数据页,然后会找到第一个数据页50%的位置,23、47是超出了50%,超出了一半了,那么此时它会先将这两个数据先移动到新开辟的的数据页当中,然后再将50插入到这个数据页。此时1#数据页下一个数据页应该是3#数据页了,所以此时需要再对链表指针再进行一个重新的设置。
设置成什么样子的呢?他会去设置1# 数据页它的下一个数据页是3# 数据页,3# 数据页下一个才是2#数据页,3# 的上一个是1# ,重新设置链表指针,那么这种现象称之为页分裂。
页合并:当删除一行记录时,实际上记录并没有被物理删除,只是记录被标记(flaged)为删除并且它的空间变得允许被其他记录声明使用。当页中删除的记录到达 MERGE_THRESHOLD(默认为页的50%),InnoDB会开始寻找最靠近的页(前后)看看是否可以将这两个页合并以优化空间使用。
这三个数据页里面存放了这么多的数据,接下来我们要展示删除操作,删14、15、16
接下来再往下删除,那么可能发生一个现象,当页中所删除的记录达到一定的阈值之后呢,接下来innodb它就会做一个操作,它就会去查找它的上一页或者下一页,看一下相邻的这两个数据页有没有合并的可能性,如果可以合并,它会合并两个页以优化空间,这个阈值默认是页的50%。
主键设计原则:
① Using filesort:通过表的索引或全表扫描,读取满足条件的数据行,然后在排序缓冲区 sort buffer 中完成排序操作,所有不是通过索引直接返回排序结果的排序都叫 FileSort 排序
② Using index:通过有序索引顺序扫描直接返回有序数据,这种情况即为 using index,不需要额外排序,操作效率高
如果order by字段全部使用升序排序或者降序排序,则都会走索引,但是如果一个字段升序排序,另一个字段降序排序,则不会走索引,explain的extra信息显示的是Using index, Using filesort
,如果要优化掉Using filesort,则需要另外再创建一个索引,如:
create index idx_user_age_phone_ad on tb_user(age asc, phone desc);
此时使用
select id, age, phone from tb_user order by age asc, phone desc;
会全部走索引。
索引默认是 asc 升序
如果两个同为升序, 那么会走这个索引,从索引当中返回的数据就已经是有序的了;那如果同为降序,也直接走这个索引 进行反向的扫描就可以了。
总结:
根据排序字段建立合适的索引,多字段排序时,也遵循最左前缀法则
尽量使用覆盖索引
多字段排序,一个升序一个降序,此时需要注意联合索引在创建时的规则(ASC/DESC)
如果不可避免出现filesort,大数据量排序时,可以适当增大排序缓冲区大小 sort_buffer_size(默认256k)
如果256k占满了,这个时候就会涉及到磁盘文件,它会在磁盘文件当中进行排序,那这个时候性能就会比较低,所以如果是在大数据量排序的时候出现了Using filesort
,可以适当的把这个参数往上调, 从而来提升排序的效率。
show variables like 'sort_buffer_size';
# 设置global级别的sort_buffer_size值,设置sort_buffer_size=1M
set global sort_buffer_size = 1024*1024;
# 设置session级别的sort_buffer_size值,设置sort_buffer_size=2M
set session sort_buffer_size = 2*1024*1024;
如索引为idx_user_pro_age_stat
,则句式可以是select ... where profession order by age
,这样也符合最左前缀法则
常见的问题如limit 2000000, 10,此时需要 MySQL 排序前2000000条记录,但仅仅返回2000000 - 2000010的记录,其他记录丢弃,查询排序的代价非常大。
优化方案:一般分页查询时,通过创建覆盖索引能够比较好地提高性能,可以通过覆盖索引加子查询形式进行优化。
例如:
-- 此语句耗时很长
select * from tb_sku limit 9000000, 10;
-- 通过覆盖索引加快速度,直接通过主键索引进行排序及查询
select id from tb_sku order by id limit 9000000, 10;
-- 下面的语句是错误的,因为 MySQL 不支持 in 里面使用 limit
-- select * from tb_sku where id in (select id from tb_sku order by id limit 9000000, 10);
-- 通过连表查询即可实现第一句的效果,并且能达到第二句的速度
select * from tb_sku as s, (select id from tb_sku order by id limit 9000000, 10) as a where s.id = a.id;
explain select count(*) from 表名;
MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个数,效率很高(前提是不使用where);
InnoDB 在执行 count(*) 时,需要把数据一行一行地从引擎里面读出来,然后累计计数。
优化方案:自己计数,如创建key-value表存储在内存或硬盘,或者是用redis
count的几种用法:
各种用法的性能:
按效率排序:count(字段) < count(主键) < count(1) <≈ count(*),所以尽量使用 count(*)
InnoDB 的行锁是针对索引加的锁,不是针对记录加的锁,并且该索引不能失效,否则会从行锁升级为表锁。
如以下两条语句:
update student set no = ‘123’ where id = 1;,这句由于id有主键索引,所以只会锁这一行;
update student set no = ‘123’ where name = ‘test’;,这句由于name没有索引,所以会把整张表都锁住进行数据更新,解决方法是给name字段添加索引
视图(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] 视图名称[,视图名称] ...
数据库db_study.sql
链接:https://pan.baidu.com/s/15nYOoyQrZTfc77hnO2r3EA?pwd=1024
提取码:1024
-- 创建视图
create or replace view stu_v_1 as select student_id,student_name from tb_student where student_id<=20220101010;
-- 查看视图
-- 查看创建视图语句
show create view stu_v_1;
-- 查看视图数据
select * from stu_v_1;
select * from stu_v_1 where student_id<20220101005;
-- 修改视图
create or replace view stu_v_1 as select student_id,student_name,student_gender from tb_student where student_id<=20220101010;
alter view stu_v_1 as select student_id,student_name,student_gender,student_phone from tb_student where student_id<=20220101010;
-- 删除视图
drop view if exists stu_v_1;
create or replace view stu_v_1 as select student_id,student_name from tb_student where student_id<=20220101020;
select * from stu_v_1;
insert into stu_v_1 values(20220101006,'小梦');# 报错:已存在
# 这条数据到底插入到哪了呢?
# 我们提到,这个视图实际上并不存储数据,具体的数据都是在基表当中存在的,
# 那么也就意味着,现在这个视图它对应的基表也就是tb_student表,
# 也就意味着我们所插入的数据实际上是插入到tb_student表中的
这条数据到底插入到哪了呢?
我们提到,这个视图实际上并不存储数据,具体的数据都是在基表当中存在的, 那么也就意味着,现在这个视图它对应的基表也就是tb_student表, 也就意味着我们所插入的数据实际上是插入到tb_student表中的。
insert into stu_v_1 values(1,'小梦1');
select * from stu_v_1;
select * from tb_student;
insert into stu_v_1 values(90220101006,'小梦9 ');
select * from tb_student;# 可以看到基表有添加这么一条数据
select * from stu_v_1;-- 因为创建视图的时候条件是where student_id<=20220101020;所以查不到
通过视图所插入的数据,通过这个视图结果查不到,为了避免这种情况的出现,在MySQL中,在视图创建的时候给我们提供了一种语法,我们可以在后面加上这么一个检查选项,避免这种情况的发生。
-- 通过视图所插入的数据,通过这个视图结果查不到,为了避免这种情况的出现,在MySQL中,在视图创建的时候给我们提供了一种语法,我们可以在后面加上这么一个检查选项,避免这种情况的发生。
create or replace view stu_v_1 as
select student_id, student_name from tb_student where student_id <= 20220101020
with cascaded check option ;
select * from stu_v_1;
insert into stu_v_1 values (2,'小易');
insert into stu_v_1 values (20220101999,'小易cascaded');
select * from stu_v_1;
或者
create or replace view stu_v_1 as
select student_id, student_name from tb_student where student_id <= 20220101020
with local check option ;
报错:CHECK OPTION failed ‘db_study.stu_v_1’
检查选项失败。这样就阻止了我们这条数据的插入。为什么要阻止呢?原因是因为你所插入的这条数据和我们视图创建时的条件违背的,这个check option
检查选项 就会阻止我们插入。
当使用WITH CHECK OPTION子句创建视图时,MySL会通过视图检查正在更改的每个行,例如 插入,更新,删除,以使其符合视图的定义。MySQL允许基于另一个视图创建视图,它还会检查依赖视图中的规则以保持一致性。为了确定检查的范围,mysql提供了两个选项: CASCADED 和 LOCAL,默认值为 CASCADED。
cascaded 和 local 到底有什么区别?
cascaded 和 local 都会递归往所依赖的视图进行查询。只不过local不会将当前的检查选项 强行加到所依赖的视图。cascaded 会强行加检查选项给所依赖的视图。
cascaded会检查上级条件,location只会检查加了with…的。
local:本地的 只管自己这一条 cascaded:级联的 跟它有关系的都管。
local只将自身进行捆绑,检查自身数据,不会强制依赖的视图进行检查(若依赖的视图有检查选项就检查,反之不检查)。
会传递检查选项
create view v1 as select id,name from student where id <= 20;
后面没有加with cascaded check option
,接下来你对这个v1视图进行增删改操作 它是不会去检查这个条件的。当我们再去定义一个视图v2 create view v2 as select id , name from v1 where id >= 10 with cascaded check option ;
注意这个v2视图是基于v1视图的,而v2视图在定义的时候指定了条件为id>=10 并且后面给它指定了with cascaded check option
,一旦加上了这个选项,那么此时当我们再去操作v2这个视图时,它会去检查是否满足id>=10的这个条件,由于你加的是cascaded 翻译过来叫级联,那么它不仅要去检查你当前的操作是否满足v2的这个条件(id>=10)还得检查是否满足v1的条件,就相当于v1这个视图里面也加上了这么一个参数with cascaded check option;
。假如说v1又关联了另外一个视图,那么紧接着就相当于在它所依赖的视图后面也加上了with cascaded check option
。
-- 演示 cascaded
create or replace view v1 as select student_id, student_name from tb_student where student_id <= 20220101020;
insert into v1 values (3,'小易v1');# 能插入成功
insert into stu_v_1 values (80220101999,'小易v1');# 能插入成功,原因是因为语句后面没有加上 with local check option 那么mysql不会去检查我们所执行的增删改操作是否违背这个条件。
create or replace view v2 as select student_id, student_name from v1 where student_id >= 20220101001 with cascaded check option;
insert into v2 values (4,'小易v2视图');# 报错: [HY000][1369] CHECK OPTION failed 'db_study.v2' 检查选项检查失败
insert into v2 values (80220101888,'小易v2');# 不能插入成功,会检查v1
insert into v2 values (20220101006,'小易v2');# 先去tb_student表中删掉id为20220101006的数据,再来执行这条语句,可以执行,执行成功!!!
在原基础上又创建了一个视图v3(基于v2视图的),但是并没有加with cascaded check option
。v3不检查,v2检查(有with cascaded check option
),v1因为v2的with cascaded check option
也会检查。
一旦我们在某一个视图创建的时候加了
with cascaded check option
,那么此时我们在操作这个视图的时候,他会去检查当前视图,还会去检查当前这个视图所依赖的所有视图它的条件是否满足,这就是检查选项 cascaded (级联)。
create or replace view v1 as select student_id, student_name from tb_student where student_id <= 20220101020;
create or replace view v2 as select student_id, student_name from v1 where student_id >= 20220101001 with cascaded check option;
create or replace view v3 as select student_id, student_name from v2 where student_id <= 20220101010;
# 先去tb_student表、tb_grade表中删掉id为xxx的数据,再来执行这条语句
insert into v3 values (20220101009,'09小易v3');# 可以执行
insert into v3 values (20220101011,'11小易v3');# 可以执行
# 不满足v3条件,但是可以执行,因为在创建v3的时候没有with cascaded check option,没有就不会检查这个视图,紧接着就去检查v2视图了,v2视图再一看是否满足>= 20220101001,满足,
# 然后紧接着由于v2视图当中加了with cascaded check option 它会向下再去检查v1视图,20220101011是否<= 20220101020,所以这条数据可以插入成功
insert into v3 values (20220101021,'21小易v3');# 报错CHECK OPTION failed 'db_study.v3'
# 原因是因为 20220101021 满足v2( >= 20220101001) 不满足v1(<= 20220101020),所以报错
当我们在操作视图的时候,它会递归的去找当前视图所依赖的视图,如果说当前视图以及依赖的视图后面定义有with (cascaded | local) check option ;
这样的检查选项,将会去判定我们所操作的数据是否满足视图的条件,如果在递归的过程当中找到某一个视图在定义的时候没有增加这样的检查选项with (cascaded | local) check option ;
。此时,我们在操作这块数据的时候,将不对这个条件做检查。
不会传递检查选项
create view v1 as select id,name from student where id <= 15;
。我们再去定义一个视图v2 create view v2 as select id , name from v1 where id >= 10 with local check option ;
我们再基于v2视图定义一个v3视图create view v3 as select id , name from v2 where id < 20;
当我们往v1视图再去插入数据时,它是不会再去检查这个条件的;
当我们再往v2视图再去插入数据时,由于我们在v2视图创建的时候定义了检查选项local,所以此时它会去检查条件,检查v2条件没有问题。要不要再去检查满不满足v1的条件呢?此时它要去检查v1的条件,如果说v1定义的有条件要去判定,“我当前所操作的数据是否满足v1的条件?” 如果v1里面没有定义条件则不检查,就指的是这个含义。也就是说它也会递归的去找它所依赖的视图,然后要去看这个依赖的视图当中在定义的时候有没有添加检查选项,如果有检查选项,进行校验,如果没有,则不进行校验,当然这些都是基于MySQL8.0的效果。
create view v4 as select student_id,student_name from tb_student where student_id <= 15;
insert into v4 values (5,'梦里不知身是客');# 能插入
insert into v4 values (16,'清风微泫');# 不满足v4条件,但是能插入
create view v5 as select student_id , student_name from v4 where student_id >= 10 with local check option ;
# v5依赖于v4
# 检查的时候先要去看当前所操作的数据是否满足v5的条件,如果满足v5的条件还要去递归去找它所依赖的视图v4,所以还要去检查是否满足v4的条件,
# 而由于v4在检查定义的时候,没有指定检查选项,所以v4就不检查了,而如果说v4定义了检查选项,那么还需要去检查啥v4的条件。
insert into v5 values (13,'IceYi');# 能插入
insert into v5 values (17,'Yi');# 满足v5,不满足v4条件,由于v4没有定义检查选项的,所以它是不会去检查v4对应的条件的,即使这个17>15的,所以最后能插入
create view v6 as select student_id , student_name from v5 where student_id < 20;
insert into v6 values (14,'Yi');
# 先去看v6是否满足,由于v6没有定义检查选项,所以这个条件不做检查,
# 接下来他要递归的去找v5,v5是定义了检查选项的,所以它要去校验是否满足v5的条件,14>10是满足的,
# 再往下,由于v5依赖于v4,此时插入的时候还要去看是否满足v4的条件,由于v4后面没有定义检查选项,所以不做检查。
# 所以这条记录是可以插入成功的!
# 假如v4定义了检查选项local
create or replace view v4 as select student_id,student_name from tb_student where student_id <= 15 with local check option ;
create view v5 as select student_id , student_name from v4 where student_id >= 10 with local check option ;
create view v6 as select student_id , student_name from v5 where student_id < 20;
insert into v5 values (18,'Yi');# 会报错:[HY000][1369] CHECK OPTION failed 'db_study.v5'
# 18 满足v5(>=10),不满足v4条件(<= 15),由于v4在创建的时候有定义检查选项的,
# 所以它还要去检查v4对应的条件的,18不满足<=15,所以执行这条insert语句的时候会报错
任何的视图都能够进行增删改吗?
并不是这样的。
符合什么样条件的视图才可以更新呢?什么样条件的视图不可以更新?
要使视图可更新,视图中的行与基础表中的行之间必须存在一对一
的关系。如果视图包含以下任何一项,则该视图不可更新:
-- --- 演示:创建视图使用了聚合函数
create view stu_v_count as select count(*) from tb_student;
insert into stu_v_count values(10);# 报错:[HY000][1471] The target table stu_v_count of the INSERT is not insertable-into
# 翻译过来: 当前视图是不能进行插入操作的,原因是因为使用了聚合函数,
# 使用了聚合函数之后我们会看到这个视图当中的数据和表中的数据不是 一一对应 的,
# 所以此时视图是不可以进行插入, 当然也不能进行更新。
# 起别名
create or replace view v4 as select student_id as id,student_name as name from tb_student where student_id <= 15 with local check option ;
存储过程是事先经过编译并存储在数据库中的一段 SQL语句的集合,调用存储过程可以简化应用开发人员的很多工作,减少数据在数据库和应用服务器之间的传输,对于提高数据处理的效率是有好处的。
存储过程思想上很简单,就是数据库SQL语言层面的代码封装与重用。
特点
创建存储过程
create procedure 存储过程名称( [参数列表] )
begin
-- sql语句
end ;
调用存储过程
call 名称([参数]);
查看存储过程
select * from information_schema.routines where routine_schema='数据库名xxx';-- 查询指定数据库的存储过程及状态信息
show create procedure 存储过程名称;-- 查询某个存储过程的定义
删除存储过程
drop procedure [if exists] 存储过程名称;
procedure 程序 /手续 /过程 /步骤
information 信息;资料;情报;资讯; 消息;通知;讯息
schema (计划或理论的)纲要,图解;架构 /模式 /纲目结构 /方案
routines 程序;常规;惯例;例行公事;日常的; 例程;类和例程;常式
注意: 在命令行中,执行创建存储过程的SQL时,需要通过关键字 delimiter 指定SQL语句的结束符.
# 创建存储过程
create procedure p1()
begin
select count(*) from tb_student;
end;
# 调用
call p1();# 一旦调用了这个存储过程,也就意味着他就要去执行存储过程当中所封装的逻辑
# 查看存储过程
select * from information_schema.ROUTINES where ROUTINE_SCHEMA='db_study';-- 查询指定数据库的存储过程及状态信息
show create procedure p1;-- 查询某个存储过程的定义
# 删除存储过程
drop procedure if exists p1;
# 结束的时候以两个$结束
delimiter $$
create procedure p1()
begin
select count(*) from tb_student;
end$$
delimiter ;
# 把结束符改为 ;
GLOBAL
)、会话变量(SESSION
)查看系统变量
show [session | global] variables;-- 查看所有系统变量,默认session
show [session | global] variables like '......'; -- 可以通过LIKE模糊配方式查找变量
select @@[ session. | global. ]系统变量名; -- 查看指定变量的值
设置系统变量
set [session|global] 系统变量名=值;
set @@[session|global] 系统变量名=值;
注意:
如果没有指定SESSION/GLOBAL,默认是SESSION,会话变量。
mysql服务重新启动之后,所设置的全局参数会失效,要想不失效,可以在 /etc/my.cnf 中配置。
# 查看系统变量
show session variables;-- 查看所有会话级别系统变量
show global variables;-- 查看所有全局变量
show session variables like 'auto%'; -- 可以通过LIKE模糊配方式查找变量
show global variables like 'auto%';
select @@autocommit;
select @@session.autocommit;
select @@global.autocommit;
# 设置系统变量
set autocommit = 0;
set session autocommit = 1;
set @@session.autocommit = 0;
set global autocommit = 0;
两个@指的是系统变量
一个@指的是用户自定义变量
用户定义变量 是用户根据需要自己定义的变量,用户变量不用提前声明,在用的时候直接用“@变量名”使用就可以。其作用域为当前连接。
赋值
set @var_name = expr [, @var_name = expr]... ;
set @var_name := expr [, @var_name := expr]... ;# 推荐:=
# 因为在mysql中它的比较运算符也是=,没有==,所以=既可以作为赋值运算符,又可以作为比较运算符。为了以示区分,推荐使用:=
# 在使用的时候,如果是赋值,使用:=
select @var_name := expr [, @var_name := expr] ...
select 字段名 into @var_name from 表名; # 从这张表当中所查询出来的这一块的数据赋值给这个变量
使用
select @var_name;
注意: 用户定义的变量无需对其进行声明或初始化,只不过获取到的值为NULL。
-- 用户自定义变量
# 赋值
set @myname = '梦里不知身是客' ;
set @myage := 18 ;
set @mygender:='男',@myhobby:='听音乐';
select @mycolor := '绿色';
select count(*) into @mycount from tb_student;
# 使用
select @myname,@myage,@mygender,@myhobby,@mycolor,@mycount;
局部变量 是根据需要定义的在局部生效的变量,访问之前,需要DECLARE声明。可用作存储过程内的局部变量和输入参数,局部变量的范围是在其内声明的BEGIN … END块。
声明
DECLARE 变量名 变量类型[DEFAULT ...];
变量类型就是数据库字段类型:INT、BIGINT、CHAR、VARCHAR、DATE、TIME等。
赋值
SET 变量名=值;
SET 变名 := 值;
SELECT 字段名 INTO 变量名 FROM 表名
灰色
-- 变量: 局部变量
-- 声明 declare
-- 赋值
create procedure p2()
begin
declare student_count int default 0;
set student_count := 99;
select count(*) into student_count from tb_student;
select student_count;
end;
call p2();
IF 条件1 THEN
......
ELSEIF 条件2 THEN -- 可选
......
ELSE -- 可选
.......
END IF;
create procedure p3()
begin
declare score int default 58;
declare result varchar(10);
if score >= 80 then
set result := '优秀';
elseif score >= 60 then
set result :='及格';
else
set result :='不及格';
end if;
select result;
end;
call p3();
drop procedure if exists p3;
类型 | 含义 | 备注 |
---|---|---|
IN | 该类参数作为输入,也就是需要调用时传入值 | 默认 |
OUT | 该类参数作为输出,也就是该参数可以作为返回值 | |
INOUT | 既可以作为输入参数,也可以作为输出参数 |
用法:
CREATE PROCEDURE 存储过程名称([ IN/OUT/INOUT 参数名 参数类型])
BEGIN
-- SQL语句
END ;
create procedure p4(in score int,out result varchar(10))
begin
if score >= 80 then
set result := '优秀';
elseif score >= 60 then
set result :='及格';
else
set result :='不及格';
end if;
end;
call p4(60,@result);
select @result;
create procedure p5(inout score double)
begin
set score := score * 0.5;
end;
set @result := 200;
call p5(@result);
select @result;
语法一
CASE case_value表达式
WHEN when_value1值 THEN statement_list1
[ WHEN when_value2 THEN statement_list2] ...
[ ELSE statement_list ]
END CASE;
如果表达式既不是when_value1 也不是when_value2 ,则执行 ELSE 之后的逻辑。
语法二
CASE
WHEN search_condition1条件表达式 THEN statement_list1
[WHEN search_condition2 THEN statement_list2] ...
[ELSE statement_list]
END CASE;
演示
create procedure p6(in score int)
begin
declare result varchar(10);
case
when score >= 80 then set result := '优秀';
when score >= 60 then set result := '及格';
else set result := '不及格';
end case;
select concat('您输入的成绩为: ', score, ',所属的等级为: ', result);
end;
call p6(60);
call p6(90);
while 循环是有条件的循环控制语句。满足条件后,再执行循环体中的SQL语句
。具体语法为:
#先判定条件,如果条件为true,则执行逻辑,否则,不执行逻辑
WHILE 条件 DO
SQL逻辑...
END WHILE;
演示
delimiter $$
create procedure p7(in n int)
begin
# 计算从1累加到n的值,n为传入的参数值。
declare num int default 0;
while n > 0 do
set num := num + n;
set n := n - 1;
end while;
select num;
end $$
delimiter ;
call p7(10);
repeat是有条件的循环控制语句, 当满足条件的时候退出循环
。具体语法为:
#先执行一次逻辑,然后判定逻辑是否满足,如果满足,则退出。如果不满足,则继续下一次循环
REPEAT
SQL逻辑...
UNTIL 条件
END REPEAT;
UNTIL 直到…为止
演示
delimiter $$
create procedure p8(in n int)
begin
# 计算从1累加到n的值,n为传入的参数值。
declare num int default 0;
repeat
set num := num + n;
set n := n - 1;
until n <= 0
end repeat;
select num;
end $$
delimiter ;
call p8(10);
call p8(100);
LOOP 实现简单的循环,如果不在SQL逻辑中增加出循环的条件,可以用其来实现简单的死循环。LOOP可以配合一下两个语句使用:
[begin_label:] LOOP
SQL逻辑...
END LOOP [end_label];
begin_label 实际上是我们自己定义的一个标记,来标识当前的这次循环,需要注意,在这个标记之后就有一个冒号:
LEAVE label; --退出指定标记的循环体
ITERATE label; --直接进入下一次循环
演示
# loop--计算从1累加到n的值,n为传入的参数值。
create procedure p9(in n int)
begin
declare num int default 0;
sum:loop
if n<=0 then
leave sum;
end if;
set num := num + n;
set n := n - 1;
end loop sum;
select num;
end;
call p9(10);
# loop--计算从1到n之向的偶数累加的值,n为传入的参数值。
create procedure p10(in n int)
begin
declare num int default 0;
sum:loop
if n<=0 then
leave sum;
elseif n%2=1 then
set n := n - 1;
iterate sum;
end if;
set num := num + n;
set n := n - 1;
end loop sum;
select num;
end;
call p10(10);
游标 (CURSOR) 是用来存储查询结果集的数据类型,在存储过程和函数中可以使用游标对结果集进行循环的处理。游标的使用包括游标的声明、OPEN、FETCH 和 CLOSE,其语法分别如下。
declare 游标名称 cursor for 查询语句;
open 游标名称;
fetch 游标名称 into 变量[,变量];
close 游标名称
注意: 要先声明普通变量, 再声明游标
演示
-- 通过变量记录查询结果
create procedure p11(in uheight int)
begin
declare sname varchar(100);
declare sheight tinyint unsigned;
declare s_cursor cursor for select student_name,student_height from tb_student where student_height>=uheight;
drop table if exists tb_student_pro;
create table if not exists tb_student_pro(
id int primary key auto_increment,
name varchar(100),
height tinyint unsigned
);
open s_cursor;
while true do
fetch s_cursor into sname,sheight;
insert into tb_student_pro value (null,sname,sheight);
end while;
close s_cursor;
end;
call p11(175);
表结构有数据, 这些数据都有一个特点, 身高都是大于175
[02000][1329] No data - zero rows fetched, selected, or processed
当我们循环到最后一条之后,接下来再进行下一次循环,游标里面没有数据了,所以此时这一块就报错了。
我们要解决这个问题:一旦我们发现游标中没有这个数据了,我们就要退出循环。我们可以使用MySQL提供的条件处理程序。
条件处理程序(Handler)
可以用来定义在流程控制结构执行过程中遇到问题时相应的处理步骤。具体语法为:
DECLARE handler_action HANDLER FOR condition_value [, condition_value]... statement ;
handler_action
CONTINUE: 继续执行当前程序
EXIT: 终止执行当前程序
condition_value
SOLSTATE sglstate_value: 状态码,如 02000
SQLWARNING: 所有以01开头的SQLSTATE代码的简写
NOT FOUND: 所有以02开头的SOLSTATE代码的简写
SQLEXCEPTION: 所有没有被SQLWARNING 或 NOT FOUND捕获的SQLSTATE代码的简写
更多状态码信息看官方文档吧:https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
演示
drop procedure if exists p11;
create procedure p11(in uheight int)
begin
declare sname varchar(100);
declare sheight tinyint unsigned;
declare s_cursor cursor for select student_name,student_height from tb_student where student_height>=uheight;
declare EXIT handler for SQLSTATE '02000' close s_cursor;
# 声明一个条件处理程序,满足sql状态码为02000时触发,触发退出操作,在退出的时候我要将游标关闭掉
drop table if exists tb_student_pro;
create table if not exists tb_student_pro(
id int primary key auto_increment,
name varchar(100),
height tinyint unsigned
);
open s_cursor;
while true do
fetch s_cursor into sname,sheight;
insert into tb_student_pro value (null,sname,sheight);
end while;
close s_cursor;
end;
call p11(175);
就解决了之前的报错问题。
create procedure p12(in uheight int)
begin
declare sname varchar(100);
declare sheight tinyint unsigned;
declare s_cursor cursor for select student_name,student_height from tb_student where student_height>=uheight;
declare EXIT handler for NOT FOUND close s_cursor;
# 声明一个条件处理程序,满足sql状态码为02xxx时触发,触发退出操作,在退出的时候我要将游标关闭掉
drop table if exists tb_student_pro;
create table if not exists tb_student_pro(
id int primary key auto_increment,
name varchar(100),
height tinyint unsigned
);
open s_cursor;
while true do
fetch s_cursor into sname,sheight;
insert into tb_student_pro value (null,sname,sheight);
end while;
close s_cursor;
end;
call p12(186);
存储函数是有返回值的存储过程,存储函数的参数只能是IN类型的 (不写也是in类型) 。具体语法如下:
存储函数可以被存储过程替代
create function 存储函数名称([参数列表])
returns type [characteristic...]
begin
-- SQL语句
return ...;
end;
characteristic说明: 当前存储参数它的特性
mysql8.x版本来说mysql当中的binary logging(二进制日志)默认是开启的,
那么它就要求我们需要去指定当前存储函数它具体的特性是怎么样子的,
需要描述一下这个存储函数的特性
- deterministic: 相同的输入参数总是产生相同的结果
- no sql: 不包含SQL语句
- reads sql data: 包含读取数据的语句,但不包含写入数据的语句
演示
-- 存储函数
-- 从1到n累加
create function fun01(n int)
returns int deterministic
begin
declare num int default 0;
while n > 0
do
set num := num + n;
set n := n - 1;
end while;
return num;
end;
select fun01(100);
触发器是与表有关的数据库对象,指在 insert/update/delete 之前或之后,触发并执行触发器中定义的SOL语句集合。触发器的这种特性可以协助应用在数据库端确保数据的完整性,日志记录,数据校验等操作。
使用别名 OLD 和 NEW 来引用触发器中发生变化的记录内容,这与其他的数据库是相似的。现在触发器还只支持行级触发
,不支持语句级触发。
行级触发: 影响多少行就会触发多少次。
触发器类型 | NEW 和 OLD |
---|---|
INSERT型触发器 | NEW 表示将要或者已经新增的数据 |
UPDATE型触发器 | OLD 表示修改之前的数据,NEW 表示将要或已经修改后的数据 |
DELETE型触发器 | OLD 表示将要或者已经删除的数据 |
create trigger trigger_name
before/after insert/update/delete
on table_name for each row --行级触发器
begin
trigger_stmt;
end;
show triggers;
drop trigger [schema_name.]trigger_name; -- 如果没有指定schema_name,默认为当前数据库。
通过触发器记录 tb_student 表的数据变更日志,将变更日志插入到日志表student_logs中,包含增加,修改,删除;
create table student_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 bigint not null comment '操作的ID',
operate_params varchar(500) comment '操作参数',
primary key(id)
)engine=innodb default charset=utf8;
insert into tb_student(student_id,student_name, student_gender, student_height, student_birthday, class_id, student_phone)
value(20230101201,'清风微泫','男',175,'2004-09-19 02:53:39','B4009','141-5402-0000');
update tb_student set student_name='梦里不知身是客' where student_id=20220101001;
delete from tb_student where student_id=20230101201;
-- 定义插入数据时的触发器
create trigger tb_student_insert_trigger
after insert on tb_student for each row
begin
insert into student_logs(id, operation, operate_time, operate_id, operate_params) VALUES
(null,'insert',now(),NEW.student_id,
concat('插入的数据内容为:id=',NEW.student_id,', name=',NEW.student_name,', phone=',NEW.student_phone));
end;
insert into tb_student(student_id,student_name, student_gender, student_height, student_birthday, class_id, student_phone)
value(20230101201,'清风微泫','男',175,'2004-09-19 02:53:39','B4009','141-5402-0000');
show triggers;-- 查看
drop trigger tb_student_insert_trigger;-- 删除
触发器有没有触发,我们只需要去日志表看看有没有数据就可以了。
-- 定义更新数据时的触发器
create trigger tb_student_update_trigger
after update on tb_student for each row
begin
insert into student_logs(id, operation, operate_time, operate_id, operate_params) VALUES
(null,'update',now(),NEW.student_id,
concat('更新之前的数据:id=',OLD.student_id,', name=',OLD.student_name,', phone=',OLD.student_phone,
' | 更新之后的数据:id=',NEW.student_id,', name=',NEW.student_name,', phone=',NEW.student_phone));
end;
update tb_student set student_name='梦里不知身是客' where student_id=20220101001;
-- 删除数据触发器
create trigger tb_student_delete_trigger
after delete on tb_student for each row
begin
insert into student_logs(id, operation, operate_time, operate_id, operate_params) VALUES
(null,'delete',now(),OLD.student_id,
concat('删除之前的数据:id=',OLD.student_id,', name=',OLD.student_name,', phone=',OLD.student_phone));
end;
delete from tb_student where student_id=20230101201;
锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中,除传统的计算资源(CPU、RAM、I/O)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。
全局锁就是对整个数据库实例加锁,加锁后整个实例就处于只读状态,后续的DML的写语句,DDL语句,已经更新操作的事务提交语句都将被阻塞。
性能较差
其典型的使用场景是做全库的逻辑备份,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性。
演示
flush tables with read lock; -- 加全局锁
mysqldump -uroot -p123456 db_study > D:/db_study.sql -- 导出备份文件
unlock tables;
在InnoDB引擎中,我们可以在备份时加上参数 --single-transaction
参数来完成不加锁的一致性数据备份。
mysqldump --single-transaction -uroot -p123456 db_study > D:/db_study.sql
实际上,在MySQL innodb 引擎的底层,它实际上是通过快照读来实现的,这里不用再加全局锁。
表级锁,每次操作锁住整张表。锁定粒度大,发生锁冲突的概率最高,并发度最低。应用在MylSAM、InnoDB、BDB等存储引擎中。
对于表锁,分为两类:
1.表共享读锁 (read lock)
只能读不能写,而且读锁不会阻塞其他客户端的读,但是会阻塞其他客户端的写。
2.表独占写锁 (write lock)
其他客户端不能读,当然其他客户端也不能写。对于当前的客户端是可以读和写的。
语法:
1.加锁:lock tables 表名... read/write;
2.释放锁:unlock tables;
/ 客户端断开连接
MDL加锁过程是系统自动控制,无需显式使用,在访问一张表的时候会自动加上。MDL锁主要作用是维护表元数据的数据一致性,在表上有活动事务的时候,不可以对元数据进行写入操作。为了避免DML与DDL冲突,保证读写的正确性。
在MySQL5.5 中引入了MDL,当对一张表进行增删改查的时候,加MDL读锁(共享);当对表结构进行变更操作的时候,加MDL写锁(排他)。
对应SQL | 锁类型 | 说明 |
---|---|---|
lock tables xxx read / write | SHARED_READ_ONLY / SHARED_NO_READ_WRITE | |
执行select语句时 、select … lock in share mode 行锁的共享锁 | SHARED_READ 元数据锁当中的读锁 |
与SHARED_READ、SHARED_WRITE兼容,与EXCLUSIVE互斥 |
insert 语句、update语句、delete语句、 select … for update | SHARED_WRITE 元数据写锁(共享) |
与SHARED_READ、SHARED_WRITE兼容,与EXCLUSIVE互斥 |
alter table … 修改表结构 | EXCLUSIVE 排他锁 | 与其他的MDL都互斥 |
shared 分享的
exclusive 独有的,专用的
查看元数据锁
select object_type,object_schema,object_name,lock_type,lock_duration from performance_schema.metadata_locks;
为了避免DML在执行时,加的行锁与表锁的冲突
,在InnoDB中引入了意向锁,使得表锁不用检查每行数据是否加锁,使用意向锁来减少表锁的检查。
1.意向共享锁 (IS)
: 由语句 select …lock in share mode添加。与表锁共享锁(read)兼容,与表锁排它锁 (write)互斥。
select * from tb_student where student_id=20220101001 lock in share mode;
2.意向排他锁 (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;
行级锁,每次操作锁住对应的行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。应用在innoDB存储引擎中。
InnoDB的数据是基于索引组织的,行锁是通过对索引上的索引项加锁来实现的,而不是对记录加的锁。对于行级锁,主要分为以下三类:
1.行锁(Recqrd Lock): 锁定单个行记录的锁,防止其他事务对此行进行update和delete。在RC、RR隔离级别下都支持。锁的就是具体的某一行。
2.间隙锁(Gap Lock): 锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行insert,产生幻读。在RR隔离级别下都支持。锁住的是两条记录之间的间隙
3.临键锁(Next-Key Lock):行锁和间隙锁组合,同时锁住数据,并锁住数据前面的间隙Gap。在RR隔离级别下支持。锁的是记录和间隙
InnoDB实现了以下两种类型的行锁:
1.共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排它锁, 共享锁之间是兼容的、共享锁与排他锁是互斥的。
2.排他锁(X):允许获取排他锁的事务更新数据,阻止其他事务获得相同数据集的共享锁和排他锁
MySQL8.0的某个版本后,SELECT…LOCK IN SHARE MODE被标记为不推荐
推荐的完全等效新语句是:SELECT…FOR SHARE
默认情况下,innoDB在 REPEATABLE READ事务隔离级别运行 (默认) ,innoDB使用 next-key 锁进行搜索和索引扫描,以防止幻读。
1.针对唯一索引进行检索时,对已存在的记录进行等值匹配时,将会自动优化为行锁。
2.lnnoDB的行锁是针对于索引加的锁,不通过索引条件检索数据,那么lnnoDB将对表中的所有记录加锁,此时 就会升级为表锁。
可以通过以下SQL,查看意向锁及行锁的加锁情况:
select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from performance_schema.data_locks;
默认情况下,lnnoDB在REPEATABLE READ事务隔离级别运行,InnoDB使用 next-key 锁进行搜索和索引扫描,以防止幻读
。
1.索引上的等值查询(唯一索引),给不存在的记录加锁时,优化为间隙锁。 例如:
update stu set age = 10 where id=5;# 表中没有id=5的数据,有id=1和id=9,它会锁(1-9)之间的间隙,注意不包括1、9
2.索引上的等值查询(普通索引),向右遍历时最后一个值不满足查询需求时,next-key lock 退化为间隙锁
总结就是:这行数据加行锁18+这行数据之前临键锁(16,18] +之后间隙锁(18,29)
3.索引上的范围查询(唯一索引)–会访问到不满足条件的第一个值为止。临键锁
select * from stu where id >= 19 lock in share mode;# id是唯一索引, 表中有id=19,25的数据
19加一个行锁+(19,25]临键锁+临键锁(25,正无穷大]
注意: 间隙锁唯一目的是防止其他事务插入间隙。间隙锁可以共存,一个事务采用的间隙锁不会阻止另一个事务在同一间隙上采用间隙锁。
事物提交
commit;
之后这些锁会自动释放掉
锚点
页是innodb磁盘管理的最小单元,一个页的大小默认是16k,也就是说一个区当中它可以包含64个页。
表空间(ibd文件),一个mysql实例可以对应多个表空间,用于存储记录、索引等数据。
Windows: C:\ProgramData\MySQL\MySQL Server 8.0\Data\my_db
linux: 数据库存放的数据 /var/lib/mysql/
段,分为数据段(Leaf node segment)、索引段(Non-leaf node segment)、回滚段(Rollback segment),InnoDB是索引组织表,数据段就是B+树的叶子节点,索引段即为B+树的非叶子节点。段用来管理多个Extent (区)。
区,表空间的单元结构,每个区的大小为1M
。默认情况下,InnoDB存储引擎页大小为16K,即一个区中一共有64
个连续的页。
页,是lnnoDB 存储引擎磁盘管理的最小单元,每个页的大小默认为 16KB
。为了保证页的连续性,InnoDB 存储引擎每次从磁盘申请4-5
个区,从而来保证申请到的页是连续的。
行,InnoDB存储引擎数据是按行进行存放的。
MysQL5.5 版本开始,默认使用lnnoDB存储引擎,它擅长事务处理,具有崩溃恢复特性,在日常开发中使用非常广泛。下面是lnnoDB架构图,左侧为内存结构,右侧为磁盘结构。
针对一个专门的数据库服务器来说 , 那么会把80%左右的内存都分配给缓冲区,因为通过缓冲区,那么就可以提高我们数据库的并发访问性能。
四块区域
buffer poll存的是一堆行数据,change buffer存的是数据的变化
Change Buffer 更改缓冲区
5.x版本是没有这个区的,在5.x版本叫插入缓冲区insert buffer。在8.0之后呢提到了,在这一块引入了Change Buffer 更改缓冲区。
更改缓冲(针对于非唯一二级索引页)
,在执行DML语句时,如果这些数据Page没有在Buffer Pool中,不会直接操作磁盘,而会将数据变更存在更改缓冲区Change Buffer 中,在未来数据被读取时,再将数据合并恢复到Buffer Pool中,再将合并后的数据刷新到磁盘中。
Change Buffer的意义是什么?
与聚集索引不同,二级索引通常是非唯一的,并且以相对随机的顺序插入二级索引。同样,删除和更新可能会影响索引树中不相邻的二级索引页,如果每一次都操作磁盘,会造成大量的磁盘IO。有了ChangeBuffer之后,我们可以在缓冲池中进行合并处理,减少磁盘IO。
Log Buffer 日志缓冲区
日志缓冲区,用来保存要写入到磁盘中的log日志数据 (redo log、undo log),默认大小为16MB,日志缓冲区的日志会定期刷新到磁盘中。如果需要更新、插入或删除许多行的事务,增加日志缓冲区的大小可以节省磁盘I/O。
参数:
innodb_log_buffer_size:缓冲区大小
innodb_flush_log_at_trx_commit: 日志刷新到磁盘时机
1:日志在每次事务提交时写入并刷新到磁盘。
0:每秒将日志写入并刷新到磁盘一次。
2:日志在每次事务提交后写入,并每秒刷新到磁盘一次。
show variables like '%log_buffer%';
show variables like '%flush_log%';
Adaptive Hash Index 自适应哈希索引
自适应hash索引,用于优化对Buffer Pool数据的查询。InnoDB存储引擎会监控对表上各索引页的查询,如果观察到hash索引可以提升速度,则建立hash索引,称之为自适应hash索引。
自适应哈希索引,无需人工干预,是系统根据情况自动完成
参数: adaptive_hash_index 自适应哈希索引的开关(默认开启)
前面提到 InnoDB引擎默认不支持哈希索引,它支持的是B+树索引,自适应哈希索引指的是什么意思呢? 哈希索引最大的优势就是快,因为他只需要一次匹配就可以了,当然前提是不存在哈希冲突的情况下,B+树往往可能需要匹配两三次,哈希索引虽然说有优势,也就是说它的查询速度快,但是它也有弊端,它不适合范围查询,只能做等值匹配这类的操作, 所以呢InnoDB引擎就给我们提供了这样一个功能,叫自适应哈希,用于优化Buffer Pool当中的数据查询,也就是说InnoDB存储引擎会监控我们业务当中对各个表的索引页的查询,如果观察到哈希索引可以提高我们这一块的查询速度,则自动的会建立哈希索引,这个功能就称之为自适应哈希索引。
show variables like '%hash_index%';
对于MySQL服务器来说,如果说我们mysql是有一台专门的服务器,那么这台服务器通常来说80%的内存可能都会分配给它的缓冲区,以此来提高mysql的执行效率。
System Tablespace
系统表空间是更改缓冲区的存储区域。如果表是在系统表空间而不是每个表文件或通用表空间中创建的,它也可能包含表和索引数据。(在MySQL5.x版本中还包含InnoDB数据字典、undolog等)
参数:innodb_data_file_path
File-Per-Table Tablespaces
每个表的文件表空间包含单个InnoDB表的数据和索引,并存储在文件系统上的单个数据文件中。
参数:innodb_file_per_table
GeneralTablespaces
通用表空间,需要通过CREATE TABLESPACE 语法创建通用表空间,在创建表时,可以指定该表尘间。
-- 创建通用表空间
create tablespace xxx表空间名字 add datafile 'file_name我们表空间关联的表空间文件' engine=engine_name存储引擎;
-- 在创建表的时候就可以去指定这个表的数据就存放在这个表空间里面
CREATE TABLE xxx ... TABLESPACE ts_name表空间名字;
演示
create tablespace db_study add datafile 'my_db_study.ibd' engine=innodb;
use db_study;
create table y(
id int primary key auto_increment,
name varchar(10)
)engine=innodb tablespace db_study;
Undo Tablespaces
撤销表空间,MySQL实例在初始化时会自动创建两个默认的undo表空间(初始大小16M),用于存储undo log日志。
TemporaryTablespaces
InnoDB 使用会话临时表空间和全局临时表空间。存储用户创建的临时表等数据。
Doublewrite Buffer Files: 双写缓冲区,innoDB引擎将数据页从Buffer Pool刷新到磁盘前,先将数据页写入双写缓冲区文件中,便于系统异常时恢复数据,保证数据的安全性。
Redo Log
重做日志,是用来实现事务的持久性。该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log), 前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都会存到该日志中,用于在刷新脏页到磁盘时,发生错误时,进行数据恢复使用。所以redo log是循环写的,redo log不会永久保存,它会每隔一段时间会去清理之前没有用的redo log,因为redo log当事务提交,那么这一块redo log实际上意义也就不大了,因为它的主要作用是保证异常时进行数据恢复,从而保证我们事物的持久性。
事物的持久性就依赖于undo log日志。
内存当中的数据是怎么刷新到磁盘中?这里涉及到一组后台线程。
后台线程的作用:就是将Innodb存储引擎的缓冲池当中的数据,在合适的时机刷新到磁盘文件当中,对于Innodb存储引擎 后台的线程有这么四类:
线程类型 | 默认个数 | 职责 |
---|---|---|
Read thread | 4 | 负责读操作 |
Write thread | 4 | 负责写操作 |
Log thread | 1 | 负责将日志缓冲区刷新到磁盘 |
Insert buffer thread | 1 | 负责将写缓冲区内容刷新到磁盘 |
aio reads: [0, 0, 0, 0] , aio writes: [0, 0, 0, 0]
0代表等待接收请求。
总结: 对Innodb引擎的整个体系结构, 就是当我们业务在操作的时候呢,会直接去操作这一块的缓冲区,如果缓冲区当中没有数据,会将磁盘当中的数据加载回来,然后再存储在缓冲区当中,我们在增删改查的时候,那么都会去操作这一块的缓冲区(图片左侧),然后缓冲区当中的数据会以一定的频率或者说一定的时机,要通过这组后台线程(图片中间蓝色箭头=>)刷新到磁盘当中,然后在磁盘当中进行永久化的保留(主要指的是我们表当中的数据、索引等相关信息)下来,对于undo log和redo log 这个并不是说把所有的东西都保留下来,这个是需要去回收以释放对应的磁盘空间。
事务:是一组操作的集合,它是个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作(回滚)请求,即这些操作要么同时成功,要么同时失败。
事务四大特性 ACID
重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性。
该日志文件由两部分组成: 重做日志缓冲(redo log bufer)以及重做日志文件(redo log file),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都存到该日志文件中,用于在刷新脏页到磁盘,发生错误时,进行数据恢复使用。
具体的操作流程
首先客户端呢在进行事务操作时 ,会发起请求去操作我们的mysql服务器,在mysql服务器的innodb引擎当中,分为内存结构、磁盘结构,内存结构里面存放了很多的一些数据文件,磁盘结构当中有很大的一块区域叫做Buffer Pool(缓冲池),在缓冲池当中缓冲了我们一个一个的数据页的信息,接下来当客户端发起这次事务操作,在这次事务操作当中包含了多条update和delete语句,此时,当我们进行update语句执行的时候,首先他要去操作缓冲区,在缓冲区当中,要去查找有没有我们所更新的这一块的数据,如果没有 此时它会通过后台线程 把我们的数据从磁盘中读取出来 然后再缓存在缓冲区当中,接下来呢数据就已经缓冲到缓冲区当中了,接下来我们就可以直接执行更新以及删除的操作,直接去操作缓冲区当中的数据就可以了。直接操作缓冲区当中的数据,然后缓冲区当中的数据就发生了变更,但是磁盘当中的数据没有变更,这个时候这个数据页我们称之为脏页,这个脏页我们会在一定的时机,要把脏页的数据通过后台线程要刷新到磁盘当中。
刷新到磁盘,那么这个时候我们缓冲区当中的数据和磁盘当中的数据就保持了一致。
但是呢我们提到脏页的数据并不是实时刷新的,而是一段时间之后会通过后台线程 把增页的数据刷新到磁盘当中的数据 没有刷新到磁盘当中,事务都已经提交了,也告诉用户事务提交成功了,但是最终在脏野刷新的时候失败了,那这个时候 持久性(Durability) 就没有得到保障
在刚才所介绍的这个结构当中,redo log还没有出现,那么接下来呢我们再来看一下redo log(重做日志)出现之后 它的流程是什么样子的。
当redo log出现之后,当我们对缓冲区当中的数据 进行了增删改之后呢,他首先会把我们增删改的这个数据 记录在redo log buffer当中,在redo log buffer当中就会去记录数据页的物理变化,接下来,当我们事物在提交的时候,它会把我们redo log buffer,也就是重做日志缓冲区当中的数据页变化 直接刷新到磁盘当中,那么持久化的保存在磁盘文件当中,接下来在过一段时间之后呢,在进行脏页刷新的时候,那假如说在刷新的时候出错了,那么此时我可以通过redo log来进行恢复,这就是我们提到redo log它的作用。因为在redo log当中就记录了当次数据的变化,所以就可以通过redo log进行数据的恢复。这个redo log主要就是在刷新脏页到磁盘发生错误时进行数据恢复的。
为什么我们每一次提交的时候,要把redo log直接刷新到磁盘当中,那么我们不需要redo log,我们每一次提交的时候,就直接将Buffer Pool当中的变更的数据页直接刷新到磁盘当中不就完事了吗?
注意它们之间是有区别的,如果我们每一次在提交的时候不需要redo log,直接把Buffer Pool当中的数据直接刷新到磁盘文件当中,可以这么去做,但是存在严重的性能问题,为什么?因为我们一般在事物当中,在进行一组操作的时候,那么通常来说会操作很多条记录,而这些记录都是随机的去操作我们的数据页的,那么这个时候就会涉及到大量的随机磁盘io,性能是比较低的,而如果说我们在进行操作的时候,用到了这个redo log,在事务提交时我不会把脏页直接刷新,我先把redo log文件我在这一块异步刷新到磁盘当中,由于它是log日志文件,日志文件都是追加的,那么此时它就是顺序磁盘io,那么它的性能是要高于随机磁盘IO的,那么这种机制实际上叫做WAL(Write-Ahead-Logging 先写日志)。
写入日志之后,接下来过一段时间之后,我们再将脏页的数据再刷新到磁盘当中就可以了。
如果说脏页的数据我们顺利的刷新到磁盘当中了,那么此时redo log日志里面所记录的数据变更实际上也就不需要了,所以我们每隔一段时间就会去清理redo log日志,所以这两份日志呢实际上它是循环写的,并不会永久的保留下来,这就是我们在这一块所提到的redo log,简单来说redo log就是为了保证我们在进行脏页刷新发生错误时进行数据恢复,从而保证事务的持久性。
回滚日志,用于记录数据被修改前的信息,作用包含两个: 提供回滚和 MVCC(多版本并发控制)。
undo log和redo log记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。当执行rollback时,就可以从undoloo中的逻辑记录读取到相应的内容并进行回滚。
Undo log销毁:undolog在事务执行时产生,事务提交时,并不会立即删除undo log,因为这些日志可能还用于MVCC。
Undo log存储: undo log采用段的方式进行管理和记录,存放在前面介绍的 rollback segment 回滚段中,内部包含1024个undo log segment。
基本概念
当前读
读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如: select … lock in share mode(共享锁),select … for update、update、insert、delete(排他锁)都是一种当前读。
简单说,当前读 读取到的就是最新的数据记录。
正常的select … 语句是一个快照读,读取到的还是历史版本(之前的数据)。
快照读
简单的select(不加锁)就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读.
并发事务隔离级别:
隔离级别 脏读 不可重复读 幻读 Read uncommitted 读未提交 √ √ √ Read committed(oracle数据库默认)读已提交 × √ √ Repeatable Read (mysql默认) 可重复读 × × √ Serializable 串行化 × × × √表示在当前隔离级别下该问题会出现
Serializable 性能最低;Read uncommitted 性能最高,数据安全性最差
它的作用就是在快照读的时候 , 要通过MVCC来查找对应的历史版本。
innodb引擎当中,它实际上会为我们这张表再额外的再隐式的增加三个字段(DB_TRX _ID、DB_ROLL_PTR、DB_ROW_ID)。
隐藏字段 | 含义 |
---|---|
DB_TRX _ID | 最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID。 |
DB_ROLL_PTR | 回滚指针,指向这条记录的上一个版本,用于配合undo log,指向上一个版本。 |
DB_ROW_ID | 隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段。并不是每一张表都生成,当某一张表没有主键时会自动生成这个隐式字段作为隐藏主键。如果这张表有主键,那么这个隐藏字段将不会出现。 |
mysql提供了一条指令用来查看ibd文件当中的一些字典信息的
ibd2sdi tb_stu.ibd
undo log 日志
回滚日志,在insert、update、delete的时候产生的便于数据回滚的日志。
当insert的时候,产生的undo log日志只在回滚时需要,在事务提交后,可被立即删除。
而update、delete的时候,产生的undo log日志不仅在回滚时需要,在快照读时也需要,不会立即被删除。
undo log 日志 两点作用:1. 数据回滚 2. MVCC。
这四个事务是同时开启的,接下来呢事物二首先做了一个操作,要修改id为30的记录,把age修改为3,那也就意味着要修改这条记录,在修改记录之前,Innodb引擎他先要去做一件事,也要去记录undo log日志,用来进行数据回滚的,所头此时先会去记录一条undo log日志,在undo log日志当中将会记录 我原来的数据是什么样子的,会把原来的这个数据,在我们的undo log当中产生这么一份,undo log里面记录的就是变更之前什么样子的,0x00001表示这条undo log日志它的地址值,undo log日志记录完毕之后,事务2再去执行更新操作,那么此时这个age就会更新为3,此时大家会看到后面的这两个隐藏字段也发生了变化,DB_TRX_ID为2指的是当前这次事务的id是2,因为我们提到事务id是自增的,那么记录的就是当前哪一个事务 最后一次操作的这个数据,然后紧接着这个回滚指针就指向了下面的这条回滚日志0x00001,也就是说将来如果在提交事务的时候出错了,我要进行回滚,那我就要通过这个回滚指针找到,我要把数据回滚成什么样子了,我要回滚到这个版本就可以了。
这是我们在事务当中进行增删改操作的时候,那么这两个字段是怎么去更新的,怎么去关联的,那接下来呢我们继续来看第三个事务。
开启事务之后,这一块事务二先执行,执行完了之后他提交了事务,然后紧接着事务三在执行,事务三依然要修改id为30的这条记录,但是事务三要把name修改为A3,当然在更新之前,他也需要做一件事,就是要记录undo log日志,所以此时在下面的undo log日志这一块也会再生成一条日志,它记录的就是当前这张表当中的数据是什么样子的,直接在undo log记录下来,那前面这个0x00002对应的就是这一条记录对应的地址值,然后接下来呢再更新我们表结构当中的这条记录,那么在更新这条记录的时候,后面的translation id(DB_TRX_ID)以及如pointer(DB_ROLL_PTR)也会进行更新,首先将name更新为A3 ,translation id 最后一次操作的事务id此时要更新为3,然后对应的这个回滚指针指向我们刚才的这个版本指向0x00002,所以此时这个指针指向的就是0x00002,而对应的0x00002这条记录当中的上一个版本是0x00001,所以此时0x00001指向下面的这条记录,然后我们的这一块的这个回归指针指向0x00002。
事务三执行完成之后,就可以提交事务了,提交事务之后,注意undo log它不会立即删除,原因是因为有活动事务正在用这条undo log,所以它不会立即删除。
接下来我们看事务4,事务4在这一块执行开始的时候,先查询了一次id为30的记录,然后在这儿又去修改id为30的记录 把age修改为10,那么相同的操作在操作之前呢,他先需要做一件事 就是记录undo log日志,会把上面的这条记录在我们的undo log日志当中记录一份,记录数据变更之前长什么样,那么前面这个0x00003指的是这条undo log日志它所对应的地址值,然后紧接着再把我们表结构当中的这个记录,在更新的时候不仅要把age更新为10,同时还要更新事务ID(DB_TRX_ID),这次事务id为4,所以把事务id(DB_TRX_ID)更新为4.然后呢,再把回滚指针指向(DB_ROLL_PTR)0x00003这个版本,这样就更新完了,更新完了之后那继续看这个指针也需要再次更新0x00003这条记录它的上一个版本是0x00002,而我们的当前的表结构当中的这条记录它的对应的这个回滚的指针指向的是0x00003,这样呢大家就会看到,这样就形成了undo log的版本链。
不同事务或相同事务对同一条记录进行修改,会导致该记录的undo log生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录。
当我们查询的时候到底应该返回哪一个版本呢?
这个实际上不是由undo log的版本链来控制的, 具体要返回哪个版本,这个时候又涉及到mvcc实现原理当中的第三个组件read view。
ReadView(读视图)是 快照读
SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id。ReadView中包含了四个核心字段:
字段 | 含义 |
---|---|
m_ids | 当前活跃的事务ID集合 |
min_trx_id | 最小活跃事务ID |
max_trx_id | 预分配事务ID,当前最大事务ID+1(因为事务ID是自增的) , 下一个我要分配的事务id是几 |
creator_trx_id | ReadView创建者的事务ID |
通过这四个属性来确定当前这一次快照读到底应该来读取哪一个版本。
不同的隔离级别,生成ReadView的时机不同:
- READ COMMITTED (RC) : 在事务中每一次执行快照读时生成ReadView。
- REPEATABLE READ (RR): 仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView。
RC隔离级别下,在事务中每一次执行快照读时生成ReadView。
PS: 就是找离当前事务最近提交的那一次事务
活动事务id,最小的活动事务id,最大的活动事务id,当前事务id
判断
总结:只能获取【事务完毕】或者当前事务 的id数据,然后获取视图,会根据这个4个条件获取undo log版本
RR隔离级别下,仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView。
原子性 :undo log
持久性 :redo log
一致性 :undo log + redo log
隔离性 :锁+ MVCC
Mysql数据库安装完成后,自带了一下四个数据库,具体作用如下:
不同的版本,提供的数据库也略有不同,就以8.x之后的版本为例,来简单介绍一下所自带的四个数据库。
数据库 | 含义 |
---|---|
mysql | 存储MySQL服务器正常运行所需要的各种信息 (时区、主从、用户、权限等) |
information_schema | 提供了访问数据库元数据的各种表和视图,包含数据库、表、字段类型及访问权限等 |
performance_schema | 为MySQL服务器运行时状态提供了一个底层监控功能,主要用于收集数据库服务器性能参数 |
sys | 包含了一系列方便DBA和开发人员利用 performance_schema性能数据库进行性能调优和诊断的视图 |
元数据:指的是数据库本身的一些数据
db 主要提到的就是一些性能指标的一些系统库
slave_master_info、slave_relay_log_info、slave_worker_info 主从复制的相关信息就会记录在这里面。
slow_log 是慢日志
ENGINES表: 支持哪些存储引擎
INNODB_TABLESPACES: Innodb引擎的表空间
TABLESPACES:表空间
TABLE_PRIVILEGES:表的一些权限
SCHEMA_PRIVILEGES: 数据库的一些权限
除了这些表,还有一些视图。
ROUTINES视图:存储过程以及存储函数在这里面也都会展示出来,我们之前创建出来的存储过程存储函数在这都有记录,创建时候定义的语句也能看见。
SCHEMATA:数据库的说明、字符集
TABLES:当前数据库有哪些表,这些表是在哪个数据库里面的,这些表是什么引擎的…
TRIGGERS: 触发器
VIEWS:视图
data_locks:当我们在去查看意向锁和行级所的时候,我们就会从这张表当中来查看加锁情况
metadata_locks:查看元数据锁的情况
还有其他信息:二进制日志的一些信息、错误日志的信息、一些事件的信息、事务的相关信息在这个里面也都有记录说明
只有一张表,其他的还是一些跟性能相关的一些视图。
该mysql不是指mysql服务,而是指mysql的客户端工具。
语法:
mysql [options][database]
选项 :
-u,--user=name #指定用户名
-p,--password[=name] #指定密码
-h, --host=name #指定服务器IP或域名
-P,--port=port #指定连接端口
-e,--execute=name #执行SOL语句并退出
主要是用在一些脚本文件当中
-e
选项可以在Mysql客户端执行SQL语句,而不用连接到MySQL数据库再执行,对于一些批处理脚本,这种方式尤其方便。 (执行SQL并退出)
示例:
mysql -uroot -p123456 db_study -e "select * from tb_stu"
mysql -h127.0.0.1 -P3306 -uroot -p123456 db_study -e "select * from tb_stu"
mysqladmin 是一个执行管理操作的客户端程序。可以用它来检查服务器的配置和当前状态、创建并删除数据库等。
通过帮助文档查看选项:
mysqladmin --help
这些都是一些管理级别的指令。
示例:
mysqladmin -uroot -p123456 create test01
mysqladmin -uroot -p123456 drop test01
mysqladmin -uroot -p123456 version
mysqladmin -uroot -p123456 variables 查看系统变量信息
由于服务器生成的二进制日志文件以二进制格式保存,所以如果想要检查这些文本的文本格式,就会使用到mysqlbinlog 日志管理工具。
语法:
mysglbinlog [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 指定位置间隔内的所有日志
示例:
Linux:cd /var/lib/mysql binlog.000008文件(名字不固定,有几个)
Windows: C:\Windows\System32\cmd.exe
mysqlbinlog DESKTOP-9MKBF3V-bin.000002
mysqlbinlog -s DESKTOP-9MKBF3V-bin.000002 看到的信息会更紧凑一些
mysqlshow 客户端对象查找工具,用来很快地查找存在哪些数据库、数据库中的表、表中的列或者索引。
语法 :
mysqlshow [options][db_name [table_name [col_name]]]
选项 :
--count 显示数据库及表的统计信息 (数据库,表 均可以不指定)
-i 显示指定数据库或者指定表的状态信息
示例:
#查询每个数据库的表的数量及表中记录的数量
mysqlshow -uroot -p123456 --count
#查询db_study库中每个表中的字段数,及行数
mysqlshow -uroot -p123456 db_study --count
#查询db_study库中tb_student表的详细情况
mysqlshow -uroot -p123456 db_study tb_student --count
#查看某一个字段
mysqlshow -uroot -p123456 db_study tb_student student_id --count
# 查看状态
mysqlshow -uroot -p123456 db_study tb_student -i
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文件,数据文件
示例:
mysqldump -uroot -p123456 db_study > E:\db_study01.sql
mysqldump -uroot -p123456 -t db_study > E:\db_study02.sql
mysqldump -uroot -p123456 -d db_study > E:\db_study03.sql
mysql> show variables like '%secure_file_priv%';
mysqldump -uroot -p123456 -T "C:/ProgramData/MySQL/MySQL Server 8.0/Uploads/" db_study tb_student
# 指定的目录要是MySQL官方指定的目录,MySQL就认为这个目录是不安全的
mysqldump -u root -p --xml db_study tb_class > E://tb_class.xml
mysql -u root -p -e "SELECT * FROM tb_class " db_study > E://tb_class.txt
mysqlimport 是客户端数据导入工具,用来导入mysqldump 加 -T 参数后导出的文本文件。
语法:
mysqlimport [options] db_name textfile1 [textfile2 ...]
示例 :
mysqlimport -uroot -p123456 db_study "C:\ProgramData\MySQL\MySQL Server 8.0\Uploads\tb_student.txt"
# txt文件名称就是表名
如果需要导入sql文件, 可以使用mysql中的source 指令
语法:
use db_study;
source E:\db_study01.sql
\G
会将结果的表格形式转换成行文本形式SELECT table_schema "Database Name", SUM(data_length + index_length) / (1024 * 1024) "Database Size in MB"
FROM information_schema.TABLES
GROUP BY table_schema;