MySQL核心内容【完结】

news2025/5/12 18:35:45

MySQL核心内容

文章目录

  • MySQL核心内容
    • 1.MySQL核心内容目录
    • 2.MySQL知识面扩展
    • 3.MySQL安装
    • 4.MySQL配置目录介绍
      • Mysql配置远程ip连接
    • 5.MySQL基础
      • 1.MySQL数据类型
        • 1.数值类型
        • 2.字符串类型
        • 3.日期和时间类型
        • 4.enum和set
      • 2.MySQL运算符
        • 1.算数运算符
        • 2.逻辑运算符
        • 3.比较运算符
      • 3.MySQL完整性约束
        • 1.主键,自增键,非空,默认值约束
        • 2.外键
    • 6.表设计原则
      • 1.一对一
      • 2.一对多
      • 3.多对多
    • 7.关系型数据库的范式设计
      • 1.第一范式(1NF)
      • 2.第二范式(2NF)
      • 3.第三范式(3NF)
      • 4.BC范式(BCNF)
      • 5.第四范式(4NF)
      • 6.总结
    • 8.MySQL核心SQL
      • 1.检查MySQL服务器状态
      • 2.库操作
      • 3.表操作
      • 4.CRUD操作
      • 5.select查询
      • 6.limit分页查询
        • 1.limit基本用法
        • 2.limit效率优化
        • 3.limit分页
      • 7.order by排序
      • 8.group by分组
      • 9.SQL语句常见面试题
    • 9.连接查询SQL
      • 1.内连接查询
        • 1.内连接的基本使用
        • 2.内连接和limit的结合
        • 3.内连接流程复习及效率分析
      • 2.外连接查询
        • 1.左连接
        • 2.右连接
        • 3.外连接的应用
    • 10.MySQL存储引擎
      • 1.什么是存储引擎
      • 2.各存储引擎的区别
    • 11.MySQL索引
      • 1.索引分类
      • 2.索引创建和删除
      • 3.B树索引
      • 4.B+树索引
      • 5.InnoDB的主键和二级索引树
      • 6.聚集索引和非聚集索引
      • 7.哈希索引
      • 8.InnoDB自适应哈希索引
      • 9.索引常见问题
      • 10.索引和慢查询日志
    • 12.MySQL事务
      • 1.事务概念
      • 2.事务的ACID特性
      • 3.事务并发存在的问题
      • 4.事务的隔离级别
      • 1.表级锁和行级锁
      • 2.排他锁和共享锁
      • 3.间隙锁
      • 4.意向共享锁和意向排它锁
      • 5.死锁
      • 6.锁的优化建议
    • 14.MVCC和undo log
    • 15.再谈redo log
    • 16.MySQL的优化
    • 17.MySQL日志
      • 1.mysqlbinlog数据恢复
      • 2.mysqldump数据备份
    • 18.一次SQL详细处理流程
    • 19.MySQL主从复制
      • 1.主从复制基本原理
      • 2.主从复制的配置
    • 20.MySQL读写分离
      • 1.读写分离基本原理
      • 2.读写分离实践操作
    • 21.MySQL分库分表
      • 1.库表问题
      • 2.拆分策略
      • 3.垂直拆分
      • 4.水平拆分

1.MySQL核心内容目录

image-20250506155449536

2.MySQL知识面扩展

  • 关系型数据库 SQLite(安卓上的) ,NoSQL(key-value):redis、 leveldb、rocksdb、大数据分析列式数据库 Hbase、ceph开源的分布式db
  • MySQL目前属于Oracle,大家熟悉的关系型数据库还有SQL Server、Oracle、MySQL,MariaDB(和MySQL一个作者)、DB2(金融行业)
  • MySQL分为企业版和社区版,其中社区版是完全免费并且开源的
  • MySQL区别于其他关系型数据库很大的一个特点就是支持插件式的存储引擎,支持如InnoDB,MyISAM,Memory等
  • 目前google、腾讯、淘宝、百度、新浪、facebook等公司都在使用MySQL作为数据存储层方案
  • MySQL设计成C/S模型 mysqlclient、mysqlserver
  • MySQL的服务器模型采用的是I/O复用+可伸缩的线程池,是实现网络服务器的经典模型,使用的是 select+线程池!为什么不用epoll,Linux uring呢?因为MySQL还涉及磁盘IO,业务处理很慢,网络IO不需要那么强的能力,这里面的网络IO能力只要能和数据库的磁盘IO能力相匹配就行了!

经典的mysql通信的整体结构如下:

image-20250506162400823

3.MySQL安装

​ 博主使用的是华为云服务器(华为云打钱bushi),Linux版本为:Ubuntu 22.04.4 LTS,安装的MySQ版本是5.7,安装方法参考:ubuntu22.04安装mysql5.7.29 - 卷心菜的奇妙历险 - 博客园 即可!

​ 博主一开始是只要apt-get直接下载mysql,下载的版本是mysl8.0,但博主毕竟(贫穷),服务器配置只有2核2G,跑mysql8.0太吃内存了,故换成mysq5.7,当然感兴趣的同学也可以修改mysql8.0的相关配置降低其消耗。安装成功后 mysql -u root -p,输入密码登录即可。
image-20250506164432560

​ 如果需要对服务重启:service mysqld restart即可。

4.MySQL配置目录介绍

​ Mysql的系统表一般放在/var/lib/mysql/mysql

image-20250506164829099

​ Mysql的配置文件一般放在/etc/mysql/mysql.conf.d下的mysqld.cnf中,如果有需要进行修改或者添加的配置项,写入到这个my.cnf文件即可!

image-20250506171933020

​ 我们可以打开该文件进行查看,里面记录了详细的mysql配置信息:

image-20250506172056124

​ 我们一般需要进行如下两条配置:

  1. 允许所有的IP连接(默认绑定的是127.0.0.1)
  2. 跳过名称解析(提升连接速度)

image-20250506172321529

​ 更新完配置以后,需要重启服务才能生效sudo systemctl restart mysql

Mysql配置远程ip连接

​ 按理说,Mysql是要支持远程ip的连接的,通过mysql -h ip -P 3306 -u root -p,可是当前试了下并不行,需要我们进行配置!
image-20250506170311760

​ 我们先本地登录,进入mysql数据库中,通过select Host,User,authentication_string from user;查看配置
image-20250506170613462

​ 可以看到,目前的用户只支持localhost,自然无法被远程用户所访问了!我们可以通过 grant all privileges on *.* to 'root'@'%' identified by 'XXXXXX' with grant option,便可以让远程用户也进行访问了!
image-20250506171007667

​ 此时我们再去查看user表的相关表项,发现多了一行所有用户可连接!此时我们对配置进行刷新image-20250506171230180

​ 再次尝试下远程登录,输入前面自己授权时设置的密码,发现就可以登录成功了!【如果不行,可能是mysql的配置没有改,mysql网络配置上设置了只允许本地连接】
image-20250506172729670

5.MySQL基础

1.MySQL数据类型

​ MySQL中数据和索引都存储在磁盘上,MySQL数据类型定义了数据大小范围,因此使用时选择合适的类型,不仅会降低表占用的磁盘空间,间接减少了磁盘I/O的次数,提高了表的访问效率,而且索引的效率也和数据的类型相关。

1.数值类型

image-20250506173824185

  • 在金融计算中,浮点数类型推荐使用decimal类型(保存为字符串格式),数据溢出会报错,而FLOAT/DOUBLE数据溢出是截断
  • 一般 TINYINT unsigned类型就非常适合用来定义年龄create table(age TINYINT unsigned not null default 0);
2.字符串类型

image-20250506174919297

  • 经常被问的点:age INT(8),这里这个8是什么意思?整形占用内存的大小是固定的,和具体的类型是强相关的,(M)只是表示整数显示的宽度。
  • char(12)和varchar(12),如果存的是“hello”,在char(12)用的是12个字符,而varchar(12)会去适配“hello”,一般占6个字符。
  • TEXT一般用于存储文本信息,聊天记录什么的,大家总结的工作报告一般来说用TEXT就不够了,微博留言框限制字数一般就是这个原因。
  • 字符串因为拼接操作经常会被注入式攻击!
3.日期和时间类型

image-20250506175538524

  • 我们一般很少用MySQL的相关操作,MySQL会遇到磁盘IO,我们一般都让MySQL做最少的事情,像存储过程,存储函数,触发器,外键约束,让MySQL帮我们执行一些逻辑,这是需要避免的!我们尽量只让他做CRUD,其他的通过服务层去控制,不要给MySQL server徒增压力。
  • 查看时间和时间戳,我们在业务中使用的最多的就是时间戳
    image-20250506180018839
4.enum和set
  • 这两个类型,都是限制该字段只能取固定的值,但是枚举字段只能取一个唯一的值,而集合字段可以取任意个值。
  • 用法是用来限制值:比如性别 sex enum(‘M’,’W’) default ‘M’

2.MySQL运算符

1.算数运算符

image-20250506195946791

​ 举例:常用到的updata user set age = age + 1;

2.逻辑运算符

image-20250506200133372

​ 举例:常用到逻辑与and select * from user where sex=‘M’ and score >= 90.0;,男生并且成绩大于90的

3.比较运算符

image-20250506200322670

​ 举例:常用select * from user where age between 20 and 22; 年龄在20到22之间 select * from user where score in(99.0,100.0);只能是99.0和100.0中的其中一个,如果有99.5的话就选不到了。
​ 如何判断一个字段能不能为空,select * from user where score is not NULL;,另外,在外连接查询的时候也会经常的出现NULL。
​ 通配符LIKE也非常常见,select * from user where name like ‘zhang%’;‘zheng_’匹配一个字符,%匹配多个字符。LIKE能不能用到索引?答:通配符加到后面能用到索引,加到前面用不到索引!

3.MySQL完整性约束

1.主键,自增键,非空,默认值约束

​ primary key,主键:不能为空不能重复,我们将主键设置为自增,之后在插入数据的时候就不用考虑插入id了。一个表里面只能创建一个主键,但可以创建多个唯一键。唯一键要求不能重复,但可以为空。

CREATE TABLE user(
	id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT '用户的主键id',
    nickname varchar(50) UNIQUE NOT NULL COMMENT '用户的昵称',
    age TINYINT UNSIGNED NOT NULL DEFAULT 18,
    sex ENUM('male','female')
);

​ 建表成功后我们通过desc查看表就是下面这样的:
image-20250506202100030

​ 通过desc user\G;查看表格就是下面的逐行形式;
image-20250506202325948

2.外键

​ 表1:学生信息表,张三,年龄 ,xxx(父表)
​ 表2:考试信息表,张三id,考试成绩(子表)

​ 两张表的信息是有关联的!如果张三被退学了,学生信息表中张三的信息被删除了,那考试信息表中的张三的信息怎么办?我删除张三的时候,要么应该出错,要么应该把别的表中的张三的数据也删除!这里就要用到外键!主表的id字段 关联 子表的id字段,让他们产生关联关系。

​ 但我们之前也说过,外键,存储函数,存储过程,触发器这些限制逻辑,代码逻辑是由MySQL本身控制的。会限制MySQL存储层的性能,这些逻辑我们一般都放到业务层上去做。所以MySQL的外键我们一般不用。

6.表设计原则

1.一对一

​ 一对一关系:
​ 用户User:【父表】

uidnameagesex
1000zhang20M
1020liu21W
2010wang22M

​ 身份信息Info:

cardidaddrinfo
112233aaa
334455bbb

​ 我们在身份信息表中增加一列,叫做uid【子表】

uidcardidaddrinfo
1020112233aaa
2010334455bbb

子表中的uid关联了父表的uid,实现一对一的关系。当有一对一关系的表进行关联时,在子表中增加一列,关联父表的主键。注意必须要类型相同!这样以后在查询时:select * form Info where uid=2010;便可以找到子表中的父表uid为2010的信息。

2.一对多

​ 我现在要做一个电商系统,涉及的实体为用户User,商品Product,订单Order。分析如下:
​ 用户-商品:没有关系!
​ 用户-订单:一对多的关系,一个用户可以有多个订单!
​ 商品-订单:多对多的关系,一个商品可以属于多个订单,一个订单可以有多个商品!

