索引

索引是什么

官方介绍索引是帮助MySQL高效获取数据数据结构。更通俗的说,数据库索引好比是一本书前面的目录,能加快数据库的查询速度

聚簇&非聚簇索引

聚簇索引(主索引)

聚簇索引也叫聚集索引或者主索引,它并不是一种单独的索引类型,而是一种数据存储方式。在聚簇索引中,数据行的物理存储顺序与索引顺序相同,即数据是按照索引键的顺序存储的。聚簇索引的叶子节点保存了一行记录的所有列信息,也就是说,叶子节点中包含了一个完整的记录行。

聚簇索引的自动创建

特点

聚簇索引是将数据放在了叶子节点,找到索引也就是找到了数据。由于是按照创造索引的字段进行排序,所以进行范围查询时非常的高效。但是每一个表只能有一个聚簇索引,如果频繁的插入或者删除数据,可能导致数据页的频繁分裂和合并。

非聚簇索引(辅助索引)

非聚簇索引也叫辅助索引、普通索引。非聚簇索引的叶子节点只包含聚簇索引的键(或指向聚簇索引的指针),而不包含完整的记录行。因此,通过非聚簇索引查找记录时,需要先找到主键,然后再通过主键到聚簇索引中找到对应的记录行,这个过程被称为回表

特点

非聚簇索引可以有多个

非聚簇索引是分开存储的,所以适合更新或者插入数据的场景。因为更新时候只需要更新索引而不需要移动数据。

范围查询的效率较低,需要回表查询,所以效率较低

索引的实现方式

Hash

Hash是一种基于Hash函数实现的数据结构,底层原理包括了以下三个方面:

  1. 哈希函数(Hash Function): 哈希函数是哈希表的核心,它接受一个键作为输入,并输出该键对应的哈希值。哈希函数应该具有以下特性:
    • 对于相同的键,哈希函数应该始终返回相同的哈希值。
    • 哈希函数应该尽可能均匀地将不同的键映射到不同的哈希值,以减少哈希冲突的发生。
    • 哈希函数的计算速度应该尽可能快,以保证高效的插入和查找操作。
  2. 哈希表数组(Array): 哈希表通常由一个数组来存储数据。数组的每个元素称为一个桶(Bucket),每个桶可以存储一个或多个键值对。哈希表的长度通常是固定的,但也可以根据需要进行动态扩容。
  3. 解决哈希冲突: 由于不同的键可能映射到数组中的同一个位置,可能会发生哈希冲突。常见的解决哈希冲突的方法包括:
    • 链地址法(Separate Chaining):在哈希表的每个桶中维护一个链表或者其他数据结构(如红黑树),用于存储发生哈希冲突的键值对。
    • 开放地址法(Open Addressing):在发生哈希冲突时,线性地探查哈希表的其他位置,直到找到一个空闲的位置来存储键值对。常见的探查方法包括线性探查、二次探查、双重哈希等。
  4. 哈希表的装载因子(Load Factor): 哈希表的装载因子是指哈希表中已存储的键值对数目与哈希表长度之比。装载因子的大小会影响哈希表的性能,通常会根据装载因子的阈值进行动态扩容或者收缩,以保持哈希表的性能。
BTree(多路平衡二叉树)

普通的二叉树,根节点如果选的不好的话最后的树会达不到加速查询的效果,普通二叉树的进化平衡二叉树在我们使用的时候已经好了很多了,但是他如果节点变得很多,那么遍历的路径也会很长,查询的效果也会变差,并且还不能支持范围查找。所以我们需要多路平衡二叉树来解决我们这个问题。

B 树

每一个节点都是由 : 键值 + data + 子节点的指针构成

  • 键值:设置索引的值
  • data:保存了当前记录中除了索引项外的其他数据
  • 子节点指针:指向了叶子节点

【Java八股面试系列】数据库(总结市面所有数据库知识点)-LMLPHP

**优点:**查询的时候只需要比较索引的值就可以快速的找出目标值,不用把全部的信息读入内存中进行比较。

**缺点:**不支持范围查询,需要将范围内的每一个从根节点挨个查询

B+树(Mysql现在使用树结构)

B+树的非叶子节点都只是存储查找的键值,不存储数据,而叶子节点存储了全部的索引项数据,每一次查询都是需要查询到叶子节点。

