目录
1、索引
1.1、概念
1.2、索引的作用
1.3、 索引的缺点
1.4、数据库中实现索引的数据结构
1.4.1、B树/B-树
1.4.2、B+树
1.4.3、回表
1.5、使用场景
1.6、索引的使用
1.6.1、查看索引
1.6.2、创建索引
1.6.3、 删除索引
1.7、索引的分类
2、事务
2.1、为什么使用事务
2.2、事务的概念
2.3、回滚机制(rollback)
2.4、事务的使用
2.5、事务的特性
2.6、事务的隔离级别
2.6.1、MySQL中的四种事务隔离级别
2.7、脏读问题
2.8、不可重复度问题
2.9、幻读问题
1、索引
索引的创建会占用一定的物理空间,索引值是存放在磁盘空间中的,但是为了减少IO访问次数,提高程序的效率,在访问数据的时候他会先从磁盘空间中预读一定长度的索引数据放在缓存中。
1.1、概念
在MySQL中,索引(index)也叫做键(key)
数据库的索引就是为了提高数据的查询速率的方法。数据库的索引类似于书籍的目录。在书籍中,目录允许用户不必翻阅整本书就能迅速找到所需要的信息。在数据库中同理,索引也允许数据库程序迅速的找到表中的数据,而不必扫描整个数据库。
1.2、索引的作用
- 我们的数据库的数据是存储在硬盘中的,而查询数据的时候要从硬盘中读取数据。而在硬盘中读取数据相比于在内存中读取数据是慢了不止一个数量级。
- 如果我们通过遍历在硬盘空间中查找数据,每查找一次数据都需要程序与硬盘进行一次交互(数据的交互,也就是硬盘的IO),而一段程序中最拖慢程序速度的就是硬盘的IO,可想而知程序的速度会非常慢。
- 但是我们通过索引的方式来查找数据,我们将索引值存在内存空间中,通过查找内存空间的值,来找到索引所对应的表中的完整数据。这样就减少了程序与硬盘空间的交互(减少了硬盘的IO次数)。这样就提高了程序的效率。这就是我们常说的以空间换时间。
1.3、 索引的缺点
- 创建索引和维护索引需要耗费时间,这种时间随着数据量的增加而增加。
- 需要付出额外的空间代价来保存索引数据
- 当对表中的数据进行增加、删除和修改的时候,索引也会跟着改变,降低了数据的维护速度,也可以理解为拖慢了增加、删除和修改数据的速度。
1.4、数据库中实现索引的数据结构
- 我们在数据结构中学习过二叉搜索树、红黑树、avl树和哈希表这样的数据结构,二叉搜索树在平衡的情况下,可以实现快速查找数据。哈希表可以更快的查找数据。
- 哈希表查找的时间复杂度为O(1),二叉搜索树在树平衡的时候它的时间复杂度为书的高度。但是在最坏的情况下,它的时间复杂度为O(N).
上述说到的这些数据结构都不能作为数据库中实现索引的数据结构。
- 二叉搜索树、avl树、红黑树。他们都是单个存储的,将一个完整的数据存入结点中,而数据库中完整的数据就是一张表中的一条记录。这样每个结点占用的空间就比较大,所以不能将这些结点存在内存中,就需要存入硬盘中,而每查找一个数据就需要一个结点一个结点的比对,每比对一次结点中的数据,就相当于在硬盘中进行了程序与硬盘的交互(也就是硬盘的IO) 。这样程序的效率就非常低。
- 而哈希表是一个非常快速的查找数据的数据结构,但是它只能进行值相等的比较,但是像大于,小于这样的范围比较,还有模糊匹配,他都是不能实现的。
所以数据库使用的是B+树
1.4.1、B树/B-树
我们在学习B+树的时候,先来了解一下它的前身B树。
B树,也叫B-树,此处的(-),不是减号,而是一个连接符。
每个结点中存放一定量的数据表中的每行数据(id,name,score,....),当结点中达到了规定的元素个数是,采取调整,B树就可以解决树过高的问题也就是IO访问次数过多的问题。但是这个结构MySQL中没有采用,而是使用B+树作为数据库数据存储的一种结构。
1.4.2、B+树
B+树的特点:
- 一个结点,可以存储N个key,同时N个key划分出了N个区间。
- 每个结点中的key的值,都会在子节点中存在,同时该key是子节点的最大值
- B+树的叶子节点,是首位相连,类似于一个链表
- 由于叶子节点是一个完整的数据集合,所以只在叶子节点这里存储数据表中的每一行的数据,而非叶子结点只存储key值本身即可
B+树的优势:
- 当前一个结点保存更多的key,最终树的高度更矮。查询的时候减少了IO访问(这里指硬盘访问次数)次数(和B树相同)
- 所有的查询最终都会落在叶子节点上。(查询任何一个数据,经过的IO访问次数是一样的,因为叶子节点保存完整的数据) 。所以B+树的IO访问次数是更稳定的。这样就可以对程序的执行效率有一个跟稳定的评估。
- B+树的所有的叶子节点,构成了链表,此时比较方便进行范围查询。(比如查询学号5~11的同学)
- 由于数据都在叶子结点上,非叶子节点只存储key,导致非叶子结点占用空间是比较小的,这些非叶子结点就可能在内存中缓存(或者缓存一部分)。进一步减少了IO访问次数。
1.4.3、回表
❓❓❓上述B+ 树这个结构,我们默认id是表的主键了。如果一个表中存在多个索引查询的时候该怎样查呢?针对id 有主键索引,name又存在一个索引。
❗❗❗表的数据还是按照id为主键,构建出B+树,通过叶子节点组织所有的数据行,其次,针对name这一列,会构建另外一个B+树。但是这个B+树的叶子结点就不再存储这一行的完整数据,而是存主键id。此时如果根据name来查询,查到叶子结点得到的只是主键id,还需要通过主键id去主键的B+树里再查一次。所以这里就需要查两次B+树了。这个操作就叫做回表
1.5、使用场景
要考虑对数据库表的某列或某几列创建索引,需要考虑以下几点
- 数据量较大,且经常对这些列进行条件查询
- 该数据看表的插入操作,及对这些列的修改操作频率较低
- 索引会占用额外的磁盘空间
满足以上条件时,考虑对表中的这些字段创建索引,以提高查询效率。 反之,如果非条件查询列,或经常做插入、修改操作,或磁盘空间不足时,不考虑创建索引。
1.6、索引的使用
1.6.1、查看索引
格式:
show index from 表名;
查询学生表中额索引,学生表中并没有手动创建索引,我们再设置主键的时候,自带的索引。
1.6.2、创建索引
对于非主键,非唯一约束、非外键的字段,可以创建普通索引(列名/字段名可以有多个,用逗号隔开,一个索引包含多个字段就叫做组合索引)
格式
create index 索引名 on 表名(列名); create index 索引名 on 表名(列名1,列名2...);
案例:我们基于name这一列创建一个普通索引。(相当于student表有基于name这一列创建出一个目录。)
create index index_student_name on student(name);
❗❗❗创建索引时因注意的事项:
- 当一个表已经创建好了,并且里面存在大量数据,这时候再根据表的某一列创建索引,这个操作有可能就很危险。表中的数据很大,建立索引的开销也就很大。可以想象一下有一本很厚的书,现在让你手动写一个目录出来,这个工作量就会很大。
- 好的做法就是再创建表的时候,就设定好索引。
- 如果表中存在很多数据了,索引就不要动了。
1.6.3、 删除索引
格式
drop index 索引名 on 表名;
案例:删除student表中的index_student_name索引。
❗❗❗ 注意:删除索引也存在风险。
1.7、索引的分类
MySQL目前的主要索引类型有:普通索引,唯一索引,主键索引,组合索引,全文索引,聚簇索引,非聚簇索引等。
- 普通索引:就是在数据表的字段设立索引的时候,不需要添加任何额外的限制条件(唯一,非空等的限制),该类型的索引可以创建再任何数据类型的字段中。
- 唯一索引:在数据表中设立索引的时候,限制索引修饰的字段必须具有唯一性,根据这个唯一索引可以比普通索引更快的查询到数据库中的某条记录。
- 主键索引:当创建表的时设立了主键约束,这张表也就存在了索引。也就是主键索引,也叫做聚簇索引。
- 全文索引:主要是对字符串类型建立的索引,基于分词的索引,主要是基于char,varchar,text类型的字段上,以便于能够更快的查询到数据量较大的字符串类型字段。
- 聚簇索引:聚簇索引一般指的是主键索引(表中存在主键索引的情况下)。
- 非聚簇索引:非聚簇索引的叶子节点仍然是索引值。如上边说到的存在主键索引(id)的情况下,又创建了一个普通索引(针对name),我们根据name查找数据的时候,会在name对应的B+树的叶子节点处找到索引值,再去id的B+树中找数据。
❗❗❗注意:如果一张表中没有主键,MySQL会每一行记录生成一个唯一的字段,使用这个字段作为索引。
2、事务
2.1、为什么使用事务
1️⃣现在转账的操作已经很普遍了,这里有一个账户表(account表),第一个用户又1000元,第二个用户没有钱。
2️⃣现在第一个用户个第二个用户转500元。这时候两步操作实现这个过程。
通过给2的账户加500,给1的账户减500实现。
update account set balance = balance - 500 where id = 1; update account set balance = balance + 500 where id = 2;
假设,在执行转账这个操作的时候,第一步操作执行完成之后,数据库奔溃了或者主机宕机了。此时只执行了第一句代码,第二句代码还没有被执行。呈现的一个情况就是1用户的钱扣了,但是2的钱还没有到账。显然这种操作是不现实的。
要让这种情况不发生,就需要使用事务来控制,保证让这两个SQL要么全部执行,要么执行失败。
2.2、事务的概念
- 本质就是把多个SQL语句打包成一个整体,要么全部执行成功,要么就一个都不执行,不会出现执行一半这样的中间状态。
在不同的环境中,都可以有事务。对应在数据库中,就是数据库事务。
2.3、回滚机制(rollback)
上面说到的,将多个SQL语句打包成一个整体,有么执行成功,要么不执行。这里的"不执行"并不是真的不执行,而是执行了,中间出了错误。选择了恢复现场,把数据还原到未执行之前的状态了。就像我们使用的Ctrl+Z 。我们把这个恢复数据的操作,称为"回滚"(rollback).事务的原子性就是通过回滚这个机制来保证的。
❓❓❓这里又存在一个问题,进行回滚的时候,咋知道回滚是要恢复到什么状态呢?
数据库中存在日志文件,他是专门用来记录事务的操作步骤的。这个日志文件是保存在硬盘空间的
2.4、事务的使用
三步操作
- 开启事务:start transaction;
- 执行多条SQL语句
- 回滚或提交rollback/commit;
rollback即是全部失败,commit即是全部成功。
案例:
start transaction;//开启事务
//执行多条SQL语句
update account set balance = balance - 500 where id = 1;
update account set balance = balance + 500 where id = 2;
commit;//提交事务
2.5、事务的特性
1、原子性(Atomicity)【核心特点】
将多个SQL语句打包成一个整体,这样的操作就叫做事务的原子性。表示事务的不可分割的最小单位。事务的出现就是为了解决原子性的问题。
2.一致性
事务执行之前于执行之后,要保持正确的结果。就像银行转账一样,1用户少了五百,2用户相应的是多了500,而不能是多了5000.
3、持久性
事务修改的内容是写到硬盘上的,持久存在的。重启服务器也不会丢失。
4、隔离性
隔离性是为了解决"并发"执行事务,所引起的问题。确保多个事务执行的过程中不互相干扰。
2.6、事务的隔离级别
MySQL服务器可以支持多个客户端的访问,如果多个客户端对多个数据表进行数据操作,不会产生问题,但是多个客户端对一个数据表中的数据进行操作。那么就会产生很多问题。这里就需要不同的隔离级别对可能出现的问题进行解决。
隔离级别越低,可以支持同时访问同一个数据表的客户端就越多,数据的安全性就越低;如果隔离级别越高,那么可以支持同时访问同一个数据表的客户端就越少,性能变低,数据安全性变高。
2.6.1、MySQL中的四种事务隔离级别
- read uncommitted (读未提交):没有进行任何限制,并发高(效率最高),隔离性最低(准确性最低)(脏读,不可重复读、幻读的问题都有可能出现)
- read committed(读提交):给写加锁,并发程度降低,隔离性提高了(不可重复读和幻读可能出现)
- repeatable read (可重复读):给写和读都加锁,并发程度有降低了,隔离性有提高了(幻读的问题可能出现)
- serializable(串行化):并发程度最低,隔离性最高。
2.7、脏读问题
一个事务A正在对数据进行修改的过程中,结果还没有提交之前,另外一个事务B,也对同一个数据进行了读取。读取的数据并不是提交后的数据。此时B的操作就成为"脏读",读到的数据也称为"脏数据"。这里的脏表示的意思是无效。
为什么说是无效的数据,事务A在对数据进行修改的时候,发生了回滚。那之前修改的数据,全都无效了。
❗❗❗解决脏读问题:给写操作加锁
给写操作的事务A加上一把锁,在事务A开始进行写操作的时候加锁,事务提交或者回滚的时候释放锁,被锁的事务不能与其他事务共存,写锁也叫排他锁,这样写操作与读操作就不能同时执行了。
这个给写加锁的操作,就降低了并发程度(降低了效率),提高了隔离性(提高了数据的准确性)。
2.8、不可重复度问题
脏读问题被解决了,又出现了新的问题。有三个事务A,B,C,事务A读取数据,事务B,C对同一条数据进行修改。现在事务B对这条数据进行了修改,并且提交了,在执行事务A的时候,对这一条数据进行了读取。得到了数据B。然后事务C对数据进行了修改,并提交了,现在事务B在执行过程中,又需要读取这条数据,现在读取的数据为数据C。这时就意味着同一个事务A之内,多次读取数据,读出来的结果是不相同的,我们预期是一个事务中,多次读取的结果得是一致的。这就叫做"不可重复读"。
解决不可重复读问题:给读操作加锁。
在读的事务也加锁,但是这个锁是一把(共享锁),多个读锁可以共存,但是由于写锁是排他锁,所以读锁不能与写锁共存,也就是说,在加了读锁之后,不能进行写操作。
通过这个读加锁,有进一步的降低了事务的并发处理能力(处理效率也降低),提高了事务的隔离性(数据的准确性有提高了)。
2.9、幻读问题
当前已经进行了写加锁和读加锁。简单来说就是在写操作的时候不能进行其他操作,在读的时候,不能进行写操作。读加锁这时针对一条记录来说,在读的时候别的事务不能对这条记录进行修改,但是可以在这个数据表中添加别的记录。幻读就是一个事务用一样的 SQL 多次查询,结果每次查询都会发现查到一些之前没看到过的数据。之前读到的数据在这次读到的数据中没有发生变化,但是它的结果集发生了变化。
💥举个例子:
你一个事务 A,先发送一条 SQL 语句,里面有一个条件,要查询一批数据出来,如
SELECT * FROM table WHERE id > 10
。然后呢,它一开始查询出来了 10 条数据。接着这个时候,别的事务 B往表里插了几条数据,而且事务 B 还提交了,此时多了几行数据。接着事务 A 此时第二次查询,再次按照之前的一模一样的条件执行
SELECT * FROM table WHERE id > 10
这条 SQL 语句,由于其他事务插入了几条数据,导致这次它查询出来了 12 条数据
解决幻读问题:串行化
数据库使用"串行化"这样的方式来解决幻读。彻底放弃并发处理事务,一个接一个的串行的处理事务。这样做并发的程度是最低的(效率是最慢的),隔离性是最高的(准确性也是最高的)