MySQL学习笔记-进阶篇
MySQL学习笔记-进阶篇
一、存储引擎
MySQL体系结构:
- 连接层
最上层是一些客户端和链接服务,主要完成一些类似于连接处理、授权认证、最大连接数及相关的安全方案。服务器也会为安全接入的每个客户端验证它所具有的操作权限。 - 服务层
第二层架构主要完成大多数的核心服务功能,如SQL接口,并完成缓存的查询,SQL的分析和优化,部分内置函数的执行。所有跨存储引擎的功能也在这一层实现,如过程、函数等。如:SQL接口(DML、DDL、存储过程、视图、触发器等等),解析器,查询优化器,缓存等等 - 引擎层
存储引擎真正的负责了MySQL中数据的存储和提取,服务器通过API和存储引擎进行通信。不同的存储引擎具有不同的功能,这样我们可以根据自己的需要,来选取合适的存储引擎。Index是在引擎层实现的,所以不同的引擎会有不同的索引结构。 - 存储层
主要是将数据存储在文件系统之上,并完成与存储引擎的交互。
存储引擎就是存储数据、建立索引、更新/查询数据等技术的实现方式。
存储引擎是基于表而不是基于库的,所以存储引擎也可以被称为表类型。
在同一个 MySQL 数据库实例中,甚至在同一个数据库(Schema)里,不同的表可以使用不同的存储引擎。
数据库 (Database/Schema): 主要是一个逻辑容器,用于组织和归类表、视图、存储过程等对象。它定义了命名空间。
表 (Table): 才是实际存储数据的物理实体。而如何存储这个物理实体,就由存储引擎来决定。
示例:
假设你有一个名为 my_app
的数据库,里面有三张表:
users
: 需要支持事务(如银行转账、积分变更)和行级锁,以保证数据一致性和高并发。你会选择 InnoDB。product_catalog
: 主要是只读的静态信息(如商品描述、型号),需要极高的读取速度。你可能会选择 MyISAM(在旧版本中)或者专为只读优化的引擎。session_logs
: 主要用于高速写入日志,不需要事务支持,并且数据可以按时间归档。你可能会选择 Archive 引擎。
默认存储引擎是InnoDB。
-- 查询建表语句
show create table account;
-- 建表时指定存储引擎
CREATE TABLE 表名(...
) ENGINE=INNODB;
-- 查看当前数据库支持的存储引擎
show engines;
InnoDB
InnoDB 是一种兼顾高可靠性和高性能的通用存储引擎,在 MySQL 5.5 之后,InnoDB 是默认的 MySQL 引擎
特点:
- DML 操作遵循 ACID 模型(原子性,一致性,隔离性,持久性),支持事务
- 行级锁,提高并发访问性能
- 支持外键约束,保证数据的完整性和正确性
文件:
- xxx.ibd: xxx代表表名,InnoDB 引擎的每张表都会对应这样一个表空间文件,存储该表的表结构(frm、sdi)、数据和索引。
- 每个InnoDB表会有自己的
.ibd
文件来存储该表的数据和索引。 - 表的结构(表名、列名、类型等元数据)则存放在单独的
.frm
文件或系统表中。
参数:innodb_file_per_table,决定多张表共享一个表空间还是每张表对应一个表空间
show variables like 'innodb_file_per_table';--查看 Mysql 变量
从idb文件提取表结构数据:
(在cmd运行)
ibd2sdi xxx.ibd
- 表空间(Tablespace):
- 一个表空间是InnoDB存储引擎的最高层逻辑存储单元。
- 对于系统表空间(共享表空间),多个表可能存储在同一个表空间(如
ibdata1
)中;但对于独立表空间(每个表一个.ibd
文件),每个表通常对应一个表空间(除非表分区,此时每个分区可能有一个表空间)。 - 因此,准确地说:一个表由一个或多个表空间组成(如果表分区,每个分区有自己的表空间;否则通常一个表一个表空间)。
- 段(Segment):
- 一个表空间由多个段组成。段是分配给特定数据结构(如B+树索引)的空间单元。
- 每个索引有两个段:一个用于非叶子节点(内部节点),一个用于叶子节点。此外,表数据本身也存储在段中(如聚簇索引的叶子段)。
- 因此,一个表空间包含多个段(例如,一个主键索引有两个段,二级索引也有两个段)。
- 区(Extent):
- 一个段由多个区组成。区是连续分配的一组页,通常大小为1MB(当页大小为16KB时,一个区包含64页)。
- 区是为了提高空间分配效率和减少碎片化。InnoDB一次性分配一个区给段,而不是单个页。
- 页(Page):
- 一个区由多个页组成。页是InnoDB磁盘管理的最小单元,通常大小为16KB(可配置)。
- 页用于存储行数据、索引、undo日志等。常见的页类型有数据页、索引页、undo页等。
- 行(Row):
- 一个页由多行组成。行是实际的数据记录,每行包含表的列值以及一些元数据(如事务ID、回滚指针等)。
- 行格式可以是Compact、Redundant、Dynamic或Compressed,影响存储效率和性能。
MyISAM
MyISAM 是 MySQL 早期的默认存储引擎。
特点:
- 不支持事务,不支持外键
- 支持表锁,不支持行锁
- 访问速度快
文件:
- xxx.sdi: 存储表结构信息
- xxx.MYD: 存储数据
- xxx.MYI: 存储索引
Memory
Memory 引擎的表数据是存储在内存中的,受硬件问题、断电问题的影响,只能将这些表作为临时表或缓存使用。
特点:
- 存放在内存中,速度快
- hash索引(默认)
文件:
- xxx.sdi: 存储表结构信息
存储引擎特点
特点 | InnoDB | MyISAM | Memory |
---|---|---|---|
存储限制 | 64TB | 有 | 有 |
事务安全 | 支持 | - | - |
锁机制 | 行锁 | 表锁 | 表锁 |
B+tree索引 | 支持 | 支持 | 支持 |
Hash索引 | - | - | 支持 |
全文索引 | 支持(5.6版本之后) | 支持 | - |
空间使用 | 高 | 低 | N/A |
内存使用 | 高 | 低 | 中等 |
批量插入速度 | 低 | 高 | 高 |
支持外键 | 支持 | - | - |
存储引擎的选择
在选择存储引擎时,应该根据应用系统的特点选择合适的存储引擎。对于复杂的应用系统,还可以根据实际情况选择多种存储引擎进行组合。
- InnoDB: 如果应用对事物的完整性有比较高的要求,在并发条件下要求数据的一致性,数据操作除了插入和查询之外,还包含很多的更新、删除操作,则 InnoDB 是比较合适的选择
- MyISAM: 如果应用是以读操作和插入操作为主,只有很少的更新和删除操作,并且对事务的完整性、并发性要求不高,那这个存储引擎是非常合适的。
- Memory: 将所有数据保存在内存中,访问速度快,通常用于临时表及缓存。Memory 的缺陷是对表的大小有限制,太大的表无法缓存在内存中,而且无法保障数据的安全性
电商中的足迹和评论适合使用 MyISAM 引擎,缓存适合使用 Memory 引擎。
二、索引
索引是帮助 MySQL 高效获取数据的数据结构(有序)。在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查询算法,这种数据结构就是索引。
优缺点:
优点:
- 提高数据检索效率,降低数据库的IO成本
- 通过索引列对数据进行排序,降低数据排序的成本,降低CPU的消耗
缺点:
- 索引列也是要占用空间的
- 索引大大提高了查询效率,但降低了更新的速度,比如 INSERT、UPDATE、DELETE
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
结构图:
与 B-Tree 的区别:
- 所有的数据都会出现在叶子节点
- 叶子节点形成一个单向链表
MySQL 索引数据结构对经典的 B+Tree 进行了优化。在原 B+Tree 的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的 B+Tree,提高区间访问的性能。
Hash
哈希索引就是采用一定的hash算法,将键值换算成新的hash值,映射到对应的槽位上,然后存储在hash表中。
如果两个(或多个)键值,映射到一个相同的槽位上,他们就产生了hash冲突(也称为hash碰撞),可以通过链表来解决。
- Hash索引只能用于对等比较(=、in),不支持范围查询(betwwn、>、<、…)
- 无法利用索引完成排序操作
- 查询效率高,通常只需要一次检索就可以了(如果出现hash碰撞,需要到对应的链表中去检索),效率通常要高于 B+Tree 索引
存储引擎支持:
在MySQL中
- Memory:支持hash索引
- InnoDB: 具有自适应hash功能,hash索引是存储引擎根据 B+Tree 索引在指定条件下自动构建的
面试题
- 为什么 InnoDB 存储引擎选择使用 B+Tree 索引结构?
- 相对于二叉树,层级更少,搜索效率高
- 对于 B-Tree,无论是叶子节点还是非叶子节点,都会保存数据,这样导致一页中存储的键值减少,指针也跟着减少,要同样保存大量数据,只能增加树的高度,导致性能降低。而B+Tree,不管查找哪个数据都要到叶子节点中才能找到对应数据,此时搜索效率稳定,另外叶子节点之间形成了双向链表,便于范围搜索和排序。
- 相对于 Hash 索引,B+Tree 支持范围匹配及排序操作
2、索引分类
分类 | 含义 | 特点 | 关键字 |
---|---|---|---|
主键索引 | 针对于表中主键创建的索引 | 默认自动创建,只能有一个 | PRIMARY |
唯一索引 | 避免同一个表中某数据列中的值重复 | 可以有多个 | UNIQUE |
常规索引 | 快速定位特定数据 | 可以有多个 | |
全文索引 | 全文索引查找的是文本中的关键词,而不是比较索引中的值 | 可以有多个 | FULLTEXT |
在 InnoDB 存储引擎中,根据索引的存储形式,又可以分为以下两种:
分类 | 含义 | 特点 |
---|---|---|
聚集索引(Clustered Index) | 将数据存储与索引放一块**,索引结构的叶子节点保存了行数据** | 必须有,而且只有一个 |
二级索引(Secondary Index) | 将数据与索引分开存储,索引结构的叶子节点关联的是对应的主键 | 可以存在多个 |
聚集索引选取规则:
- 如果存在主键,主键索引就是聚集索引
- 如果不存在主键,将使用第一个唯一(UNIQUE)索引作为聚集索引
- 如果表没有主键或没有合适的唯一索引,则 InnoDB 会自动生成一个 rowid 作为隐藏的聚集索引
演示图:
思考题
- 以下 SQL 语句,哪个执行效率高?为什么?
select * from user where id = 10;
select * from user where name = 'Arm';
-- 备注:id为主键,name字段创建的有索引
答:第一条语句,因为第二条需要回表查询,相当于两个步骤。第一条语句直接通过根据主键id建立的索引进行查找,叶子节点下直接挂载行数据直接返回数据,第二条语句通过查找根据name字段建立的索引,叶子节点挂载的是对应的主键,再根据对应的主键查询(回表查询),最后的到行数据。
- 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
。
另外,如果有成千上万的数据,那么就要考虑分表,涉及运维篇知识。
语法
创建索引:
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;
表结构:
3、使用规则
最左前缀法则
如果索引关联了多列(联合索引),要遵守最左前缀法则,最左前缀法则指的是查询从索引的最左列开始,并且不跳过索引中的列。
如果跳跃某一列,索引将部分失效(后面的字段索引失效)。
联合索引中,出现范围查询(<, >),范围查询右侧的列索引失效。可以用>=或者<=来规避索引失效问题。
索引失效情况
-
在索引列上进行运算操作,索引将失效。
explain select * from tb_user where substring(phone, 10, 2) = '15';
-
字符串类型字段使用时,不加引号,索引将失效。
explain select * from tb_user where phone = 17799990015; #此处phone的值没有加引号
-
模糊查询中,如果仅仅是尾部模糊匹配,索引不会是失效;如果是头部模糊匹配,索引失效。
explain select * from tb_user where profession like '%工程'; #前后都有 % 也会失效。
-
用 or 分割开的条件,如果 or 其中一个条件的列没有索引,那么涉及的索引都不会被用到。
-
如果 MySQL 评估使用索引比全表更慢,则不使用索引。
优化器的决策过程可以概括为:
访问磁盘的成本 >> 在内存中计算和过滤的成本
优化器会分别估算全表扫描的成本和使用索引的成本,然后选择成本更低的那个。
优化器如何进行成本评估?
- 全表扫描的成本计算
全表扫描的成本相对简单直接:
- I/O 成本:将整个表的数据页从磁盘加载到内存的成本。成本 ≈ 表的总数据页数量 * 加载一个数据页的成本常数。
- CPU 成本:在内存中检查所有数据行,并判断它们是否满足
WHERE
条件的成本。成本 ≈ 表的总记录数 * 访问一条记录的成本常数。
- 使用索引的成本计算
使用索引的成本更为复杂,通常分为两步:
- 第一步:索引扫描
- I/O 成本:将索引页从磁盘加载到内存的成本。这取决于索引树(B+Tree)的深度和需要扫描的索引记录范围。
- CPU 成本:在内存中遍历索引树并找到符合条件的记录主键 ID 的成本。
- 第二步:回表(Row Lookups)
- 这是关键的一步!对于第一步通过索引找到的每一个主键 ID,都需要回到主键索引(聚簇索引)中去查找完整的行数据。
- I/O 成本:这部分成本通常很高。成本 ≈ 预估的需要回表的记录数 * 加载一个数据页的成本常数。如果这些主键 ID 是随机的,可能导致大量的随机 I/O,性能极差。
- CPU 成本:获取到完整的行数据后,可能需要再次用
WHERE
子句的其他条件进行过滤(因为索引可能只覆盖了部分查询条件)。
- 最终比较
优化器将 全表扫描的总成本
与 (索引扫描成本 + 回表成本)
进行对比,选择总成本更低的方案。
哪些情况下,优化器会认为“使用索引”比“全表扫描”更慢?
以下情况会导致索引成本过高,从而促使优化器放弃使用索引:
-
需要回表的记录数过多(最常见的原因)
- 场景:如果一个查询要返回表中超过 20% - 30% 的数据(这个阈值不是固定的,取决于表结构、磁盘速度等因素),使用索引并回表会产生大量的随机 I/O,其成本很可能超过顺序读取整个表的全表扫描。
- 例子:
SELECT * FROM products WHERE category_id = 5;
如果category_id=5
的产品占了全表的 40%,优化器很可能选择全表扫描。
-
索引的选择性(Cardinality)太低
- 选择性指索引列中不同值的数量与总记录数的比例。比例越高,选择性越好,索引过滤效果越强。
- 低选择性索引:例如在
gender
(性别)字段上建立索引,只有 ‘M‘ 和 ’F‘ 两个值。通过索引会筛选出大约一半的数据,然后进行大量回表操作,不如直接全表扫描。 - 你可以通过
SHOW INDEX FROM table_name;
查看Cardinality
值来大致判断选择性。
-
查询使用了强制类型转换或函数,导致索引失效
- 场景:
WHERE text_field = 123
(字段是字符串类型,条件是数字)或者WHERE YEAR(date_field) = 2023
。 - 原因:优化器无法使用索引来定位数据,因为需要对索引列中的每一行数据都应用函数或进行转换后才能做比较。这相当于扫描了整个索引,然后再过滤,其成本可能比全表扫描还高(因为索引树通常比数据表小,但扫描后依然要回表),因此优化器可能直接选择全表扫描。
- 场景:
-
统计信息不准确或过期
- MySQL 通过
STATISTICS
来了解表的数据分布情况(如每个索引有多少不同的值、数据的分布等),以便进行成本估算。 - 如果这些统计信息很久没有更新(例如表经过大量
DELETE
/UPDATE
后),优化器基于错误的信息可能会做出错误的判断。 - 可以使用
ANALYZE TABLE table_name;
来重新生成统计信息。
情况 原因 优化器可能的选择 需要返回大量数据 回表成本 > 全表扫描成本 全表扫描 索引选择性差(如性别字段) 索引筛选后数据量依然巨大,回表成本高 全表扫描 查询条件导致索引失效(如使用函数) 无法高效使用索引进行定位,相当于要扫描整个索引 全表扫描(或索引扫描) 统计信息过期 成本计算错误,误判 可能选错 - MySQL 通过
SQL 提示
是优化数据库的一个重要手段,简单来说,就是在SQL语句中加入一些人为的提示来达到优化操作的目的。
使用索引:
explain select * from tb_user use index(idx_user_pro) where profession="软件工程";
不使用哪个索引:
explain select * from tb_user ignore index(idx_user_pro) where profession="软件工程";
必须使用哪个索引:
explain select * from tb_user force index(idx_user_pro) where profession="软件工程";
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, 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_users_email ON users(email(15));
前缀长度:可以根据索引的选择性来决定,而选择性是指不重复的索引值(基数)和数据表的记录总数的比值,索引选择性越高则查询效率越高,唯一索引的选择性是1,这是最好的索引选择性,性能也是最好的。
求选择性公式:
-- 计算不同前缀长度的选择性
SELECT COUNT(DISTINCT LEFT(column_name, 10)) / COUNT(*) AS selectivity_10,COUNT(DISTINCT LEFT(column_name, 15)) / COUNT(*) AS selectivity_15,COUNT(DISTINCT LEFT(column_name, 20)) / COUNT(*) AS selectivity_20,COUNT(DISTINCT column_name) / COUNT(*) AS full_selectivity
FROM table_name;select count(distinct email) / count(*) from tb_user;
select count(distinct substring(email, 1, 5)) / count(*) from tb_user;
show index 里面的sub_part可以看到接取的长度。
注意事项:
- 唯一索引 (
UNIQUE
): 如果您要创建的是唯一索引,不能使用前缀索引,因为前缀索引只保证前n
个字符是唯一的,不能保证整个字段的唯一性。 - 排序和分组: 如果查询中包含
ORDER BY column_name
或GROUP BY column_name
,但column_name
是前缀索引,MySQL 可能无法使用该索引进行排序/分组,仍需进行额外的文件排序(filesort
)。 - 覆盖索引: 前缀索引不能用于覆盖扫描(即查询的字段都在索引中)。因为索引只包含了字段的一部分,而不是完整值。
前缀索引查询详细流程
假设我们有一个 user
表,结构如下:
id
INT (主键)email
VARCHAR(255)- 其他字段…
并在 email
字段上创建了一个前缀索引:
CREATE INDEX idx_email ON user(email(15)); -- 假设前缀长度为15
执行查询:
SELECT * FROM user WHERE email = '11345679866@163.com';
具体查询流程如下:
- 解析查询并截取前缀
- MySQL 解析器识别到查询条件
email = '11345679866@163.com'
。 - 由于索引
idx_email
是前缀索引,只存储了email
字段的前15个字符。 - MySQL 会从查询值
'11345679866@163.com'
中截取前15个字符,得到'11345679866@16'
。- 注意:
'11345679866@163.com'
的前15个字符是'11345679866@16'
(11位数字 + @ + ‘16’)
- 注意:
- 在前缀索引中查找
- 使用截取后的值
'11345679866@16'
在辅助索引idx_email
的 B+Tree 中进行查找。 - 找到所有索引项中前缀与
'11345679866@16'
匹配的记录。 - 获取这些记录对应的主键 ID 集合。
- 回表查询(Row Lookup)
- 对于上一步得到的每个主键 ID,回到聚簇索引(主键索引)中进行查找。
- 通过主键 ID 在聚簇索引的 B+Tree 中找到完整的行数据。
- 这一步可能涉及多次磁盘 I/O,尤其是当匹配的前缀索引项很多时。
- 完整字段验证
- 从聚簇索引中获取到完整的行数据后,MySQL 会比较完整的
email
字段值与查询条件'11345679866@163.com'
。 - 这是至关重要的一步:因为前缀索引只保证了前15个字符匹配,但无法保证整个字符串完全匹配。
- 只有完整字段值完全匹配的行才会被加入到结果集中。
- 返回结果
- 将所有通过验证的行返回给客户端。
单列索引&联合索引
单列索引:即一个索引只包含单个列
联合索引:即一个索引包含了多个列
在业务场景中,如果存在多个查询条件,考虑针对于查询字段建立索引时,建议建立联合索引,而非单列索引。
单列索引情况:
explain select id, phone, name from tb_user where phone = '17799990010' and name = '韩信';
这句只会用到phone索引字段
- 多条件联合查询时,MySQL优化器会评估哪个字段的索引效率更高,会选择该索引完成本次查询
设计原则
- 针对于数据量较大,且查询比较频繁的表建立索引
- 针对于常作为查询条件(where)、排序(order by)、分组(group by)操作的字段建立索引
- 尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高
- 如果是字符串类型的字段,字段长度较长,可以针对于字段的特点,建立前缀索引
- 尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表,提高查询效率
- 要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价就越大,会影响增删改的效率
- 如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它。当优化器知道每列是否包含NULL值时,它可以更好地确定哪个索引最有效地用于查询
4、性能分析
SQL执行频率
通过SQL执行频率来确定,当前数据增删改查的比例,从而确定对应优化方案。如果增删改为主,优化程度可以放轻。
查看执行频次
通过show [session|global] status 命令可以提供服务器状态信息
查看当前数据库的 INSERT, UPDATE, DELETE, SELECT 访问频次:
SHOW GLOBAL STATUS LIKE 'Com_______';SHOW SESSION STATUS LIKE 'Com_______';
慢查询日志
慢查询日志记录了所有执行时间超过指定参数(long_query_time,单位:秒,默认10秒)的所有SQL语句的日志。
通过慢查询日志可以定位出哪些sql语句执行耗时比较长,从而对这类sql语句进行优化。
MySQL的慢查询日志默认没有开启,需要在MySQL的配置文件(/etc/my.cnf)(Linux)中配置如下信息:
C:\ProgramData\MySQL\MySQL Server 8.0\my.ini(windows)
# 开启慢查询日志开关
slow_query_log=1
# 设置慢查询日志的时间为10秒,SQL语句执行时间超过10秒,就会视为慢查询,记录慢查询日志
long_query_time=10
更改后记得重启MySQL服务,日志文件位置:/var/lib/mysql/localhost-slow.log(Linux)
C:\ProgramData\MySQL\MySQL Server 8.0\Data\XXXXX-XXXXX-slow.log(windows)
查看慢查询日志开关状态:
show variables like 'slow_query_log';#默认是OFF
profile(查看SQL语句耗时)
show profile 能在做SQL优化时帮我们了解时间都耗费在哪里。通过 have_profiling 参数,能看到当前 MySQL 是否支持 profile 操作:
SELECT @@have_profiling;
profiling 默认关闭,可以通过set语句在session/global级别开启 profiling:
SELECT @@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;
explain执行计划
EXPLAIN 或者 DESC 命令获取 MySQL 如何执行 SELECT 语句的信息,包括在 SELECT 语句执行过程中是否用到了索引,表如何连接和连接的顺序。
语法:
# 直接在select语句之前加上关键字 explain / desc
EXPLAIN SELECT 字段列表 FROM 表名 HWERE 条件;
-- 创建student表
CREATE TABLE student (id INT PRIMARY KEY AUTO_INCREMENT,name VARCHAR(100) NOT NULL,no VARCHAR(50) UNIQUE NOT NULL
);-- 创建course表
CREATE TABLE course (id INT PRIMARY KEY AUTO_INCREMENT,name VARCHAR(100) NOT NULL
);-- 创建student_course关联表
CREATE TABLE student_course (id INT PRIMARY KEY AUTO_INCREMENT,studentid INT NOT NULL,courseid INT NOT NULL,FOREIGN KEY (studentid) REFERENCES student(id),FOREIGN KEY (courseid) REFERENCES course(id),UNIQUE KEY unique_enrollment (studentid, courseid) -- 防止重复选课
);
explain SELECT s.*,c.*
FROM student s,course c, student_course sc
WHERE s.id = sc.studentid
AND c.id = sc.courseid;
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
NULL性能最好, ALL 性能最差。
访问系统表时为system。
一般通过主键或唯一索引访问时是const。
ref:通过非唯一索引进行访问。
all:全表扫描。
- possible_key:可能应用在这张表上的索引,一个或多个
- Key:实际使用的索引,如果为 NULL,则没有使用索引
- Key_len:表示索引中使用的字节数,该值为索引字段最大可能长度,并非实际使用长度,在不损失精确性的前提下,长度越短越好
- rows:MySQL认为必须要执行的行数,在InnoDB引擎的表中,是一个估计值,可能并不总是准确的
- filtered:表示返回结果的行数占需读取行数的百分比,filtered的值越大越好
- extra:额外信息
三、SQL优化
插入数据
普通插入:
1.采用批量插入(一次插入的数据不建议超过1000条)
insert into test values(1,'Tom'),(2,'Cat'),(3,'Jerry');
2.手动提交事务
start transaction;
insert into test values(1,'Tom'),(2,'Cat'),(3,'Jerry');
insert into test values(5,'Tom'),(6,'Cat'),(7,'Jerry');
insert into test values(4,'Tom'),(8,'Cat'),(9,'Jerry');
commit;
3.主键顺序插入(主键顺序插入性能优于乱序插入)
大批量插入:
如果一次性需要插入大批量数据,使用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';
主键优化
数据组织方式:在InnoDB存储引擎中,表数据都是根据主键顺序组织存放的,这种存储方式的表称为索引组织表(Index organized table, IOT)
页分裂:主键乱序插入可能会导致页分裂现象,页可以为空,也可以填充一般,也可以填充100%,每个页包含了2-N行数据(如果一行数据过大,会行溢出),根据主键排列。
页合并:当删除一行记录时,实际上记录并没有被物理删除,只是记录**被标记(flaged)**为删除并且它的空间变得允许被其他记录声明使用。当页中删除的记录到达 MERGE_THRESHOLD(默认为页的50%),InnoDB会开始寻找最靠近的页(前后)看看是否可以将这两个页合并以优化空间使用。
MERGE_THRESHOLD:合并页的阈值,可以自己设置,在创建表或创建索引时指定
主键设计原则:
- 满足业务需求的情况下,尽量降低主键的长度
- 插入数据时,尽量选择顺序插入,选择使用 AUTO_INCREMENT 自增主键
- 尽量不要使用 UUID 做主键(UUID无序,插入时可能会导致也分裂)或者是其他的自然主键,如身份证号
- 业务操作时,避免对主键的修改
order by优化
- Using filesort:通过表的索引或全表扫描,读取满足条件的数据行,然后在排序缓冲区 sort buffer 中完成排序操作,所有不是通过索引直接返回排序结果的排序都叫 FileSort 排序。
- Using index:通过有序索引顺序扫描直接返回有序数据,这种情况即为 using index,不需要额外排序,操作效率高
- Backward index sacn:反向扫描索引
如果order by字段顺序与索引建立的字段顺序不一致,则不会走索引,explain的extra信息显示的是Using index, Using filesort
,
多字段排序时,也遵循最左前缀法则。
#建立的索引顺序 age phone
select id,age,phone from user order by phone,age; --Using index, Using filesortselect id,age,phone from user order by age,phone;
如果order by字段全部使用升序排序或者降序排序,则都会走索引,但是如果一个字段升序排序,另一个字段降序排序,则不会走索引,
select id,age,phone from user order by age asc,phone desc; --Using index, Using filesort
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 user order by age asc, phone desc; --会全部走索引
总结:
- 根据排序字段建立合适的索引,多字段排序时,也**遵循最左前缀法则****
- 尽量使用覆盖索引
- 多字段排序,一个升序一个降序,此时需要注意联合索引在创建时的规则(ASC/DESC)
- 如果不可避免出现filesort,大数据量排序时,可以适当增大排序缓冲区大小 sort_buffer_size(默认256k)
group by优化
GROUP BY
的本质是先排序再分组。因此,优化 GROUP BY
的规则与优化 ORDER BY
的规则完全一致。
-
如果
GROUP BY
的字段顺序符合最左前缀法则,MySQL 就可以利用索引已经排好序的特性,直接分组,而无需额外的排序操作。 -
USE TEMPORARY:如果不符合,MySQL 则需要先创建一个临时表,然后在临时表上进行排序和分组,性能会差很多。
-
在分组操作时,可以通过索引来提高效率
-
分组操作时,索引的使用也是满足最左前缀法则的
如索引为idx_user_pro_age_stat
,则句式可以是select ... where profession (=、IN) order by age
,这样也符合最左前缀法则
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 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;
count优化
MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个数,效率很高(前提是不适用where);
InnoDB 在执行 count(*) 时,需要把数据一行一行地从引擎里面读出来,然后累计计数。
优化方案:自己计数,如创建key-value表存储在内存或硬盘,或者是用redis
count的几种用法:
- 如果count函数的参数(count里面写的那个字段)不是NULL(字段值不为NULL),累计值就加一,最后返回累计值
- 用法:count(*)、count(主键)、count(字段)、count(1)
- count(主键)跟count(*)一样,因为主键不能为空;count(字段)只计算字段值不为NULL的行;count(1)引擎会为每行添加一个1,然后就count这个1,返回结果也跟count()一样;count(null)返回0。
各种用法的性能:
- count(主键):InnoDB引擎会遍历整张表,把每行的主键id值都取出来,返回给服务层,服务层拿到主键后,直接按行进行累加(主键不可能为空)
- count(字段):没有not null约束的话,InnoDB引擎会遍历整张表把每一行的字段值都取出来,返回给服务层,服务层判断是否为null,不为null,计数累加;有not null约束的话,InnoDB引擎会遍历整张表把每一行的字段值都取出来,返回给服务层,直接按行进行累加
- count(1):InnoDB 引擎遍历整张表,但不取值。服务层对于返回的每一层,放一个数字 1 进去,直接按行进行累加
- count(*):InnoDB 引擎并不会把全部字段取出来,而是专门做了优化,不取值,服务层直接按行进行累加
按效率排序:count(字段) < count(主键) < count(1) < count(**)。
update优化(避免行锁升级为表锁)
InnoDB 的行锁是针对索引加的锁,不是针对记录加的锁,并且该索引不能失效,否则会从行锁升级为表锁(并发性能就会降低)。
如以下两条语句:
update student set no = '123' where id = 1;#第一个进程执行,这句由于id有主键索引,所以只会锁这一行;
update student set no = '1234' where id = 4;#第二个进程执行,执行成功
由于id有主键索引,只会给id = 1 的这一行数据上锁,id = 4 的这一行数据不会上锁,所以会操作成功。
update student set no = '123' where name = 'test';#这句由于name没有索引,所以会把整张表都锁住进行数据更新,解决方法是给name字段添加索引
update student set no = '1234' where id = 4;#第二个进程执行,进行阻塞
由于name没有索引,所以会把整张表都锁住,此时进行数据更新,会进行阻塞。解决方法是给name字段添加索引。
更新某个字段时是一定要走索引,否则走全表扫描会变成表级锁。
四、视图
视图(View)是一种虚拟存在的表。视图中的数据并不在数据库中实际存在,行和列数据来自定义视图的查询中使用的表,并且是在使用视图时动态生成的。
通俗的讲,视图只保存了查询的SQL逻辑,不保存查询结果。所以我们在创建视图的时候,主要的工作就落在创建这条SQL查询语句上。
视图(View)的本质就是一个被存储起来的 SELECT
查询语句,数据库引擎会将其包装成一个虚拟表供用户使用。
核心本质:保存的查询
当你创建一个视图时,数据库并不是将查询结果保存下来,而是保存了构建这个结果的 SELECT
语句。
虚拟表的含义
- 不存储数据:视图不像普通表(基表)那样在磁盘上存储数据行。它只是一个逻辑表。
- 动态生成:每次查询视图时,数据库都会实时执行其背后的
SELECT
语句,从基表中获取最新数据并生成结果集。 - 架构(Schema):视图拥有类似表的架构,包括列名、数据类型等,这些都源自其背后的查询。
创建视图
CREATE [OR REPLACE] VIEW 视图名称(列名列表)】AS SELECT语句[WITH[CASCADED|LOCAL] CHECK OPTION];-- 创建视图
create or replace view stu_v_1 as select id, name from student where id <= 10;
查询视图
查看创建视图语句:SHOW CRETE VIEW 视图名称;
查看视图数据:查看视图数据:SELECT*FROM 视图名称…;
-- 查看创建视图语句
SHOW CRETE VIEW 视图名称;
-- 查看视图数据
SELECT*FROM 视图名称…;-- 查询视图
show create view stu_v_1;
select * from stu_v_1;
修改视图
-- 方式一:
CREATE [OR REPLACE]VIEW 视图名称(列名列表)AS SELECT语句[WITH[CASCADED | LOCAL] CHECK OPTION;
-- 方式二:
ALTER VEW 视图名称(列名列表)AS SELECT语句[WITH[CASCADED|LOCAL]CHECK OPTION];-- 修改视图
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]视图名称[,视图名称];-- 删除视图
drop view if exists stu_v_1;/* */
检查选项
视图的检查选项:
当使用WITH CHECK OPTION子句创建视图时,MySQL会通过视图检查正在更改的每个行,例如 插入,更新,删除,以使其符合视图的定义。MVSOL允许基于另一个视图创建视图,它还会检查依赖视图中的规则以保持一致性。
为了确定检查的范围,MySQL提供了两个选项:CASCADED 和 LOCAL,默认值为CASCADED(级联)。
cascaded:在对创建时含有该字段的视图,插入数据时,该视图依赖的视图都会加上检查,需要所有条件都满足才能够插入成功。
local:在对创建时含有该字段的视图,插入数据时,对于该视图依赖的视图中含有检查语句的条件进行检查判断。
-- 创建视图 stu_v_1,只包含id<=20的学生信息
create or replace view stu_v_1 as
select id,name from student where id <= 20;-- 成功:id=5满足视图的where条件(id<=20)
insert into stu_v_1 values (5,'Tom');-- 失败:id=25不满足视图的where条件(id<=20)
insert into stu_v_1 values (25,'Tom');-- 创建视图 stu_v_2,基于stu_v_1并添加id>=10的条件,同时启用级联检查选项
-- with cascaded check option 会强制检查当前视图和所有底层基视图的条件
create or replace view stu_v_2 as
select id,name from stu_v_1 where id >= 10
with cascaded check option;-- 失败:id=7不满足stu_v_2的id>=10条件(级联检查生效)
insert into stu_v__2 values (7,'Tom');-- 失败:id=25不满足底层视图stu_v_1的id<=20条件(级联检查生效)
insert into stu_v_2 values (25,'Tom');-- 成功:id=15同时满足stu_v_1(id<=20)和stu_v_2(id>=10)的条件
insert into stu_v_2 values (15,'Tom');-- 创建视图 stu_v_3,基于stu_v_2并添加id<=15的条件
-- 注意:这里没有使用with check option,所以插入时不会检查stu_v_3自身的条件
create or replace view stu_v_3 as
select id,name from stu_v_2 where id <= 15;-- 成功:id=11满足所有视图的条件(stu_v_1: id<=20, stu_v_2: id>=10, stu_v_3: id<=15)
insert into stu_v_3 values (11,'Tom');-- 成功:虽然id=17不满足stu_v_3的id<=15条件
-- 但由于创建stu_v_3时没有声明with check option,MySQL不会检查stu_v_3自身的条件
-- 只会级联检查底层视图stu_v_2(有cascaded check option)和stu_v_1的条件
-- id=17满足stu_v_2(id>=10)和stu_v_1(id<=20)的条件,因此插入成功
insert into stu_v_3 values (17,'Tom');-- 失败:id=28不满足底层视图stu_v_1的id<=20条件
-- 虽然stu_v_3没有检查选项,但级联检查会追溯到有检查选项的底层视图
insert into stu_v_3 values (28,'Tom');-- 创建视图 stu_v_4,定义只显示id<=20的学生(但无检查选项,插入时不强制校验)
create or replace view stu_v_4 as
select id,name from student1 where id <= 20;-- 成功:id=5<=20,符合视图定义(但并非因为校验,而是因为没有检查选项直接插入基表)
insert into stu_v_4 values (5,'Tom');-- 成功:id=25>20,不符合视图定义,但由于无检查选项,数据仍会插入基表(在视图中不可见)
insert into stu_v_4 values (25,'Tom');-- 创建视图 stu_v_5,基于stu_v_4并添加id>=10的条件,启用本地检查选项
create or replace view stu_v_5 as
select id,name from stu_v_4 where id >= 10
with local check option;-- 失败:id=7<10,不满足stu_v_5自身的条件(LOCAL检查选项生效)
insert into stu_v_5 values (7,'Tom');-- 成功:id=27>=10,满足stu_v_5的条件(LOCAL只检查当前视图,不检查底层stu_v_4的id<=20条件)
-- 注意:数据会插入基表,但在stu_v_4中不可见(因为27>20)
insert into stu_v_5 values (27,'Tom');-- 成功:id=15>=10,满足stu_v_5的条件
insert into stu_v_5 values (15,'Tom');-- 创建视图 stu_v_6,基于stu_v_5并添加id<=15的条件(无检查选项)
create or replace view stu_v_6 as
select id,name from stu_v_5 where id <= 15;-- 成功:stu_v_6无检查选项,向上找到stu_v_5的LOCAL检查,只校验id>=10(11>=10)
-- stu_v_6自身条件(id<=15)和stu_v_4条件(id<=20)不被检查
insert into stu_v_6 values (11,'Tom');-- 成功:stu_v_6无检查选项,向上找到stu_v_5的LOCAL检查,只校验id>=10(17>=10)
-- 注意:id=17>15,不符合stu_v_6定义,但数据仍会插入基表(在stu_v_6中不可见)
insert into stu_v_6 values (17,'Tom');-- 成功:stu_v_6无检查选项,向上查找直到stu_v_5(有LOCAL检查选项)
-- 只校验stu_v_5的条件:id>=10(28>=10,满足条件)
-- 不校验stu_v_6自身的条件(id<=15),因为stu_v_6无检查选项
-- 不校验stu_v_4的条件(id<=20),因为:
-- 1. stu_v_5使用LOCAL选项,只检查自身条件,不传播检查到底层视图
-- 2. stu_v_4本身无检查选项
-- 因此数据会插入基表,但在stu_v_4和stu_v_6中均不可见
insert into stu_v_6 values (28,'Tom');
关键概念总结:
WITH CHECK OPTION
:确保通过视图插入或修改的数据满足视图定义的WHERE条件。CASCADED
(级联):检查当前视图和所有底层基视图的条件。LOCAL
(本地):只检查当前视图的条件(MySQL中与CASCADED行为相同)。- 检查范围:如果没有
WITH CHECK OPTION
,插入操作只需要满足有检查选项的视图的条件。
视图的创建原理
数据库系统会在数据字典中存储视图的定义(即那条 SELECT
语句)。当用户对视图进行查询时,SQL 引擎的工作流程如下:
- 解析用户对视图的查询。
- 从数据字典中取出视图的定义。
- 将用户的查询与视图的定义合并,转换成对底层基表的等价查询。
- 执行这个转换后的查询并返回结果。
这个过程称为视图消解(View Resolution)。
视图的优缺点
基于其“虚拟表”的本质,视图具有以下优点和缺点:
✅ 优点:
-
简化复杂查询:将多表连接、复杂过滤和计算的查询封装起来,用户只需像查单表一样操作。
-
增强安全性:可以屏蔽基表的敏感字段。只授予用户访问视图的权限,而不是底层基表的权限。
-- 创建一个不包含工资和身份证号的视图给普通员工看 CREATE VIEW vw_employee_public AS SELECT emp_id, name, dept_name FROM vw_employee_details;
-
逻辑数据独立性:如果基表的 schema 发生变化(如字段名修改、表拆分),只需修改视图的定义即可保证依赖它的应用程序无需改动。
-
简化权限管理:只需对视图授权一次,而不需要对多个基表分别授权。
❌ 缺点:
- 性能可能不佳:复杂的视图可能涉及多表连接和聚合,每次查询视图都会执行这些操作,可能无法像优化单表查询那样被充分优化。但现代数据库的优化器已经非常智能,通常能很好地进行处理。
- 更新限制:并非所有视图都是可更新的(Updatable View)。通常只有基于单表的、不包含聚合函数、
DISTINCT
、GROUP BY
的简单视图才允许进行INSERT
/UPDATE
/DELETE
操作。这些操作最终会映射到对基表的修改。
视图 vs. 普通表
特性 | 视图 (View) | 普通表 (Table) |
---|---|---|
物理存储 | 不存储数据,只存储定义 | 存储数据 |
数据来源 | 动态查询基表生成 | 直接存储 |
更新性 | 多数不可更新 | 可以更新 |
索引 | 通常不能在视图上创建索引* | 可以创建索引 |
性能 | 每次查询可能需计算 | 直接读取,性能高 |
*(注意:有些数据库,如 SQL Server、Oracle,支持物化视图 Materialized View,它确实物理存储数据,可以创建索引,但这属于高级特性,与普通视图不同。)
视图的更新
要使视图可更新,视图中的行与基础表中的行之间必须存在一对一的关系。
如果视图包含以下任何一项,则该视图不可更新:
- 聚合函数或窗口函数(SUM()、MIN()、MAX()、COUNT()等
- DISTINCT
- GROUP BY
- HAVINGA
- UNION 或者 UNION ALL
作用:
- 简单
视图不仅可以简化用户对数据的理解,也可以简化他们的操作。那些被经常使用的查询可以被定义为视图,从而使得用户不必为以后的操作每次指定全部的条件。 - 安全
数据库可以授权,但不能授权到数据库特定行和特定的列上。通过视图用户只能查询和修改他们所能见到的数据。 - 数据独立
视图可帮助用户屏蔽真实表结构变化带来的影响。
案例
-- 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;
五、存储过程
存储过程其实就类似 java,c 这种语言,这一部分可以通过文档快速学习,不懂的再回过头看视频。
存储过程是事先经过编译并存储在数据库中的一段 SQL语句的集合,调用存储过程可以简化应用开发人员的很多工作,减少数据在数据库和应用服务器之间的传输,对于提高数据处理的效率是有好处的。
存储过程思想上很简单,就是数据库 SOL语言层面的代码封装与重用。
特点:
- 封装,复用
- 可以接收参数,也可以返回数据
- 减少网络交互,效率提升
基本语法
查看视图数据:SELECT*FROM 视图名称…;
查看:
SELECT* FROM INFORMATION SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA='xx';--查询数据库的存储过程及状态信息
SHOW CREATE PROCEDURE 存储过程名称;--查询某个存储过程的定义
删除:
DROP PROCEDURE [IF EXISTS]存储过程名称;
案例:
-- 存储过程基本语法
-- 创建
create procedure p1()
beginselect count(*)from student;
end;-- 调用
call p1();-- 查看
select * from information_schema.ROUTINES where ROUTINE_SCHEMA = 'zzz';
show create procedure p1;-- 删除
drop procedure if exists p1;
变量
系统变量
系统变量 是MySQL服务器提供,不是用户定义的,属于服务器层面。分为全局变量(GLOBAL)、会话变量(SESSION)。
查看系统变量
SHOW [SESSION |GLOBAL] VARIABLES ; --查看所有系统变量
SHOW[SESSION|GLOBAL] VARIABLES LIKE ''; --可以通过LKE模糊匹配方式查找变量
SELECT @@[SESSION|GLOBAL] 系统变量名; -- 查看指定变量的值
-- 变量:系统变量
-- 查看系统变量
show session variables;
show session variables like 'auto%';
show global variables like 'auto%';
select @@global.autocommit;-- 设置系统变量
set global autocommit = 0;
insert into course(name) values ('ES');
COMMIT;
注意:
- 如果没有指定 session / global,默认 session,会话变量
- mysql 服务器重启之后,所设置的全局参数会失效,要想不失效,需要更改/etc/my.cnf 中的配置。
用户定义变量
用户定义变量 是用户根据需要自己定义的变量,用户变量不用提前声明,在用的时候直接用“@变量名”使用就可以。其作用域为当前连接。
赋值:
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 = 'zzz';
set @myage := 10;
set @mygender := '男',@myhobby := 'java';select @mycolor := 'red';
select count(*) into @mycount from tb_user;-- 使用
select @myname, @myage, @mycolor, @mycount,@mygender,@myhobby;select @abc; -- 输出为NULL
注意:
用户定义的变量无需对其进行声明或者初始化,只不过获取到的值为 NULL。
局部变量
局部变量 是根据需要定义的在局部生效的变量,访问之前,需要DECLARE声明。可用作存储过程内的局部变量和输入参数,局部变量的范围是在其内声明的BEGIN … END块。
声明:
DECLARE 变量名 变量类型 [DEFAULT..];
变量类型就是数据库字段类型:INT、BIGINT、CHAR、VARCHAR、DATE、TIME等。
赋值:
SET 变量名=值;
SET 变量名:=值;
SELECT 字段名 INTO 变量名 FROM 表名 ...;
案例:
-- 变量:局部变量
-- 声明 - declare
-- 赋值 -
create procedure p2()
begindeclare stu_count int default 0;select count(*) into stu_count from student;select stu_count;
end;call p2();
if 判断sql
语法:
IF 条件1 THEN...
ELSEIF 条件2 THEN -- 可选...
ELSE -- 可选...
END IF;
案例:
create procedure p3()
begindeclare score int default 58;declare result varchar(10);if score >= 85 thenset result :='优秀';elseif score >= 60 thenset result :='及格';elseset 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 p4(in score int, out result varchar(10))
beginif score >= 85 thenset result :='优秀';elseif score >= 60 thenset result :='及格';elseset result :='不及格';end if;select result;
end;set @score = 18;
CALL p4(@score,@result);
SELECT @result;-- 将传入的200分制的分数,进行换算,换算成百分制,然后返回分数 --> inout
create procedure p5(inout score double)
beginset score := score * 0.5;
end;set @score = 198;
call p5(score);
select @score;
case
语法一:
CASE case valueWHEN when_value1 THEN statement_list1[WHEN when_value2 THEN statement_list2]...[ELSE statement_list ]
END CASE;
语法二:
CASEWHEN search_conditionl THEN statement_list1WHEN 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 thenset result := '第一季度';when month >= 4 and month <= 6 thenset result := '第二季度';when month >= 7 and month <= 9 thenset result := ' 第三季度';when month >= 10 and month <= 12 thenset result := '第四季度';elseset result := '非法参数';end case;select concat('你输入的月份为:', month, ',所属季度为:', result);
end;SET @month := 10;
CALL p6(@month);
循环
while
while 循环是有条件的循环控制语句。满足条件后,再执行循环体中的SQL语句。
语法:
#先判定条件,如果条件为true,则执行逻辑,否则,不执行逻辑
WHILE 条件 DOSQL逻辑...
END WHILE;
案例:
-- while计算从1累加到 n 的值,n 为传入的参数值。
-- A.定义局部变量,记录累加之后的值;
-- B.每循环一次,就会对 n 进行减1,如果 n 减到0,则退出循环create procedure p7(in n int)
begindeclare total int default 0;while n>0 doset total := total + n;set n:=n-1;end while;select total;
end;SET @n := 100;
call p7(@n);
repeat
repeat是有条件的循环控制语句,当满足条件的时候退出循环。
与 while 区别:
- 先进行循环一次再判断。相当于 c 语言中的 do while();
- 满足条件则退出
语法:
#先执行一次逻辑,然后判定逻辑是否满足,如果满足,则退出。如果不满足,则继续下一次循环
REPEATSOL逻辑.UNTIL 条件
END REPEAT;
案例:
-- while计算从1累加到 n 的值,n 为传入的参数值。
-- A.定义局部变量,记录累加之后的值;
-- B.每循环一次,就会对 n 进行减1,如果 n 减到0,则退出循环create procedure p8(in n int)
begindeclare total int default 0;repeatset total := total + n;set n := n - 1;until n <= 0end repeat;select total;
end;call p8(100);
loop
LOOP 实现简单的循环,如果不在SQL逻辑中增加退出循环的条件,可以用其来实现简单的死循环。LOOP可以配合一下两个语句使用。
- LEAVE:配合循环使用,退出循环。
- ITERATE:必须用在循环中,作用是跳过当前循环剩下的语句,直接进入下一次循环。
[begin label:] LOOPSQL逻辑..
END LOOP [end label];LEAVE label; -- 退出指定标记的循环体
ITERATE label;-- 直接进入下一次循环
案例:
-- loop 计算从1到n之间的偶数累加的值,n为传入的参数值。
-- A.定义局部变量,记录累加之后的值;
-- B.每循环一次,就会劝进行-1,如果n减到0,则退出循环。------> leave xx
-- C.如果当次累加的数据是奇数,则直接进入下一次循坏。-------> iterate xxcreate procedure p10(in n int)
begin declare total int defatult 0;sum: loopif n <= 10 thenleave sum;end if;if n %2 = 1 thenset n := n - 1;iterate sum;end if;set total := total + n;set n := n - 1;end loop sum;select total;
end;CALL p9(100);
游标-cursor
游标(CURSOR)是用来存储查询结果集的数据类型,在存储过程和函数中可以使用游标对结果集进行循环的处理。游标的使用包括游标的声明、OPEN、FETCH和 CLOSE,其语法分别如下。
通俗点讲:类似于 c 语言中的结构体,java 中的实体类。
声明游标
DECLARE 游标名称 CURSOR FOR 查询语句;
**打开游标:**sql
OPEN 游标名称;
获取游标记录:
FETCH 游标名称 INTO 变量[,变量];
关闭游标:
CLOSE 游标名称;
案例:
-- 游标
-- 根据传入的参数uage,来查询用户表tb_user 中, 所有的用户年龄小于uage的用户姓名(name)和专业(profession),
-- 并将用户的姓名和专业插入到所创建的一张新表(id,name,profession)中。
-- 逻辑:
-- A.声明游标,存储查询结果集-
-- B.准备:创建表结构
-- C.开启游标-
-- D.获取游标中的记录
-- E.插入数据到新表中-
-- F.关闭游标create procedure p10(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 dofetch u_cursor into uname,upro;insert into tb_user_pro values(null, uname, upro);end while;close u_cursor;
end;CALL p10(30);
ps: 普通变量的声明和游标的声明是有先后顺序的,游标要在后面。
此时报错,但是表还是创建了,数据也插入进去了。因为我们循环条件是true,此时会一直循环下去,所以就会告诉你游标中已经没有数据可以去获取了,这时就要用到条件处理程序handler来终止我们的循环。
条件处理程序-handler
条件处理程序(Handler)可以用来定义在流程控制结构执行过程中遇到问题时相应的处理步骤。
语法:
DECLARE handler action HANDLER FOR condition value l, condition value.... statement;
handler actionCONTINUE: 继续执行当前程序EXIT: 终止执行当前程序
condition valueSQLSTATE sqlstate_value: 状态码,如 02000SQLWARNING: 所有以01开头的SQLSTATE代码的简写NOT FOUND: 所有以02开头的SQLSTATE代码的简写SQLEXCEPTION: 所有没有被SQLWARNING 或 NOT FOUND捕获的SQLSTATE代码的简写
案例:
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 dofetch u_cursor into uname,upro;insert into tb_user_pro values(null, uname, upro);end while;close u_cursor;
end;
此时便不会报错:
存储函数
存储函数是有返回值的存储过程,存储函数的参数只能是IN类型的。
存储函数用的较少,能够使用存储函数的地方都可以用存储过程替换。
语法:
CREATE FUNCTION 存储函数名称([ 参数列表 ])
RETURNS type [characteristic ...]
BEGIN-- SQL语句RETURN ...;
END ;
characteristic说明:
· DETERMINISTIC:相同的输入参数总是产生相同的结果
· NO SQL:不包含 SQL语句。
· READS SQL DATA:包含读取数据的语句,但不包含写入数据的语句,
案例:
create function fun1(n int)
returns int DETERMINISTIC
begindeclare total int default 0;while n > 0 do set total := total + n;set n := n - 1;end while;return total;
end;
我们必须指定一个选项,否则报错1418 - This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA in its declaration and binary logging is enabled (you might want to use the less safe log_bin_trust_function_creators variable)
加上特性:
存储函数(Function) vs 存储过程(Procedure)
特性 | 存储函数 | 存储过程 |
---|---|---|
返回值 | 必须有一个返回值 | 可以有零个或多个输出参数 |
调用方式 | 在SQL语句中直接调用 | 使用CALL语句调用 |
使用场景 | 计算并返回单个值 | 执行一系列操作,可能修改数据 |
事务处理 | 通常不包含事务 | 可以包含事务 |
执行权限 | 通常需要EXECUTE权限 | 通常需要EXECUTE权限 |
六、触发器
触发器是与表有关的数据库对象,指在 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 --行级触发器
BEGINtrigger_stmt;
END;
查看:
SHOW TRIGGERS;
删除:
DROP TRIGGER [schema_name.]trigger_name; --如果没有指定 schema_name,默认为当前数据库
案例:
-- 创建user_logs
CREATE TABLE user_logs (id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,operation VARCHAR(50) NOT NULL COMMENT '操作类型,insert/update/delete',operate_time DATETIME NOT NULL COMMENT '操作时间',operate_id INT NOT NULL COMMENT '操作的id',operate_params VARCHAR(500) COMMENT '操作参数'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- 插入数据触发器
create trigger tb_user_insert_triggerafter insert on tb_user for each rowbegin 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, phone, email, profession, age, gender, status, createtime) values(25, '二皇子', '1880901212', 'erhuangzi@163.com', '软件工程', 23, '1', '1', now());-- 修改数据触发器
create trigger tb_user_update_triggerafter update on tb_user for each rowbegin 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 procession = '会计' where id = 23;
update tb_user set procession = '会计' where id <= 5; -- 触发器为行级触发器,所以更改几行数据则出发几次,该语句出发5次-- 删除数据触发器
create trigger tb_user_delete_triggerafter delete on tb_user for each rowbegin insert into user_logs(id, operation, operate_time, operate_id, operate_params)values(null, 'insert', now(), old.id, concat('删除之前的数据:id=', old.id, ',name=', old.name, ', phone=', old.phone, ', email=', old.email, ', profession=', old.profession));
end;delete from tb_user where id = 26;
七、锁
介绍:
锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中,除传统的计算资源(CPU、RAM、I/0)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。
分类:
MySQL中的锁,按照锁的粒度分,分为一下三类:
- 全局锁:锁定数据库中的所有表。
- 表级锁:每次操作锁住整张表。
- 行级锁:每次操作锁住对应的行数据。
全局锁
介绍:
全局锁就是对整个数据库实例加锁,加锁后整个实例就处于只读状态,后续的DML的写语句,DDL语句,已经更新操作的事务提交语句都将被阻塞。
其典型的使用场景是做全库的逻辑备份,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性。
基本操作:
-- 使用全局锁:
flush tables with read lock;
-- 释放全局锁:
unlock tables;
默认情况下,这把锁只施加在所有非系统数据库的业务表上
特点:
数据库中加全局锁,是一个比较重的操作,存在以下问题:
- 如果在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆。
- 如果在从库上备份,那么在备份期间从库不能执行主库同步过来的二进制日志(binlog),会导致主从延迟。(该结构会在后续主从复制讲解)
解决方法:
在InnoDB引擎中,我们可以在备份时加上参数 –single-transaction 参数来完成不加锁的一致性数据备份。(快照读实现)
mysqldump --single-transaction -uroot -p123456 itcast > itcast.sql
(只适用于支持「可重复读隔离级别的事务」的存储引擎)
原理补充:通过加上这个参数,确保了在备份开始时创建一个一致性的快照,通过启动一个新的事务来实现这一点。(该事务的隔离级别是Repeatable Read级别),从而实现在该事务读取下一直读取的是创建时的数据,而不影响其他事务的读写操作。
表级锁
每次操作锁住整张表。锁定粒度大,发生锁的冲突的概率最高,并发度最低。应用在MyISAM、InnoDB、BDB等存储引擎中。
对于表级锁,主要分为一下三类:
- 表锁
- 元数据锁(meta data lock,MDL)
- 意向锁
表锁
对于表锁,分为两类:
- 表共享读锁(read lock)
- 表独占写锁(write lock)
读锁不会阻塞其他客户端的读,但是会阻塞写。
写锁既会阻塞其他客户端的读,又会阻塞其他客户端的写。
语法:
#表级别的共享锁,也就是读锁;
#允许当前会话读取被锁定的表,但阻止其他会话对这些表进行写操作。
lock tables tb_user read;#表级锁的独占锁,也是写锁;
#允许当前会话对表进行读写操作,但阻止其他会话对这些表进行任何操作(读或写)。
lock tables t_stuent write;
释放所有锁:
unlock tables #会话退出,也会释放所有锁
元数据锁
MDL(meta data lock)加锁过程是系统自动控制,无需显式使用,在访问一张表的时候会自动加上。MDL锁主要作用是维护表元数据的数据一致性,在表上有活动事务的时候,不可以对元数据进行写入操作。为了避免DML与DDL冲突,保证读写的正确性。
- 对一张表进行 CRUD 操作时,加的是 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都互斥 |
查看元数据锁:
select object_type,object_schema,object_name,lock_type,lock_duration from performance_schema.metadata_locks;
1. 进行 SELECT
查询时
加的锁:MDL共享读锁(MDL SHARED_READ)。
作用:这把锁的目的是保护表的结构。它确保在你查询的过程中,不会有其他会话(Session)来 ALTER TABLE
(修改表结构),否则会导致查询报错。
兼容性:MDL SHARED_READ
锁与其他的 MDL SHARED_READ
锁是兼容的。这意味着多个会话可以同时进行 SELECT
查询,互不阻塞。它也与 MDL SHARED_WRITE
锁兼容。
结论:SELECT
不会堵塞其他的 SELECT
或 INSERT
/UPDATE
/DELETE
,但会堵塞(或需要等待) 想要获取 MDL EXCLUSIVE
锁的 ALTER TABLE
等DDL操作。
2. 进行 INSERT
, UPDATE
, DELETE
时
加的锁:MDL共享写锁(MDL SHARED_WRITE)。
作用:同样是为了保护表结构。它确保在修改数据的过程中,表结构不会被改变。
兼容性:MDL SHARED_WRITE
锁与其他的 MDL SHARED_READ
和 MDL SHARED_WRITE
锁都是兼容的。这是一个非常关键的点。
- 这意味着一个会话在
UPDATE
时,不会阻止其他会话进行SELECT
或UPDATE
。 - 它们之间真正的阻塞和互斥,是在存储引擎层面通过行锁或表锁来控制的,而不是在MDL层面。
结论:DML操作不会因为MDL而堵塞其他的读或写操作。它们只会堵塞(或需要等待) 想要修改表结构的DDL操作。
3. 进行 ALTER TABLE
等DDL时
加的锁:MDL排他锁(MDL EXCLUSIVE)。
作用:修改表结构需要最高级别的权限,以确保操作的安全性。
兼容性:MDL EXCLUSIVE
锁与任何其他类型的MDL锁都不兼容。
- 这意味着,在
ALTER TABLE
执行期间(包括准备和提交阶段),它不仅会阻塞后续所有的SELECT
和 DML 操作,它自己也必须等待当前正在执行的所有SELECT
和 DML 操作全部结束并释放它们的MDL SHARED_READ
或MDL SHARED_WRITE
锁之后,才能开始执行。
结论:ALTER TABLE
是“霸道”的,它一来,所有人都得给它让路,并且它在工作时,别人什么都干不了。这正是线上业务最怕遇到的情况,因此很多公司会在业务低峰期执行DDL,或使用 pt-online-schema-change
(OSC) 等在线改表工具来避免长时间的MDL锁等待。
意向锁
为了避免DML在执行时,加的行锁与表锁的冲突,在InnoDB中引入了意向锁,使得表锁不用检查每行数据是否加锁,使用意向锁来减
少表锁的检查。
意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁(lock tables … read)和独占表锁(lock tables … write)发生冲突
如果没有「意向锁」,那么加「独占表锁」时,就需要遍历表里所有记录,查看是否有记录存在独占锁,这样效率会很慢。
那么有了「意向锁」,由于在对记录加独占锁前,先会加上表级别的意向独占锁,那么在加「独占表锁」时,直接查该表是否有意向独占锁,如果有就意味着表里已经有记录被加了独占锁,这样就不用去遍历表里的记录。
意向锁的目的是为了快速判断表里是否有记录被加锁。
加锁方式:
意向共享锁:(先在表上加上意向共享锁,然后对读取的记录加共享锁)
由 select ... lock in share mode
添加
意向独占锁:(先表上加上意向独占锁,然后对读取的记录加独占锁)
由 insert、update、delete、select ... for update
添加
总结
锁类别 | 主要目的 | 具体锁类型 | 说明 |
---|---|---|---|
表锁 | 粗粒度锁,直接锁定整张表。 | READ (读锁) | 共享锁,允许其他会话读,但不允许写。 |
WRITE (写锁) | 独占锁,不允许其他会话读或写。 | ||
元数据锁 | 保护表结构,防止正在查询或修改时,表结构被更改。 | SHARED_READ / SHARED_WRITE | DML操作(SELECT, INSERT, UPDATE等)自动加。 |
EXCLUSIVE | DDL操作(ALTER TABLE, DROP TABLE等)自动加。 | ||
意向锁 | 沟通协议,向表级汇报当前行锁的意图,辅助表锁判断能否加锁。 | IS (意向共享锁) | 表示事务打算在某些行上加共享锁(S锁)。 |
IX (意向排他锁) | 表示事务打算在某些行上加排他锁(X锁)。 |
1、表锁 (Table Locks)
这是最粗粒度的锁,直接锁住整张表。粗粒度锁
- 表共享读锁 (Table Read Lock):
- 作用: 当一个会话获得某表的读锁后,自己可以读,其他会话也可以读该表。但是,任何会话(包括自己)都不能写(INSERT/UPDATE/DELETE)该表。
- 使用场景: 手动执行
LOCK TABLES table_name READ;
时使用。InnoDB引擎通常不需要手动加表锁,因为有意向锁等更高效的机制。
- 表独占写锁 (Table Write Lock):
- 作用: 当一个会话获得某表的写锁后,自己可以读和写。但其他会话既不能读也不能写该表。
- 使用场景: 手动执行
LOCK TABLES table_name WRITE;
时使用。
总结:表锁简单粗暴,一锁就锁整张表,并发性能差。InnoDB引擎很少使用手动表锁,更推崇使用行锁。
2、元数据锁 (Metadata Lock, MDL)
MDL锁是MySQL自动加上的,你无法手动控制,目的是为了保证表结构的一致性。
- SHARED_READ / SHARED_WRITE:
- 作用: 当执行DML操作(
SELECT
,INSERT
,UPDATE
,DELETE
)时,会自动给表加一个MDL读锁。 - 并发场景: 多个事务可以同时对一个表进行DML操作(即多个MDL读锁可以共存),因为这只是读表结构。
- 作用: 当执行DML操作(
- EXCLUSIVE:
- 作用: 当执行DDL操作(
ALTER TABLE
,DROP TABLE
,RENAME TABLE
)时,会自动给表加一个MDL写锁。 - 并发场景:
- MDL写锁与MDL读锁互斥:这意味着,如果一个事务正在执行
SELECT
查询(持有MDL读锁),另一个事务想ALTER TABLE
(申请MDL写锁)就会被阻塞,直到所有读锁释放。反之亦然。 - MDL写锁之间也互斥:同一时间只能有一个会话修改表结构。
- MDL写锁与MDL读锁互斥:这意味着,如果一个事务正在执行
- 作用: 当执行DDL操作(
总结:MDL锁防止了“你正查着数据,我却把表删了”或者“你正插入数据,我却把字段改了”这种灾难性场景。
3、意向锁 (Intention Locks)
这是最让人困惑的锁,它的存在纯粹是为了提高效率,是行锁和表锁之间的沟通机制。
核心要义:意向锁是表级锁,但它表示的是事务在表中的“意图”( intention),即“我打算对表中的某些行加什么类型的行锁”。
- 意向共享锁 (Intention Shared Lock, IS):
- 作用: 一个事务在请求某行的S锁(共享锁)之前,必须先在表级别获得一个IS锁。
- 它想表达的是:“我打算对这个表中的某些行加读锁,你们加表写锁的时候注意点(可能会冲突)。”
- 意向排他锁 (Intention Exclusive Lock, IX):
- 作用: 一个事务在请求某行的X锁(排他锁)之前,必须先在表级别获得一个IX锁。
- 它想表达的是:“我打算对这个表中的某些行加写锁,你们加表锁(无论是读锁还是写锁)的时候都注意点(肯定会冲突)。”
为什么需要意向锁?—— 避免全表扫描检查行锁
假设没有意向锁。会话A想给整个表加一个表写锁(LOCK TABLES ... WRITE
)。MySQL为了判断是否能加这个锁,必须一行一行地去检查表中是否有行已经被其他事务加上了行锁。如果表很大,这个检查过程效率极低。
有了意向锁之后:
会话B要更新一行数据(加行级别的X锁),它必须先给表加一个IX锁。
此时,会话A想给整张表加表写锁。它只需要检查一下这张表上是否有意向锁(IS或IX) 存在。如果发现已经有一个IX锁(意向排他锁)存在,它就知道已经有其他事务正在修改表中的某些行了,因此自己的表写锁请求会被立即阻塞,而无需进行耗时的全表行锁检查。
意向锁之间的兼容性:
X (表写锁) | S (表读锁) | IX | IS | |
---|---|---|---|---|
X (表写锁) | 冲突 | 冲突 | 冲突 | 冲突 |
S (表读锁) | 冲突 | 兼容 | 冲突 | 兼容 |
IX | 冲突 | 冲突 | 兼容 | 兼容 |
IS | 冲突 | 兼容 | 兼容 | 兼容 |
- IX和IX兼容:多个事务可以同时对不同的行进行修改(每个事务先加IX锁,再给各自的行加X锁)。这正是InnoDB实现高并发的基础。
- IS和IS兼容:多个事务可以同时读取不同的行。
想象一个图书馆:
- 表锁:就像图书馆管理员说:“现在我要盘点整个图书馆(写锁),所有人都不准进也不准出。”或者“现在只允许看书不准借书(读锁),但大家可以同时看。”
- 元数据锁 (MDL):就像图书馆的建筑管理规定。你可以进去看书、借书(DML,MDL读锁),但你不能在大家看书的时候突然开始重新装修、移动书架(DDL,MDL写锁),这必须等所有人都离开才行。
- 意向锁 (IS/IX):就像图书馆入口的一个意图登记板。
- 你想进去借一本特定的书(对某行加X锁),你需要在板上登记:“我打算借书(IX锁)”。
- 管理员(表锁)看到这个登记板,就知道不能进行全馆盘点(加表写锁)了,因为有人正在借书。他无需跑到每个书架去问有没有人在借书。
- 同时,另一个想看书(对某行加S锁) 的人也可以在板上登记“我打算看书(IS锁)”,并且可以进入,因为“打算借书”和“打算看书”的意图登记本身并不冲突(IX和IS兼容)。
最终结论:
这些锁各司其职,共同构建了MySQL的并发控制体系。行锁提供了高并发的基础,意向锁使得表锁能够高效地判断能否加锁,而元数据锁则保护了表结构的安全。理解它们的协作关系是理解数据库并发的关键。
口述小记:(个人理解)
MySQL中的锁分为三种,全局锁,表级锁,行级锁,
全局锁操作的是整个数据库,通常用于数据备份时,防止数据不一致,手动加锁,解锁, 或者是innodb中–single-transaction 参数来完成不加锁的一致性数据备份。(快照读实现),
表级锁分为表锁,元数据锁,意向锁,
表锁,分为表读锁,表写锁,直接锁住整张表,表读锁自己能读,其他人也能读,自己能写,其他表不能写,表写锁自己能读写,其他人不能读也不能写,表写锁和表读锁需要手动加,不是mysql自动添加。
元数据锁,也是表级锁 mysql自动加 目的是为了保持表结构的一致性,分为MDL读锁(shard_read和shard_write)和MDL写锁(exclusive) MDL读锁执行DML操作时自动添加,即只操作表中的数据,但是不操作表的结构,多个MDL读锁可以共存,即索格事务可以同时增删改查同一个表,和MDL写锁互斥,即有一个事务操作表结构时,其他事务不能操作表中数据,反之,多个事务操作表数据时,有一个事务想修改表结构时会阻塞。MDL写锁执行DDL操作时自动添加,即操作表结构,MDL写锁之间也互斥,即同一时间只能有一个会话修改表结构。
意向锁,也是表级锁,提高效率,(标记作用)行锁与表锁之间的沟通机制,分为意向共享锁(IS),意向排他锁(IX),当事务想给表中某一行上共享锁或排他锁时,必须先获取表级别获取对应的IS锁或IX锁,比如当事务A想给表添加一个表级锁的表写锁,表示只有事务A可以对此表进行读写,其他事务不能读写,Mysql为了判断能否加锁,必须要一行一行检查检查表中是否有行已经被其他事务加上了行锁,如果有,那么事务A不能完全获得此表的控制权,加表写锁就失败,得阻塞,等待其他事务提交释放行锁,如果没有那么便会便会加锁,但是要全部遍历一遍,如果有意向锁,在加表级锁时会先检查 意向锁在不在,如果不在说明有其他事务在操作表中的某些行,那么就阻塞等待,如果在那么说明没有事务在操作表中数据,那么就可以加表级锁控制整个表。
如果要操作表中行数据(加排他锁-X锁),就要给表声明表中有某一行在被操作(此时就会有意向排他锁-IX锁),此时如果要加表锁就不用一行一行排查,因为有意向排他锁(IX锁)。如果要查询表中行数据(加共享锁-S锁),就要给表声明表中有某一行在被查询(此时就会有意向共享锁-IS锁)。
行级锁
InnoDB的数据是基于索引组织的,行锁是通过对索引上的索引项加锁来实现的,而不是对记录加的锁。
行级锁,每次操作锁住对应的行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。应用在InnoDB存储引擎中。
- 行锁(Record Lock):锁定单个行记录的锁,防止其他事务对此行进行update和delete。在RC(read commit )、RR(repeatable-read)隔离级别下都支持。
- 间隙锁(GapLock):锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行insert,产生幻读。在RR(repeatable-read)隔离级别下都支持。比如说 两个临近叶子节点为 15 23,那么间隙就是指 [15 , 23],锁的是这个间隙。
- 临键锁(Next-Key Lock):行锁和间隙锁组合,同时锁住数据,并锁住数据前面的间隙Gap。在RR(repeatable-read)隔离级别下支持。
行锁(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 Locks)
间隙锁是加在索引记录之间的间隙上的锁,用于防止其他事务在范围内插入新记录。
-
作用:锁定一个范围,但不包括记录本身。防止"幻读"(Phantom Read)现象。
-
加锁条件:在可重复读(REPEATABLE-READ)隔离级别下,当查询使用范围条件时自动加间隙锁。
-
示例:
-- 假设id有索引,且存在id=10和id=20的记录 SELECT * FROM table WHERE id BETWEEN 15 AND 25 FOR UPDATE;
这个查询会锁定(10,20]和(20,30](如果有id=30的记录)或者(20,+∞)的间隙,防止其他事务插入id在15到25之间的记录。
-
并发场景:主要用于防止其他事务在您正在操作的数据范围内插入新数据,确保查询结果的一致性。
临键锁 (Next-Key Locks)
临键锁是行锁和间隙锁的组合,是InnoDB默认的行锁算法。
- 作用:锁定索引记录本身以及索引记录之前的间隙。既防止其他事务修改当前记录,也防止在范围内插入新记录。
- 锁定范围:左开右闭区间。例如,如果索引中有值10, 20, 30,那么临键锁可能锁定的区间有:
- (-∞, 10]
- (10, 20]
- (20, 30]
- (30, +∞)
- 并发场景:在可重复读隔离级别下,InnoDB使用临键锁来解决幻读问题。(幻读:读的时候没读到,插入的时候有显示有数据了)
锁类型 | 作用 | 加锁范围 | 主要解决什么问题 |
---|---|---|---|
记录锁 | 锁定单行记录 | 具体的索引记录 | 保证行数据的一致性 |
间隙锁 | 锁定记录之间的间隙 | 索引记录之间的区间 | 防止幻读(插入新记录) |
临键锁 | 锁定记录+间隙 | 左开右闭的区间 | 同时防止修改和插入,彻底解决幻读 |
- 读已提交(RC)级别:只有行锁,没有间隙锁和临键锁
- 可重复读(RR)级别:有行锁、间隙锁和临键锁(默认)
八、InnoDB引擎
逻辑结构
1.表空间(ibd文件)
- 一个 MySQL 实例可以对应多个表空间,用于存储记录、索引等数据。
2.段(Segment)
- 分为:
- 数据段(Leaf node segment)
- 索引段(Non-leaf node segment)
- 回滚段(Rollback segment)
- InnoDB 是索引组织表:
- 数据段就是 B+ 树的叶子节点
- 索引段即为 B+ 树的非叶子节点
- 段用来管理多个 Extent(区)
3.区(Extent)
- 表空间的单元结构,每个区的大小为 1MB
- 默认情况下,InnoDB 存储引擎页大小为 16KB,即一个区中一共有 64 个连续的页
4.页(Page)
- InnoDB 存储引擎磁盘管理的最小单元,每个页的大小默认为 16KB
- 为了保证页的连续性,InnoDB 存储引擎每次从磁盘申请 4-5 个区
5.行(Row)
- InnoDB 存储引擎数据是按行进行存放的
- 隐藏字段:
- trx_id:每次对某条记录进行改动时,都会把对应的事务 ID 赋值给 trx_id 隐藏列
- roll_pointer:每次对某条记录进行改动时,都会把旧的版本写入到 undo 日志中,这个隐藏列相当于一个指针,可以通过它来找到该记录修改前的信息
架构
内存结构
1.Buffer Pool(缓冲池)
- Buffer Pool:位于主内存中的区域,用于缓存磁盘上经常操作的数据。
- 执行增删改查时,先操作缓冲池中的数据(若无数据则从磁盘加载并缓存),再以一定频率刷新到磁盘,减少磁盘 I/O,提高性能。
- 以 Page(页) 为单位管理,底层使用链表结构。
- Page 分为三种状态:
- free page:空闲页,未被使用
- clean page:被使用,但数据未被修改
- dirty page:脏页,数据被修改过,与磁盘不一致
2.Change Buffer(更改缓冲区)
- Change Buffer:针对非唯一二级索引页的优化机制。
- 执行 DML 语句时,如果相关数据页不在 Buffer Pool 中,不会立即操作磁盘,而是将变更暂存到 Change Buffer 中。
- 当这些数据被读取时,再将变更合并到 Buffer Pool,并刷新到磁盘。
- 意义:
- 二级索引通常是非唯一的,插入、删除、更新操作可能影响不相邻的索引页。
- 若每次都直接操作磁盘,会造成大量磁盘 I/O。
- 使用 Change Buffer 可在内存中合并处理,减少磁盘 I/O。
3.Adaptive Hash Index(自适应哈希索引)
- Adaptive Hash Index:自适应哈希索引,用于优化对 Buffer Pool 数据的查询。
- InnoDB 会监控对表上各索引页的查询,如果观察到建立哈希索引可以提升速度,则自动建立,称为自适应哈希索引。
- 无需人工干预,系统根据情况自动完成。
- 相关参数:
adaptive_hash_index
4.Log Buffer(日志缓冲区)
- Log Buffer:日志缓冲区,用于保存将要写入磁盘的日志数据(如 redo log、undo log)。
- 默认大小为 16MB。
- 日志会定期刷新到磁盘中。
- 相关参数:
innodb_log_buffer_size
:设置缓冲区大小innodb_flush_log_at_trx_commit
:控制日志刷新到磁盘的时机- 0:每秒将日志写入并刷新到磁盘一次
- 1:每次事务提交时写入并刷新到磁盘(最安全)
- 2:每次事务提交后写入,但每秒刷新一次到磁盘
磁盘结构
1.System Tablespace**(系统表空间)**
系统表空间是 InnoDB 最核心的共享表空间,它是所有表的“老家”,也是 InnoDB 自己存储核心元数据的地方。
它主要存放以下内容:
- 数据字典(Data Dictionary):
- 这是最重要的部分。它像一个“户口本”,存储了关于数据库、表、列、索引等对象的元数据(Metadata)。例如,你创建了一张表
users
,它的表结构、有哪些列、每个列的类型、有哪些索引等信息都保存在数据字典中。
- 这是最重要的部分。它像一个“户口本”,存储了关于数据库、表、列、索引等对象的元数据(Metadata)。例如,你创建了一张表
- Change Buffer(变更缓冲区):
- 一种特殊的数据结构,用于缓存非唯一二级索引(Secondary Indexes) 的变更(如
INSERT
,UPDATE
,DELETE
操作)。 - 当这些索引页不在内存(Buffer Pool)中时,为了避免昂贵的随机磁盘 I/O,修改会先被记录在 Change Buffer 里,待未来该索引页被读取到内存时,再将这些修改合并(Merge)进去。这大大提升了写操作的性能。
- 一种特殊的数据结构,用于缓存非唯一二级索引(Secondary Indexes) 的变更(如
- Doublewrite Buffer(双写缓冲区):
- 一个用于提高数据页写入可靠性的机制。在将数据页从内存(Buffer Pool)刷写到数据文件(.ibd)之前,InnoDB 会先将这些页写入双写缓冲区。
- 这样如果在写入数据文件的过程中发生意外(如系统崩溃),InnoDB 在崩溃恢复时就可以从双写缓冲区中找到该页的一个完好副本,从而防止部分写(partial page write) 问题,确保数据页的完整性。
- Undo Logs(撤销日志):
- 在 MySQL 8.0 之前,Undo Logs 默认也存储在系统表空间中。Undo Log 用于实现事务的回滚和多版本并发控制(MVCC)。
- 从 MySQL 8.0 开始,Undo Logs 默认被剥离出来,存放在独立的Undo Tablespaces(撤销表空间) 中,这是一个重要的改进。
- 用户表数据和索引(如果配置如此):
- 在默认启用
innodb_file_per_table=OFF
的旧版本 MySQL 中,所有用户创建的表的数据和索引也会和上面的内容混合存放在系统表空间中。这非常不利于管理,现在强烈不推荐这么做。
- 在默认启用
默认文件:
- 默认情况下,系统表空间由一个或多个名为
ibdata1
的文件(通常位于数据目录下)组成。其大小和数量可以通过innodb_data_file_path
参数配置。
2.File-Per-Table Tablespaces**(独立表空间)**
这是现代 MySQL 版本(5.6及以上)的默认行为(innodb_file_per_table=ON
)。启用后,每个 InnoDB 表和它的索引都会存储在一个独立的 .ibd
文件里,而不是系统表空间中。
它主要存放以下内容:
- 单个表的数据和索引:顾名思义,一个
.ibd
文件只包含对应那张表的数据(B+Tree 的叶子节点)和所有索引(B+Tree 的非叶子节点,以及所有二级索引)。
启用独立表空间的巨大优势:
- 空间回收(磁盘空间管理):
- 非常重要! 当你对一张表执行
DROP TABLE
操作时,MySQL 可以直接删除那个.ibd
文件,操作系统会立即回收磁盘空间。 - 与之对比:如果表存放在共享的系统表空间(
ibdata1
)中,DROP TABLE
操作只会标记该表所占用的空间为“可复用”,但不会将空间返还给操作系统,导致ibdata1
文件只会越来越大,很难收缩。
- 非常重要! 当你对一张表执行
- 性能优化:
- 可以更高效地执行一些操作,如
TRUNCATE TABLE
。在独立表空间下,这个操作相当于直接删除并重建.ibd
文件,速度极快。
- 可以更高效地执行一些操作,如
- 备份和恢复灵活性:
- 可以使用
Transportable Tablespaces
功能,在不同的 MySQL 实例之间快速地迁移单个表(比如先锁表,拷贝.ibd
和.frm
文件,然后导入)。 - 第三方备份工具(如
XtraBackup
)可以更高效地备份和恢复单个表。
- 可以使用
- 监控与管理:
- 你可以直接在操作系统层面看到每个表的大小,便于监控和管理。
- I/O 优化:
- 系统可以将不同的表文件分布到不同的物理磁盘上,以实现 I/O 负载均衡。
特性 | System Tablespace(系统表空间) | File-Per-Table Tablespaces(独立表空间) |
---|---|---|
默认文件 | ibdata1 | 表名.ibd |
存储内容 | 共享的:数据字典、Change Buffer、Doublewrite Buffer、(旧版)Undo Logs | 独占的:单张表的数据和索引 |
磁盘空间管理 | 差。DROP TABLE 后空间不释放,只可复用,文件持续增长。 | 优。DROP TABLE 会删除 .ibd 文件,空间立即被操作系统回收。 |
推荐配置 | 必需存在,但应只存放它必须存的东西(元数据、缓冲区等)。 | 强烈推荐启用 (innodb_file_per_table=ON ),这是现代 MySQL 的默认设置。 |
作用 | InnoDB 引擎的“大脑”和“共享工作区”,管理核心元数据和提供关键功能。 | 用户数据的“私人住宅”,每个表住自己家,互不干扰,管理方便。 |
3.General Tablespaces**(通用表空间)**
-
需通过
CREATE TABLESPACE
语法手动创建 -
.ibd文件存储,一个表空间对应一个.ibd文件
-
创建表时可指定使用该表空间
-
示例:
CREATE TABLESPACE xxx ADD DATAFILE 'file_name' ENGINE=engine_name; CREATE TABLE xxx ... TABLESPACE ts_name;
4.Temporary Tablespaces**(临时表空间)**
- 包括会话临时表空间和全局临时表空间
- 用于存储用户创建的临时表等数据
5.Undo Tablespaces**(撤销表空间)**
- MySQL 实例初始化时会自动创建两个默认的 undo 表空间(初始大小为 16MB)
- 用于存储 undo log 日志
6.Redo Log**(重做日志)**
- Redo Log 用于实现事务的持久性
- 由两部分组成:
- Redo Log Buffer(重做日志缓冲区):位于内存中
- Redo Log File(重做日志文件):位于磁盘中
- 事务提交后,所有修改信息都会记录到 Redo Log 中
- 在刷新脏页到磁盘过程中若发生错误,可通过 Redo Log 恢复数据
- 由两部分组成:
- 示例文件名:
Dib_logfile0
7.Doublewrite Buffer Files**(双写缓冲区)**
-
- InnoDB 在将数据页从 Buffer Pool 刷新到磁盘前,会先将数据页写入双写缓冲区文件
- 用于在系统异常时恢复数据,防止部分写失效问题
- 示例文件名:
D#ib_16384_0.dblwr
D#ib_16384_1.dblwr
后台线程
1.Master Thread
- 核心后台线程,负责调度其他线程
- 将缓冲池中的数据异步刷新到磁盘,保持数据一致性
- 职责包括:
- 脏页刷新
- 合并插入缓存(Insert Buffer)
- undo 页回收
2.IO Thread
InnoDB 大量使用 AIO(异步 I/O) 处理 I/O 请求,提高性能
IO Thread 负责这些 I/O 请求的回调处理
- Read thread
- Write thread
- Log thread
- Insert buffer thread
线程类型 | 默认个数 | 职责说明 |
---|---|---|
Read thread | 4 | 负责读操作 |
Write thread | 4 | 负责写操作 |
Log thread | 1 | 将日志缓冲区刷新到磁盘 |
Insert buffer thread | 1 | 将写缓冲区内容刷新到磁盘 |
3.Purge Thread
- 用于回收已提交事务的 undo log
- 事务提交后,undo log 不再使用,由 Purge Thread 清理
4.Page Cleaner Thread
- 协助 Master Thread 刷新脏页到磁盘
- 减轻 Master Thread 压力,减少阻塞
事务原理
事务:
事务 是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。
1.原子性 - undo log
事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
2.持久性 - redo log
事务完成时,必须使所有的数据都保持一致状态。
3.一致性 - undo log + redo log
数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
4.隔离性 - 锁 + MVCC
事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。
redo log
重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性。
该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log file),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都存到该日志文件中,用于在刷新脏页到磁盘,发生错误时,进行数据恢复使用。
Buffer Pool在产生脏页数据的时候,会先将数据存储到 redo log buffer 再存储到 redo log 中进行磁盘持久化存储,在内存出现异常(比如突然断电)时,通过redo log中持久化的数据进行回滚。
redo log 要写到磁盘,数据也要写磁盘,为什么要多此一举?
写入 redo log 的方式使用了追加操作,所以磁盘操作是顺序写,而写入数据需要先找到写入位置,然后才写到磁盘,所以磁盘操作是随机写。
undo log
回滚日志,用于记录数据被修改前的信息,作用包含两个:提供回滚 和 MVCC(多版本并发控制)。
undo log 和 redo log 记录物理日志不一样,它是逻辑日志。可以认为当 delete 一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当 update 一条记录时,它记录一条对应相反的 update 记录。当执行 rollback 时,就可以从 undo log 中的逻辑记录读取到相应的内容并进行回滚。
Undo log 销毁:undo log 在事务执行时产生,事务提交时,并不会立即删除undol0g,因为这些日志可能还用于 MVCC。
Undo log 存储:undo log 采用段的方式进行管理和记录,存放在前面介绍的 rollback segment 回滚段中,内部包含1024个 undo log segment.
MVCC
全称 Multi-Version Concurrency Control,多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,快照读为MySQL实现MVCC提供了一个非阻塞读功能。MVCC的具体实现,还需要依赖于数据库记录中的三个隐式字段、undo log日志、read View。
当前读:
读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:select…lock in share mode(共享锁),select… for update、update、insert、delete(排他锁)都是一种当前读。
快照读:
简单的select(不加锁)就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。
- Read committed:每次select,都生成一个快照读。
- Repeatable Read:开启事务后第一个select语句才是快照读的地方。
- Serializable:快照读会退化为当前读。
1.记录隐藏字段
- 最近修改事务id(DB_TRX_ID)
- 回滚指针(DB_ROLL_PTR)
- 隐藏主键(DB_ROW_ID)
字段名 | 含义说明 |
---|---|
DB_TRX_ID | 最近修改事务 ID,记录插入这条记录或最后一次修改该记录的事务 ID。 |
DB_ROLL_PTR | 回滚指针,指向这条记录的上一个版本,用于配合 undo log,找到历史版本数据。 |
DB_ROW_ID | 隐藏主键,如果表结构没有显式指定主键,InnoDB 会自动生成该隐藏字段作为主键使用。 |
2.undo log 版本链
头部最新,尾部最旧
回滚日志,在insert、update、delete的时候产生的便于数据回滚的日志。
当insert的时候,产生的undoloq日志只在回滚时需要,在事务提交后,可被立即删除。
而update、delete的时候,产生的undo log日志不仅在回滚时需要,在快照读时也需要,不会立即被删除。
那么何时删除?
- 事务提交后
- 对于
INSERT
操作,事务提交后,undo log可以被立即删除,因为不再需要用于回滚。 - 对于
UPDATE
和DELETE
操作,undo log不会立即被删除,因为它们可能在后续的快照读取中被使用。
- 对于
- 快照读取结束
- 当所有依赖于该undo log的快照读取操作结束后,undo log才会被删除。这意味着如果有一个事务正在进行快照读取,并且依赖于某个undo log,那么这个undo log会一直保留直到该事务结束。
不同事务或相同事务对同一条记录进行修改,会导致该记录的 undo log 生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录。
3.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 |
版本链数据访问规则:
-
trx_id == creator_trx_id
?
→ 可以访问,数据是当前事务自己修改的。 -
trx_id < min_trx_id
?
→ 可以访问,数据已提交。 -
trx_id > max_trx_id
?
→ 不可访问,该事务在 ReadView 生成之后才开启。 -
min_trx_id <= trx_id <= max_trx_id
且trx_id
不在m_ids
中?
→ 可以访问,数据已提交。
不同隔离级别生成 ReadView 的时机:
- READ COMMITTED:每次快照读都重新生成 ReadView。
- REPEATABLE READ:仅在第一次快照读时生成 ReadView,后续复用。
依次比较 undo log 日志中版本数据链,找到可以进行访问的版本数据。
九、MySQL管理
Mysql数据库安装完成后,自带了一下四个数据库,具体作用如下:
数据库 | 含义 |
---|---|
mysql | 存储MVSQL服务器正常运行所需要的各种信息(时区、主从、用户、权限等) |
information_schema | 提供了访问数据库元数据的各种表和视图,包含数据库、表、字段类型及访问权限等 |
performance_schema | 为MySQL服务器运行时状态提供了一个底层监控功能,主要用于收集数据库服务器性能参数 |
sys | 包含了一系列方便 DBA和开发人员利用 performance_schema性能数据库进行性能调优和诊断的视图 |