叶子节点:可能不是一个单个的值,会是一块数据,里面有好几个data或者指向data的指针(InnoDB中Data存储的为行数据,而MyIsam中存储的是磁盘地址)。叶子节点都是使用指针连接起来的

【Java八股面试系列】数据库(总结市面所有数据库知识点)-LMLPHP

**优点:**可以进行范围查询,只需要找到起始节点就可以。磁盘读写很快,因为非叶子节点只存储关键信息,不存储data;

**缺点:**更新操作复杂,空间占用会相较于B树多一点。

两种实现方式区别

索引的实现其实主流有两种方式:

  • 一种是Hash方式,但是这种的根据key - value的存储方式,很适合直接的等值查找,不适合经常使用范围查找。
  • 一种是Btree,这种实现方式在等值查询的时候速度没有hash快,但是在进行范围查询的时候是不用遍历全表的,支持范围查询。

引申

红黑树

也是一种自平衡二叉树,它具有以下特性:

红黑树具有以下特性:

  • 节点颜色: 每个节点要么是红色,要么是黑色。
  • 根节点: 根节点是黑色的。
  • 叶子节点(NIL节点): 叶子节点是黑色的,也被称为NIL节点。它们不存储任何数据,只是作为树结构的辅助。所有指向NIL节点的指针都被认为是黑色的。
  • 颜色限制: 不能有两个相连的红色节点,即红色节点不能相邻,每个红色节点的子节点都是黑色的。
  • 黑高度: 从任意节点出发,到达其每个叶子节点的路径上的黑色节点数量必须相同,这被称为黑高度。这个性质保证了树的平衡。
红黑树和AVL树的区别
  1. 平衡性:

红黑树:红黑树保证了一种弱平衡,即树的高度最多是2倍的对数级别。这使得红黑树在插入和删除操作时具有更高的灵活性,但可能导致一些操作稍微不如AVL树高效。
AVL树:AVL树是一种严格的平衡树,保证任何节点的左子树和右子树的高度差(平衡因子)不超过1。这确保了AVL树在平衡方面表现更好,但在插入和删除操作时可能需要更多的旋转来维持平衡

  1. 插入和删除操作的性能:

红黑树:由于红黑树的平衡性要求相对较弱,插入和删除操作通常需要更少的旋转操作,因此在这些操作上性能可能比AVL树更好。
AVL树:AVL树的严格平衡性可能导致插入和删除操作需要更频繁的旋转操作,因此在这些操作上可能比红黑树略逊一筹。

  1. 搜索性能:

两者在搜索操作上都表现良好,因为它们都是二叉搜索树,保持了有序性。

  1. 存储空间:

红黑树:红黑树在维护平衡的同时,需要存储额外的颜色信息,因此通常比AVL树占用更少的存储空间。
AVL树:AVL树由于需要维护严格的平衡,可能需要更多的额外指针和信息,因此通常比红黑树占用更多的存储空间。

  1. 适用场景:

红黑树:适用于在插入和删除操作较频繁、搜索操作相对较少的场景,例如在数据库索引中。
AVL树:适用于搜索操作频繁、插入和删除操作相对较少的场景,以及对于对树的平衡性要求较高的场景。

最大堆和最小堆

最大堆和最小堆都是由完全二叉树或者是近似完全二叉树实现的,每个节点的左子树和右子树都是个最大最小堆。这种结构从根节点到任意节点的路径都是有序的

最大堆:即根节点是此堆中最大的元素

最小堆:即根节点是此堆中最小的元素

最大堆的插入

最大堆的插入节点的思想就是先在堆的最后添加一个节点,然后沿着堆树上升。

最大堆的删除

将要删除的节点和最后的一个节点进行互换,然后从新的节点处向下检查是否符合最大最小堆的原则

索引的优劣

优点
  • 可以提高数据检索的效率,降低数据库的IO成本,类似于书的目录。
  • 通过索引列对数据进行排序,降低数据排序的成本,降低了CPU的消耗。
    • 被索引的列会自动进行排序,包括【单列索引】和【组合索引】,只是组合索引的排序要复杂一些。
    • 如果按照索引列的顺序进行排序,对应order by语句来说,效率就会提高很多。
劣势
  • 索引会占据磁盘空间
  • 索引虽然会提高查询效率,但是会降低更新表的效率。比如每次对表进行增删改操作,MySQL不仅要保存数据,还有保存或者更新对应的索引文件。