​ 用户User表:

uidnameagesex
1000zhang20M
1020liu21W
2010wang22M

​ 商品Product:

pidpnamepriceamount
1手机600.0100
2笔记本2000.050
3电池10.0200

​ 订单Order:

orderidpidnumbermoneytotalpriceaddrinfo
O100011600.04640.0海淀区
O1000224000.04640.0海淀区
O10003440.04640.0海淀区
O2000212000.02000.0平谷区

​ 当前的设计中:Order明显存在数据冗余!先不考虑这个,我们先看订单Order表如何跟用户User表关联起来呢?
​ 答:一对多和一对一关系一样,在子表【订单Order】中加入一列uid,关联父表【用户User表】中的id,这样就产生Order表和User表中的关联关系了。

orderiduidpidnumbermoneytotalpriceaddrinfo
O1000100011600.04640.0海淀区
O10001000224000.04640.0海淀区
O100010003440.04640.0海淀区
O20002010212000.02000.0平谷区

​ 总结:对于一对多的关系的处理与一对一相同,在子表中增加一列数据去关联父表中的id字段。

3.多对多

​ 对于上例中的用户-商品 没有关系 以及 用户-订单:一对多的关系,都是通过在子表中增加一列关联父表的主键。而对于 商品-订单 这种多对多的关系,则需要增加一个中间表
从上面的例子中,我们可以很明显的看出来:一个订单并不需要存那么多相同的orderid,totalprice,addrinfo。我们可以通过产生中间表来解决这个问题。
​ 我们对订单Order这个表进行拆表,产生中间表的同时还可以解决Order表自身数据冗余的问题。
​ 具体拆解方法如下:
image-20250506212428731

​ 在商品Product表中,pid是主键,订单Order表中orderid作为主键,而在中间表订单内容OrderList表中【orderid+pid】联合作为主键。
​ 解决完多对多的关系后,我们总共得到了三张表:
​ 订单Order表:

orderiduidtotalpriceaddrinfo
O100010004640.0海淀区
O200020102000.0平谷区

​ 订单内容OrderList表

orderidpidnumbermoney
O10001160.0
O1000224000.0
O10003440.0
O2000212000.0

​ 以及最初的用户User表,和商品Product表,一共四张表,解决了所有的一对多关系,多对多关系。

7.关系型数据库的范式设计

应用数据库范式可以带来许多好处,但是最重要的好处归结于三点

  1. 减少数据冗余(这是最主要的好处,其他好处都是由此而附带的)
  2. 消除异常(插入异常,更新异常,删除异常)
  3. 让数据组织的更加和谐

​ 但是数据库范式绝对不是越高越好,范式越高,意味着表越多,多表联合查询的机率就越大,SQL的效率就变低。

1.第一范式(1NF)

每一列都要保持原子特性

​ 列都是基本数据项,不能够再进行分割,否则设计成一对多的实体关系。例如表中的地址字段,可以再细分为省、市、区等不可再分割(即原子特性)的字段,如下:
image-20250506215502795
​ 上图的表就是把地址字段分成了更详细的city,country,street三个字段【附表】,通过addressid字段将主表和附表关联。注意,不符合第一范式不能称作关系型数据库【非关系型数据库,比如key-value型数据库】。在查询时可以使用下面的带in的子查询来建立两张表之间的连接。
select * from Employeeaddress where addressID in (select AddressID from noNF where EmployeeID=1);

2.第二范式(2NF)

属性完全依赖于主键 - 主要针对联合主键

​ 非主属性完全依赖于主关键字,如果不是完全依赖主键,应该拆分成新的实体,设计成一对多的实体关系。

​ 例如:选课关系表为SelectCourse(学号,姓名,年龄,课程名称,成绩,学分),(学号,课程名称)是联合主键,但是学分字段只和课程名称有关,和学号无关,同理:姓名和年龄也只跟学号有关,和课程名称无关,相当于只依赖联合主键的其中一个字段,不符合第二范式。

​ 为什么数据冗余?因为学分并不依赖于学号,不同学生,只要选的是同一门课,课程学分并不会根据所选择的学生不同而不同 !

​ 如何解决呢?要解决学号和课程名称之间多对对的关系,我们想到前面提到的建立中间表!将原表拆分为:学生表(学号,姓名,年龄),课程表(课程id,课程名称,学分),以及中间表:选课关系/情况(学号,课程id,成绩)。

​ 下图也展示了一个常见的第二范式的设计:但是这里面jobDescription仅依赖job,不符合第三范式(3NF)
image-20250506223142307

3.第三范式(3NF)

属性不依赖于其他非主属性

​ 要求一个数据库表中不包含已在其他表中已包含的非主关键字信息。

​ 示例:学生关系表为Student(学号, 姓名, 年龄, 所在学院, 学院地点, 学院电话),学号是主键,但是学院电话只依赖于所在学院,并不依赖于主键学号,因此该设计不符合第三范式,应该把学院专门设计成一张表,学生表和学院表,两个是一对多的关系。

​ 设计原则,把所有不依赖于主键的属性都单独的抽离出来!如下图,把job,department拆出来。但也有缺陷,因为拆出来的表越多,多表查询时的效率就越低!

image-20250506224533669

4.BC范式(BCNF)

每个表中只有一个候选键(能标识唯一性的键)

​ 简单的说,BC范式是在第三范式的基础上的一种特殊情况,即每个表中只有一个候选键(在一个数据库中每行的值都不相同,则可称为候选键),在上面第三范式的noNF表(上面图3)中可以看出,每一个员工的email都是唯一的(不可能两个人用同一个email),则此表不符合BC范式,对其进行BC范式化后的关系图为:
image-20250506224927639

5.第四范式(4NF)

消除表中的多值依赖

​ 简单来说,第四范式就是要消除表中的多值依赖,也就是说可以减少维护数据一致性的工作。比如下图中的noNF表中的skill技能这个字段,有的人是“java,mysql”,有的人描述的是“Java,MySQL”,这样数据就不一致了,解决办法就是将多值属性放入一个新表,所以满足第四范式的关系图如下:

image-20250506225113217

​ 在拆出来的skill表里面,就不存多值了,存储方式如下,这样一来就解决了数据不一致的问题(大小写的问题!)

skillEmployeeID
C++1000
Java1000
MySQL2000
Golang2000

6.总结

​ 从上面对于数据库范式进行分解的过程中不难看出,应用的范式越高,表越多。表多会带来很多问题:

1查询时需要连接多个表,增加了书写SQL查询语句的复杂度

2查询时需要连接多个表,降低了数据库查询性能

​ 因此,并不是应用的范式越高越好,视实际情况而定。第三范式已经很大程度上减少了数据冗余,并且基本预防了数据插入异常,更新异常,和删除异常了

8.MySQL核心SQL

​ SQL是结构化查询语言(Structure Query Language),它是关系型数据库的通用语言。

​ SQL主要可以划分为以下3个类别:

  • DDL(Data Definition Languages)语句
    数据定义语言,这些语言定义了不同的数据库、表、列、索引等数据库对象的定义。常用的语句关键字主要包括create、drop、alter等。
  • DML(Data Manipulation Language)语句
    数据操纵语句,用于添加、删除、更新和查询数据库记录,并检查数据完整性,常用的语句关键字主要包括insert、delete、updata和select等。
  • DCL(Data Control Language)语句
    数据控制语句,用于控制不同的许可和访问级别的语句。这些语句定义了数据库、表、字段、用户的访问权限和安全级别。主要的语句关键字包括grant、revoke等。

1.检查MySQL服务器状态

​ 检查下MySQL服务是否启动:netstat -tanp
image-20250507090545197

2.库操作

​ 查询数据库

show databases;

​ 创建数据库

create database ChatDB;

​ 删除数据库

drop database ChatDB;

​ 选择数据库

use ChatDB;

3.表操作

​ 查看表

show tables;

​ 创建表,默认的engine和charset可以去mysql的配置文件/etc/mysql/mysql.conf.d中修改,前面已经提到过了。

create table user(id int unsigned primary key not null auto_increment,
                 name varchar(50) unique not null,
                 age tinyint not null,
                 sex enum('M','W') not null)engine=INNODB default charset=utf8;

​ 查看表结构,有两种显示格式

desc user;
desc user\G;

​ 第一种显示格式:

image-20250507093454836

​ 另一种显示格式:

image-20250507093523378

​ 查看建表sql,相当完整

show create table user\G;

image-20250507093437457

​ 删除表【使用这个操作一定要注意,整个表都没有了!】

drop table user;

4.CRUD操作

insert增加

# 建议不要这样写,这样写就必须按照建表时字段的顺序逐个给值,但我们的id是自增的,其实不需要给他赋值
insert into user values()

# 推荐的写法1
insert into user(name,age,sex) values('zhang san',22,'M');
# 推荐的写法2
insert into user(name,age,sex) values('li si',21,'W'),('gao yang','20','M');

image-20250507094512283

​ 我们再来看一个很有意思的问题:

我们将写法1重复操作两次,和将写法2操作一次,最终表里面存放的数据都是一样的,那请问了,他们到底有什么区别???

image-20250507100055162

​ 我们客户端的每次sql请求,都要经历三个阶段:

  1. client和server要进行tcp三次握手
  2. client发送sql到server上接收并处理,返回处理结果
  3. server和client断开连接,tcp四次挥手

​ 所以写法1相当于进行了2*(3)个步骤,但是写法2只有 1 *(3)个步骤,写法2更快!!!在实际项目中,我们都会通过数据库连接池,提前创建一些数据库连接,以备使用。

update修改

update user set age=23 where name='zhang san';
update user set age=age+1 where id=3;

image-20250507094736975

delete删除

delete from user where age=23;
delete from user where age between 20 and 22;
# 将表中的数据全部删除
delete from user;

​ 这里有一个很有意思的问题,当我们删除掉张三后,我们再去插入一个张三,这时候的id是1呢,还是3呢,还是递增呢?答案:递增!我们的自增id是一个开弓没有回头箭的东西!这个id是会不断增加的,如果表中要存储的数据过多的话,在表创建的时候还是尽量把id设置成bigint类型(8字节)

image-20250507095005593

​ 下面是范围删除以及全部删除

image-20250507095403153

5.select查询

​ 简单的select查询

# 不推荐,加大查询数量的大小,增加Mysql的压力
select * from user;

# 推荐的写法——制定需要选择的列
select name,age,sex from user;

# 过滤数据
select name from user where age>21;
select name,age,sex from user where age>=21;
select name,age,sex from user where age>=21 and sex='M';
select name,age,sex from user where age>=21 or sex='M';

# between包含左右断点
select name,age,sex from user where age between 20 and 22;

# like+通配符 后缀匹配,前缀匹配,部分匹配
select name,age,sex from user where name like "zhang%";
select name,age,sex from user where name like "zhang_";

# 带in的子查询
select name,age,sex from user where age in(20,21);
# 等价于 select name,age,sex from user where age=20 or age=21;
select name,age,sex from user where age not in(20,21);

​ 去重distinct

# 查看有多少不重复的age
select distinct age from user;

​ 空值查询

# 判断null
select name,age,sex from user where name is null;
select name,age,sex from user where name is not null;

​ union合并查询,UNION [ALL | DISTINCT] # 注意:union默认去重,不用修饰distinct,all表示显示所有重复值,union all在效果上其实就等于 or

# 把两个select的结果union all
select name,age,sex from user where age>=21 union all select name,age,sex from user where sex='M';

​ 注意:网上会有些人说or用不到索引,and可以用到索引,这是错的!MySQL可以对or进行优化,优化为union all,而union all的操作相当于进行两次select,然后对结果进行合并,是可以用到索引的!

image-20250507103421819

6.limit分页查询

1.limit基本用法

​ select * from user limit N;把整表数据拿出来,只显示前N个。
​ select * from user limit M,N; M表示offset,N表示lines
​ select * from user limit N offset M; 和上面等价

# 默认是limit 0,3
select * from user limit 3;
# 偏移一个,再取三个
select * from user limit 1,3; 
select * from user limit 3 offset 1;# 两种写法等价

image-20250507105153286

2.limit效率优化

