一、存储引擎
- MySQL体系结构:
- 连接层
最上层是一些客户端和链接服务,主要完成一些类似于连接处理、授权认证、及相关的安全方案。服务器也会为安全接入的每个客户端验证它所具有的操作权限。
- 服务层
第二层架构主要完成大多数的核心服务功能,如SQL接口,并完成缓存的查询,SQL的分析和优化,部分内置函数的执行。所有跨存储引擎的功能也在这一层实现,如过程、函数等。
- 引擎层
存储引擎真正的负责了MySQL中数据的存储和提取,服务器通过API和存储引擎进行通信。不同的存储引擎具有不同的功能,这样我们可以根据自己的需要,来选取合适的存储引擎。
- 存储层
主要是将数据存储在文件系统之上,并完成与存储引擎的交互。
存储引擎就是存储数据、建立索引、更新/查询数据等技术的实现方式。存储引擎是基于表而不是基于库的,所以存储引擎也可以被称为表引擎。默认存储引擎是InnoDB。
- 相关操作:
-- 查询建表语句
show create table account;
-- 建表时指定存储引擎
CREATE TABLE 表名(
...
) ENGINE=INNODB;
-- 查看当前数据库支持的存储引擎
show engines;
1、InnoDB
一种兼顾高可靠性和高性能的通用存储引擎,在MySQL 5.5 之后,InnoDB 是默认的引擎。
- 特点:
- DML 操作遵循 ACID 模型,支持事务。
- 行级锁,提高并发访问性能。
- 支持外键约束,保证数据的完整性和正确性。
- 文件:
- xxx.ibd: xxx代表表名,InnoDB 引擎的每张表都会对应这样一个表空间文件,存储该表的表结构(frm、sdi)、数据和索引。
参数:innodb_file_per_table,决定多张表共享一个表空间还是每张表对应一个表空间
- 知识点:
# 查看 Mysql 变量:
show variables like 'innodb_file_per_table';
# 从idb文件提取表结构数据:
# (在cmd运行)
# ibd2sdi xxx.ibd
- InnoDB 逻辑存储结构:
2、MyISAM
MySQL早期的默认存储引擎。
- 特点:
- 不支持事务,不支持外键
- 支持表锁,不支持行锁
- 访问速度快
- 文件:
- xxx.sdi: 存储表结构信息
- xxx.MYD: 存储数据
- xxx.MYI: 存储索引
3、Memory
表数据是存储在内存中的,受硬件问题、断电问题的影响,只能将这些表作为临时表或缓存使用。
- 特点:
- 存放在内存中,速度快
- hash索引(默认)
- 文件:
- xxx.sdi: 存储表结构信息
4、存储引擎特点
特点 | InnoDB | MyISAM | Memory |
存储限制 | 64TB | 有 | 有 |
事务安全 | 支持 | – | – |
锁机制 | 行锁 | 表锁 | 表锁 |
B+tree索引 | 支持 | 支持 | 支持 |
Hash索引 | – | – | 支持 |
全文索引 | 支持(5.6版本之后) | 支持 | – |
空间使用 | 高 | 低 | N/A |
内存使用 | 高 | 低 | 中等 |
批量插入速度 | 低 | 高 | 高 |
支持外键 | 支持 | – | – |
5、存储引擎的选择
在选择存储引擎时,应根据应用系统的特点选择合适的存储引擎。对于复杂的应用系统,还可根据实际情况选择多种存储引擎进行组合。
- InnoDB:如果应用对事物的完整性有比较高的要求,在并发条件下要求数据的一致性,数据操作除了插入和查询之外,还包含很多的更新、删除操作,则 InnoDB 是比较合适的选择
- MyISAM:如果应用是以读操作和插入操作为主,只有很少的更新和删除操作,并且对事务的完整性、并发性要求不高,那这个存储引擎是非常合适的。
- Memory:将所有数据保存在内存中,访问速度快,通常用于临时表及缓存。Memory 的缺陷是对表的大小有限制,太大的表无法缓存在内存中,而且无法保障数据的安全性
电商中的足迹和评论适合使用 MyISAM 引擎,缓存适合使用 Memory 引擎。
二、性能分析
1、查看执行频次
# 查看当前数据库的 INSERT, UPDATE, DELETE, SELECT 访问频次:
SHOW GLOBAL STATUS LIKE 'Com_______';
SHOW SESSION STATUS LIKE 'Com_______';
show global status like 'Com_______'
2、慢查询日志
记录了所有执行时间超过指定参数(long_query_time,单位:秒,默认10秒)的所有SQL语句的日志。
MySQL的慢查询日志默认没有开启,需要在MySQL的配置文件(/etc/my.cnf)中配置如下信息:
# 开启慢查询日志开关
slow_query_log=1
# 设置慢查询日志的时间为2秒,SQL语句执行时间超过2秒,就会视为慢查询,记录慢查询日志
long_query_time=2
# 更改后记得重启MySQL服务,日志文件位置:/var/lib/mysql/localhost-slow.log
# 查看慢查询日志开关状态:
show variables like 'slow_query_log';
3、profile
# show profile 能在做SQL优化时帮我们了解时间都耗费在哪里。
# 通过 have_profiling 参数,能看到当前 MySQL 是否支持 profile 操作:
SELECT @@have_profiling;
# profiling 默认关闭,可以通过set语句在session/global级别开启 profiling:
SET profiling = 1;
# 查看所有语句的耗时:
show profiles;
# 查看指定query_id的SQL语句各个阶段的耗时:
show profile for query query_id;
# 查看指定query_id的SQL语句CPU的使用情况
show profile cpu for query query_id;
4、explain
MySQL 中用于分析 SQL 查询执行计划的重要工具。
- 基本语法
EXPLAIN SELECT 字段列表 FROM 表名 WHERE 条件;
-- 或者
DESC SELECT 字段列表 FROM 表名 WHERE 条件;
- explain各字段含义:
- id:
select 查询的序列号,表示查询中执行 select 子句或者操作表的顺序(id相同,执行顺序从上到下;id不同,值越大越先执行)
- select_type:
SELECT 的类型,常见取值:
SIMPLE:简单查询,不使用表连接或子查询
PRIMARY:主查询(最外层的查询)
SUBQUERY:子查询(出现在 SELECT 或 WHERE 中)
DERIVED:派生表(FROM 子句中的子查询)
UNION:UNION 中的第二个或后面的查询语句
UNION RESULT:UNION 的结果
- type:
连接类型,常见类型:
NULL:不用访问表或索引就能得到结果(如 SELECT 1)
system:表中只有一行数据(系统表)
const:通过索引一次就找到,用于主键或唯一索引的等值查询
eq_ref:唯一索引扫描,通常出现在多表连接中使用主键或唯一索引关联
ref:非唯一索引扫描,返回匹配某个单独值的所有行
range:索引范围扫描(如 BETWEEN、IN、>、< 等)
index:全索引扫描(比 ALL 好,因为只扫描索引树)
ALL:全表扫描(性能最差)
- possible_key:
可能应用在这张表上的索引(一个或多个)
- Key:
实际使用的索引,若为 NULL,则没有使用索引。
- Key_len:
索引中使用的字节数,该值为索引字段最大可能长度,并非实际使用长度,在不损失精确性的前提下,长度越短越好
- rows:
MySQL认为必须要执行的行数,在InnoDB引擎的表中,是一个估计值,可能并不总是准确的
- filtered:
返回结果的行数占需读取行数的百分比,值越大越好。
三、索引
一种有序的数据结构(如B+树、哈希表等),能帮助 MySQL 高效获取数据,它通过额外维护特定查找算法的结构来加速数据检索,类似于书籍的目录。
- 优点:
- 提高查询效率。减少全表扫描,降低磁盘 I/O 次数,如B+树索引只需 3-4 次磁盘访问即可定位百万级数据。
- 优化排序和分组。如果 ORDER BY 或 GROUP BY 的列有索引,可直接利用索引的有序性,避免临时表和额外排序。
- 加速表连接。连接条件列有索引时,嵌套循环连接效率更高。
- 缺点:
- 占用存储空间。索引需要独立存储,尤其是组合索引。如InnoDB 的二级索引会包含主键列,可能导致索引比数据本身更大。
- 降低写操作性能。写入时需要同步更新索引,如一张表有 5 个索引,插入一条数据实际需要写入 6 次(1 次数据 + 5 次索引)。
- 索引失效风险。不当的查询(如对索引列使用函数、隐式类型转换)会导致索引失效,反而增加开销。
1、索引结构
索引结构 | 描述 |
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-Tree
二叉树缺点:顺序插入时,会形成一个链表,查询性能大大降低。大数据量情况下,层级较深,检索速度慢。
上述缺点可用红黑树来解决: 红黑树也存在大数据量情况下,层级较深,检索速度慢的问题。为解决上述问题,可使用 B-Tree 结构。
B-Tree(多路平衡查找树):以一棵最大度数(max-degree,指一个节点的子节点个数)为5(5阶)的 b-tree 为例(每个节点最多存储4个key,5个指针)
B-Tree 的数据插入过程动画参照:https://www.bilibili.com/video/BV1Kr4y1i7ru?p=68
演示地址:https://www.cs.usfca.edu/~galles/visualization/BTree.html
- B+Tree
演示地址:https://www.cs.usfca.edu/~galles/visualization/BPlusTree.html
与 B-Tree 的区别:①所有的数据都会出现在叶子节点;②叶子节点形成一个单向链表。
MySQL 索引数据结构对经典的 B+Tree 进行了优化。在原 B+Tree 的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的 B+Tree,提高区间访问的性能。
- Hash索引
采用一定的hash算法,将键值换算成新的hash值,映射到对应的槽位上,然后存储在hash表中。
若两个(或多个)键值,映射到一个相同的槽位上,就产生了hash冲突(hash碰撞),可通过链表来解决。
- 特点:
Hash索引只能用于对等比较(=、in),不支持范围查询(betwwn、>、<、…)
无法利用索引完成排序操作
查询效率高,通常只需要一次检索就可以了,效率通常要高于 B+Tree 索引
- 存储引擎支持:
Memory
InnoDB: 具有自适应hash功能,hash索引是存储引擎根据 B+Tree 索引在指定条件下自动构建的
- 为什么 InnoDB 存储引擎选择使用 B+Tree 索引结构?
相对于二叉树,层级更少,搜索效率高,对于 B-Tree,无论是叶子节点还是非叶子节点,都会保存数据,这样导致一页中存储的键值减少,指针也跟着减少,要同样保存大量数据,只能增加树的高度,导致性能降低,相对于 Hash 索引,B+Tree 支持范围匹配及排序操作。
2、索引分类
分类 | 含义 | 特点 | 关键字 |
主键索引 | 针对于表中主键创建的索引 | 默认自动创建,只能有一个 | PRIMARY |
唯一索引 | 避免同一个表中某数据列中的值重复 | 可以有多个 | UNIQUE |
常规索引 | 快速定位特定数据 | 可以有多个 | |
全文索引 | 全文索引查找的是文本中的关键词,而不是比较索引中的值 | 可以有多个 | FULLTEXT |
在 InnoDB 存储引擎中,根据索引的存储形式,又可以分为以下两种:
分类 | 含义 | 特点 |
聚集索引(Clustered Index) | 将数据存储与索引放一块,索引结构的叶子节点保存了行数据 | 必须有,而且只有一个 |
二级索引(Secondary Index) | 将数据与索引分开存储,索引结构的叶子节点关联的是对应的主键 | 可以存在多个 |
- 聚集索引选取规则:
若存在主键,主键索引就是聚集索引
若不存在主键,将使用第一个唯一(UNIQUE)索引作为聚集索引
若表没有主键或没有合适的唯一索引,则 InnoDB 会自动生成一个 rowid 作为隐藏的聚集索引
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。
若树的高度为2,那么他能存储的数据量大概为:1171 * 16 = 18736;
若树的高度为3,那么他能存储的数据量大概为:1171 * 1171 * 16 = 21939856。
另外,如果有成千上万的数据,那么就要考虑分表,涉及运维篇知识。
3、语法
案例:# 创建索引:
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;
# 案例:
-- name字段为姓名字段,该字段的值可能会重复,为该字段创建索引
create index idx_user_name on tb_user(name);
-- phone手机号字段的值非空,且唯一,为该字段创建唯一索引
create unique index idx_user_phone on tb_user (phone);
-- 为profession, age, status创建联合索引
create index idx_user_pro_age_stat on tb_user(profession, age, status);
-- 为email建立合适的索引来提升查询效率
create index idx_user_email on tb_user(email);
-- 删除索引
drop index idx_user_email on tb_user;
4、使用规则
- 最左前缀法则:必须从最左列开始。
联合索引 (a,b,c) 生效的条件:查询条件必须包含 a,否则索引完全失效。
# ✅ 有效:
WHERE a=1、WHERE a=1 AND b=2、WHERE a=1 AND b=2 AND c=3
# ❌ 无效:
WHERE b=2、WHERE c=3、WHERE b=2 AND c=3(索引完全不被使用)
当联合索引中的某一列使用范围查询(>、<、BETWEEN),其右侧的所有列索引失效。
# ❌ 无效:
WHERE a=1 AND b>2 AND c=3:仅 a,b 走索引,c 失效
# ✅ 有效:
WHERE a=1 AND b=2 AND c=3:所有列均走索引
- 索引失效情况
# 对索引列进行运算或函数操作。索引存储的是原始值,运算后无法直接匹配索引结构。
-- 索引失效(函数操作)
SELECT * FROM users WHERE SUBSTRING(phone, 1, 3) = '138';
-- 索引失效(数学运算)
SELECT * FROM products WHERE price + 100 > 200;
-- 改为对常量进行计算:
SELECT * FROM users WHERE phone LIKE '138%';
# 字符串类型未加引号(隐式类型转换)。MySQL会隐式将字符串转为数值,导致索引失效。
-- 索引失效(phone是varchar类型)
SELECT * FROM users WHERE phone = 17799990015;
-- 正确写法(加引号)
SELECT * FROM users WHERE phone = '17799990015';
# 模糊查询(LIKE)。
WHERE name LIKE '张%' # %在后面才有效
-- 优化方案:使用全文索引(FULLTEXT)或搜索引擎(如 Elasticsearch)处理复杂模糊查询。
# OR 条件导致索引失效。OR 连接的多个条件中,只要有一个列没有索引,整个查询的索引都会失效。
SELECT * FROM users WHERE id = 1 OR age = 20; # 假设age列无索引,即使id有索引也会全表扫描
-- 优化方案:改为 UNION ALL(需保证结果无重复)
SELECT * FROM users WHERE id = 1
UNION ALL
SELECT * FROM users WHERE age = 20;
# MySQL 优化器放弃索引。如果 MySQL 评估使用索引比全表更慢,则不使用索引。
- SQL 提示
人工干预 MySQL 优化器决策的一种方式,通过在 SQL 中显式指定索引使用策略来覆盖优化器的默认选择。
USE INDEX # 软性建议,优化器会综合成本决定是否采纳(可能被忽略)。
FORCE INDEX # 硬性要求,除非索引完全不适用(如对非索引列查询),否则必须使用。
IGNORE INDEX # 强制排除,即使该索引是最优选择。
-- 优化器误选了低效索引 idx_age,强制使用覆盖索引 idx_name_age
SELECT * FROM users FORCE INDEX(idx_name_age) WHERE name = '张三' AND age > 20;
-- 忽略不合适的索引 idx_status(该列区分度低)
SELECT * FROM orders IGNORE INDEX(idx_status) WHERE user_id = 1001 AND create_time > '2023-01-01';
-- 建议但不强制使用 idx_profession(优化器可能仍选择其他索引)
SELECT * FROM employees USE INDEX(idx_profession) WHERE profession = '工程师' AND department = '研发部';
- 覆盖索引和回表查询
核心机制
# 一张表,有四个字段(id, username, password, status),由于数据量大,需要对以下SQL语句进行优化,该如何进行才是最优方案:
select id, username, password from tb_user where username='itcast';
# 创建联合索引:(username, password),则不需要回表查询,直接覆盖索引。
- 前缀索引
对字符串列的前面部分字符建立索引,而不是对整个字符串建立索引。
语法:create index idx_xxxx on table_name(columnn(n));
# 语法:
create index idx_xxxx on table_name(columnn(n));
# 如何确定合适的前缀长度?
# 计算完整列的选择性:
SELECT COUNT(DISTINCT column_name) / COUNT(*) FROM table_name;
# 计算不同前缀长度的选择性:
SELECT
COUNT(DISTINCT LEFT(column_name, 5)) / COUNT(*) AS selectivity_5,
COUNT(DISTINCT LEFT(column_name, 10)) / COUNT(*) AS selectivity_10,
COUNT(DISTINCT LEFT(column_name, 15)) / COUNT(*) AS selectivity_15
FROM table_name;
# 查看前缀索引信息
SHOW INDEX FROM table_name;
- 单列索引和联合索引
单列索引:即一个索引只包含单个列;
联合索引:即一个索引包含了多个列。
在业务场景中,如果存在多个查询条件,考虑针对于查询字段建立索引时,建议建立联合索引,而非单列索引。
EXPLAIN SELECT id, phone, name FROM tb_user WHERE phone = '17799990010' AND name = '韩信';
# 假设有单独的phone索引和name索引,MySQL优化器通常只会选择其中一个索引使用,然后对结果集再使用name条件进行过滤。——>效率较低
# 如果创建联合索引 (phone, name):先在索引中定位 phone = '17799990010',然后在这些记录中筛选 name = '韩信'
注:多条件联合查询时,MySQL优化器会评估哪个字段的索引效率更高,会选择该索引完成本次查询
5、设计原则
-- 选择大数据量、高频查询的表建索引;
-- 优先为WHERE/ORDER BY/GROUP BY的字段建索引
-- 选择唯一性高(如>90%)的列作为索引
-- 长字符串使用前缀索引(如email(10))
-- 多用联合索引,少用单列索引
-- 单表索引不超过5-6个
-- 索引列设为NOT NULL
四、SQL 优化
1、插入数据
- 普通插入:
- 采用批量插入(一次插入的数据不建议超过1000条)
- 手动提交事务
- 主键顺序插入
- 大批量插入:
若一次性需插入大批量数据,使用insert语句插入性能较低,此时可使用MySQL数据库提供的load指令插入。
# 客户端连接服务端时,加上参数 --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';
2、主键优化
- InnoDB采用索引组织表(Index organized table,IOT)结构,数据按主键顺序物理存储,形成B+树结构。这种设计使得:
- 数据存储和主键索引合二为一;
- 主键索引的叶子节点直接包含完整数据记录
- 页分裂(Page Split):当向已满的数据页插入新数据时发生:
- 原数据页分裂为两个页;
- 新记录按主键顺序插入到适当位置
- 导致性能下降和空间碎片
- 页合并(Page Merge):当删除记录导致页利用率过低时触发:
- 默认阈值(MERGE_THRESHOLD)为页大小的50%
- InnoDB会尝试合并相邻页
- 提高空间利用率,减少碎片
PPT演示过程:https://www.bilibili.com/video/BV1Kr4y1i7ru?p=90
- 主键设计原则:
- 长度最小化。小主键 → 小索引 → 存储更多键值,如优先用BIGINT(8)而非CHAR(32)。
- 尽量选择顺序插入。选择使用 AUTO_INCREMENT 自增主键,避免频繁页分裂。
- 避免自然主键。身份证号/UUID,太大了。
- 禁止修改。避免对主键的修改
3、order by优化
排序方式 | 原理 | 性能 | Extra显示 |
Using index | 直接通过索引的有序性返回数据,无需额外排序 | 最佳 | Using index |
Using filesort | 将数据加载到排序缓冲区(sort_buffer)进行排序,可能使用磁盘临时文件 | 较差 | Using filesort |
如果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/DESC)
- 如果不可避免出现filesort,大数据量排序时,可以适当增大排序缓冲区大小 sort_buffer_size(默认256k)
4、group by优化
- 在分组操作时,可以通过索引来提高效率
- 分组操作时,索引的使用也是满足最左前缀法则的
5、limit优化
# 问题: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;
6、count优化
- 存储引擎差异
- MyISAM:把一个表的总行数存在了磁盘上,执行 count(*) 的时候会直接返回(无WHERE时)。
- InnoDB:执行 count(*) 时,必须逐行扫描计算(MVCC机制导致)
- 四种COUNT用法性能对比
用法 | 处理方式 | 效率排名 |
COUNT(*) | 引擎优化版,不取具体字段 | 🏆 最佳 |
COUNT(1) | 不取值,每行填充1 | ⭐️ 次佳 |
COUNT(主键) | 取出所有主键值 | 🥉 第三 |
COUNT(字段) | 需判断NULL,有NOT NULL约束稍快 | 🐢 最慢 |
按效率排序:count(字段) < count(主键) < count(1) < count(*)
注:永远优先用count(*)。
7、update优化(避免行锁升级为表锁)
InnoDB 的行锁是针对索引加的锁,不是针对记录加的锁,并且该索引不能失效,否则会从行锁升级为表锁。
如以下两条语句:update student set no = ‘123’ where id = 1;,这句由于id有主键索引,所以只会锁这一行;update student set no = ‘123’ where name = ‘test’;,这句由于name没有索引,所以会把整张表都锁住进行数据更新,解决方法是给name字段添加索引
五、视图/存储过程/触发器
- 视图(VIEW):虚拟存在的表,不保存查询结果,只保存查询的SQL逻辑。简单、安全、数据独立
- 存储过程(PROCEDURE):事先定义并存储在数据库中的一段SQL语句的集合。减少网络交互,提高性能、封装重用。
- 存储函数(FUNCTION):存储函数是有返回值的存储过程,参数类型只能为IN类型。存储函数可以被存储过程替代
- 触发器(TRIGGER):可在表数据进行INSERT、UPDATE、DELETE之前或之后触发。保证数据完整性、日志记录、数据校验
1、视图
基于SQL查询结果的逻辑表,不存储实际数据,每次查询视图时,都会重新执行其定义的SELECT语句。
# 创建视图:
CREATE [OR REPLACE] VIEW 视图名称(列名列表)】AS SELECT语句[WITH[CASCADED|LOCAL] CHECK OPTION]
# 查看创建视图语句:
SHOW CRETE VIEW 视图名称;
# 查看视图数据:
SELECT*FROM 视图名称…;
# 修改视图:
CREATE [OR REPLACE]VIEW 视图名称(列名列表)AS SELECT语句[WITH[CASCADEDLLOCAL] CHECK OPTION
ALTER VEW 视图名称(列名列表)AS SELECT语句[WITH[CASCADED|LOCAL]CHECK OPTION]
# 删除视图:
DROP VIEW [IF EXISTS]视图名称[,视图名称]
-- 创建视图
create or replace view stu_v_1 as select id, name from student where id <= 10;
-- 查询视图
show create view stu_v_1;
select * from stu_v_1;
-- 修改视图
create or replace view stu_v_1 as select id, name, no from student where id <= 10;
alter view stu_v_1 as select id, name from student where id <= 10;
-- 删除视图
drop view if exists stu_v_1;/* */
- 检查选项
确保通过视图修改的数据必须符合视图的查询条件。
- 两种模式:
- CASCADED(默认):严格检查,会检查所有底层视图的条件
- LOCAL:只检查当前视图和明确设置了检查选项的底层视图条件
- 视图的更新:
视图能被更新(增删改)必须满足:
- 视图与基础表是一对一关系
- 不能包含以下元素:
- 聚合函数(SUM/COUNT等)
- DISTINCT去重
- GROUP BY分组
- HAVING过滤
- UNION联合查询
- 视图的三大好处:
- 简化:把常用查询保存为视图,避免重复写复杂SQL
- 安全:通过视图限制用户只能访问特定数据
- 解耦:视图可以屏蔽表结构变化,不影响上层应用
- 案例
-- 1.为了保证数据库表的安全性,开发人员在操作tb_user表时,只能看到的用户的基本字段,屏蔽手机号和邮箱两个字段。
create view tb user view as select id,name,profession, age,gender,status,createtime from tb_user;
select *from tb user view;
-- 2.查询每个学生所选修的课程(三张表联查),这个功能在很多的业务中都有使用到,为了简化操作,定义一个视图。
create view tb_stu_course_view
select s.name student_name, s.no student_no, c.name course_name
from student s, stuent_course sc, course c
where s.id = sc.studentid and sc.courseid = c.id;
-- 以后每次只需要进行查询视图即可
select * from tb_stu_course_view;
2、存储过程
一段预先编译好的SQL代码,存储在数据库中,可以重复调用,类似Java的方法。
- 特点
- 封装SQL:把复杂操作打包,调用更简单。
- 支持参数:可以传入数据,也能返回结果。
- 减少交互:应用端只需调用,不用发多条SQL。
# 查看视图数据:
SELECT*FROM 视图名称…;
# 查看:
SELECT* FROM INFORMATION SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA='xx'; --查询数据库的存储过程及状态信息`
`SHOW CREATE PROCEDURE 存储过程名称; --查询某个存储过程的定义
# 删除
DROP PROCEDURE [IF EXISTS]存储过程名称;
-- 存储过程基本语法
-- 创建
create procedure p1()
begin
select count(*)from student;
end;
-- 调用
call p1();
-- 查看
select * from information_schema.ROUTINES where ROUTINE_SCHEMA = 'itcast';
show create procedure p1;
-- 删除
drop procedure if exists p1;
- 变量
(1)系统变量
系统变量是MySQL服务器提供,不是用户定义的,属于服务器层面。
分为全局变量(GLOBAL)、会话变量(SESSION)。
-- 查看所有系统变量
SHOW [SESSION |GLOBAL] VARIABLES ;
-- 可以通过LKE模糊匹配方式查找变量`
SHOW[SESSION|GLOBAL] VARIABLES LIKE;
-- 查看指定变量的值
SELECT @@[SESSION|GLOBAL]系统变量名;
-- 变量:系统变量
-- 查看系统变量
show session variables;
show session variables like 'auto%';
show glabal variables like 'auto%';
select @@global.autocommit;
-- 设置系统变量
set session autocommit = 1;
insert intto course(id, name) values (6, 'ES');
set global auto commit = 0;
注意:
- 若未指定 session / global,默认session,会话变量
- mysql 服务器重启之后,所设置的全局参数会失效,要想不失效,需要更改/etc/my.cnf 中的配置。
(2)用户定义变量
用户根据需要自己定义的变量,用户变量不用提前声明,在用的时候直接用“@变量名”使用就可以。其作用域为当前连接。
# 赋值
SET @var name = expr [, @var_name = expr]...;
SET @var name := expr [, @var_name := expr]...;
SELECT @var name := expr , @var name := expr ...;
SELECT 字段名 INTO @var_name FROM 表名;
# 使用
SELECT @var_name;
# 案例
-- 变量:用户变量
-- 赋值
set @myname = 'itcast';
set @myage := 10;
select @mycolor := 'red';
select count(*) into @mycount from tb_user;
-- 使用
select @myname, @myage, @mycolor, @mycount;
select @abc; -- 输出为NULL
注意:用户定义的变量无需对其进行声明或者初始化,只不过获取到的值为 NULL。
(3)局部变量
根据需要定义的在局部生效的变量,访问前,需DECLARE声明。可用作存储过程内的局部变量和输入参数,局部变量的范围是在其内声明的BEGIN .. END块。
# 声明
DECLARE 变量名 变量类型 [DEFAULT..];
# 变量类型就是数据库字段类型:INT、BIGINT、CHAR、VARCHAR、DATE、TIME等。
# 赋值
SET 变量名=值;
SET 变量名:=值;
SELECT 字段名 INTO 变量名 FROM 表名 ...;
# 案例
-- 变量:局部变量
-- 声明 - declare
-- 赋值 -
create procedure p2()
begin
declare stu_count int default 0;
select count(*) into stu_count from student;
select stu_count;
end;
call p2();
- if 判断
# 语法:
IF 条件1 THEN
...
ELSEIF 条件2 THEN -- 可选
...
ELSE -- 可选
...
END IF;
# 案例:
create procedure p3()
begin
declare score int default 58;
declare result varchar(10);
if score >= 85 then
set result :='优秀';
elseif score >= 60 then
set result :='及格';
else
set result :='不及格';
end if;
select result;
end;
- 参数(in, out, inout)
类型 | 含义 | 备注 |
IN | 该类参数作为输入,也就是需要调用时传入值 | 默认 |
OUT | 该类参数作为输出,也就是该参数可以作为返回值 | |
INOUT | 既可以作为输入参数,也可以作为输出参数 |
# 案例
CREATE PROCEDURE 存储过程名称([IN/OUT/INOUT 参数名 参数类型 ])
BEGIN
-- SQL语句
END :
-- 根据传入(in)参数score,判定当前分数对应的分数等级,并返回(out)
-- score >= 85分,等级为优秀。
-- score >= 60分 且 score < 85分,等级为及格
-- score < 60分,等级为不及格。
create procedure p3(in score int, out result varchar(10))
begin
if score >= 85 then
set result :='优秀';
elseif score >= 60 then
set result :='及格';
else
set result :='不及格';
end if;
select result;
end;
-- 将传入的200分制的分数,进行换算,换算成百分制,然后返回分数 --> inout
create procedure p5(inout score double)
begin
set score := score * 0.5;
end;
set @score = 198;
call p5(score);
select @score;
- case
语法一:
# 语法一:
CASE case value
WHEN when_value1 THEN statement_list1
[WHEN when_value2 THEN statement_list2]...
[ELSE statement_list ]
END CASE;
# 语法二:
CASE
WHEN search_conditionl THEN statement_list1
WHEN search_condition2 THEN statement_list2]...
[ELSE statement_list]
END CASE;
# 案例:
-- case
-- 根据传入的月份,判定月份所属的季节(要求采用case结构)
-- 1-3月份,为第一季度
-- 4-6月份,为第二季度
-- 7-9月份,为第三季度
-- 10-12月份,为第四季度
create procedure p6(in month int)
begin
declare result varchar(10);
case
when month >= 1 and month <= 3 then
set result := '第一季度';
when month >= 4 and month <= 6 then
set result := '第二季度';
when month >= 7 and month <= 9 then
set result := ' 第三季度';
when month >= 10 and month <= 12 then
set result := '第四季度';
else
set result := '非法参数';
end case;
select concat('你输入的月份为:', month, ',所属季度为:', result);
end;
- 循环
(1)while
有条件的循环控制语句。满足条件后,再执行循环体中的SQL语句。
# 语法:
# 先判定条件,如果条件为true,则执行逻辑,否则,不执行逻辑
WHILE 条件 DO
SOL逻辑...
END WHILE;
# 案例:
-- while计算从1累加到 n 的值,n 为传入的参数值。
-- A.定义局部变量,记录累加之后的值;
-- B.每循环一次,就会对 n 进行减1,如果 n 减到0,则退出循环
create procedure p7(in n int)
begin
declare total int default 0;
while n>0 do
set total := total + n
set n:=n-1;
end while;
select total;
end;
call p7( n: 100);
(2)repeat
有条件的循环控制语句,当满足条件的时候退出循环。
先进行循环一次再判断,满足条件则退出。
# 语法:
# 先执行一次逻辑,然后判定逻辑是否满足,如果满足,则退出。如果不满足,则继续下一次循环
REPEAT
SOL逻辑.
UNTIL 条件
END REPEAT;
# 案例:
-- while计算从1累加到 n 的值,n 为传入的参数值。
-- A.定义局部变量,记录累加之后的值;
-- B.每循环一次,就会对 n 进行减1,如果 n 减到0,则退出循环
create procedure p8(innint)
begin
declare total int default 0;
repeat
set total := total + n;
set n := n - 1;
until n <= 0
end repeat;
select total;
end;
call p8( n: 10);
call p8( n: 100);
(3)loop
实现简单的循环,若不在SQL逻辑中增加退出循环的条件,可以用其来实现简单的死循环。LOOP可以配合一下两个语句使用。
LEAVE:配合循环使用,退出循环。
ITERATE:必须用在循环中,作用是跳过当前循环剩下的语句,直接进入下一次循环。
[begin label:] LOOP
SQL逻辑..
END LOOP [end label];
LEAVE label; -- 退出指定标记的循环体
ITERATE label;-- 直接进入下一次循环
# 案例
-- loop 计算从1到n之间的偶数累加的值,n为传入的参数值。
-- A.定义局部变量,记录累加之后的值;
-- B.每循环一次,就会劝进行-1,如果n减到0,则退出循环。------> leave xx
-- C.如果当次累加的数据是奇数,则直接进入下一次循坏。-------> iterate xx
create procedure p10(in n int)
begin
declare total int defatult 0;
sum: loop
if n <= 10 then
leave sum;
end if;
if n %2 = 1 then
set n := n - 1;
iterate sum;
end if;
set total := total + n;
set n := n - 1;
end loop sum;
select total;
end;
- 游标-cursor
用来存储查询结果集的数据类型,在存储过程和函数中可以使用游标对结果集进行循环的处理。
游标的使用包括游标的声明、OPEN、FETCH和 CLOSE,其语法分别如下。
类似于 c 语言中的结构体,java 中的实体类。
# 声明游标
DECLARE 游标名称 CURSOR FOR 查询语句;
# 打开游标:
OPEN 游标名称;
# 获取游标记录:
FETCH 游标名称 INTO 变量[,变量];
# 关闭游标:
CLOSE 游标名称;
# 案例:
-- 游标
-- 根据传入的参数uage,来查询用户表tb_user 中, 所有的用户年龄小于uage的用户姓名(name)和专业(profession),
-- 并将用户的姓名和专业插入到所创建的一张新表(id,name,profession)中。
-- 逻辑:
-- A.声明游标,存储查询结果集-
-- B.准备:创建表结构
-- C.开启游标-
-- D.获取游标中的记录
-- E.插入数据到新表中-
-- F.关闭游标
create procedure p11(in uage int)
begin
declare uname varchar(100);
declare upro varchar(100);
declare u_cursor cursor for select name, profession from tb_user where age <= uage;
drop table if exists tb_user_pro;
create table if not exists tb_user_pro(
id int primary key auto_increment,
name varchar(100),
profession varchar(100)
);
open u_cursor;
while true do
fetch u_cursor into uname,upro;
insert into tb_user_pro values(null, uname, upro);
end while;
close u_cursor;
end;
end;
- 条件处理程序-handler
可以用来定义在流程控制结构执行过程中遇到问题时相应的处理步骤。
# 语法:
DECLARE handler action HANDLERFOR condition value l, condition value.... statement;
handler action
CONTINUE: 继续执行当前程序
EXIT: 终止执行当前程序
condition value
SOLSTATE sqlstate_value:状态码,如 02000
SQLWARNING:所有以01开头的SQLSTATE代码的简写
NOT FOUND:所有以02开头的SOLSTATE代码的简写
SOLEXCEPTION:所有没有被SOLWARNING 或 NOT FOUND捕获的SOLSTATE代码的简写
# 案例:
create procedure p11(in uage int)
begin
declare uname varchar(100);
declare upro varchar(100);
declare u_cursor cursor for select name, profession from tb_user where age <= uage;
-- 监控到02000的状态码后,关闭游标后执行exit退出操作。
declare exit handler for not found close u_cursor;
drop table if exists tb_user_pro;
create table if not exists tb_user_pro(
id int primary key auto_increment,
name varchar(100),
profession varchar(100)
);
open u_cursor;
while true do
fetch u_cursor into uname,upro;
insert into tb_user_pro values(null, uname, upro);
end while;
close u_cursor;
end;
3、存储函数:
有返回值的存储过程,存储函数的参数只能是IN类型的。
存储函数用的较少,能够使用存储函数的地方都可以用存储过程替换。
# 语法:
CREATE FUNCTION 存储函数名称([ 参数列表 ])
RETURNS type [characteristic ...]
BEGIN
-- SQL语句
RETURN ...;
END ;
characteristic说明:
· DETERMINISTIC:相同的输入参数总是产生相同的结果
· NO SQL:不包含 SQL语句。
· READS SOL DATA:包含读取数据的语句,但不包含写入数据的语句,
# 案例:
create function fun1(n int)
returns int deterministic
begin
declare total int default 0;
while n > 0 do
set total := total + n;
set n := n - 1;
end while;
return total;
end;
4、触发器
与表有关的数据库对象,指在 insert/update/delete 之前或之后,触发并执行触发器中定义的SQL语句集合。
触发器的这种特性可以协助应用在数据库端确保数据的完整性,日志记录,数据校验等操作。
使用别名 OLD 和 NEW 来引用触发器中发生变化的记录内容,这与其他的数据库是相似的。现在触发器还只支持行级触发,不支持语句级触发。
触发器类型 | NEW 和 OLD |
insert 型触发器 | NEW 表示将要或者已经新增的数据 |
update 型触发器 | OLD 表示修改之前的数据,NEW 表示将要或已经修改后的数据 |
delete 型触发器 | OLD 表示将要或者已经删除的数据 |
# 创建:
CREATE TRIGGER trigger name
BEFORE/AFTER INSERT/UPDATE/DELETE
ON tbl name FOR EACH ROW --行级触发器BEGIN
trigger_stmt;
END;
# 查看:
SHOW TRIGGERS;
# 删除:
DROP TRIGGER [schema_name.]trigger_name; --如果没有指定 schema name,默认为当前数据库
# 案例:
-- 插入数据触发器
create trigger tb_user_insert_trigger
after insert on tb_user for each row
begin
insert into user_logs(id, operation, operate_time, operate_id, operate_params)values
(null, 'insert', now(), new.id, concat('插入的数据内容为:id=', new.id, ',name=', new.name, ', phone=', new.phone, ', email=', new.email, ', profession=', new.profession));
end;
-- 查看
show triggers;
-- 删除
drop trigger tb_user_insert_trigger;
-- 插入数据tb_user
insert into tb_user(id, name, phtone, email, profession, age, gender, status, createtime) values(25, '二皇子', '1880901212', 'erhuangzi@163.com', '软件工程', 23, '1', '1'1, now());
-- 修改数据触发器
create trigger tb_user_update_trigger
after update on tb_user for each row
begin
insert into user_logs(id, operation, operate_time, operate_id, operate_params)values
(null, 'update', now(), new.id,
concat('更新之前的数据:id=', old.id, ',name=', old.name, ', phone=', old.phone, ', email=', old.email, ', profession=', old.profession,
'更新之后的数据:id=', new.id, ',name=', new.name, ', phone=', new.phone, ', email=', new.email, ', profession=', new.profession));
end;
update tb_user set age = 32 where id = 23;
update tb_user set age = 32 where id <= 5; -- 触发器为行级触发器,所以更改几行数据则出发几次,该语句出发5次
-- 删除数据触发器
create trigger tb_user_delete_trigger
after delete on tb_user for each row
begin
insert into user_logs(id, operation, operate_time, operate_id, operate_params)values
(null, 'insert', now(), old.id,
concat('删除之前的数据:id=', new.id, ',name=', old.name, ', phone=', old.phone, ', email=', old.email, ', profession=', old.profession));
end;
delete from tb_user where id = 26;
六、锁
保证多个线程/事务安全访问同一数据,解决并发冲突(比如防止同时修改导致数据错乱)。
- 分类:按锁的粒度分
- 全局锁:锁定数据库中的所有表。
- 表级锁:每次操作锁住整张表。
- 行级锁:每次操作锁住对应的行数据。
1、全局锁
对整个数据库实例加锁,加锁后整个实例就处于只读状态,后续的DML的写语句,DDL语句,已经更新操作的事务提交语句都将被阻塞。
使用场景:做全库的逻辑备份,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性。
- 基本操作:
# 使用全局锁:
flush tables with read lock
# 释放全局锁:
unlock tables
- 演示图:
- 全局锁的问题
- 主库备份 → 业务停摆(所有写操作阻塞)。
- 从库备份 → 主从延迟(无法同步主库的binlog)。
- 解决方案:
# 加–single-transaction,InnoDB引擎的“无锁备份”方案,通过事务快照实现备份,不阻塞其他操作。
mysqldump --single-transaction -uroot -p123456 itcast > itcast.sql
# 不锁表,业务正常运转。适用引擎:InnoDB。
# 就像你截图微信聊天记录,截图后别人继续发消息,但你的截图内容不会变。
2、表级锁
每次操作锁住整张表。锁定粒度大,发生锁的冲突的概率最高,并发度最低。应用在MyISAM、InnoDB、BDB等存储引擎中。
对于表级锁,分:表锁;元数据锁(meta data lock,MDL);意向锁。
(1)表锁
对于表锁,分:表共享读锁(read lock);表独占写锁(write lock)。
# 读锁:其他会话能读不能写。
# 允许当前会话读取被锁定的表,但阻止其他会话对这些表进行写操作。
LOCK TABLES 表名 READ; -- 加读锁
# 写锁:其他会话不能读也不能写。
LOCK TABLES 表名 WRITE; -- 加写锁
# 解锁:
UNLOCK TABLES; -- 释放所有表锁
(2)元数据锁(Metadata Lock, MDL)
MySQL自动加的表级锁,用于保护表结构不被修改的同时允许数据操作。
- 两种类型:
- MDL读锁:进行数据操作时自动加锁(如SELECT, INSERT, UPDATE, DELETE)
- MDL写锁:修改表结构时自动加锁(如ALTER TABLE)
MDL加锁过程是系统自动控制,无需显式使用,在访问一张表的时候会自动加上。
主要作用是维护表元数据的数据一致性,在表上有活动事务的时候,不可对元数据进行写入操作。为了避免DML与DDL冲突,保证读写的正确性。
对应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 … | EXCLYSIVE | 与其他的MDL都互斥 |
查看元数据锁:
select object_type,object_schema,object_name,lock_type,lock_duration from performance_schema.metadata_locks;
(3)意向锁
InnoDB中用来快速判断表中是否有行被加锁的表级锁,它解决了表锁和行锁之间的冲突检测问题。
- 两种意向锁:
- 意向共享锁(IS):表示表中某些行加了共享锁(SELECT…LOCK IN SHARE MODE)
- 意向排他锁(IX):表示表中某些行加了排他锁(INSERT/UPDATE/DELETE/SELECT…FOR UPDATE)
- 设计目的:
- 避免加表锁时需要逐行检查是否有行锁
- 通过表级的意向锁快速判断表中是否有行被锁定
- 锁的兼容性:
- 意向锁之间互相兼容(IS和IX不互斥)
- 意向锁不会和行锁冲突
- 意向锁会与表锁(LOCK TABLES)冲突
(4)AUTO-INC锁(自增锁)
自增锁是MySQL用来保证自增主键连续性的特殊机制。
- 自增主键的两种实现方式:
- 传统AUTO-INC锁:整个插入语句完成后才释放(像”包场”)
- 轻量级锁:拿到自增值后立即释放(像”取号机”)
- 三种模式:
- 模式0:完全传统方式,所有插入都”包场”
- 模式1:普通插入用”取号机”,批量插入用”包场”(MySQL默认)
- 模式2:全部用”取号机”(性能最好但有复制风险)
- 主从复制问题:
- 当使用”取号机”模式+语句复制(statement)时,主从可能编号不一致
- 因为主库和从库的插入顺序可能不同
举个例子,考虑下面场景:
session A往表t中插入了4行数据,然后创建了一个相同结构的表t2,然后两个session同时执行向表t2中插入数据。执行向表t2中插入数据。
如果innodb_autoinc_lock_mode=2,意味着「申请自增主键后就释放锁,不必等插入语句执行完」。那么就可能出现这样的情况:
- sessionB先插入了两个记录,(1,1,1)、(2,2,2);
- 然后,sessionA来申请自增id得到id=3,插入了(3,5,5);
- 之后,sessionB继续执行,插入两条记录(4,3,3)、(5,4,4)。
可以看到,sessionB的insert语句,生成的id不连续。
当「主库」发生了这种情况,binlog面对t2表的更新只会记录这两个session的insert语句,如果binlog_format=statement,记录的语句就是原始语句。记录的顺序要么先记sessionA的insert语句,要么先记sessionB的insert语句。
但不论是哪一种,这个binlog拿去「从库」执行,这时从库是按「顺序」执行语句的,只有当执行完一条SQL语句后,才会执行下一条SQL。因此,在从库上「不会」发生像主库那样两个session「同时]执行向表t2中插入数据的场景。所以,在备库上执行了session B的insert语句,生成的结果里面,id都是连续的。这时,主从库就发生了数据不一致。
要解决这问题,binlog日志格式要设置为row,这样在binlog里面记录的是主库分配的自增值,到备库执行的时候,主库的自增值是什么,从库的自增值就是什么。
所以,当innodb_autoinc_lock_mode=2时,并且binlog_format=row,既能提升并发性,又不会出现数据一致性问题。
3、行级锁
InnoDB最精细的锁机制。每次操作锁住对应的行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。
- 三种行级锁对比
锁类型 | 锁定范围 | 主要作用 | 支持的隔离级别 | 类比说明 |
行锁(Record Lock) | 单行记录 | 防止该行被修改或删除 | RC、RR | 给特定座位贴”已预订”标签 |
间隙锁(Gap Lock) | 索引记录间的空隙 | 防止在间隙插入新行(防幻读) | RR | 在电影院空排两端放”留空”标志 |
临键锁(Next-Key Lock) | 行记录+前一个间隙 | 同时防止修改和插入 | RR | “已预订”标签延伸到前一排空位 |
- 行锁(Record Lock):锁定单个行记录的锁,防止其他事务对此行进行update和delete。在RC、RR隔离级别下都支持。
- 间隙锁(GapLock):锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行insert,产生幻读。在RR隔离级别下都支持。
- 临键锁(Next-Key Lock):行锁和间隙锁组合,同时锁住数据,并锁住数据前面的间隙Gap。在RR隔离级别下支持。
Record Lock(行锁)
Record Lock 称为记录锁,锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分。
InnoDB实现了以下两种类型的行锁:
- 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排它锁。
- 排他锁(X):允许获取排他锁的事务更新数据,阻止其他事务获得相同数据集的共享锁和排他锁。
S(共享锁) | X(排他锁) | |
S(共享锁) | 兼容 | 冲突 |
X(排他锁) | 冲突 | 冲突 |
行锁类型:
SQL | 行锁类型 | 说明 |
insert,update,delete … | 排他锁 | 自动加锁 |
select | 不加任何锁 | |
select … lock in share mode | 共享锁 | 需要手动select之后加上lock in share mode |
select … for update | 排他锁 | 需要手动在select之后for update |
默认情况下,InnoDB在 REPEATABLE READ事务隔离级别运行,InnoDB使用 next-key锁进行搜索和索引扫描,以防止幻读。
- 针对唯一索引进行检索时,对已存在的记录进行等值匹配时,将会自动优化为行锁。
- InnoDB的行锁是针对于索引加的锁,不通过索引条件检索数据,那么!nnoDB将对表中的所有记录加锁,此时 就会升级为表锁。
查看意向锁及行锁的加锁情况:
select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from peformance_schema.data_locks;
Gap Lock(间隙锁)
间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。
Next-Key Lock(临键锁)
临键锁,是Record Lock+Gap Lock的组合,锁定一个范围,并且锁定记录本身。
默认情况下,InnODB在 REPEATABLE READ事务隔离级别运行,InnoDB使用 next-key 锁进行搜索和索引扫描,以防止幻读。
索引上的等值查询(唯一索引),给不存在的记录加锁时,优化为间隙锁 。
索引上的等值查询(普通索引),向右遍历时最后一个值不满足查询需求时,next-keylock退化为间隙锁。
索引上的范围查询(唯一索引)–会访问到不满足条件的第一个值为止。
七、InnoDB引擎
1、逻辑存储结构
2、架构
- 内存架构
- 磁盘结构
- 后台线程
3、事务原理
事务:一组操作的集合,一个不可分割的工作单位,事务会把所有操作一起提交或撤销,要么同时成功,要么同时失败。
- 特征:
特性 | 说明 | 实现机制 |
原子性(Atomicity) | 事务是最小工作单元,要么全部完成,要么全部不执行 | 通过undo log(回滚日志)实现,记录修改前的数据 |
一致性(Consistency) | 事务执行前后,数据库从一个合法状态变为另一个合法状态 | 由应用程序和数据库共同保证(外键、约束等) |
隔离性(Isolation) | 多个并发事务执行时,一个事务不应影响其他事务 | 通过锁机制和MVCC(多版本并发控制)实现 |
持久性(Durability) | 事务提交后,对数据的修改是永久性的,即使系统故障也不会丢失 | 通过redo log(重做日志)实现,记录修改后的数据 |
- 特性原理分类图:
- redo log
重做日志:用于保证事务的持久性,记录事务提交时数据页的物理修改。
由内存中的redo log buffer和磁盘上的redo log file组成。事务提交后,修改信息会持久化到redo log file中,用于异常恢复。当Buffer Pool产生脏页时,数据会先写入redo log buffer,再持久化到redo log file。若系统崩溃,可通过redo log恢复数据,确保事务修改不丢失。
- 为什么需要 redo log?
虽然数据最终都要写入磁盘,但 redo log 和直接写数据文件的方式不同:
- 为什么需要 redo log?
虽然数据最终都要写入磁盘,但 redo log 和直接写数据文件的方式不同:
顺序写 vs. 随机写:
redo log 是追加写入(顺序 I/O),性能高。
直接写数据文件 是随机 I/O,需要先定位数据位置,速度慢。
WAL(Write-Ahead Logging)机制
MySQL 采用 “先写日志,再写数据” 的策略,事务提交时只需保证 redo log 落盘,数据可以异步刷盘,提高吞吐量。
崩溃恢复
如果 MySQL 崩溃,可以通过 redo log 恢复未刷盘的脏页,确保事务持久性。
- undo log 的作用
回滚(Rollback)
记录数据修改前的逻辑状态(如 INSERT → DELETE,UPDATE → 反向 UPDATE)。
事务回滚时,根据 undo log 恢复数据。
MVCC(多版本并发控制)
提供事务的快照读,避免读写冲突。
例如,SELECT 查询可能读取旧版本数据,而不会阻塞写操作。
存储方式
存放在 rollback segment(回滚段) 中,每个回滚段包含 1024 个 undo log segment。
事务提交后不会立即删除,可能仍被 MVCC 使用,后续由后台线程清理。
4、MVCC
- 当前读(Locking Read):
读取记录的最新版本,并且会对记录加锁,防止其他事务修改。
适用场景:
SELECT ... LOCK IN SHARE MODE; -- 加共享锁(S锁)
SELECT ... FOR UPDATE; -- 加排他锁(X锁)
UPDATE, INSERT, DELETE; -- 自动加排他锁
# 特点:
# 保证读取的数据是最新的。
# 可能阻塞其他事务的修改。
- 快照读(Non-Locking Read / Snapshot Read):
读取数据的可见版本(可能是历史版本),不加锁,不会阻塞其他事务。
# 适用场景:
SELECT; -- 普通查询(不加锁)
隔离级别 | 快照读行为 |
Read Committed (RC) | 每次 SELECT 都生成新的快照读(可能读到其他事务已提交的最新数据) |
Repeatable Read (RR) | 事务第一次 SELECT 生成快照读,后续查询沿用该版本(避免不可重复读) |
Serializable | 退化为当前读(所有查询加锁,避免幻读) |
- MVCC(Multi-Version Concurrency Control):
通过多版本控制实现读写并发优化。快照读为MVSOL实现MVCC提供了一个非阻塞读功能。
-- 核心依赖:
# 隐式字段(InnoDB 额外存储的元数据)
# Undo Log(存储历史版本)
# Read View(判断数据可见性)
-- 隐式字段:InnoDB 每行记录额外存储
# DB_TRX_ID:事务ID,记录最后修改该行的事务
# DB_ROLL_PTR:回滚指针,指向 Undo Log 中的旧版本
# DB_ROW_ID:行ID(如果没有主键,InnoDB 自动生成)
-- Undo Log:存储数据的历史版本,形成版本链(通过 DB_ROLL_PTR 串联)。
# 作用:回滚;MVCC
-- Read View(读视图):决定事务能看到哪些版本的数据.
# trx_list:当前活跃(未提交)事务ID列表。
# up_limit_id:最小活跃事务ID。
# low_limit_id:下一个待分配事务ID(当前最大事务ID +1)。
# creator_trx_id:创建该 Read View 的事务ID。
undo log: InnoDB 实现事务回滚和多版本并发控制(MVCC)的关键组件
当insert的时候,产生的undoloq日志只在回滚时需要,在事务提交后,可被立即删除。
而update、delete的时候,产生的undo log日志不仅在回滚时需要,在快照读时也需要,不会立即被删除。
那么何时删除?
- 事务提交后:
- 对于INSERT操作,事务提交后,undo log可以被立即删除,因为不再需要用于回滚。
- 对于UPDATE和DELETE操作,undo log不会立即被删除,因为它们可能在后续的快照读取中被使用。
- 快照读取结束:
- 当所有依赖于该undo log的快照读取操作结束后,undo log才会被删除。这意味着如果有一个事务正在进行快照读取,并且依赖于某个undo log,那么这个undo log会一直保留直到该事务结束。
- readview
ReadView(读视图)是 快照读 SOL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id。
ReadView中包含了四个核心字段:
字段 | 含义 |
m_ids | 当前活跃的事务ID集合 |
min_trx_id | 最小活跃事务ID |
max_trx_id | 预分配事务ID,当前最大事务ID+1(因为事务ID是自增的) |
creator_trx_id | ReadView创建者的事务ID |
依次比较 undo log 日志中版本数据链,找到可以进行访问的版本数据。
八、MySQL管理
1、系统数据库介绍
数据库安装完成后,自带四个数据库,具体作用如下:
数据库 | 含义 |
mysql | 存储MYSQL服务器正常运行所需要的各种信息(时区、主从、用户、权限等) |
information_schema | 提供访问数据库元数据的各种表和视图,包含数据库、表、字段类型及访问权限等 |
performance_schema | 为MySQL服务器运行时状态提供了一个底层监控功能,主要用于收集数据库服务器性能参数 |
sys | 包含了一系列方便 DBA和开发人员利用 performance_schema性能数据库进行性能调优和诊断的视图 |
2、常用工具
- mysql
- mysqladmin
- mysqlbinlog
- mysqlshow
- mysqldump
- mysqlimport/source