索引的使用

  1. 如果表中的某一项数据具有唯一性,可以创建唯一索引,既保证数据的唯一性还能加速查询
  2. 经常进行where的字段可以创建一个索引来加速查询
  3. 更新频率较低的数据,可以创建一个索引加速查询

不适合使用的场景:

  1. 经常进行更新的数据表,不适合做索引,每一次更新也都会重新更新索引,而且大量的更新可能会导致索引的重新创建
  2. 表较小的也不适合

索引失效的时候

  1. 组合查询的时候or,一边如果不是索引的话则不走索引
  2. 模糊查询的时候使用通配符也会失效
  3. 使用运算符也会失效

事务

事务指的是逻辑上的一组操作,要的都执行, 要么都不执行.它确保了数据的一致性和完整性,提高了系统的性能和数据的安全性。同时还具有ACID特性。

【Java八股面试系列】数据库(总结市面所有数据库知识点)-LMLPHP

特性

  • 原子性:事务被视为最小的单元,所有的操作要么都完成,要么都不完成**(Undo Log实现)**
  • 一致性:数据库总是从一个一致性的状态转换到另外一个一致性的状态,也就是说在某个时间是A,另一个时间是B。(Undo Log实现)
  • 隔离性:一个事务做的修改在最终的提交之前,那么对其他的事务都是不可见的,锁和多版本控制实现就符合隔离性。
  • 持久性:一旦事务提交,则其所做的修改将会永远的保存到数据库中,即使系统发生崩溃,事务执行的结果也不能丢**(Redo Log实现)**

事务并发问题

  • 脏读(Dirty Read):事务所做的修改,即使未提交,对其他事务也是可见的。事务可以读取未提交的数据,也称为脏读
  • 不可重复读(Non-repeatable Read):事务 A 在事务 B 开始前读和事务 B 结束后读的数据不一样,因为数据被事务 B 修改了,称为不可重复读
  • 幻读(Phantom Read):当同一个查询在不同时间产生不同的结果集时,称之为幻读。

隔离级别

实现隔离机制的方法主要有两种:

  • 读写锁 (RR,RC)的实现方式
  • 一致性快照读,即MVCC (RU,S)的实现方式

RU 读未提交

最低的隔离级别,允许三个事务的并发问题,什么都不需要做。

RC 读已提交

Read Committed:只有在事务提交后,其更新结果才会被其他事务看见。可以解决脏读问题。

解决:

  • 事务对当前被读取的数据加 行级共享锁(当读到时才加锁),一旦读完该行,立即释放该行级共享锁;
  • 事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加 行级排他锁,直到事务结束才释放。

RR 可重复读(Mysql默认的隔离级别)

通过Mvcc多版本控制

S 串行化

通过多版本控制

锁的分类

行锁

行锁就是一锁锁一行或者多行记录,mysql的行锁是基于索引加载的,所以行锁是要加在索引响应的行上,即命中索引。如果某一个事务是在修改某一行的数据,那么在事务提交之前就会进行锁定,其他的事务不能进行修改,但是可以进行访问

**行锁的特征:**锁冲突概率低,并发性高,但是会有死锁的情况出现。

自动释放

当两个线程同时修改数据库的两条数据的时候,就可能发生死锁

如果一个事务一直不进行提交,那么mysql检测到这种情况的时候,会自动的回滚事务并且释放行锁

表锁

表锁就是一锁就是一整张表,mysql的表锁是基于非索引字段的在表锁定期间,其他的事务不能对表进行操作,必须等待表的锁被释放才能进行操作。

为什么行锁基于索引,表锁基于非索引?

行锁是为了增加并发度,而并发度高说明查询访问的频率是比较高的,所以这时候走索引是最合适的,所以就是基于索引进行加行锁

意向锁

意向锁就是类似于表示这个表中是否已经有行锁,如果有的话则无法申请表锁

**情景:**线程A需要申请一个表锁,那么他首先会判断表中是否已经上锁,其次判断表中的每一行是否有行锁,如果没有这个意向锁的话,那么就会一行一行进行扫描,那么这样是非常耗费时间的。

锁算法

Mvcc(多版本并发控制)原理

Mvcc叫多版本并发控制,它主要是解决了数据库的读写和写写锁冲突导致的阻塞和性能问题,提高了数据库的并发性能和可扩展性。它的底层实现是依赖于隐式字段,undo日志,快照读&当前读,Read View。