​ 思考:limit只是对select的结果的显示条数加以限制吗?对查询的效率有没有提升?
​ explian:查看SQL语句的执行计划【看不到MySQL做的优化】,我们查询下数据表的第一行数据,加上explian,查看结果image-20250507105928759 当我们查询age时,发现没有用到索引,并且进行了整表搜索image-20250507110515914

​ 那如果,我们没有索引的情况下,还想要提升效率,应该怎么做?我们如果只是想查表中的前几行,但是没有索引,就可以使用limit N来优化【前提是我们知道需要查询的数据再前N行当中】,于是我们便写出了下面这样的sql语句【age=21对应的是第一行数据】,效率非常的高!

select * from user where age=21 limit 1;

image-20250507111240733

​ 但是非常的遗憾啊,这里explain并不能识别到MySQL在这里做的优化。这里我创建一个新的表,插入大量的数据进行测试,下面我创建了一个只有主键索引的t_user表来进行测试:

image-20250507113948686

​ 我写一个存储过程的逻辑代码,向t_user表中添加数据

# 修改结束标识符
delimiter $

create procedure add_t_user(IN n INT)
BEGIN
DECLARE i INT;
SET i=0;

WHILE i<n DO
INSERT INTO t_user VALUES(NULL,CONCAT(i+1,'@fixbug.com'),i+1);
SET i=i+1;
END WHILE;
END$

# 恢复结束标识符
delimiter ;

# 增加200w行数据
call add_t_user_2(2000000);

​ 根据下图的测试结果,在大量数据的情况下,借助limit N,在查询键没有索引的情况下,确实可以提升效率。

image-20250507115856704

​ 我们再通过explain查看,发现explain确实是没有解释到MySQL的优化行为。但并不影响我们的结论。

image-20250507120351657

3.limit分页

​ pageno:一共多少页,pagenum:一页中有多少行

select * from t_user limit (pageno-1)*pagenum,pagenum;	

​ 但是我们简单的以这样的方式进行分页显示的话,效率是不均等的!处于前面的页会更快的被遍历到,效率低在M的偏移!

​ 我们一般的做法是:借助主键过滤

select * from t_user where id > 上一页最后一条数据的id值 limit pagenum;

​ 这种方式会借助主键的索引,可以通过比较均匀的时间获取到每一页中的数据。image-20250507162654648

7.order by排序

# 升序排序【默认的】
select * from user order by XXX asc;

# 降序排序
select * from user order by xxx desc;

# 多字段排序
select * from user order by xxx,xxx [asc|desc];

# 数据过滤 + 排序
select * from user where sex='M' order by name;

image-20250507163224948

​ 当我们在过滤数据的时候,如果是通过无索引字段过滤的,会发生什么呢?我们可以通过explain进行解释来看看:
image-20250507163723211

​ type:整表搜索,using filesort:文件排序!【n路归并排序,磁盘IO高】,这会导致性能非常的低!

​ 但是我们的name字段是有索引的,我们如果只select name,再根据name进行排序的时候,就可以用到索引了【其实只遍历的辅助索引树,没有进行回表操作!】

image-20250507164156125

8.group by分组

​ 对某一个字段进行分组,统计

# 统计有多少种年龄分布情况
select age from user group by age;

#分组并且统计个数,group by+聚合函数的使用
select age,count(age) as number from user group by age;

# 分组并且剔除掉不符合规定的数据(只保留年龄大于20的数据)
select age,count(age) as number from user group by age having age>20;
# 效果等价于 
select age,count(age) as number from user where age>20 group by age;

# 分组+排序
select age,count(age) as number from user group by age order by age;

image-20250507165519227

image-20250507165733967

image-20250507170344883

​ 我们前面谈到,order by是会涉及到索引的,那么group by是否会涉及到索引呢?相同的,我们再通过explain进行查看:
image-20250507170527484

​ filesort:涉及外排序,产生磁盘IO。通过explain分析的时候,group by虽然不是order by,但是group by也涉及到了using filesort,说明group by也是会对分组后的结果进行排序的!

​ 我们再对有索引的name字段进行group by试一试,发现group by也是会使用到索引的!效率相当的快!
image-20250507170938327

9.SQL语句常见面试题

​ 下表bank_bill是某银行代缴话费的主流水表结构:

字段名描述
serno流水号
date交易日期
accno账号
name姓名
amount金额
brno缴费网点
  1. 统计表中缴费的总笔数和总金额
  2. 给出一个sql,按网点和日期统计每个网点每天的营业额,并按照营业额进行倒序排序

我们进入我们的mysql进行演示:

create table bank_bill(
    serno bigint unsigned primary key not null auto_increment,
    date DATE not null,
    accno varchar(100) not null,
    name varchar(50) not null,
    amount decimal(10,1) not null,
    brno varchar(150) not null
    );

image-20250507173049803

​ 向表中插入数据,一次操作插入多行效率优于多次插入;

INSERT INTO bank_bill VALUES
('101000','2021-3-1','111','zhang',100,'Beijing'),
('101001','2021-3-1','222','qian',200,'Shanghai'),('101002','2021-3-2','333','sun',300,'Anhui'),('101003','2021-3-2','444','li',500,'Beijing'),('101004','2021-3-3','555','zhou',600,'Anhui');

​ 第一题sql代码:

select count(serno),sum(amount) from bank_bill;

image-20250507173817033

​ 第二题sql代码:

select brno,date,sum(amount) as money from bank_bill group by brno,date order by money desc;

image-20250507174210469

9.连接查询SQL

​ 为什么需要连接查询?为什么不能拆分成两次查询呢?

  • mysql-client每次向mysql-server查询都是网络通信,要进行tcp三次握手,四次挥手等过程,浪费效率。

image-20250507215422991

​ 对于内连接查询(inner join)如下图所示:

image-20250507215509330

​ 对于左连接查询(left join),查询的是下图中蓝色区域的数据,即下图中的蓝色区域(表1特有的数据);

image-20250507215555195

​ 对于右连接查询(right join),查询的是下图中绿色区域中的数据(表2特有的数据)

image-20250507215850860

​ 场景1:学生,课程,考试结果

# student:uid,name,age,sex
create table student(
	uid int unsigned primary key not null auto_increment,
    name varchar(50) not null,
    age tinyint unsigned not null,
    sex enum('M','W') not null
);

# course: cid,cname,credit
create table course(
	cid int unsigned primary key not null auto_increment,
    cname varchar(50) not null,
    credit tinyint unsigned not null
);

# exam:uid,time,score
create table exam(
    uid int unsigned not null,
    cid int unsigned not null,
    time date not null,
    score float not null,
    primary key(uid,cid)
);

​ 创好表之后就是下面这样的:
image-20250507221216937

​ 我们向创建好的表中增加一些数据:

insert into student(name,age,sex) values
('zhangsan',18,'M'),
('gaoyang',20,'W'),
('chenwei',22,'M'),
('linfeng',21,'W'),
('liuxiang',19,'W');

insert into course(cname,credit) values
('C++base',5),
('C++enhance',10),
('C++project',8),
('C++algrathm',12);

insert into exam(uid,cid,time,score) values 
(1,1,'2021-04-09',99.0),
(1,2,'2021-04-10',80.0),
(2,2,'2021-04-10',90.0),
(2,3,'2021-04-12',85.0),
(3,1,'2021-04-09',56.0),
(3,2,'2021-04-10',93.0),
(3,3,'2021-04-12',89.0),
(3,4,'2021-04-11',100.0),
(4,4,'2021-04-11',99.0),
(5,2,'2021-04-10',59.0),
(5,3,'2021-04-12',94.0),
(5,4,'2021-04-11',95.0);
  • 查看张三同学某一门课的成绩(张三的学号uid:1,cid:2)

    select score from exam where uid=1 and cid=2;
    

    image-20250507222945163

1.内连接查询

1.内连接的基本使用

​ 内连接查询的标准语法:

SELECT a.属性名1,a.属性名2,...,b,属性名1,b.属性名2... FROM table_name1 a inner join table_name2
b on a.id = b.id where a.属性名 满足某些条件;
  1. 现在我们除了张三同学的成绩信息,如果我们还想拿到张三同学的其他信息呢?
select a.uid,a.name,a.age,a.sex from student as a where a.uid=1;

select c.score from c.exam as c where c.uid=1 and c.cid=2;

# 通过【内连接】合并上面两条查询信息
select a.uid,a.name,a.age,a.sex,c.score from student a inner join exam c on a.uid=c.uid where c.uid=1 and c.cid=2;
  1. on a.uid=c.uid 时连接的过程:首先需要区分大表和小表,按照表的数据量来区分!小表永远是整表扫描(建索引也没用!),然后去大表搜索(一定要建索引!)

  2. 具体过程:判断student表和exam表的数据量,显然student表是小表,而exam表为大表,接着从student这个小表中取出所有的a.uid,然后拿着这些uid去exam大表中搜索。

  3. 最后,通过where进行条件筛选,找到张三
    image-20250507224516026
    我们通过explain指令进行分析,发现在查询的过程中也确实使用了我们建表时创建的主键索引/联合索引。
    image-20250507224652934

  4. 如果我们还想要拿到张三同学选修的course信息的话,应该如何去做呢?答:再去inner join course表,但这时需要注意的是,必须把具有联合索引字段的exam表放在inner join查询语句的最前面,因为只有这张表上既有cid又有uid。写出来的sql语句为:

    select a.uid,a.name,a.age,a.sex,b.cid,b.cname,b.credit,c.score from exam c 
    inner join student a on c.uid=a.uid 
    inner join course b on c.cid=b.cid
    where c.uid=1 and c.cid=2;
    

    ​ 查询结果如下:

    image-20250508091655518

  5. 现在我想查看C++enhance这门课程有多少人考到了90分以上,但是我不需要知道具体的学生下姓名,那么便可以写出如下的sql语句出来:

    select b.cid,b.cname,b.credit,count(*)
    from exam c
    inner join course b on c.cid=b.cid
    where c.score>=90.0 # 筛选出所有成绩大于90的课
    group by c.cid
    having c.cid=2; # 指定是C++enhance这门课
    

    查询结果如下:
    image-20250508093322666

2.内连接和limit的结合

​ 还记得我们之前创建的t_user表吗,我们现在有一个业务场景,我们只能使用limit M,N的方式,但是想要优化下面两种查询方式,让他们的效率相近,应该如何去做呢?

select * from t_user limit 1500000,10;

# 方法1:只查询索引,但是字段少
select id from t_user limit 1500000,10;

# 方法2:之前的优化方案,借助有索引的id字段过滤
select * from t_user where id>2500000 limit 10;

# 方法3:inner join 临时表
select a.id,a.email,a.password from t_user_2 a
inner join (select id from t_user_2 limit 1500000,10) b
on a.id=b.id;

​ 实践看效率:

image-20250508102421976

3.内连接流程复习及效率分析

​ 首先先看下面这条sql语句:

select a.*,b.* from student a inner join exam b on a.uid=b.uid;

​ 显然,student表是小表,exam表是大表,所以这条查询语句会遍历一遍student表,然后根据student表的uid,通过索引去exam表中的uid去匹配,我们通过explain指令也能分析出来:
image-20250508104353804

​ 那现在我们想要转换一下角色,让student表变为大表,exam表变为小表应该怎么做呢,只需要加入一个where筛选条件就可以啦

select a.*,b.* from student a inner join exam b on
a.uid=b.uid where b.cid=3; 

image-20250508104714690

​ 我们分析explain的分析结果,发现确实会先去遍历exam表,但是没用用到索引!这不对啊,我们之前在创表的时候不是建立的(uid,cid)的联合索引了嘛,这里到后面讲索引的时候会提,使用联合索引时,必须要包含联合索引的第一个字段uid,才会使用到索引!

​ 最后,还有人会写出这样的sql语句,下面这条语句和上面使用where在结果上看是一样的,过程中有什么区别吗?我们通过explain分析:

select a.*,b.* from student a inner join exam b on
a.uid=b.uid and b.cid=3; 

image-20250508105101571

​ 从explain的结果中可以看出,使用and也会被sql解释为使用where!执行过程是一模一样的!

2.外连接查询

​ 为了表示内连接和外连接的区别,我们这里再向之前的student表中增加一个数据:

