MySQL 基础篇 1.1 执行一条SQL语句会发生什么
1. MySQL架构一共分为两层 server 和 存储引擎层(一般为Innodb引擎)
主要执行流程都在server层:连接器,查询缓存,解析SQL(解析器),执行SQL(预处理器,优化器,执行器)
存储引擎层:索引数据结构由引擎层实现,Innodb引擎支持的是B+树索引。数据表中创建的主键索引和二级索引都由B+树实现。
执行一条SQL语句会发生什么
1. 连接器: 与客户端进行TCP三次连接(长连接)
校验用户名和密码
用户名验证通过后,会基于此时读取到的用户权限来进行逻辑判断
TCP长连接不会轻易断开,所以使用长连接的好处就是减少建立连接和断开连接的过程。
但是时间长了它也会占用内存过多,因此会采用以下两种方式来断开连接:
1) 定期断开长连接
2)客户端主动重置连接
2.查询缓存:查询语句如果命中查询缓存则直接返回,否则继续往下执行。MySQL 8.0 已删除该模块;(因为当有表更新之后,对应的语句就会被清除,所以会比较鸡肋)
3. 解析SQL:两个过程:
1. 词法分析:根据输入的字符识别出来关键字,构建SQL语法树,方便后面的模块获取SQL类型,表名,字段名。
2.语法分析:根据词法分析的语法树,判断该语句是否符合MySQL的语法。
4. 执行SQL
1. 预处理器:
检查SQL语句中的表和字段是否存在
将SQL语句中的 * 扩展为表中的所有列
2. 优化器:
基于查询成本考虑,选择成本最小的执行计划。(主要选择主键索引还是二级索引)
3. 执行器:
根据执行计划执行 SQL 查询语句,从存储引擎读取记录,返回给客户端;
MySQL一行记录是怎么存储的
MySQL数据文件存放在哪个文件?
一共有三个文件: db.opt 用来存放当前数据库的默认字符集和字符校验规则
t_order.frm 用来存放当前表结构
t_order.ibd 用来存放当前表数据(MySQL数据存放在这里)
表空间的文件结构:
行:数据库表中的数据都是按行的形式存储,每行记录根据不同的行格式采用不同的存储结构。
页:每次只读取一行效率会很低,所以数据库读取数据都是以页为单位来读取数据。默认每个页的大小都是16KB。
区:Innodb存储引擎是以B+树的形式存储,B+树的每一层都是以双向链表的形式连接起来。但是链表中相邻的两个页之间并不是连续的,所以使用区的形式,让页与与页之间相邻。所以,当表中的数据量较大时,按照区为单位分配内存空间,每个区的大小为1MB。
段:表空间是由多个段组成,段由多个区组成。段有数据段,索引段,回滚段等
索引段:存放B+树中非叶子节点的区的集合
数据段:存放B+树中叶子节点的集合
回滚段:存放的是回滚数据的区的集合。
Innodb的行格式:COMPACT
Innodb的行格式一共分为两个部分:记录的额外信息,记录的真实数据
记录的额外信息一共分为三个部分:变长字段长度列表,NULL值列表,记录头信息。
记录的真实数据:
前面包含三个字段:
row_id : 当建表的时候设置了主键或者唯一约束,那么就不会有row_id字段。
trx_id : 事务id,表明了当前数据是由哪个事务生成的。
roll_pointer : 这条记录上一个版本的指针。
总结:
MySQL的NULL值是怎么存放的:
是由Compact行格式中的NULL值列表来标记NULL的列,NULL不会存储在行格式中的真实数据部分。
MySQL如何知道varchar(n) 实际占用的数据大小:
利用行格式中的变长字段长度列表。
行溢出后,多余的数据怎么处理:
当一行存储不了所有的数据后,会将多余的数据存储到溢出页。并且在真实数据部分流出20KB的空间用来指向溢出页的地址。
_____________________________________________________________________________
MySQL 索引篇
什么是索引,索引就相当于一本书的目录。通过索引可以快速查找到对应的数据。
索引常见面试题:
1. 索引的分类:
按数据结构分类:
按物理存储分类:一般分为聚簇索引(主键索引),二级索引(辅助索引)
主键索引的B+树的叶子节点上一般存储的是数据,所有完整的用户数据都存放在主键索引的叶子节点上
二级索引的B+树的叶子节点上一般存储的是主键值,而不是实际数据。然后再通过主键值再查找到相应的数据,也就是说二级索引要经过两次查询。
回表:
如果在二级索引的第一次查询中获得相应的数据,就不需要再进行一次查询。这就叫做覆盖索引。如果第一次查询中没有获得相应数据,只获得主键值,那么就需要再进行一次主键检索。这个过程就叫做回表。
按字段特性分类:
主键索引:主键索引就是建立在主键上的索引,一般在建表的时候创建,一张表只能有一个主键索引,并且主键索引的列的值不允许为空值。
唯一索引:唯一索引建立在UNIQUE字段上的索引,一张表可以有多个唯一索引。索引列的值必须唯一,允许有空值。
普通索引:建立在普通字段上的索引,既不要求字段为主键索引,也不要求字段为唯一索引。
前缀索引:前缀索引是建立在字符型字段的前几个字符建立的索引。使用前缀索引的目的是减少索引占用的内存空间。提升查询效率。
按字段个数分类:
单列索引:建立在单列字段上的索引
联合索引:建立在多列字段上的索引(采用最左匹配原则)。
2. 什么时候需要创建索引 / 什么时候不需要创建索引
索引的好处和缺点:
索引最大的好处就是提高查询速度。
索引的缺点:需要占用物理空间,数量越大,占用空间越大。
创建索引和维护索引需要耗费时间,这种时间随着数据量的增大而增大。索引就是典型的以空间换时间
会降低表的增删改查的效率,每次增删改查,B+数为了维护索引有序性,都要进行动态维护。
什么时候需要创建索引:
字段有唯一性限制,就像商品编码
经常用于where 语句查询语句的字段,这样能够提高整个表的查询效率。如果查询字段不是一个,可以进行联合查询。
语句中含有ORDER BY和GROUP BY的字段,这样在进行查询时就不用去做一次排序。因为建立好索引之后,B+树上的数据都是有序的。
什么时候不需要查询数据:
WHERE 和 ORDER BY 和 GROUP BY 用不到的语句。
字段中出现大量的数据,比如性别字段,每次查询都只会出现一般的数据。
表数据太少的时候不需要创建索引
经常更新的字段,B+树需要维护数据的有序性,频繁的增删改查会影响数据库的性能。
3. 索引优化的方法:
前缀索引优化方法:前缀索引就是使用某个字段的前几个字符作为索引,在一些大的字段作为索引时,使用前缀索引可以减少索引项的大小。
覆盖索引优化:在二级索引的查询中,如果在第一次查询就获得相应的数据,就可以避免回表操作。方法:我们可以建立一个联合查询。即「商品ID、名称、价格」作为一个联合索引。如果索引中存在这些数据,查询将不会再次检索主键索引,从而避免回表。
主键最好是自增的:如果我们将主键设置为自增的,那么每增加一次数据就会按顺序添加到对应的叶子节点上。不需要移动数据。每插入一次新数据,都是追加操作。
索引最好设置成NOT NULL:索引中出现NULL会让优化器在做索引选择时会更加麻烦。
NULL值是一个没意义的数据,但是会在行格式中的NULL值列表增加1KB的空间。
防止索引失效:
- 当我们使用左或者左右模糊匹配的时候,也就是
like %xx
或者like %xx%
这两种方式都会造成索引失效;- 当我们在查询条件中对索引列做了计算、函数、类型转换操作,这些情况下都会造成索引失效;
- 联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则就会导致索引失效。
- 在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。
从数据页的角度看B+树
Innodb是如何存储数据的
Innodb是按照数据页的形式读取数据的(以行的形式效率会很低),每个页默认大小为16KB,意味着数据库每次读取数据都是以16KB为单位,一次最少读取16KB的数据到内存中。一次最少把内存中16KB的数据刷新到磁盘中。
数据页的主要作用就是存储数据,也就是数据库里的数据。数据页中的记录按照主键顺序组成单向链表。单向链表的优点主要是方便插入和删除。但是检索效率不高。
数据页中有一个页目录,起到记录的检索作用。
每个槽相当于指针指向了不同组的最后一个记录。、
页目录就是由多个槽准备的,槽相当于分组记录的索引。我们查找数据可以通过二分法快速定位到对应的槽,在遍历槽内的所有记录,从而找到对应的记录。
B+树是如何进行查询的
磁盘的I/O次数对索引的使用效率至关重要。因此,采用B+树是最合适的。
B+树中的每个节点都是一个数据页。
B+树中非叶子节点仅用来存放目录项来进行索引,B+树的叶子节点用来存放数据。
所有的节点按照索引键的大小排序,构成一个双向链表,便于范围查询。
为什么MySQL采用B+树的形式来存储数据
设计一个适合MySQL索引的数据结构需要满足以下几点
能在尽可能少的磁盘中进行I/O操作
能高效的查询一个记录,也要能高效的执行范围查找
什么是二分查找
二分查找每次都能将范围减半,时间复杂度会降低。
二分查找树
每一个节点的左子树的所有节点都小于这个节点。右子树的所有节点都大于这个节点。
自平衡二叉树
每个节点的左子树和右子树的高度差不能超过1 . 但是不管是平衡二叉树,随着插入数据的增多,树的高度也会增加。 也就意味着磁盘I/O的次数会增加。会影响整体查询效率。它每个节点都会有两个子节点。
B树
B树的每个子节点都会由多个子结点。降低了树的高度问题。
B+树
B+ 树与 B 树差异的点,主要是以下这几点:
- 叶子节点(最底部的节点)才会存放实际数据(索引+记录),非叶子节点只会存放索引;
- 所有索引都会在叶子节点出现,叶子节点之间构成一个有序链表;
- 非叶子节点的索引也会同时存在在子节点中,并且是在子节点中所有索引的最大(或最小)。
- 非叶子节点中有多少个子节点,就有多少个索引;
_____________________________________________________________________________
MySQL 事务篇
事务有哪些特性?
原子性:
一个事务中的所有操作,必须全部执行。要么全部完成要么就不完成。中间如果出现错误,就要回滚到初始状态。
持久性:
事务处理结束后,对数据的修改就是永久的,就是系统故障也不会改变
一致性:
指事务操作前后,数据保持完整约束性。
隔离性:
数据库具有多个并发事务同时对数据进行读写和修改的能力。隔离性可以防止多个并发事务在执行时由于交叉执行而导致的数据不一致。
为了实现以上四个特性。数据库实现了一下机制:
持久性通过redo log (重做日志)来实现
原子性通过undo log (回滚日志)实现
隔离性通过MVCC多并发控制或者锁来实现
一致性通过 持久性+原子性+隔离性 实现
首先讲隔离性:
并发事务会引发的问题:
脏读:一个事务读到了另个事务未提交的数据
不可重复度:在一个事务内多次读取同一个数据,发现两次读取的数据不一致
幻读:在一个事务中多次查询某个符合查询条件的记录数量,发现两次查询的记录数量不一致。
SQL提供四种隔离级别来规避以上四种现象,隔离级别越高,性能效率越低
读未提交:可以读取到另一个事务未提交的数据 (可能会发生脏读,不可重复读,幻读)
读提交:可以读取到另一个事务已提交的数据(可能会发生不可重复读,幻读)
可重复读:从一个事务开启,他读取到的数据从始至终就是一致的 (可能会发生幻读)
Innodb 引擎的默认隔离级别是可重复读。但是他很大程度上避免了幻读。采用了以下两种方式
1. 针对快照读(普通select语句)
它采用了MVCC多并发控制,因为事务在执行过程中看到的数据,一直和事务启动时看到的数据是一致的。所以即使其他事务中途插入一条数据,也不会被该事务看到。
2. 针对当前读(select ...for update)
间隙锁+记录锁来实现。当执行语句时,会加上记录锁和间隙锁。如果有其他事务在在间隙锁和记录锁内插入一条数据,就会被阻塞。无法插入数据,就很好的避免了幻读现象。
下面讲讲ReadView在MVCC里如何工作的
先了解ReadView的两个重要知识
1. ReadView 的四个字段
2. 聚簇索引记录中两个跟事务有关的两个隐藏列
1. m_ids 当前数据库中活跃事务id列表,活跃事务指的是启动了但是还未提交的数据
2. min_trx_id 创建ReadView时,当前数据库活跃且未提交的事务中最小事务的事务id
3. max_trx_id 创建ReadView当前数据库中应该给下一个事务的id值,当前数据库最大id值+1
4. creator_trx_id 指创建该事务时的id值
聚簇索引记录中的两个隐藏列
trx_id
当一个事务对某条聚簇索引记录进行改动时,就会把该事物的 id 隐藏在trx_id 中
roll_pointer
每次对聚簇索引记录进行改动时,就会把旧版本的记录写入到 undo 日志中,然后roll_pointer 是一个指针,指向每一个旧版本记录,可以通过它找到修改前的记录。
创建ReadView之后,可以将记录中的trx_id 分为三种情况:
一个事务访问记录时,除了自己更新的记录总是可见之外,还有以下几种情况:
1. 如果记录的 trx_id 值小于min_trx_id ,说明这个版本的记录是在创建ReadView前已经提交的十五生成的。所以该版本记录对该事务可见。
2. 如果记录的trx_id 值大于max_trx_id ,说明这个版本的记录是在创建ReadView后的事务提交生成的,所以该版本记录对该事务不可见。
3. 如果该记录的trx_id在min_trx_id和max_trx_id之间,需要判断trx_id是否在m_ids 列表中
1)在m_ids 列表中,表示生成该记录的事务仍然活跃着,所以该记录不可见
2)不在m_ids 列表中,表示该事务已经提交,该记录可见。
这种通过版本链(通过undo日志来实现,使用roll_pointer 来实现指向旧版本的记录)的形式来控制多并发事务访问同一个记录的行为叫做MVCC(多版本并发控制)
可重复读和读提交都是通过ReadView来实现,可重复读是从始至终每个事务就有一个ReadView
而读提交在每次读取数据时都会创建一个新的ReadView。
_____________________________________________________________________________
MySQL 锁篇
1. MySQL 有哪些锁?
1)全局锁 执行后整个数据库就处于只读状态,一般用于全库逻辑备份
2)表级锁:
表锁:
表级别的共享锁:读锁
表级别的独占锁:写锁
元数据锁(MDL):当我们对数据库进行操作的时候,就会给表加上元数据锁
当我们对一张表进行CRUD时,加的是MDL读锁
对一张表进行表结构变更时,加的是MDL写锁。
意向锁:当执行增删改查,需要先对表加上意向独占锁,然后再对该纪录加独占锁
加意向锁的目的是快速判断表中是否有记录被加锁
AUTO-INC 锁:表里的主键通常设为自增的,这是通过对字段声明AUTO-INCREMENT 实现的
3)行级锁
Record lock 记录锁(对表中某一行的数据进行上锁) :
S锁:当一个事务对记录加了S锁后,另一个事务也可以加S锁,但是不能加X锁
X锁:当一个事务对记录加了X锁后,其他事务不能再加X锁,也不能再加S锁
间隙锁:就是在用于可重复读隔离级别,目的是就解决可重复读隔离级别下幻读的情况
间隙锁是可以兼容的,即两个事务可以拥有共同的间隙范围的间隙锁。并不存在互斥关系。
临建锁:是记录锁+间隙锁 锁住一个范围,并锁住记录本身。
2. MySQL是怎么加锁的
唯一索引等值查询:
当查询的记录是存在的,在索引树上定位到这一条记录之后,将记录的索引的临建锁退化成记录锁,仅靠记录所就可以避免幻读现象。X型的意向锁和独占锁。
当查询的记录不存在时,在索引书上找到第一条大于该查询记录的记录后,将该记录的索引的临建锁退化成间隙锁。
非唯一索引等值查询:
- 当查询的记录「存在」时,由于不是唯一索引,所以肯定存在索引值相同的记录,于是非唯一索引等值查询的过程是一个扫描的过程,直到扫描到第一个不符合条件的二级索引记录就停止扫描,然后在扫描的过程中,对扫描到的二级索引记录加的是 next-key 锁,而对于第一个不符合条件的二级索引记录,该二级索引的 next-key 锁会退化成间隙锁。同时,在符合查询条件的记录的主键索引上加记录锁。
- 当查询的记录「不存在」时,扫描到第一条不符合条件的二级索引记录,该二级索引的 next-key 锁会退化成间隙锁。因为不存在满足查询条件的记录,所以不会对主键索引加锁。
非唯一索引和主键索引的范围查询的加锁规则不同之处在于:
- 唯一索引在满足一些条件的时候,索引的 next-key lock 退化为间隙锁或者记录锁。
- 非唯一索引范围查询,索引的 next-key lock 不会退化为间隙锁和记录锁。