隐式字段

对于InnoDB存储引擎,每一行记录都有两个隐藏列**DB_TRX_ID、DB_ROLL_PTR,如果表中没有主键和非NULL唯一键时,则还会有第三个隐藏的主键列DB_ROW_ID**。

  • DB_TRX_ID,记录每一行最近一次修改(修改/更新)它的事务ID,大小为6字节;
  • DB_ROLL_PTR,这个隐藏列就相当于一个指针,指向回滚段的undo日志,大小为7字节;
  • DB_ROW_ID,单调递增的行ID,大小为6字节;

undo日志

undo日志是数据库生成的一个记录日志,记录了对数据的修改历史,能够对我们的事务或者操作进行回滚。每一条数据通过回滚指针来连接undo日志链。

undo日志中存储了事务的id,进行的什么操作,还有下一个指针

快照读&当前读

快照读:
读取的是记录数据的可见版本(有旧的版本),不加锁,普通的select语句都是快照读,如:

select * from account where id>2;

当前读:
读取的是记录数据的最新版本,显示加锁的都是当前读

select * from account where id>2 lock in share mode;
select * from account where id>2 for update;

Read View

  • Read View就是事务执行快照读时,产生的读视图。
  • 事务执行快照读时,会生成数据库系统当前的一个快照,记录当前系统中还有哪些活跃的读写事务,把它们放到一个列表里。
  • Read View主要是用来做可见性判断的,即判断当前事务可见哪个版本的数据~
可见性算法

Read View遵循一个可见性算法,主要是将要被修改的数据的最新记录中的事务ID取出,然后通过