insert into student(name,age,sex) values 
('weixie',20,'M');

image-20250508103018990

1.左连接

​ 把left这边的表所有的数据显示出来,右表中不存的数据就显示为NULL,sql语法如下:

select a.*,b.* from student a left join exam b
on a.uid=b.uid;

​ 常用于查询判断那些同学没有考过试!

image-20250508105525969

​ 但是我们通过带in的子查询也可以实现这样的功能,为什么不推荐使用呢?主要有两点原因,我们先看下面的sql语句:

select * from student where uid not in
(select distinct uid from exam);
  • select distinct uid from exam => 会产生一张中间表存储结果供外面的sql来查询
  • not in对索引的命中率并不高

​ 推荐优化为下面的左连接:

select a.* from student a left join exma b
on a.uid=b.uid where b.cid is null;

image-20250508111637834

2.右连接

​ 把right这边的表所有的数据都显示出来,左表中不存在的数据就显示为NULL,sql语法如下:

select a.*,b.* from student a right join exam b
on a.uid=b.uid;

​ 当然在我们的设计中,所有考试记录当然都能匹配到学生,所以这里右连接和内连接没用区别。

image-20250508105756985

3.外连接的应用

​ 如果我们现在想要查询,那些同学没有考过cid=3的这门课,应该如何去写sql语句呢?

select a.* from student a left join exam b
on a.uid = b.uid 
and b.cid=3
where b.cid is null;

image-20250508113029304

​ 但是为什么不是这条sql呢,为什么调换where和and的顺序后,结果就出错了呢?

select a.* from student a left join exam b
on a.uid=b.uid
where b.cid=3
and b.cid is null;

image-20250508113321249

​ 我们还是通过explain去分析,我们发现,如果我们把where过滤放到前面,那么left join并没有先去按照我们理解中的先去a表做整表查询,然后保留住a中的数据,去b表中做匹配,而是直接通过where语句过滤了b的整表数据,然后用b的数据去a中查询,整体过程就跟内连接是一样的了,在内连接中,我们已经把b表中的数据筛的只剩下cid=3的了,那我们后面再跟and b.cid is null自然就找不到数据。所以这里就要注意了,在外连接的情况下,如果我们要进行条件查询,and一定要写在where的前面!
image-20250508114142222

10.MySQL存储引擎

1.什么是存储引擎

​ 存储引擎直接影响:表的结构、数据、索引的存储方式。

​ 我们可以通过show engines命令查看当前mysql所支持的索引:
image-20250508120515500

​ mysql5.7是一个分水岭,5.7之前用的一般都是MyISAM,5.7之后使用的是非常强大的InnoDB存储引擎,这也是我选择MySQL5.7版本的原因。如何去修改:我们需要去mysql的配置文件当中可以对齐进行修改。

image-20250508121402253

​ 我们进入/var/lib/mysql路径下,发现我们在mysql中创建的所有的数据库都会创建在这个路径下,而所有的表都是会根据存储引擎的不同而不同。

​ 对于MyISAM存储引擎,一张表会创建三个文件,分别是表名.frm(表结构)、表名.MYD(表数据)、和表名.MYI(表索引)这三个文件。而对于InnoDB存储引擎,分别是表名.frm(表结构)、表名.idb(表数据+表索引)。
image-20250508121428975

​ 这里就有一个常见的面试题:为什么InnoDB存储引擎情况下,建表的话没有加主键都会自动生成一个主键,但是MyISAM你加主键才会有主键,不会自动增加主键?就是因为MyISAM存储引擎数据和索引的存储的分开的,没有强相关。但是在InnoDB中,索引和数据放在一起,没有索引的话数据就没地方放!

2.各存储引擎的区别

​ 在回答“MySQL“不同的存储引擎有哪些区别?这样的问题时,主要从以下几点来回答就可以(问题的核心):

种类锁机制B树/哈希索引外键事务索引/数据缓存
MyISAM表锁支持/不支持不支持不支持支持/不支持
InnoDB行锁支持/不支持支持支持支持/支持
Memory表锁支持/支持不支持不支持支持/支持
  • 锁机制:表示数据库在并发请求访问的时候,多个事务在操作时并发的粒度
  • B树/哈希索引:加速SQL的查询速度
  • 外键:子表的字段依赖父表的主键,设置两张表的依赖关系。
  • 事务:多个SQL语句,保证他们共同执行时的原子操作,要么成功,要么失败,不能只成功一部分,失败需要进行回滚操作。
  • 索引/数据缓存:和MySQL Server的查询缓存相关,在没有对数据和索引做修改之前,重复查询可以不用进行磁盘I/O(数据库的性能提升,目的是为了减少磁盘I/O操作来提升数据库访问效率),读取上一次内存中查询的缓存就可以了。

11.MySQL索引

​ 索引也是一个数据结构,当我们不使用的时候,他也是存储在磁盘中的,不仅读数据要花费磁盘IO,使用索引也是需要花费磁盘IO的!所以并不是索引越多越好!

1.索引分类

​ 索引是创建在表上的,是对数据库表中一列或者多列的值进行排序的结果。索引的核心是提高查询的速度!

  • 索引的优点:提高查询效率
  • 索引的缺点:索引并非越多越好,过多的索引会导致CPU使用率居高不下,由于数据的改变,会造成索引文件的改动,过多的磁盘I/O造成CPU负荷太重。