Read View遵循一个可见性算法,主要是将要被修改的数据的最新记录中的 DB_TRX_ID(即当前事务 ID )取出来,与系统当前其他活跃事务的 ID 去对比(由 Read View 维护),如果 DB_TRX_ID 跟 Read View 的属性做了某些比较,不符合可见性,那就通过 DB_ROLL_PTR 回滚指针去取出 Undo Log 中的 DB_TRX_ID 再比较,即遍历链表的 DB_TRX_ID(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的 DB_TRX_ID , 那么这个 DB_TRX_ID 所在的旧记录就是当前事务能看见的最新老版本

快照读判断流程

假如我们现在需要生成一个基于当前活动的事务的一个快照读,那么我们判断每一条数据的应该展示哪个版本就需要使用快照读

我先简化一下 Read View,我们可以把 Read View 简单的理解成有三个全局属性

  • 首先比较 DB_TRX_ID < up_limit_id , 如果小于,则当前事务能看到 DB_TRX_ID 所在的记录,如果大于等于进入下一个判断
  • 接下来判断 DB_TRX_ID >= low_limit_id , 如果大于等于则代表 DB_TRX_ID 所在的记录在 Read View 生成后才出现的,那对当前事务肯定不可见,如果小于则进入下一个判断
  • 判断 DB_TRX_ID 是否在活跃事务之中,trx_list.contains (DB_TRX_ID),如果在,则代表我 Read View 生成时刻,你这个事务还在活跃,还没有 Commit,你修改的数据,我当前事务也是看不见的;如果不在,则说明,你这个事务在 Read View 生成之前就已经 Commit 了,你修改的结果,我当前事务是能看见的

Redo & Undo日志

Redo和Undo日志的详解

Redo和Undo日志是数据库的两种日志类型,他们在数据库的恢复和事务的处理中起了关键作用,并且确保数据库的ACID特性中的A,C,D特性。

简介

  • redo log 是存储引擎层(innodb)生成的日志,记录的是物理层面的对数据页的操作,比如在某数据页某偏移量下写入或删除了某数据。
  • undo log:存储引擎层(innodb)生成的日志,记录的是逻辑层面的操作,且记录的都是相反的操作,比如对某行数据进行了insert操作,uodo日志中就会记载相反的操作delete,主要用于事务的回滚。

Redo和Undo本质上都是一种恢复操作。
Undo是把对数据表的操作回滚回去的行为,而Redo操作是比如对表数据进行了操作,但是没有刷新到磁盘上时,数据库宕机了,在数据库重启后,可以去Redo日志中找回这种操作的记录,继续执行。

Redo日志

Redo日志是InnoDB生成的一个记录日志,记录了每一次的对数据库的修改动作,解决了数据库出问题的恢复问题。

  • Innodb采用了"日志先行"策略,每次修改,先写日志,再写磁盘,只有日志写入成功,才算事务提交成功。

数据库的存储引擎(Innodb)是以数据页为单位来管理存储空间的,所有的数据都是放在磁盘中的。
加载一个数据页中的数据,需要先将这个数据页从磁盘缓存到内存中(放在Buffer Pool中)然后才可以访问。我们对数据库的增删改操作都是对内存中数据的修改,然后内存在将数据的变动以一定的频率刷新到磁盘上
【Java八股面试系列】数据库(总结市面所有数据库知识点)-LMLPHP

优点:

  • 能够降低刷磁盘的效率,redo日志降低了刷盘的次数
  • redo日志占用的空间非常小
redo的组成

分为两个部分:缓冲区和日志文件区

缓冲区(redo log buffer)

  • 缓冲区是内存层面的
  • 缓冲区默认大小16M,最小是1M,最大4096M
  • 缓冲区会将空间划分成一个个的缓冲块:
Redo日志运行流程

【Java八股面试系列】数据库(总结市面所有数据库知识点)-LMLPHP

过程:
1.将原始数据从磁盘读入内存中,修改内存中的数据拷贝
2.生成一条日志记入redo log buffer中,记录的是数据修改的值
3.当事务提交时,将redo log buffer 中的内容刷新到redo log file ,对redo log file采取的是追加写的方式
4.隔一段时间就将内存中修改的数据刷寻到磁盘中

undo日志

undo日志是数据库生成的一个记录日志,记录了对数据库的修改历史,能够对我们的事务或者操作进行回滚

存储引擎

数据库的引擎是数据库系统中负责存储和管理数据的核心组件之一。它负责处理数据的存储、索引、查询、事务管理等功能,并且对数据库的性能、可靠性、并发性等方面有着重要影响。

InnoDB

这是Mysql的默认存储引擎,它提供了事务安全(ACID兼容)表,支持外键引用完整性约束。它支持提交、回滚和紧急恢复功能来保护数据。它还支持行级锁定。当在多用户环境中使用时,它的“一致非锁定读取”提高了性能。它将数据存储在集群索引中,从而减少了基于主键的查询的I/O。

支持外键,保持数据的一致性和完整性

innoDB拥有自己独立的缓冲池,常用的数据和索引都在缓存中

InnoDB 物理文件结构为:

.frm 文件:与表相关的元数据信息都存放在frm文件,包括表结构的定义信息等;

.ibd 文件或 .ibdata 文件: 这两种文件都是存放 InnoDB 数据的文件,之所以有两种文件形式存放 InnoDB 的数据,是因为 InnoDB 的数据存储方式能够通过配置来决定是使用共享表空间存放存储数据,还是用独享表空间存放存储数据。

MyISAM

MYISAM强调了快速读取操作,这可能就是为什么MYSQL受到了WEB开发如此青睐的主要原因:在WEB开发中你所进行的大量数据操作都是读取操作。所以,大多数虚拟主机提供商和INTERNET平台提供商只允许使用MYISAM格式。

每个MyISAM在磁盘上存储成三个文件,每一个文件的名字均以表的名字开始,扩展名指出文件类型。

存储引擎的差异

数据页

一个InnoDB数据页的存储空间大致被划分成了7个部分,有的部分占用的字节数是确定的,有的是不确定的,一共占用了16kb

【Java八股面试系列】数据库(总结市面所有数据库知识点)-LMLPHP

每个部分存储的内容:

【Java八股面试系列】数据库(总结市面所有数据库知识点)-LMLPHP

二、用户真实记录在数据页中的存储(Free Space)
在页的7个组成部分中,我们自己存储的记录会按照我们指定的行格式存储到User Records部分。但是在一开始生成页的时候,其实并没有User Records这个部分,每当我们插入一条记录,都会从Free Space部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到User Records部分,当Free Space部分的空间全部被User Records部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了。这个过程的图示如下:

【Java八股面试系列】数据库(总结市面所有数据库知识点)-LMLPHP

为了更好的管理在User Records中的这些记录,InnoDB可费了一番力气呢,在哪费力气了呢?不就是把记录按照指定的行格式一条一条摆在User Records部分么?其实这话还得从记录行格式的记录头信息中说起

03-30 19:32