​ 物理上(聚集索引&非聚集索引)/ 在逻辑上分为以下一些类别:

  1. 普通索引(二级索引):没有任何限制条件,可以给任何类型的字段创建普通索引( 创建新表&已创建表,数量不限,但一张表的一次sql查询只能使用一个索引
  2. 唯一性索引:使用unique修饰的字段,值不能够重复,主键索引就隶属唯一性索引
  3. 主键索引:使用primary key修饰的字段会自动创建主键索引
  4. 单列索引:在一个字段上创建索引
  5. 多列索引:在表的多个字段上创建索引;比如我们之前exam表中的(uid+cid)索引,多列索引必须使用到第一个列,才能用到多列索引,否则索引用不上
  6. 全文索引:使用FULLTEXT参数可以设置全文索引,只支持char,varchar,和text类型的字段上,常用于数据量较大的字符串类型上,可以提高查询速度(线上项目支持专门的搜索功能,给后台服务器增加专门的搜索引擎,支持快速高效的搜索,elasticsearch简称es,跟mysql一样C/S架构,搜狗的workflow)。

2.索引创建和删除

​ 创建表的时候指定索引字段:

create table index1(
    id int,
    name varchar(20),
    sex enum('male','female'),
    index `index name` (id,name),index(sex)
);

​ 在已经创建的表上添加索引:

create [unique] index `index name` on `table name` (属性名(length) [ASC|DESC]);

# 给name字段增加索引
create index nameidx on student(name);

image-20250508155237442

​ 下图中,为什么我们给password字段创建了索引,但是在查询的时候却没有用到呢?

image-20250508160215921

​ 那是因为我们在通过where作为过滤字段的时候,写的是password=1000000;这个1000000会发生类型转换,由1000000转换为‘1000000’,因为发生了类型转换,所以就不会查询索引了。我们修改sql语句发现就改好了。
image-20250508160557642

​ 总结三点规则:

  1. 经常作为where条件过滤的字段考虑添加索引
  2. 字符串列创建索引时,尽量规定索引的长度,而不能让索引值的长度key_len过长
  3. 索引字段涉及类型强转、mysql函数调用、表达式计算等,索引就用不上了

​ 当我们不再需要使用索引时,通过下面的sql语句删除索引即可:

DROP INDEX 索引名 ON 表名;

drop index pwdidx on t_user_2;

image-20250508161110669

3.B树索引

image-20250508203330790

​ B树是一种m阶平衡树,m一般取值为300~500,叶子节点都在同一层,由于每一个节点存储的数据量比较大,索引整个B树的层数是非常低的,基本上不超过三层。

​ 由于磁盘的读取是按block块操作的,因此B树的节点大小一般设置为和磁盘块大小一致,这样一个B树节点,就可以通过一次磁盘I/O把一个磁盘块的数据全部存储下来。对于2000W的数据量,如果使用m = 500 阶的B树,最多花费三次磁盘I/O即可。如果使用AVL树存储,构建下来有25层,最多花费二十五次磁盘I/O,这是让人无法接受的!

​ 对于我们之前的student表,主键uid会自动创建索引,当我们执行select * from student where uid = 5;
​ uid有索引 => 存储引擎 => kernel => 磁盘I/O(读索引文件)=> 内存上 => 用索引的数据构建B树加速搜索

​ B树中的data:存储的是数据本身内容?还是在磁盘上的地址? 取决于使用什么样的存储引擎,如果是MyISAM,因为数据和索引是分开存储的,所以索引中data存放的是数据在磁盘上的地址。如果是InnoDB,数据和索引一起存放,所以data中存储的是数据内容本身。

​ m-阶,表示一个节点有500个指针域,499个数据域。如果要找图中的28,分两步

  1. 磁盘I/O,通过B树
  2. 将整个块加载到内存,在内存中搜索(二分搜索logN)

4.B+树索引

​ 常问:为什么MySQL(MyISAM和InnoDB)索引底层选择B+树而不是B树???

  1. 在B树中,索引+数据的内存 全部分散在不同的节点上,各个节点中的数据互不重复,离根节点近,搜索的快,离根节点远,搜索的就慢!花费的磁盘I/O次数不平均,每一行数据搜索花费的时间也不平均。
  2. 每一个非叶子节点上,不仅仅要存储索引(key),还要存储索引值所在的那一行的data数据。一个节点所能够存放的索引key值的个数,比只存储key值的节点的个数要少得多!!!
  3. 这颗树不方便做范围搜索,整表遍历看起来也不方便。

由于上面的三个原因 ====》我们选择使用B+树,我们来看看B+树是如何解决这三个问题的!B+树的结构图如下:
image-20250508215808700

  1. B+树中,每一个非叶子节点,只存储key,不存储data!好处就是一个节点存放的key值更多【因为磁盘块的大小是固定的!】,B+树在理论上来说,层数会更低一些,搜索的效率会更好一些!
  2. 叶子节点上存储了所有的索引值和数据data:搜索每一个索引对于的值data,都需要跑到叶子节点上,这样一行记录搜索的时间是非常平均的!
  3. 此外,B+树所有叶子节点被连接成了有序链表结构,因此做整表遍历和区间查找是非常容易的。

5.InnoDB的主键和二级索引树

​ InnoDB存储引擎,数据和索引存储在一起,比如我们之前的student表,有student.frm(表结构)和student.idb(数据和索引)两个文件。

​ 场景一:uid是主键,select * from student;搜索的是整个索引树。那么就直接去B+树的叶子节点组织起来的双链表中去遍历。
​ 如果是select * from student where uid = 5; 等值查询,select * from student where uid < 5; 范围查询。那么就走B+树的索引结构。
image-20250508234509022

​ 场景二:uid是主键,name创建了普通索引(二级索引),如下图所示:
image-20250508235320219
image-20250509092832921

​ 所以现在查询分为以下三种情况:

  • select name from student where name=‘linfeng’; 因为只获取name字段,所以直接查询辅助索引树/二级索引树 便可以获取到。
  • select uid,name from student where name=‘linfeng’;索引二级索引树,也可以拿到name和uid,所以只搜索二级索引树。
  • select * from student where name=‘linfeng’;因为搜索的是*(全部字段),所以会执行下面的操作:
    1. 搜索name的二级索引树,找到linfeng对应的主键uid:4
    2. 再拿uid=4回表在主键索引树上搜索uid那一行的记录。

image-20250509000408222 前两种情况,因为只查询了辅助索引树,所以extra上显示的是using index,第三种情况由于进行了回表操作,所以extra列上是NULL。

​ 如果现在select * from student where age=20 order by name;如果只给age添加索引,行不行,还有什么没考虑到呢?

​ 我们先看下没加索引是什么样的:
image-20250509000811168

​ 我们对age字段加上索引后,发现确实用到了age的索引,但是也进行了回表操作,相比之前已经优化了,但是filesort还是存在!有人提出可以给name也加索引,不可以!因为一次搜索只会用到一个二级索引
image-20250509000900210

​ 能不能让这个二级索引树上,存储age+name呢?联合索引!key:age+name 多列索引(联合索引) 先按age排序,再按name排序,age相同时,按name进行排序!
image-20250509001419916

​ 我们发现确实跟我们分析的一样!因为在索引树上,当第一个索引相同时,就会对第二个索引进行排序!

6.聚集索引和非聚集索引

​ 对应MyISAM存储引擎,主键索引树和辅助索引树是什么样的呢?

​ 场景:uid是主键索引,name是二级索引时,MyISAM和InnoDB有什么区别呢?
image-20250509092225867

image-20250509092650858

​ 区别就是,在MyISAM存储引擎下,二级索引树下key是name,value不是主键索引uid!而是直接指向name所在数据data,存放的是name所在数据data的地址信息!

​ 因为MyISAM存储引擎,索引和数据没有放在一起,所以叫做非聚集索引,而InnoDB存储引擎,索引和数据放在一起存储,紧密相连,叫做聚集索引!

7.哈希索引

​ 哈希索引(Memory存储引擎)VS B+树索引(InnoDB和MyISAM)

  1. 哈希索引搜索的效率更好 O(1)
  2. 磁盘I/O花费要少

​ 我们如何在MySQL中创建哈希索引:

image-20250509093759635

​ 哈希索引的结构(key存放索引键,value/data存储的是指针):image-20250509094410200

​ 因为哈希表中的元素没有任何顺序可言!所以只能进行等值比较,那下面的语句就无法用到索引了!

# 可以走索引
select * from student where name='zhangsan';
# 不能走索引
select * from student where name like'zhang%';
# 范围查询,前缀查询,order by排序 这些操作,哈希索引都不适合。

​ 另外:哈希索引在磁盘上的存储也是个大问题,B+树一个磁盘块一般是16K,磁盘组织形式一个页面一般是4K,一个磁盘块block中有4个page,但是在哈希表中,离的很近的元素也可能在磁盘上距离很远,没有顺序可言。所以哈希索引只是适用于内存上,并且只适用于等值比较。

​ 综上,哈希索引有如下缺点:

  1. 没办法处理磁盘上的数据,加载到内存上构建高效的搜索数据结构,因为他没有办法减少范围查询时I/O的次数
  2. 只适合做等值搜索,其他的范围、排序等不适合。

​ 面试:你知道B+树索引和哈希索引吗??

8.InnoDB自适应哈希索引

​ InnoDB存储引擎监测到同样的二级索引不断被使用,那么它会根据这个二级索引,在内存上根据二级索引树(B+树)上的二级索引值,在内存上构建一个哈希索引,来加速搜索【等值比较】。

​ 自适应哈希索引的结构如下:
image-20250509100642857

  1. 正常流程:where index_col【二级索引】先查二级索引树,拿到主键索引值,然后去主键索引树,取到数据页。
  2. 走hash索引【替代磁盘上的二级索引】,where index_col 直接和数据页交互。

image-20250509101421167

​ 自适应哈希索引本身的数据维护也是要耗费性能的,并不 是说自适应哈希索引在任何情况下都会提升二级索引的查询性能,根据参数指标来具体分析是否打开或者关闭自适应哈希索引。这个哈希索引是存储引擎自动创建,是创建在二级索引的B+树上面的!为了提高这个二级索引回表查询的效率!

show engine innodb status\G,能看到两个比较重要的信息;

  1. RW-latch等待的线程数量(自适应哈希索引默认分配了8个分区,不同分区中可以并发操作),同一个分区等待的线程数量过多就会降低自适应哈希索引的效率
  2. 走自适应哈希索引搜索的频率(低)和二级索引树搜索的频率(高)=> it might be useful to disable adaptive hash indexing.

image-20250509102346263

9.索引常见问题

  1. 比如淘宝我的宝贝那个页面,如果要按照时间排序 select * from xxx where userid = 123 order by addtime desc ,这个索引应该怎么加?
    • 创建(userid,addtime)的联合索引,解决filesort问题。
  2. addtime建立索引 与 没有索引 这样排序的性能会有多大差别?
    • filesort,通过userid选择出来的数据越多,则filesort的排序性能越差。
  3. 对于某些区分度高的字段,整表搜索的效率足够高了,mysql可能就不会走索引,比如 enum(‘M’,’W’)image-20250509111751807image-20250509111615347
  4. 如果查询条件有多个字段,有部分创建索引的,比如 a=1 and b=2 and c=3,a和b分别有单独的索引,一定会优先找有索引的先查询缩小数据量吗,还是根据sql语句写的顺序来的。
    • 和顺序无关,MySQL会看看到底是按a筛选掉的数据多,还是按b筛选掉的数据多。
  5. 可以强制指定使用某个索引,select * from xxx force index(ix_addtime),这样就是死活都会走addtime的索引。
    image-20250509112626347
  6. not in 不一定不会走索引,就比如select age from student where age not in(20)会被mysql优化为一个范围查询select age from student where age<20 or age>20,演示如下:
    image-20250509115223716
  7. 当被问到索引的优化问题时,怎么切入?
    • 从 什么地方【?】能够获取哪些运行时间长,耗性能的sql,然后再用explain去分析它!索引和慢查询日志

10.索引和慢查询日志

​ 如何获取到项目中哪些sql语句执行速度慢,需要被优化!这里就需要使用慢查询日志!slow_query_log,这个日志的最大作用就是帮我我们寻找哪些sql语句在拖后腿,然后我们再通过加索引去优化!

​ 慢查询日志的相关参数如下所示:

show variables like "%slow_query%";

image-20250509122454423

​ 如果需要打开慢查询日志的开关:

set global slow_query=ON;

image-20250509123334464

​ 慢查询日志记录了包含所有执行时间超过参数long_query_time(单位:秒)所设置的SQL语句的日志,在MySQL上用命令可以查看,如下:

show variables like 'long%';

image-20250509122723199

​ 这个值是可以修改的,注意,这里在设置的时候没有加global语句,所以设置的结果仅在当前session中有效。

set long_query_time=1; # 注意:单位是秒

我们故意制造一条不走索引的慢查询记录:
image-20250509123928588

​ 我们进入目录下找到慢查询日志:
image-20250509124034980

​ 打开查看:
image-20250509124210200

​ 我们在日志文件中看哪些日志超出了我们预期的时间,然后再通过explain进行分析,分析是否使用索引,是否有filesort等等。这就是通过实践分析优化sql的过程。总结如下:

  1. 慢查询日志!设置合理的、业务可以接受的慢查询时间!
  2. 压测执行各种业务!!
  3. 查看慢查询日志,找出所有执行耗时的sql
  4. 用explain分析这些耗时的sql
  5. 举自己实际项目中解决问题的例子

​ 另外,对于某些语句的执行小于0,00秒,我们应该如何去得知他具体的执行时间是多少呢?通过profiling

# 查看是否启动了profiling
show variables like 'profiling';
# 设置开启
set profiling=on;
# 查询profiling启用后语句的详细执行时间
show profiles;

image-20250509125403737

12.MySQL事务

1.事务概念

​ 一个事务是由一条或者多条对数据库操作的SQL语句所组成的一个不可分割的单元,只有当事务中的所有操作都正常执行完了,整个事务才会被提交给数据库;如果有部分事务处理失败,那么事务就要回退到最初的状态,因此,事务要么全部执行成功,要么全部失败。

​ 假如现在表account表中,有(zhangsan,100)和(liuchao,200)这两个账户,在service层进行业务处理 -> 转账操作时,zhangsan -> liuchao 转账50.0钱,事务的逻辑代码表示如下:

# try中的内容组成一个完整的事务
try{
	begin; # 开启一个事务
	update account set money=money-50.0 where name='zhangsan';
	update account set money=money+50 where name='liuchao';
	# ...
	commit; # 提交一个事务
}catch{ ...
	rollback; #回滚一个事务,事务恢复到事务开始前的状态
}

​ MyISAM是不支持事务的,InnoDB:最大的特点:支持事务,支持行锁,支持外键,我们可以通过show engines\G,查看下当前mysql支持的存储引擎:
image-20250509153833289

​ 我们可以通过select @@autocommit;指令来查看下当前mysql自动提交事务的状态。
image-20250509154036349

​ 如果我们需要使用事务,第一件事就是要通过set autocommit=0;把autocommit设置成0,改为我们手动的提交事务。image-20250509154151537

​ 所以记住事务的几个基本概念,如下:

  1. 事务是一组SQL语句的执行,要么全部成功,要么全部失败,不能出现部分成功,部分失败的结果。保证事务执行的原子操作。
  2. 事务的所有SQL语句全部执行成功,才能提交(commit)事务,把结果写回磁盘上。
  3. 事务执行过程中,有的SQL出现错误,那么事务必须要回滚(rollback)到最初的状态。

2.事务的ACID特性

​ 每一个事务必须满足下面的4个特性:

  1. 事务的原则性(Atomic):事务是一个不可分割的整体,事务必须具有原子特性,及当数据修改时,要么全执行,要么全不执行,即不允许事务部分的完成
  2. 事务的一致性(Consistency):一个事务执行之前和执行之后,数据库数据必须保持一致性状态。由并发控制机制实现。就拿网上购物来说,你只有让商品出库,又让商品进入顾客的购物车才能构成一个完整的事务。
  3. 事务的隔离性(Isolation):当两个或者多个事务并发执行时,为了保证数据的安全性,将一个事务内部的操作与其他事务的操作隔离起来,不被其他正在执行的事务所看到,使得并发执行的各个事务之间不能互相影响。
  4. 事务的持久性(Durability):事务完成(commit)以后,DBMS保证它对数据库中的数据的修改是永久性的,即使数据库因为故障出错,也应该能够恢复数据。db写数据 => cache缓存 => 磁盘I/O,假如在cache缓存向磁盘I/O写入的过程中,停电了/宕机了/重启了,通过redo log:重做日志【故障恢复后mysql自动通过该日志恢复数据】,来保证数据库的持久性!mysql最重要的是日志,不是数据

总结事务的ACID特性:

  • ACD:是由mysql的redo log 和 undo log机制来保证的
  • I:隔离性,是由mysql事务的锁机制来实现保证的

3.事务并发存在的问题

​ 事务处理不经隔离,并发执行事务时通常会发生以下的问题:

  1. **脏读(Dirty Read):**一个事务读取了另一个事务未提交的数据,假如当事务A和事务B并发执行时,当事务A更新但未提交时,事务B查询读到A尚未提交的数据【中间态数据】,此时事务A回滚,那么事务B读到的就是无效的数据。有问题!
  2. 不可重复读(NonRepeatable Read):一个事务的操作导致另一个事务前后两次到不同的数据。例如当事务A和事务B并发执行时,当事务B查询读取数据后,事务A更新操作更改了事务B查询到的数据,此时事务B再次读取该数据,发现前后两次读的数据不一样。(事务B读取事务A已提交的数据)看业务需求
  3. **幻读(Pahantom Read):**一个事务的操作导致另一个事务前后两次查询的结果数据量不同。例如当事务A和事务B并发执行时,当事务B查询读取数据后,事务A新增或者删除了一条满足事务B查询条件的记录,此时事务B再次查询,发现了事务前一次不存在的记录,获取前一次查询的一些记录不见了。(事务B读取了事务A新增的数据或者读不到事务A删除的数据)看业务需求

​ 我们现在开启两个终端,把两个终端中的自动提交都关掉,然后来演示下事务并发存在的问题:

image-20250509174004139

4.事务的隔离级别

​ MySQL支持的四种隔离级别是:

  1. TRANSACTION_READ_UNCOMMITTED。未提交读。说明在提交前一个事务可以看到另一个事务的变化。这样读脏数据【不可接受】,不可重复读和虚读都是被允许的。
  2. TRANSACTION_READ_COMMITTED。已提交读。说明读取未提交的数据是不允许的。这个级别仍然允许不可重复读和虚读产生。【oracle默认的工作级别】
  3. TRANSACTION_REPEATABLE_READ。可重复读。说明事务保证能够再次读取相同的数据而不会失败,但虚读仍然会出现。【mysql的默认工作级别】
  4. TRANSACTION_SERIALIZABLE。串行化。是最高的事务级别,它防止读脏数据,不可重复读和虚读。
隔离级别脏读不可重复读幻读
未提交读可以可以可以
已提交读不可以可以可以
可重复读不可以不可以可以(update)
串行化不可以不可以不可以

​ 备注:

  • 事务隔离级别越高,为避免冲突所花费的性能也就越多。
  • 在“可重复读”级别,实际上可以解决部分的虚读问题,但是不能防止update更新产生的虚读问题,要禁止幻读产生,还是需要设置串行化隔离级别。

​ 我们看下mysql的默认工作级别select @@tx_isolation
image-20250509175333984

​ 我们先来演示下最低的隔离级别,将两个终端的mysql隔离级别都设置成未提交读:
image-20250509202740065

​ 左边开启事务,并修改li si的age,但未提交,但是右边却读到了左边事务还未提交的数据。

image-20250509203415500

​ 然后当左边事务进行回滚,右边再次查询数据由恢复了,但是在左边事务回滚前,已经处理的逻辑全部都是错的!

image-20250509203537006

​ 当我们提升事务的隔离级别为已提交读后,就不会发生脏读了。
image-20250509204333642

​ 左边已提交的数据,右边可以看到。出现了不可重复读,但这个我们站在上帝视角应该理解为对的,虽然对于右边来说很懵,因为两次读取的结果不一样。
image-20250509204531142

​ 我们在把事务隔离级别进行提升为可重复读【mysql的默认隔离级别】,就不会发生不可重复读了。下面来进行操作:
image-20250509205919104

​ 其实,在可重复读的隔离级别下,已经解决了一部分的幻读,但是没有解决彻底,通常情况下,在可重复读的隔离级别下,insert语句和delete语句是不会发生幻读的!
image-20250509210542952

​ 但是他防止不了update,下面我来演示下在可重复读隔离级别下出现的幻读:
image-20250509211120113

​ 如果我们连幻读都不想要,那就工作在串行化隔离级别。
image-20250509211642667

​ 但也不会一直的被阻塞,会有一个超时时间,如果阻塞超过了超时时间,就报下面的错误。

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

​## 13.MySQL的锁机制

​ 事务隔离级别的实现原理:锁 + MVCC,我们先来梳理下为什么要由事务的隔离级别,即:事务的ACID特性中I(事务的隔离性),事务如果要允许并发执行 => 数据的安全性&一致性 和 并发的效率问题

​ 数据的隔离级别不同会导致出现1.脏读,2.不可重复读,3.幻读等问题。因此Mysql提供了四种工作模式用来解决这三个问题:

  1. 串行化:通过锁来实现,给所有的事务都排个序,顺序执行!【并发的效率低,数据的安全性高】
  2. 未提交读:没有做任何的并发控制【并发的效率高,数据的安全性最低】
  3. 已提交读和可重复读:通过MVCC多版本并发控制机制实现的【保证数据的安全性&一致性 以及 一定的并发的效率】

1.表级锁和行级锁

​ 表级锁:对整张表加锁。开销小,加锁快,不会出现死锁;锁粒度大,发生锁冲突的概率高,并发度低。

​ 行级锁:对某行记录加锁。开销大,加锁慢,会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度高。

对于InnoDB表级锁来说

​ 在绝大部分情况下都应该使用行锁,因为事务和行锁往往是选择InnoDB的理由,但个别情况下也使用表级锁;

  1. 事务需要更新大部分或全部数据,表又比较大,如果使用默认的行锁,不仅这个事务执行效率低,而且可能造成其他事务长时间等待和锁冲突;
  2. 事务涉及多个表,比较复杂,很可能引起死锁,造成大量事务回滚。

2.排他锁和共享锁

​ 排它锁(Exclusive),又称为X 锁,写锁。

​ 共享锁(Shared),又称为S 锁,读锁。

X和S锁之间有以下的关系: SS可以兼容的,XS、SX、XX之间是互斥的

​ 如何手动的加锁呢?加排他锁,在语句后面 + for update;

image-20250510231234084

​ 如何手动的加共享锁呢,在语句后面 + lock in share mode,从下面的结果来看,排他锁和共享锁之间,也是互斥的!

image-20250510231707557

​ 但是不同行之间的排他锁与共享锁,排他锁与排他锁之间,不会被阻塞!
image-20250510231944265

​ 但是,当我们对没有索引的name字段,查询不同行的时候,排他锁之间居然发生了阻塞!这是为什么呢?
image-20250510233038088

​ 总结:InnoDB的行锁是加载索引项上面的,是给索引在加锁,并不是给单纯的行记录在加锁;索引如果过滤条件没有索引的话,使用的就是表锁,而不是行锁!!!

​ 如果现在给name创建辅助索引,在辅助索引树上,’li si’ : 7 ,‘li jia’ :9 。我们再次操作试试看,发现加了辅助索引/二级索引后,加锁加的又是行锁了!

​ 以上的结论都是MySQL工作在默认的隔离级别下,即可重复读隔离级别,我们再切换到串行化隔离级别试试看,在串行化工作级别下,每行执行都会加锁,如果只是读取数据的话就加共享锁,如果需要修改数据会加排他锁。下面是在串行化隔离级别下,走name索引,最终找到的还是id索引,最终还是在对id=7的那一行进行读操作和写操作,导致共享锁与排他锁互斥!
image-20250511000117438

3.间隙锁

​ 我们先回想一下之前幻读发生的情况,串行化隔离级别是怎么解决幻读问题的?
image-20250511102530641

​ 答案是间隙锁(gap lock),是串行化隔离级别下解决幻读的方案。我们的查询条件可以被分为两类:第一类条件:范围查询,第二类条件:等值查询。涉及到

  • record lock:行锁,
  • gap lock:间隙锁
  • next-key lock:record lock + gap lock。

​ 接下来我们在串行化隔离级别下演示下阻塞的过程:
image-20250511104845703

​ 在穿行化隔离级别下,我们先查询id>11的记录,这是一个范围查询,他会加next-key lock,即:既有行锁,又有间隙锁。加锁情况如下:
image-20250511105254779

​ 而我们新添加的数据,就处于(15,无穷)这个区间内,因为加了间隙锁,所以左边事务的insert操作被阻塞!

​ 我们当时使用的是id主键索引,因为主键索引是唯一的,那如果现在根据普通索引/二级索引来进行范围查询呢,是怎么加锁的呢?

image-20250511111327356

image-20250511111943455

​ 上面都是范围查询,接下来我们来看等值查询,现在我们的age是一个二级索引,我们select * from user where age=18;这里就不再进行演示了。

  1. age是辅助索引(值可以重复的),加record lock + gap lock
  2. 如果age是主键索引或者唯一键索引(值是不允许重复的),只加record lock即可。

image-20250511114652758

​ 详解图如下:
image-20250511114848288

4.意向共享锁和意向排它锁

​ 意向共享锁(IS锁):事务计划给记录加行共享锁,事务在给一行记录加共享锁前,必须先取得该表的IS 锁。

​ 意向排他锁(IX锁):事务计划给记录加行排他锁,事务在给一行记录加排他锁前,必须先取得该表的IX锁。

​ 使用表锁的时候,涉及一个效率的问题???要获取一张表的共享锁 S 或者 排它锁 X,最起码得确定:

  1. 这张表没有被其他事务获取过X锁!【好解决】
  2. 这张表里面的数据没有被其他事务获取过行锁X锁!【不好解决!】,意向锁就是来解决这个问题的!

​ 现在假设我的表中有一千万的数据,我得知道这些数据里面有没有被其他事务获取过行锁X锁,怎么办呢?在加行锁前,必须获取该的意向锁

image-20250511174223654

​ 当获取表的X锁时候,不需要再检查表中的哪些行锁被(X或者S)占用,只需要快速检查 IX 和 IS 锁即可!

​ 总结几个点如下:

  • 意向锁是由InnoDB存储引擎获取行锁之前自己获取的【没有办法去手动获取】
  • 意向锁之间都是兼容的,不会产生冲突
  • 意向锁存在的意义是为了更高效的获取表锁(表格中的X和S指的是表锁,不是行锁!!!)

5.死锁

​ MyISAM 表锁是 deadlock free 的, 这是因为 MyISAM 总是一次获得所需的全部锁,要么全部满足,要么等待,因此不会出现死锁。但在 InnoDB 中,除单个 SQL 组成的事务外,锁是逐步获得的,即锁的粒度比较小,这就决定了在 InnoDB 中发生死锁是可能的。

​ 死锁出现的典型场景:事务一已经获取行锁一,事务二已经获取了行锁二,此时事务一再去获取行锁二,事务二又去获取行锁一,于是就发生阻塞了!所有的事务都阻塞了,相当于进程内的所有线程都阻塞了,造成了死锁问题!

​ 多个事务/线程,获取多个相同资源锁的时候,应该按照同样的顺序,获取资源的锁!

​ 事务被阻塞了,或者死锁了,mysqld是有事务阻塞的超时时间,事务处理失败!mysqld直接能够检测到死锁问题的发生,并进行事务的回滚!演示如下:
image-20250511202439618

​ 因此我们应用在对数据库的多个表做更新的时候,不同的代码段,应对这些表按相同的顺序进行更新操作,以防止锁冲突导致死锁问题。

6.锁的优化建议

  1. 尽量使用较低的隔离级别
  2. 设计合理的索引并尽量使用索引访问数据,使加锁更加准确,减少锁冲突的机会提高并发能力
  3. 选择合理的事务大小,小事务发生锁冲突的概率小
  4. 不同的程序访问一组表时,应尽量约定以相同的顺序访问各表,对一个表而言,尽可能以固定的顺序存取表中的行。这样可以大大减少死锁的机会
  5. 尽量用相等条件访问数据,这样可以避免间隙锁对并发插入的影响
  6. 不要申请超过实际需要的锁级别
  7. 除非必须,查询时不要显示加锁

14.MVCC和undo log

​ 前面通过间隙锁,已经完全解决了幻读,至此,未提交读,和串行化的实现原理我们已经完全掌握,加下来我们来分析分析MySQL的已提交读和可重复度这两个隔离级别是如何实现的。

​ 已提交读和可重复读,底层实现原理是 MVCC(多版本并发控制),并发读取方式:快照读

​ InnoDB提供了两个读取操作:锁定读【读取的时候加S/X锁控制】 和 非锁定读【MVCC提供的快照读】,依赖底层的一个技术叫:undo log 回滚日志!

​ MySQL的事务日志:undo log 回滚日志,redo log重做日志【写入磁盘时的故障恢复】。ACD(都是通过事务日志实现),I(锁+MVCC实现)。undo log回滚日志的主要作用

  1. 事务发生错误时回滚 rollback
  2. 提供了MVCC的非锁定读(快照读) ,这就要从undolog的原理说起了!

​ 我们看下面这张图,下面这张图是针对InnoDB存储引擎的,因为只有InnoDB存储引擎才支持事务:

image-20250511151226306

  • DB_ROW_ID:InnoDB存储引擎未创建主键索引时,自动添加的主键索引。

  • DB_TRX_ID:mysql会对每一个事务,分配一个全局的,不冲突的事务ID。

  • DB_ROLL_PTR:存放的是地址,这个地址是数据存在缓存上的存储位置。

​ 假设最初的时候,表中的记录是这样的:
image-20250511152000885

​ 现在有一个事务,来更改数据:修改之后的数据,存放在表格上,修改之前的数据,存入undo log。现在数据被改动,被事务ID为1020的事务所修改。DB_ROLL_PTR中记录修改前的记录的地址,通过链表连接,存入undo log【缓存中】。
image-20250511152202658

​ 以此类推,假如现在数据又被1000事务修改,那么undo log中的内容就变成了:
image-20250511152411976

​ 以上就是undo log中的全部信息。接下来我们再以我们的user表来解释下:
image-20250511153309584

​ MVCC 中有两种读取操作:快照读(非锁定读)和当前读。所以已提交读和可重复读的主要区别就在于:已提交读:每次执行语句的时候都重新生成一次快照(Read View),每次select查询时。可重复读:同一个事务开始的时候生成一个当前事务全局性的快照(Read View),第一次select查询时。

​ 已提交读:

  1. 解决了 “ 脏读 ” 问题,每一次select,都会产生一次新的数据快照!!!
  2. 事务有两种状态 prepare/commit,在以提交读时,只有事务处于commit才会产生新的快照。于是就解决了脏读!
  3. 但是对于不可重复读:因为事务状态变成了commit,在另一个事务select的时候产生新的快照,这个commit状态会被undolog记录,所以会被看到!!!也就是说,已提交读对于不可重复读无能为力!

​ 为什么无法解决“不可重读读”?因为每一次select都会重新产生一次数据快照,其他事务更新后并且已提交的数据,可以实时反馈到当前事务的select结果当中!

​ 为什么无法解决“幻读”?因为每一次select都会重新产生一次数据快照,其它事务增加了和当前事务查询条件相同的新的事务,并且已成功commit提交,导致当前事务再次以同样的条件查询时,数据多了!

​ 我们再来看看可重复读,只有第一次select的时候生成快照,而且只产生一次:

  1. 解决了脏读问题,和已提交读相同。
  2. 为什么可以解决“不可重复读问题”?第一次select产生数据快照,其他事务虽然修改了最新的数据,但是当前事务select时,依然查看的是最初的快照数据。
  3. 为什么部分解决了“幻读”?新增数据,快照上面的数据没有增加,所以也解决了这种情况下的“幻读”。但是当前事务内自己的更新,可以读到,所以还是没有彻底的解决“幻读”。

​ 一些演示结果如下:
​ 下面这个是在事务2 第一次select前,事务1对数据进行更新并提交,发现事务2会看到事务1的更新内容。

image-20250511170727021

​ 下面演示,事务2先进行select,然后事务1再对事务2查询到的数据做更新,事务2看不到事务1的更新内容。

image-20250511170942502

​ 在这个基础上我们再来看看“幻读”;
image-20250511171210052

快照内容读取原则

  1. 版本未提交无法读取生成快照
  2. 版本已提交,但是在快照创建后提交的,无法读取
  3. 版本已提交,但是在快照创建前提交的,可以读取
  4. 当前事务内自己的更新,可以读到

15.再谈redo log

​ redo log:重做日志,用于记录事务操作的变化,确保事务的持久性。redo log是在事务开始后就开始记录,不管事务是否提交都会记录下来,在异常发生时(如数据持久化过程中掉电),InnoDB会使用redo log恢复到掉电前的时刻,保证数据的完整性。

​ 事务提交commit的时候,所做的是将 InnoDB Log Buffer 中的内容写入磁盘,将redo log中数据的状态有prepare -> commit。但如果在这个过程也有可能发生异常,掉电,服务崩溃,事务没有提交成功 redo log中数据的状态还是prepare。事务最重要的是写redo log【数据页】,而不是写数据。所以redo log也叫物理日志。undo log也叫逻辑日志。

​ Buffer Pool里面的脏数据 写入 磁盘 并不需要和commit操作同步!
image-20250511210939970

image-20250511211013455

​ innodb_log_buffer_size默认是16M,就是redo log缓冲区的大小,它随着事务开始,就开始写redo log,如果事务比较大,为了避免事务执行过程中花费过多磁盘IO,可以设置比较大的redo log缓存,节省磁盘IO。

image-20250511210253367

​ innodb_log_group_home_dir指定的目录下的两个文件:ib_logfile0,ib_logfile1,该文件被称作重做日志。我们去相应的路径下看看:
image-20250511210716096

​ Buffer_Poll的默认大小:
image-20250511211309155

​ InnoDB修改操作数据,不是直接修改磁盘上的数据,实际只是修改Buffer Pool中的数据。InnoDB总是先把Buffer Pool中的数据改变记录到redo log中,用来进行崩溃后的数据恢复。 优先记录redo log,然后再慢慢的将Buffer Pool中的脏数据刷新到磁盘上。

​ buffer pool缓存池:作用:加速读和加速写,直接操作data page,写redo log修改就算完成,有专门的线程去做把buffer pool中的dirty page写入磁盘。

​ 最后,undo log 本身也需要记录在redo log当中!因为undo log 也是需要支持事务回滚的!在事务回滚中出现异常,假设已经做了一半,我们也能在redo log中找到undo log中的内容,对事务回滚进行恢复。

16.MySQL的优化

​ 你做过MySQL的哪些优化吗?开放性。我们首先需要知道MySQL需要优化的地方有哪些?主要分为三个方面:

  1. SQL和索引的优化,开启慢查询日志 => 设置合理的慢查询时间 => 记录慢查询sql => explain分析sql执行计划 => 优化措施
  2. 应用上的优化
    • 连接数据库 => 可以引入 连接池 中间件【项目】
    • 引入缓存 (存储热点数据) 通过redis做缓存,server先去redis上查询,如果有,直接返回,如果没有,再去访问MySQL,并且缓存进redis,但是引入redis,又会出现一些问题:
      1. 缓存数据一致性问题怎么解决?
      2. 缓存穿透
      3. 缓存雪崩
      4. redis你还了解它的什么功能?
  3. mysql server上的优化
    • 各种参数的配置
    • 关闭自适应哈希索引
    • innodb_log_buffer,redo log日志缓存大小
    • innodb_buffer_pool_size,MySQL数据buffer缓存区大小

17.MySQL日志

​ redo log 和 undo log => 事务日志

image-20250512094807085

  • 错误日志:mysqld服务允许过程中出现的coredump,error exception。。
  • 查询日志:记录所有的sql
  • 二进制日志:用于数据恢复,主从复制,记录数据的更改,除了select外都会记录。
  • 慢查询日志:设置合适的超时时间,找出运行效率低的sql。

image-20250512095613742
发现我们当前的二进制日志是关闭的。我们如何开启二进制日志服务呢?我们去修改mysql的配置文件:
image-20250512100434004

​ 保存退出,然后重启我们的mysql服务,在超户下service mysqld restart。如果不行,在超户下使用/etc/init.d/mysql restart。重启后,我们再启动mysql进行相关日志查询,发现二进制日志就启动起来了。
image-20250512101313571

​ 我们可以看下错误日志,里面记录的都是一些mysql运行中的加载配置信息和错误信息。
image-20250512101636984

​ 还有我们的慢查询日志,当前默认是关闭的,我们之前已经讲过了应该如何去设置,这里就不再过度赘述了,如果想要慢查询日志永久开启,就需要将相关的配置信息写入到mysql的配置文件当中。
image-20250512101921041

1.mysqlbinlog数据恢复

​ 我们来演示下基于二进制日志的数据恢复流程:

image-20250512102340243

​ 接下来我们对数据库中是user表进行一些修改操作:
image-20250512102924980

​ 但如果我们直接打开二进制文件查看,这个数据是非明文的,需要使用mysql自带的mysqlbinlog工具进行查看:
image-20250512103158056

mysqlbinlog --no-defaults --database=school --base64-output=decode-rows -v  mysql-bin.000002 | cat

image-20250512104121558

​ 可以看出我们之前对school数据库中的user表做出的修改。mysql数据恢复 依靠:1.数据的备份 2.bin-log的数据恢复。我们之前将bin-log的过期时间设置为7天,那么每隔7天,我们可以进行一次数据的备份,备份到sql脚本文件当中 ~/data.sql,脚本文件当中的sql可以通过mysql > source ~/data.sql 指令来恢复。

​ 下面操作用于恢复mysql我们之前对user表做的修改。

mysqlbinlog --start-position=349 --stop-position=679 mysql-bin.000002 | mysql -u root -p

2.mysqldump数据备份

image-20250512114839324

​ 我们尝试下对school库下的user表做备份:

mysqldump -u root -p school user > /home/ljh/user.sql

image-20250512115053353

​ 我们去家目录打开这个user.sql,可以看到这张表做过的所有sql。现在我们把school库下的user删除,然后通过user.sql恢复。
image-20250512115520585

image-20250512115538639

​ 有了这个技术,就很方便的做数据迁移和数据备份,表重建等等。数据备份常用命令:

mysqldump -u root -p --all-databases > ~/all.sql
mysqldump -u root -p --databases school > ~/school.sql
mysqldump -u root -p school user > ~/user.sql
mysql -u root -p123456 -D school -e 'select * from user where age>18' > ~/user_data.txt

18.一次SQL详细处理流程

image-20250512121648677

​ 上图是mysql执行的详细过程,上图红框中的是mysql server模块,下面是插件式存储引擎模块。

class BaseEngine{};
class MyISAM:public BaseEngine{};
class InnoDB:public BaseEngine{};
  1. 连接器:验证连接,tcp_connection
  2. 解析器:语法解析树
  3. 优化器:生成 “执行计划 ” ,选择索引等。
  4. 执行器:调用存储引擎提供的api接口,进行数据读写操作。
  5. 执行器根据API接口,花费磁盘IO,进行数据读写,B+树索引构建等。。。事务日志。
  6. 磁盘上的数据,表结构*.frm 和表数据和表索引。

​ 下面是一个更简便的图:
image-20250512122653388

19.MySQL主从复制

​ 在实际生产环境中,如果对mysql数据库的读和写都在一台数据库服务器中操作,无论是在安全性、高可用性,还是高并发等各个方面都是不能满足实际需求的,一般要通过主从复制的方式来同步数据,再通过读写分离来提升数据库的并发负载能力

  1. 数据备份 - 热备份&容灾&高可用
  2. 读写分离,支持更大的并发

1.主从复制基本原理

​ 主从复制的流程:两个日志【binlog二进制日志和relay log日志】和三个线程(master线程和两个slave线程)

  1. 主库的更新操作写入binlog二进制日志中【需要把主库的binlog打开】
  2. master服务器创建一个binlog转储线程【master线程】,将二进制日志内容发送到从服务器。
  3. slave机器执行START SLAVE命令会在从服务器创建一个IO线程【第一个slave线程】,接收master的bin log复制到Relay log中。首先slave开始一个工作线程(I/O线程),I/O线程在master上打开一个普通的连接,然后开始 binlog dump process,binlog dump process从master的bin log中读取事件,如果已经与master数据同步,它会睡眠并等待master产生新的事件,I/O线程将这些事件写入Relay log。
  4. sql从线程【第二个slave线程】处理该过程的最后一步,sql线程从Relay log中读取事件,并重放其中的事件而更新slave机器的数据,使其与master的数据一致。只要该线程与I/O线程保持一致,Relay log通常会位于os缓存【效率高】中,所以Relay log的开销很小。

image-20250512145124904

2.主从复制的配置

​ 我以我的华为云服务器为主库,本地的windows为从库来进行配置:

​ 首先,先确定主库(Linux unbantu22.04)的ip地址:
image-20250512152843609

​ 再看从库的(Win11):
image-20250512153044059

​ 在主库中配置开启bin-log,将下面的信息写入mysql的配置文件当中:

server-id=1
expire_logs_days=7
log-bin=mysql-bin

​ 同理,在从库中也写入如下配置,因为从库是去同步主库的bin log,所以从库是不需要开启log-bin的,只需要给一个server-id去标识即可:

server-id=2

​ 写入配置信息后,需要重启服务。由于我的云服务器和本地之间ping不同【无语住了】,,,后续就不再演示了。

20.MySQL读写分离

1.读写分离基本原理

​ 读写分离就是在主服务器上修改,数据会同步到从服务器,从服务器只能提供读取数据,不能写入,实现备份的同时也实现了数据库性能的优化,以及提升了服务器安全。

image-20250512162953096

​ 我们现在扩展到三台机器,A机器作为主服务器【只提供写操作】,B和C都作为从服务器【提供读操作】,读写分离和分库分表都需要依赖数据库中间件 => Mycat 中间件是遵循Mysql通信协议的。

​ 目前较为常见的MySQL读写分离方式有:

  • 程序代码内部实现
  • 引入中间代理层
    • MySQL_proxy
    • Mycat【数据端口是8066,管理端口是9066】

​ Mycat在这里就是一个代理服务器,做一个负载均衡。支持一主一丛,一主多从,以及多主多从。客户端只关注后台提供了服务,并不关心后台是哪台服务器在提供服务。Mycat判断是读操作还是写操作,从而dispatch到相应的服务器上,各个服务器之间通过主从复制同步数据。主从复制是在Mysql数据库上配置!

​ 此外,还可以配置多个主从环境,如其中一套主从环境挂掉,Mycat会自动切换到另一台主从环境中。做到高可用&容灾。

2.读写分离实践操作

​ 读写分离前,需要先开通主从复制。其次,Mycat是java写的,需要jdk环境,jdk版本需要大于1.7。

​ Mycat通过心跳机制,判断服务器是否还存活。有switchType确定:

  • “-1”:不自动切换
  • “1”:自动切换,根据心跳select user()【可用通过打开mysql的查询日志验证】
  • “2”:基于MySQL的主从同步状态决定是否进行切换 show slave status

​ Mycat的负载均衡参数balance设置:

  • “0”:不开启读写分离
  • “1”:全部的readHost和stand by writeHost参与select语句的负载
  • “2”:所有读操作随机在readHost和writeHost上分发
  • “3”:所有读请求随机分发到writeHost对应的readHost上执行

​ writeType=“0”:所有写操作发送到配置的第一个writeHost,第一个挂掉切换到还生存的第二个writeHost。

21.MySQL分库分表

​ 刚开始多数项目用单机数据库就够了,随着服务器流量越来越大,面对的请求也越来越多,我们做了数据库读写分离, 使用多个从库副本(Slave)负责读,使用主库(Master)负责写,master和slave通过主从复制实现数据同步更新,保持数据一致。slave 从库可以水平扩展,所以更多的读请求不成问题。

​ 但是当用户量级上升,写请求越来越多,怎么保证数据库的负载足够?增加一个Master是不能解决问题的, 因为数据要保存一致性,写操作需要2个master之间同步,相当于是重复了,而且架构设计更加复杂。这时需要用到分库分表(sharding),对写操作进行切分。

1.库表问题

  1. 单库太大:单库处理能力有限、所在服务器上的磁盘空间不足、遇到IO瓶颈,需要把单库切分成更多更小的库
  2. 单表太大:CURD效率都很低、数据量太大导致索引膨胀、查询超时,需要把单表切分成多个数据集更小的表【按时间拆分,一致性哈希,取模】,mycat中提供了三种拆表算法。

2.拆分策略

​ 单个库太大,先考虑是表多还是数据多:

  • 如果因为表多而造成数据过多,则使用垂直拆分,即根据业务拆分成不同的库
  • 如果因为单张表的数据量太大,则使用水平拆分,即把表的数据按照某种规则【可根据mycat中提供的三种方法】拆分成多张表

​ 分库分表的原则应该是先考虑垂直拆分,再考虑水平拆分。

3.垂直拆分

  1. 垂直分表:也就是“大表拆小表”,基于列字段进行。一般是表中的字段较多,将不常用的, 数据较大,长度较长(比如text类型字段)的拆分到“扩展表“。一般是针对几百列的这种大表,也避免查询时,数据量太大造成的“跨页”问题。

  2. 垂直分库:所以需要将太大的表进行分库操作。mycat的分库原理如下:
    image-20250512173805931

    ​ 在逻辑上,在mycat上看是一个库,还是USERDB库,Mycat将其差分成两个逻辑节点,每个逻辑节点在后端映射到两个不同的机器上的不同库上了,缓解单服务器上的压力。

4.水平拆分

  1. 水平分表:针对数据量巨大的单张表(比如订单表),按照某种规则(RANGE,HASH取模等),切分到多张表里面去。 但是这些表还是在同一个库中,所以库级别的数据库操作还是有IO瓶颈,不建议采用。

  2. 水平分库分表:将单张表的数据切分到多个服务器上去,每个服务器具有相应的库与表,只是表中数据集合不同。 水平分库分表能够有效的缓解单机和单库的性能瓶颈和压力,突破IO、连接数、硬件资源等的瓶颈。图示如下:
    image-20250512174529923

    ​ 物理上将不同的表分布到不同的服务器上【用户负责】,在逻辑上Mycat也会做进一步的拆分将同一张表的数据分布到不同的服务器上【Mycat负责】

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2374125.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++类和对象进阶 —— 与数据结构的结合

&#x1f381;个人主页&#xff1a;工藤新一 &#x1f50d;系列专栏&#xff1a;C面向对象&#xff08;类和对象篇&#xff09; &#x1f31f;心中的天空之城&#xff0c;终会照亮我前方的路 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 文章目录 […

Django之账号登录及权限管理

账号登录及权限管理 目录 1.登录功能 2.退出登录 3.权限管理 4.代码展示合集 这篇文章, 会讲到如何实现账号登录。账号就是我们上一篇文章写的账号管理功能, 就使用那里面已经创建好的账号。这一次登录, 我们分为三种角色, 分别是员工, 领导, 管理员。不同的角色, 登录进去…

EXCEL中嵌入其他表格等文件

在EXCEL中嵌入其他表格 先放链接&#xff1a;https://jingyan.baidu.com/article/295430f11708c34d7e00509a.html 步骤如下&#xff1a; 1、打开一个需要嵌入新表格的excel表。 2、切换至“插入”菜单中&#xff0c;单击选择“对象”。 3、如下图所示&#xff0c;会弹出“对象…

21. LangChain金融领域:合同审查与风险预警自动化

引言&#xff1a;当AI成为24小时不眠的法律顾问 2025年某商业银行的智能合同系统&#xff0c;将百万级合同审查时间从平均3周缩短至9分钟&#xff0c;风险条款识别准确率达98.7%。本文将基于LangChain的金融法律框架&#xff0c;详解如何构建合规、精准、可追溯的智能风控体系…

Springboot使用事件流调用大模型接口

什么是事件流 事件流&#xff08;Event Stream&#xff09; 是一种处理和传递事件的方式&#xff0c;通常用于系统中的异步消息传递或实时数据流。在事件驱动架构&#xff08;Event-Driven Architecture&#xff09;中&#xff0c;事件流扮演着至关重要的角色。 事件流的概念…

计算机网络--2

TCP三次握手 TCP连接为什么需要三次握手 1. 由于网络情况复杂,可能会出现丢包现象,如果第二次握手的时候服务器就认为这个端口可用,然后一直开启,但是如果客户端未收到服务器发送的回复,那么就会重新发送请求,服务器就会重新开启一个端口连接,这样就会浪费一个端口。 三…

尤雨溪宣布:Vue 生态正式引入 AI

在前端开发领域,Vue 框架一直以其易用性和灵活性受到广大开发者的喜爱。 而如今,Vue 生态在人工智能(AI)领域的应用上又迈出了重要的一步。 尤雨溪近日宣布,Vue、Vite 和 Rolldown 的文档网站均已添加了llms.txt文件,这一举措旨在让大型语言模型(LLM)更方便地理解这些…

蓝桥杯第十六届c组c++题目及个人理解

本篇文章只是部分题目的理解&#xff0c;代码和思路仅供参考&#xff0c;切勿当成正确答案&#xff0c;欢迎各位小伙伴在评论区与博主交流&#xff01; 题目&#xff1a;2025 题目解析 核心提取 要求的数中至少有1个0、2个2、1个5 代码展示 #include<iostream> #incl…

硬件工程师笔记——电子器件汇总大全

目录 1、电阻 工作原理 欧姆定律 电阻的物理本质 一、限制电流 二、分压作用 三、消耗电能&#xff08;将电能转化为热能&#xff09; 2、压敏电阻 伏安特性 1. 过压保护 2. 电压调节 3. 浪涌吸收 4. 消噪与消火花 5. 高频应用 3、电容 工作原理 &#xff08;…

微软推动智能体协同运作:支持 A2A、MCP 协议

今日凌晨&#xff0c;微软宣布 Azure AI Foundry 和 Microsoft Copilot Studio 两大开发平台支持最新 Agent 开发协议 A2A&#xff0c;并与谷歌合作开发扩大该协议&#xff0c;这一举措对智能体赛道意义重大。 现状与变革意义 当前智能体领域类似战国时代&#xff0c;各家技术…

Linxu实验五——NFS服务器

一.NFS服务器介绍 NFS服务器&#xff08;Network File System&#xff09;是一种基于网络的分布式文件系统协议&#xff0c;允许不同操作系统的主机通过网络共享文件和目录3。其核心作用在于实现跨平台的资源透明访问&#xff0c;例如在Linux和Unix系统之间共享静态数据&#…

20242817李臻《Linux⾼级编程实践》第9周

20242817李臻《Linux⾼级编程实践》第9周 一、AI对学习内容的总结 第十章 Linux下的数据库编程 10.1 MySQL数据库简介 MySQL概述&#xff1a;MySQL是一个开源的关系型数据库管理系统&#xff0c;最初由瑞典MySQL AB公司开发&#xff0c;后经SUN公司收购&#xff0c;现属于O…

开源分享:TTS-Web-Vue系列:SSML格式化功能与高级语音合成

&#x1f3af; 本文是TTS-Web-Vue系列的第十二篇文章&#xff0c;重点介绍项目新增的SSML格式化功能以及SSML在语音合成中的应用。通过自动格式化和实时预览&#xff0c;我们显著提升了SSML编辑体验&#xff0c;让用户能够更精确地控制语音合成的细节&#xff0c;实现更自然、更…

FAST-LIO笔记

1.FAST-LIO FAST-LIO 是一个计算效率高、鲁棒性强的激光-惯性里程计系统。该系统通过紧耦合的迭代扩展卡尔曼滤波器&#xff08;IEKF&#xff09;将激光雷达特征点与IMU数据进行融合&#xff0c;使其在快速运动、噪声较大或环境复杂、存在退化的情况下仍能实现稳定的导航。 1…

软考中级软件设计师——UML(统一建模语言)篇

UML的词汇表包含3种构造块:事物、关系和图。事物是对模型中最具有代表性的成分的抽象;关系把事物结合在一起;图聚集了相关的事物。 一、事物 UML 事物是模型中的基本元素&#xff0c;分为 结构事物、行为事物、分组事物、注释事物。 1. 结构事物 类&#xff08;Class&#x…

TSN网络与DIOS融合:破解煤矿井下电力系统越级跳闸难题

一、引言 1.1 研究背景与意义 在现代煤矿生产中&#xff0c;井下电力系统作为整个煤矿生产的动力核心&#xff0c;其重要性不言而喻。煤矿井下的各类机械设备&#xff0c;如采煤机、刮板输送机、通风机、排水泵等&#xff0c;都依赖稳定的电力供应才能正常运行。电力系统的稳定…

SierraNet协议分析使用指导[RDMA]| 如何设置 NVMe QP 端口以进行正确解码

在解码RoCEv2数据包&#xff08;包括TCP RDMA和RoCE RDMA&#xff09;时&#xff0c;若捕获的跟踪数据无法正确解码&#xff0c;通常需要执行特定的解码步骤。对于RoCE RDMA跟踪数据的处理&#xff0c;分析器主要采用两种方式获取必要信息以实现数据包解码&#xff1a; 首先&am…

信号处理基础

一、目的 掌握信号处理的基本思想&#xff0c;理解采样信号的频谱特性,加强信号采样与重建的有关基本概念的理解&#xff0c;深入理解线性时不变系统输出与输入的关系&#xff0c;了解数字信号采样率转换前后信号频谱的特征。 二、内容与设计思想 1、给定序列&#xff0c;绘…

小刚说C语言刷题—1058 - 求出100至999范围内的所有水仙花数

1.题目描述 2.参考代码(C语言版) #include <stdio.h> int main(void) { int i; int bai,shi,ge; for( i100;i<999;i) { baii/100; shii/10%10; gei%10; if((bai*bai*bai)(shi*shi*shi)(ge*ge*ge)i) printf("%d\n",i); } return 0; } 今天内容到此结束&…

深入解析Docker:核心架构与最佳实践

文章目录 前言一、Docker 解决了什么问题&#xff1f;二、Docker 底层核心架构2.1 Docker 引擎的分层架构2.2 镜像的奥秘&#xff1a;联合文件系统&#xff08;UnionFS&#xff09;2.3 容器隔离的核心技术2.3.1 命名空间2.3.2 控制组&#xff08;Cgroups&#xff09;2.3.3 内核…