MySQL 知识体系

MySQL基本架构

Mysql是一个关系型数据库,由表结构来存储数据与数据之间的关系,同时用sql(Structured query language)翻译过来叫做 结构化查询语句来进行数据操作。
sql语句进行操作又分为几个重要的操作类型:

DQL:Data Query Language 数据检索语句 where
DML:Data Manipulation Language 添加 、删除、修改语句
DDL:Data definition language 数据库结构操作,create alter等等
DCL:权限控制语句
TCL:事务相关 比如commit/savepoint、rollback

总体上,我们可以把MySQL分成三层,跟客户端对接的连接层,真正执行操作的服务层,和跟硬件打交道的存储引擎层

1 连接层

我们的客户端要连接到MySQL服务器3306端口,必须要跟服务端建立连接,那么管理所有的连接,验证客户端的身份和权限,这些功能就在连接层完成。

连接有关的变量

2 服务层

连接层会把SQL语句交给服务层,这里面又包含一系列的流程:比如查询缓存的判断、根据SQL调用相应的接口,对我们的SQL语句进行词法和语法的解析(比如关键字怎么识别,别名怎么识别,语法有没有错误等等)。然后就是优化器,MySQL底层会根据一定的规则对我们的SQL语句进行优化,最后再交给执行器去执行。

2.1 解析器

词法解析
将sql语句打碎,转化成一个一个关键单词,然后交给语法解析器去构建语法树,判断语法是否正确。

语法解析
语法解析已经知道每个sql语句的单词了,那么在语法解析的时候,会去检查语法是否正确。表名、列名是否存在、用户是否有操作权限等等

2.2 预处理器

a. 每次执行语句时解析语句的开销更少。通常,数据库应用程序处理大量几乎相同的语句,仅更改 WHERE 查询和删除、 SET 更新和 VALUES 插入等子句中的文字或变量值。
b. 防止 SQL 注入攻击。参数值可以包含未转义的 SQL 引号和定界符。

那如果我写了一个词法和语法都正确的SQL,但是表名或者字段不存在,会在哪里报错?是在数据库的执行层还是解析器?比如:

select * from xxxx;

 解析器可以分析语法,但它怎么知道数据库里面有什么表,表里面有什么字段呢?实际上还是在解析的时候报错,解析SQL的环节里面有个预处理器。它会检査生成的解析树,解决解析器无法解析的语义。比如,它会检査表和列名是否存在,检査名字和别名,保证没有歧义。

2.3 优化器

根据上面的流程,我们知道要去执行什么语句,但是具体怎么执行会有很多的方式,比如走哪个索引,你的语句是不是可以优化。优化后会生成一个最终的执行计划,所以这个语句到底怎么走,优化器来决定。

2.4 执行器

根据执行计划,去调用数据存储的地方,也就来到了我们的存储层。执行器去根据表设置的存储引擎,调用不同存储引擎的API接口获取数据。

3 存储引擎层

存储引擎就是我们的数据真正存放的地方,在MySQL里面支持不同的存储引擎。再往下就是内存或者磁盘。

常见的存储引擎: MySQL :: MySQL 8.0 Reference Manual :: 18 Alternative Storage Engines

InnoDB MySQL 8.0默认的存储引擎。InnoDB是一个事务安全(兼容ACID)的MySQL存储引擎,具有提交、回滚和崩溃恢复功能,以保护用户数据。InnoDB行级别的锁(没有升级到更粗粒度的锁)和oracle风格一致的非锁读取提高了多用户并发性和性能。InnoDB将用户数据存储在聚集索引中,以减少常见的基于主键的查询的I/O。为了维护数据的完整性,InnoDB还支持外键引用完整性约束。
MyISAM 这些表占用空间很小。表级锁限制了读/写工作负载的性能,因此它经常用于Web和数据仓库配置中的只读或以读为主的工作负载中。

Innodb架构和原理 

MySQL :: MySQL 8.0 Reference Manual :: 17.4 InnoDB Architecture

1 内存结构

1.1 buffer pool

        数据最后落盘到我们的磁盘表空间。那么每次检索假如都去磁盘获取,明显性能会比较慢。所以InnoDB为了性能,采用了内存缓存机制,在内存中缓存相应的数据。那么这个内存区间叫做BufferPool。
        缓冲池是主内存中的一个区域,在 InnoDB 访问时缓存表和索引数据。缓冲池允许直接从内存访问频繁使用的数据,从而加快处理速度。
InnoDB设定了一个存储引擎从磁盘读取数据到内存的最小的单位,叫做页。操作系统也有页的概念。操作系统的页大小一般是4KB,而在InnoDB里面,这个最小的单位默认是16KB大小。如果要修改这个值的大小,需要清空数据重新初始化服务。
修改数据的时候,也是先写入到buffer pool,而不是直接写到磁盘。内存的数据页和磁盘数据不一致的时候,我们把它叫做脏页。InnoDB里面有专门的后台线程把Buffer Pool的数据写入到磁盘,每隔一段时间就一次性地把多个修改写入磁盘,这个动作就叫做刷脏。

1.2 change buffer

        如果一个数据页不是唯一索引,不存在数据重复的情况,也就不需要从磁盘加载索引页判断数据是不是重复(唯一性检查)。这种情况下可以先把修改记录在内存的缓冲池中,从而提升更新语句(Insert,Delete,Update)的执行速度。
  这一块区域就是Change Buffer。Change Buffer 是 Buffer Pool 的一部分。5.5之前叫Insert Buffer插入缓冲,现在也能支持 delete 和 update。
  最后把Change Buffer记录到数据页的操作叫做merge。什么时候发生merge? 有几种情况:在访问这个数据页的时候、或者通过后台线程、或者数据库shut down、 redo log写满时触发。
  如果数据库大部分索引都是非唯一索引,并且业务是写多读少,不会在写数据后立刻读取,就可以使用Change Buffer (写缓冲)。

  可以通过调大这个值,来扩大Change的大小,以支持写多读少的业务场景:

SELECT @@innodb_change_buffer_max_size; -- 默认是内存空间的25%

1.3 adaptive hash index

自适应哈希索引是 InnoDB 存储引擎特有的一个功能,它是为了优化某些热点数据的查询性能而自动构建的。自适应哈希索引不同于传统的哈希索引,因为它是自动和动态的:InnoDB 会根据查询模式和数据访问频率自动决定是否构建哈希索引,并且会根据数据的变化和查询模式的变化动态地调整哈希索引。

自适应哈希索引的工作原理是,当 InnoDB 注意到某些索引值被频繁地以等值查询的方式访问时,它会在内存中为这些值建立哈希索引,从而加速后续的等值查询。这个过程是自动的,不需要用户干预。
自适应哈希索引的优点
自动优化:自适应哈希索引会自动构建和维护,不需要用户显式创建或管理。
性能提升:对于某些等值查询,自适应哈希索引可以显著减少查找时间,哈希索引,查询消耗 O(1)
降低对二级索引树的频繁访问资源。
自适应哈希索引也有一些限制和考虑因素
内存消耗: 自适应哈希索引完全在内存中构建,因此需要足够的内存资源。在高负载下,它可能会消耗大量的内存。
不可预测性:由于是基于运行时查询模式的,所以哈希索引的存在和组成是不可预测的。
不适用于所有查询:自适应哈希索引主要优化等值查询,对于范围查询或排序操作没有帮助。
hash自适应索引会占用innodb buffer pool;

总的来说,自适应哈希索引是 InnoDB 存储引擎为了提高特定类型查询性能而自动构建的一种内存中的哈希索引结构。它可以根据查询模式和数据访问频率自动调整,以优化数据库的性能。

1.4 redo log buffer

Redo Log也不是每一次都直接写入磁盘,在Buffer Pool里面有一块内存区域(Log
Buffer)专门用来保存即将写入日志文件的数据,它一样可以节省磁盘IO。

查看Log Buffer大小

SHOW VARIABLES LIKE 'innodb_log_buffer_size';--默认16M

需要注意:redo log的内容主要是用于崩溃恢复。磁盘的数据文件,数据来自buffer pool。redo log写入磁盘,不是写入数据文件。
  在我们写入数据到磁盘的时候,操作系统本身是有缓存的。flush就是把操作系统缓冲区写入到磁盘。
  Log buffer写入磁盘的时机,由一个参数控制,默认是1:

 SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit'; //RedoLog同步方案
默认设置为1

  • 0 Log buffer将每秒一次地写入log file中,并且log file的flush操作同时 进行。该模式下,在事务提交的时候,不会主动触发写入磁盘的操作。 这个策略的性能是最佳的,但是会存在1s的数据丢失.
  • 1 每次事务提交时MySQL都会把log buffer的数据写入log file,并且刷 到磁盘中去。这个策略能保证强一致性,也是InnoDB默认的配置,为的 是保证事务的ACID特性
  • 2 每次事务提交时MySQL都会把log buffer的数据写入log file。但是 flush操作并不会同时进行。该模式下,MySQL会每秒执行一次 flush操 作。这种策略,如果操作系统出现崩溃,也可能会存在1s的数据丢失

2 磁盘结构

磁盘中的文件后缀我们知道肯定是ibd。https://dev.mysql.com/doc/refman/8.0/en/innodb-tablespace.html

每个表空间都是由相同大小的page页来组成的,默认page页的大小为16KB;也可以根据innodb_page_size来设定页的大小。

是表空间的分区,一个表空间中,会有多个段组成,常见的短有数据段、索引段、8.0之前有回滚段。

来管理页,当页的大小在16K以下,一个区的大小是1M,如果32K是2M,64K则为4M。后面磁盘释放分配都是以区为单位。所以,一个extent下最少可以存储64个page页。

表的格式决定了其行的物理存储方式,这反过来又会影响查询和 DML 操作的性能

2.1 系统表空间

系统表空间是变更缓冲区的存储区。如果在系统表空间中创建表,而不是在每个表文件或常规表空间中创建表,则它也可能包含表和索引数据。在以前的MySQL版本中,系统表空间包含InnoDB数据字典。在MySQL 8.0中,InnoDB将元数据存储在MySQL数据字典中。在以前的MySQL版本中,系统表空间还包含doublewrite 缓冲区存储区。自MySQL 8.0.20起,此存储区位于单独的 doublewrite 文件中。

系统表空间可以有一个或多个数据文件。默认情况下,在数据目录中创建一个名为 ibdata1 的系统表空间数据文件。系统表空间数据文件的大小和数量由 innodb_data_file_path 启动项定义。

归纳一下,系统表空间包含 数据字典,双写缓冲,变更缓冲区、undo日志,以及在系统表空间创建的表的数据和索引。

系统空间(space 0)在InnoDB中是特殊的,以固定页码分配的相当多页面,以存储对InnoDB操作至关重要的各种信息。由于系统空间是一个与其他空间一样的空间,它的前三个页面包括: FSP_HDR,IBUF_BITMAP 和 INODE页。

2.2 独占表空间

这个开关通过innodb_file_per_table设置,默认开启。

SHOW VARIABLES LIKE 'innodb_file_per_table';

开启后,则每张表会开辟一个表空间,这个文件就是数据目录下的ibd文件,存放表的索引和数据。但是其他类的数据,如回滚(undo)信息,插入缓冲索引页、系统事务信息,二次写缓冲(Double write buffer)等还是存放在原来的共享表空间内。

独立文件表空间包含单个InnoDB表的数据和索引,并存储在文件系统中的单个数据文件中。
独立表空间优点:
1.当删除独立表空间时,磁盘空间会释放给操作系统,但是后面我们讲到的共享表空间,空间是只能用于 InnoDB 数据。表被截断或删除后,共享表空间数据文件的大小不会缩小。
2.性能会更好,因为我操作的表是每个表独立的文件,文件更小。
3.共享表空间中的表的大小受到 64TB 表空间大小限制。相比之下,每个表文件表空间的大小限制为 64TB,这为单个表的大小增长提供了充足的空间。
缺点:可能会造成空间浪费,造成更多的碎片。文件过多,会需要更多的文件句柄。

2.3 通用表空间

   与系统表空间类似,通用表空间是能够为多个表存储数据的共享表空间。但是这个空间可以自己进行维护管理。
通用表空间的限制:
  同时通用表空间只能创建在已知目录,已知目录是由datadir、innodb_data_home_dir和innodb_directories变量定义的目录。

2.4 临时表空间

    存储临时表的数据,包括用户创建的临时表,和磁盘的内部临时表。当数据服务器正常关闭时,该表空间被删除,下次重新产生。

2.5 undo表空间

undo Log的数据默认在系统表空间中,因为共享表空间不会自动收缩,也可以单独创建一个undo表空间。
undo log(撤销日志或回滚日志)记录了事务发生之前的数据状态,分为insert undo log和update undo log。如果修改数据时出现异常,可以用undo log来实现回滚操作 (保持原子性)。

2.6 doblewrite buffer files

双写配置

我们知道了,我们的page页会异步刷新到磁盘,但是page页的大小是16k,而操作系统跟磁盘交互是4K,当然不同的操作系统会有所不同,一个页是需要经过多次才能完整同步到磁盘,如果中间如果出现操作系统,磁盘或者进程意外退出怎么办?

所以Mysql提供了一个doubleWrite机制。简单点 就是备份

SELECT @@innodb_doublewrite; -- 默认开启 会加强一致性,但是会影响一定的性能

何为双写,就是page页刷新到磁盘的时候,把这个page数据写到不同的地方去,当出现问题是,有备份来达到持久性跟数据的一致性。
在8.0.20之前,双写位于系统表空间,8.0.2开始,位于doublewrite文件中

写几个备份

SELECT @@innodb_doublewrite_files; -- 默认为2 至少2个备份 

双写磁盘的位置配置

SELECT @@innodb_doublewrite_dir;
SELECT @@innodb_data_home_dir; 

如果以上2个都没配置,则默认用的是数据目录

SELECT @@datadir; 

双写过程 

1). 当脏页被写入到缓存池(也称为内存池)时,它们将被写入到Doublewrite缓冲区中,而不是直接写入到磁盘上的表空间中。
2). Doublewrite缓冲区是一个位于内存中的缓冲区,用于存储即将写入磁盘的脏页的数据。在写入到Doublewrite缓冲区后,数据将被写入到两个不同的磁盘区域,这样即使其中一个磁盘出现问题,数据也可以从另一个磁盘恢复。在写入到磁盘之前,数据会被缓存到磁盘的WriteCache中,确保数据能够快速写入磁盘。
3). 在数据被写入磁盘之前,Doublewrite缓冲区中的每个脏页的LSN(日志序列号)都会被更新,以确保数据的一致性。这是因为LSN是一种用于恢复数据的唯一标识符,它可以确保在数据库出现故障时,数据可以恢复到一个一致的状态。
4). 一旦数据被成功写入到磁盘上的两个不同区域并且LSN已经被更新,数据就被标记为干净页并从缓存池中移除。此时,这个脏页的数据已经被持久化到磁盘中,并且数据库可以确保数据的一致性和可靠性。

所以,Doublewrite机制会占用一部分内存和磁盘的空间,同时也会导致一定的性能损失,但这是为了保证数据的安全性和可靠性而进行的权衡。

2.7 redo log

InnoDB中数据是异步刷新到磁盘时,那么假如,在刷新到磁盘之前就宕机了。数据就丢失了,那么InnoDB怎么去保证数据的一致性与持久性呢?

RedoLog又称作重做日志,当我发生异常情况,导致数据丢失的时候,我可以从我的RedoLog日志中找到我想要的数据。

因为InnoDB的数据操作是只会实时去操作我们的bufferpool的page页的,然后通过其他的一些异步方式将bufferpool中的数据同步到磁盘,所以,数据丢失是很容易产生的。那么就需要我们的RedoLog来保证数据的不丢失。它属于InnoDB存储引擎层面实现。
都是同步到磁盘,数据页的数据是同步到磁盘、redoLog也是同步到磁盘,那么为什么不直接将page页同步到磁盘,而要一个RedoLog,其实是为了性能考虑,因为数据page页同
步到磁盘有几个特点:

1.因为bufferPool跟磁盘交互的最小单位是page,所以,只要page里面改动一条数据,整个page都会进行跟磁盘同步,导致不必要的同步。RedoLog只会同步某些记录。
2.你改动的数据是随机的,不是顺序的,随机IO的性能比较慢,但是RedoLog是一直往上加,是顺序IO,速度比数据page同步要快。

 1、在事务提交时,要先同步重做日志到磁盘再修改数据页,为什么不直接将数据页同步到磁盘?

如果去掉 redo log,在页数据发生变更后直接将页同步到磁盘当然也可以保证事务的持久性,但是效率极低。默认情况下,一个页的大小为16KB,可以记录多条记录,而一个变更可能就涉及到一条或几条记录,它就要将记录所在的整个页同步到磁盘,属实浪费;其次,将页同步到磁盘涉及对 I/O 的 随机写 操作,效率低下。重做日志是对日志文件的追加操作,属于 顺序写,顺序写毫无疑问要比随机写快;此外,即使事务还没有提交,mysql 后台主线程每秒都会将重做日志同步到磁盘,因此,即使是很大的事务提交时间也很短。

2、binlog 日志也是事务提交前的日志,为啥不直接用 binlog 日志做恢复?

  •  从日志格式来看,STATEMENT 格式的日志因为动态函数的原因,肯定不能用作数据恢复;
  • 从恢复效率来看,首先,binlog 日志是逻辑日志,而 redo log 日志是物理日志,恢复更快;其次,binlog 日志是追加写文件,而 redo log 日志是循环写的,在脏页刷新回磁盘后即可被覆盖,因此在做数据恢复时,redo log 记录的内容本身就会比 binlog 更少。

主从复制模式下,保证BinLog跟RedoLog的一致性 

二阶段提交方案

1.在更新数据的时候,还没有提交事务的时候,提交的RedoLog为prepare状态
2.当commit事务后,会将BinLog Cache缓存的bin日志,同步到磁盘。
3.将RedoLog状态更改成Commit状态,整个流程结束。 

如果发生异常,怎么保证数据的一致性。
1. 如果操作①失败,数据回滚,RedoLog跟binlog都不会有
2. 如果②失败,有RedoLog的prepare状态,但是没有binlog落盘,数据回滚,操作失败
3. 当③失败,这个时候,有RedoLog并且有binlog,数据都会有,并且数据是一致的,成功。

2.8 一条更新语句的执行流程(binlog模式下)

2.8.1 binlog

在了解更新语句的执行之前,需要先了解binlong,它是属于Server层的日志文件。binlog以事件的形式记录了所有的DDL和DML语句(因为它记录的是操作而不是数据值,属于逻辑日志),可以用来做主从复制和数据恢复。跟redo log不一样,它的文件内容是可以追加的,没有固定大小限制。
  在开启了 binlog功能的情况下,我们可以把binlog导出成SQL语句,把所有的操作重放一遍,来实现数据的恢复。
  binlog的另一个功能就是用来实现主从复制,它的原理就是从服务器读取主服务器的binlog,然后执行一遍。

2.8.2 一条更新语句的执行流程

例如一条语句:update teacher set name=‘盆鱼宴’ where id=1;

  • 先查询到这条数据,如果有缓存,也会用到缓存。
  • 把name改成盆鱼宴,然后调用引擎的API接口,写入这一行数据到内存, 同时记录redo logo这时redo log进入prepare状态,然后告诉执行器,执行完成了,可以随时提交。
  • 执行器收到通知后记录binlog,然后调用存储引擎接口,设置redo log为 commit 状态。
  • 更新完成。

 

需要注意:

  • 先记录到内存,再写日志文件。
  • 记录redo log分为两个阶段。
  • 存储引擎和Server记录不同的日志。
  • 先记录redo log,再记录binlog,最后再更新redo log。

在崩溃恢复时,判断事务是否需要提交:

  • binlog无记录,redolog无记录:在redolog写之前crash,恢复操作:回滚事务。
  • binlog无记录,redolog状态prepare:在binlog写完之前的crash,恢复操作:回滚事务。
  • binlog有记录,redolog状态prepare:在binlog写完提交事务之前的crash, 恢复操作:提交事务。
  • binlog有记录,redolog状态commit:正常完成的事务,不需要恢复。

3 数据读取流程

3.1 页加载机制

我们知道磁盘里面存的数据首先是表空间,表空间里面的单元是page页,页里面的数据是行。
innoDB在内存跟磁盘交互的方式中选择了page页,所以,page页是内存跟磁盘交互的最小单位。为什么要用page,有几个点考虑。
a.如果用行交互,那么假如我查询200条数据,那么200条数据都不在我们内存的话,需要跟磁盘交互200次,但是page可能只有1.2个page页。
b.也不会用extent区来交互,因为一个extent包含64个页。可能我只需要查一条数据,但是会加载64个页到内存,导致内存浪费。

3.2 数据预读

MySQL :: MySQL 8.0 Reference Manual :: 17.8.3.4 Configuring InnoDB Buffer Pool Prefetching (Read-Ahead)首先,InnoDB的数据页并不是都是在访问的时候才缓存到buffer pool的。InnoDB有一个预读机制(read ahead)。也就是说,设计者认为访问某个page的数据的时候,相邻的一些page可能会很快被访问到,所以先把这些page放到buffer pool中缓存起来。
  这种预读的机制又分为两种类型,一种叫线性预读(异步的)(Linear read-ahead)。为了便于管理,InnoDB中把64个相邻的page叫做一个extent(区)。如果顺序地访问了一个extent的56个page,这个时候InnoDB就会把下一个extent (区)缓存到 buffer pool 中。
具体多少个页被顺序访问,具体配置为innodb_read_ahead_threshold

SELECT @@innodb_read_ahead_threshold; -- 默认为56 可以配置0-64,因为一个extent为64个页

 第二种叫做随机预读(Random read-ahead),如果buffer pool已经缓存了同一个extent (区)的数据页的个数超过13时,就会把这个extent剩余的所有page全部缓存到buffer pool。

随机预读的功能默认关闭:

show variables like 'innodb_random_read_ahead'

3.3 LRU淘汰机制

假如我们查询到的页或者与加载的页都加载到我们的bufferpool,那么肯定要进行管理,不能无休止的往bufferpool添加,比如无效的一些老数据需要进行删除.那么在InnoDB到底怎么来管理我们的数据页的呢?
Mysql也是采用LRU的算法去进行page页 淘汰的,只不过实现方式稍微不一样。

传统LRU

默认的LRU算法,是最近没有访问的优先去淘汰,所以一般我们实现就是用 链表保存,新的数据添加到链表头部,然后淘汰从链表尾部进行淘汰。

这个一般是没有问题的,但是在InnoDB里面,就这样不行,为什么不行,因为在InnoDB里面有个预读的概念。何为预读,就是用户在访问某些页的时候,关联的一些页我也可能会给你读出来保存到bufferPool,但是用户可能根本就不用。

InnoDB有预读机制,只是猜测会用,但是不一定真的会用到,那么假如如果用传统的LRU实现,那么我们会发现,会预加载很多页到bufferpool,但是可能用户根本不适用,但是又淘汰不了,既占用了内存,也没有得到很大的性能提升!

LRU变种 

Mysql里面采用了LRU算法的变种,来解决预读导致的大量无效页淘汰不了的问题。但是实现还是采用链表的方式。既然可能这个页可能用不到,那么我能不能先把这些页放到用了的页的后面。当真正用到的时候才提升到链表头部。
所以LRU列表分为2段 ,链表的前面8分之5是新页列表、后面的8分之3是老页;然后淘汰页面从后面尾部进行淘汰。

淘汰管理流程:
1.当新的页面缓存到BufferPool,先加入到oldSublist的头部,包括预读的页。
2.当old的页面被访问时,会添加到new的头部成为一个最近访问的页。
3.new sublist的链表会随着新数据的加入向后移动 同样的 old的链表会随着新数据的加入向后移动;淘汰old sublist的链尾。

思想:既然不确定用到,先放到一个中间位置,当用到了后再放到头部避免淘汰。如果加载了没用到,随着推移,慢慢的进行淘汰掉。来提升内存的利用率。

3.4 冷热分离

InnoDB把LRU list分成两部分,靠近head的叫做new sublist,用来放热数据(我们把它叫做热区)。靠近tail的叫做old sublist,用来放冷数据,我们把它叫做冷区)。中间的分割线叫做midpoint。也就是对buffer pool做 一个冷热分离。

所有新数据加入到buffer pool的时候,一律先放到冷数据区的head,不管是预读的,还是普通的读操作。所以如果有一些预读的数据没有被用到,会在old sublist (冷区)直接被淘汰。放到LRU List以后,如果再次被访问,都把它移动到热区的head。如果热区的数据长时间没有被访问,会被先移动到冷区的head部,最后慢慢在tail被淘汰。

3.5 冷区锁定时间

我们先把数据放到冷区,用来避免占用热数据的存储空间。但是如果刚加载到冷区的数据立即被访问了一次,按照原来的逻辑,这个时候我们会马上把它移动到热区。假设这一次加载然后被立即访问的冷区数据量非常大,比如我们查询了一张几千万数据的大表,没有使用索引,做了一个全表扫描。或者,dump全表备份数据,这种查询属于短时间内访问,后面再也不会用到了。这样就会导致它们全部被移动到热区的head,而很多热点数据被移动到冷区甚至被淘汰,造成了缓冲池的污染。
  InnoDB对于加载到冷区然后被访问的数据,设置一个时间窗口, 只有超过这个时间之后被访问,我们才认为它是有效的访问。由参数innodb_old_blocks_time参数控制,默认是1秒。这样就可以从很大程度上避免全表扫描或者预读的数据污染真正的热数据。

3.6 惰性移动

为了避免并发的问题,对于LRU链表的操作是要加锁的。也就是说每一次链表的移动,都会带来资源的竞争和等待。从这个角度来说,如果要进一步提升InnoDB LRU的效率,就要尽量地减少LRU链表的移动。
  对此,InnoDB对于new区还有一个特殊的优化:如果一个缓存页处于热数据区域,且在热数据区域的前1/4区域,那么当访问这个缓存页的时候,就不用把它移动到热数据区域的头部;如果缓存页处于热区的后3/4区域,那么当访问这个缓存页的时候,会把它移动到热区的头部。

4 数据写入流程

  • 服务层向innodb发起更新操作
  • innodb修改buffer pool
  • 写入redo log,状态为prepare
  • 返回服务层,服务层发起提交
  • 服务层写入binlog
  • 服务层想innodb提交事务
  • innodb层更新redo log 状态为 commit,并发起刷盘
  • 先写入doublewrite buffer
  • 写入成功后再更新刷盘

索引

索引用于快速查找具有特定列值的行。如果没有索引,MySQL必须从第一行开始,然后读取整个表以找到相关的行。表越大,花费就越多。如果表中有相关列的索引,MySQL可以快速确定数据文件中间要查找的位置,而不必查看所有数据。这比按顺序读取每一行要快得多。

1 B+树跟B树的优势

1). 叶子节点才会有完整的一行数据,而非叶子节点是目录,非叶子节点的行大小就越小,越小,那能放的数据就越多,数据越多,同样层级的树能容纳的数据也就越多,或者同样的数据量可能需要的树的高度越低,高度越低,磁盘可能IO的次数越少,性能越高。所以 整体性能比其他树更好
2). 稳定,不管你查什么,因为非叶子结点没有完整的行数据,所以都需要遍历树的高度。
3). 叶子节点是有序并且链表关联,所以可以更好的范围查询跟遍历。

2 聚簇索引

这个基于主键ID建立的索引树,叫做Clustered Index,翻译过来是主键、聚集、聚簇索引。
每个InnoDB存储引擎表都会有并且只有一个Clustered Index索引树,默认以主键排序,如果没有主键,会用非空的唯一字段,非空的唯一字段也没有,就会用隐藏的row_id。

除了Clustered Index以外,所有基于其他字段建立的索引树都是二级索引,可以基于一个字段,也可以基于多个字段联合建立二级索引

3 MySQL自身索引优化

索引条件下推

还有个优化的概念,叫做索引下推。这是5.6之后新加的一个优化。
索引条件下推 (ICP) 是针对 MySQL 使用索引从表中检索行的情况的优化。如果没有 ICP,存储引擎会遍历索引以定位基表中的行,并将它们返回给MySQL 服务器,MySQL 服务器会评估这些 WHERE 行的条件。启用 ICP后,如果 WHERE 仅使用索引中的列可以评估部分条件,则 MySQL 服务器会推送这部分条件 WHERE 条件下降到存储引擎。然后,存储引擎使用索引条目评估推送的索引条件,只有在满足条件时才会从表中读取行。ICP可以减少存储引擎必须访问基表的次数和MySQL服务器必须访问存储引擎的次数。

索引下推(Index Condition Pushdown),简称ICP,它的目的就是为了减少回表的次数以及server层跟存储层之间的数据交互。
把本来要在server层执行器里过滤的数据 移动到二级索引树。

生效场景:
1.仅对二级索引生效。
2.仅对InnoDB跟MyIsam生效

使用索引下推之前:

使用索引下推之后:

我们发现,原来需要回表的2条数据,由于在二级索引树中进行了判断过滤,只需要回表一条数据。 

MRR优化

MRR全称:Multi-Range Read,中文翻译过来就是多范围读取,主要解决的是当二级索引取出索引值后再去聚集索引中取行可能会造成大量的磁盘随机IO的问题。

MRR优化的目的就是减少磁盘随机IO的产生,其查询优化过程大致如下:
1、先把通过二级索引取出的值缓存在缓冲区中。
2、再把这部分缓冲区中的数据按照ID进行排序。
3、然后再依次根据ID去聚集索引中获取整个数据行。
可以看出,只需要通过一次排序,就使得随机IO,变为顺序IO,使得数据访问更加高效。

read_rnd_buffer_size控制了数据能放入缓冲区的大小,如果一次性不够放就会分多次完成。

BKA优化

BKA(batched key access)提高表join性能的算法,作用就是在读取被join表的记录的时候使用顺序I/O。
BAK的原理:对于多表join语句,当mysql使用索引访问第二个join表时,使用一个join buffer来收集第一个操作对象生成的相关列值。BKA构建好key后,批量传给引擎层做索引查找。
key是通过MRR结构交给引擎的,这样一来MRR是的查询更加有效。

mysql通过optimizer_switch参数中的batched_key_access选项,该选项默认是关闭的(生产关闭)。
想要开启该参数,必须先要保证是在强制使用MRR的基础上才可以。

set global optimizer_switch=‘mrr=on,mrr_cost_based=off’;
set global optimizer_switch=‘batched_key_access=on’;

当使用BKA时,执行计划的extra列会显示using join buffer(BAK)关键字。

4 索引使用原则

列的离散度

列的离散度公式:count(distinct(column name)): count(1),列的全部不同值和所有数据行的比例。数据行数相同的情况下,分子越大,列的离散度就越高。简单来说,如果列的重复值越多,离散度就越低,重复值越少,离散度就越高。我们不建议大家在离散度低的字段上建立索引。
  如果在B+Tree里面的重复值太多,MySQL的优化器发现走索引跟使用全表扫描差不了多少的时候,就算建了索引,也不一定会走索引。

最左匹配原则

前面我们说的都是针对单列创建的索引,但有的时候我们的多条件査询的时候,也会建立联合索引。单列索引可以看成是特殊的联合索引。

比如我们在user表上面,给name和phone建立了一个联合索引。

ALTER TABLE user_innodb add INDEX comidx_name_phone (name,phone);

 

联合索引在B+Tree中是复合的数据结构,它是按照从左到右的顺序来建立搜索树的 (name在左边,phone在右边)。从这张图可以看岀来,name是有序的,phone是无序的。当name相等的时候, phone才是有序的。
这个时候我们使用where name=‘青山’ and phone = '136xx’去査询数据的时候, B+Tree会优先比较name来确定下一步应该搜索的方向,往左还是往右。如果name 相同的时候再比较phone。但是如果查询条件没有name,就不知道第一步应该查哪个 节点,因为建立搜索树的时候name是第一个比较因子,所以用不到索引。 

覆盖索引

回表: 非主键索引,我们先通过索引找到主键索引的键值,再通过主键值查出索引里面没有的数据,它比基于主键索引的查询多扫描了一棵索引树,这个过程就叫回表。
在二级索引里面,不管是单列索引还是联合索引,如果select的数据列只用从索引中就能够取得,不必从数据区中读取,这时候使用的索引就叫做覆盖索引,这样就避免了回表。

5 索引失效场景

索引列使用函数

索引列上使用函数(replace\SUBSTR\CONCAT\sum count avg)、表达式计算(+ -*/)

索引列隐式转换

字符串不加引号,出现隐式转换

like条件左模糊

like查询以%开头

负向查询

负向查询:NOT LIKE 不能;!= (<>)和NOT IN在某些情况下可以。

事务

1 ACID特性

        原子性(Atomicity) 也就是我们刚才说的不可再分,因为原子是化学上(参加化学反应)最小的单位。也就意味着我们对数据库的一系列的操作,要么都是成功,要么都是失败,不可能出现部分成功或者部分失败的情况。原子性,在InnoDB里面是通过undo log来实现的,它记录了数据修改之前的值(逻辑日志),一旦发生异常,就可以用undo log来实现回滚操作。
  隔离性(Isolation) 我们有了事务的定义以后,在数据库里面会有很多的事务同时去操作我们的同一张表或者同一行数据,必然会产生一些并发或者干扰的操作。 我们对隔离性的定义,就是这些很多个的事务,对表或者行的并发操作,应该是透明的,互相不干扰的。比如两个人给青山转账100,开启两个事务,都拿到了青山账户的余额 1000,然后各自基于1000加100,最后结果是1100,就出现了数据混乱的问题。
  持久性(Durability) 持久性的意思是我们对数据库的任意的操作,增删改,只要事务提交成功,那么结果就是永久性的,不可能因为数据库掉电、 宕机、意外重启,又变成原来的状态。这个就是事务的持久性。持久性是通过redo log和double write buffer (双写缓冲)来实现的,我们操作数据的时候,会先写到内存的buffer pool里面,同时记录redo log,如果在刷盘之前出现异常,在重启后就可以读取redo log的内容,写入到磁盘,保证数据的持久性。当然,恢复成功的前提是数据页本身没有被破坏,是完整的,这个通过双写缓冲保证。
  一致性(consistent) 一致性指的是数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。数据库自身提供了一些约束:比如主键必须是唯一的,字段长度符合要求。另外还有用户自定义的完整性。比如说转账的这个场景,A账户余额减少1000, B账户余额只增加了 500,两个操作都成功了,它是满足原子性的定义的,但是它不满足用户自定义的一致性,因为它导致了会计科目的不平衡。还有一种情况,A账户余额为0,如果这个时候转账成功了,A账户的余额会变成=-1000,虽然它也满足原子性,但是我们知道,借记卡的余额是不能够小于0的,所以也违反了一致性。用户自定义的完整性通常要在代码中控制。

2 事务隔离级别

无论是脏读、不可重复读还是幻读,它们都是数据库的读一致性的问题,都是在一个事务里面前后两次读取出现了不一致的情况。读一致性的问题,必须要由数据库提供一定的事务隔离机制来解决。
1)Read Uncommitted (未提交读) 一个事务可以读取到其他事务未提交的数据,会出现脏读,所以叫做RU,它没有解决任何问题。
2)Read Committed (已提交读) 一个事务只能读取到其他事务已提交的数据,不能读取到其他事务未提交的数据,它解决了脏读的问题。
3)Repeatable Read (可重复读) 同一个事务里面多次读取同样的数据结果是一样的,它解决了不可重复读的问题。
4)Serializable (串行化) 在这个隔离级别里面,所有的事务都是串行执行的,也就是对数据的操作需要排队,已经不存在事务的并发操作了,所以它解决了所有(脏读、不可重复读、幻读)问题。

3 MVCC

如果仅仅是基于锁来实现事务隔离,一个事务读取的时候不允许其他时候修改,那就意味着不支持并发的读写操作,而我们的大多数应用都是读多写少的,这样会极大地影响操作数据的效率。所以我们还有另一种解决方案,如果要让一个事务前后两次读取的数据保持一致, 那么我们可以在修改数据的之前给它建立一个备份或者叫快照,后面再来读取这个快照就行了。这种方案我们叫做多版本的并发控制Multi Version Concurrency Control (MVCC)。
  MVCC的原则:一个事务只能看到第一次查询之前已经提交的事务的修改和本事务的修改,不能看见本事务第一次查询之后创建的事务(事务ID比我的事务ID大)的修改以及未提交的事务的修改。
  MVCC的效果:我可以査到在我这个事务开始之前已经存在的数据,即使它在后面被修改或者删除了。而在我这个事务之后新增的数据,我是查不到的

Read view

在InnoDB中,一条数据的旧版本,存放在undo log。因为修改了多次, 这些undo log会形成一个链条,叫做undo log链。所以前面我们说的DB_ROLL_PTR,它其实就是指向undo log链的指针。
为了判断各个事务的可见性情况,我们必须要有一个数据结构,把本事务ID、活跃事务ID、当前系统最大事务ID存起来。这个数据结构就叫Read View (可见性视图),每个事务都维护一个自己的Read View。

m_ids 表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。
min_trx_id 表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。
max_trx_id 表示生成ReadView时系统中应该分配给下一个事务的id值。
creator_trx_id 表示生成该ReadView的事务的事务id。
有了这个数据结构以后,事务判断可见性的规则是这样的:
1.数据版本的trx_id = creator_trx_id,本事务修改,可以访问。
2.数据版本的trx_id < min_trx_id (未提交事务的最小ID),说明这个版本在生成ReadView已经提交,可以访问。
3.数据版本的trx_id > max_trx_id (下一个事务ID),这个版本是生成ReadView 之后才开启的事务建立的,不能访问。
4.数据版本的trx_id 在min_trx_id 和max_trx_id之间,看看是否在m_ids中。 如果在,不可以。如果不在,可以。
5.如果当前版本不可见,就找undo log链中的下一个版本。

回滚指针

首先,InnoDB的事务都是有编号的,而且会不断递增。其次,InnoDB为每行记录都实现了两个隐藏字段:

DB_TRX_ID:事务ID,数据是在哪个事务插入或者修改为新数据的,就记录为当前事务ID。
DB_ROLL_PTR:回滚指针,是指向前一个版本的指针。

4 LBCC

既然要保证前后两次读取数据一致,那么我读取数据的时候,锁定我要操作的数据,不允许其他的事务修改就行了。这种方案叫做基于锁的并发控制Lock Based Concurrency Control (LBCC)。

锁类型

共享锁

        共享锁是一种读锁,它允许其他事务添加共享锁或读取数据,不允许其他事务修改数据。注意不要在加上了读锁以后去写数据,不然的话可能会出现死锁的情况。
        我们可以用select … lock in share mode 的方式手工加上一把读锁

排他锁

        排它锁又叫写锁,它不允许其他事务对数据加锁或修改数据。
  排它锁的加锁方式有两种,第一种是自动加排他锁,我们在操作数据的时候,包括增删改,都会默认加上一个排它锁。第二种是手工加锁,我们用一个FOR UPDATE给一行数据加上一个排它锁,这个无论是在我们的代码里面还是操作数据的工具里面,都比较常用。

意向锁

        意向锁是由数据库自己维护的一种表锁。当我们给一行数据加上共享锁之前,数据库会自动在这张表上面加一个意向共享锁。当我们给一行数据加上排他锁之前,数据库会自动在这张表上面加一个意向排他锁。
  意向锁是为表锁而设计的,如果没有意向锁,当我们要加表锁时需要逐行判断里面的数据是否被加锁,只有所有数据都没加锁才可以加表锁,不仅效率低下,还存在原子性问题。有了意向锁之后,如果要加表锁,只需要判断这张表上有没有被加意向锁即可,大大提升了加表锁的效率。

锁范围

记录锁

记录锁指的是锁住某条记录。当我们对于唯一性的索引(包括唯一索引和主键索引)使用等值查询,精准匹配到一条记录的时候,使用的就是记录锁。

间隙锁

间隙锁指的是锁住某个左开右开的区间。当我们査询的记录不存在,没有命中任何一个record,无论是用等值査询还是范围查询的时候,它使用的都是间隙锁。间隙锁主要是阻塞插入insert,相同的间隙锁之间不冲突。

临键锁

临键锁指的是一个间隙锁加上最右边的记录,形成一个左开右闭的区间。当我们使用了范围査询,不仅仅命中了 Record记录,还包含了 Gap 间隙,在这种情况下我们使用的就是临键锁,它是MySQL里面默认的行锁算法,相当于记录锁加上间隙锁。

死锁

两个或多个事务相互等待对方释放锁而陷入无限等待的状态,从而导致事务无法继续执行。

我们在并发的时候,学过死锁的4个条件:
1.互斥 2个事务拿互斥的资源
2.请求和保持条件 个事务在等待其他事务持有的资源时,仍然保持自己所持有的资源不释放
3.不可剥夺 一个事务持有的资源不能被其他事务强制性地抢占
4.循环等待

防止死锁:

1).随时发出问题 SHOW ENGINE INNODB STATUS 来确定最近一次死锁的原因。这可以帮助您调整应用程序以避免死锁。
2).如果频繁的死锁警告引起关注,请通过启用该变量来收集更广泛的调试信息 innodb_print_all_deadlocks 。有关每个死锁的信息(而不仅仅是最新的死锁)都记录在 MySQL 错误日志中。完成调试后禁用此选项。
3).如果交易因死锁而失败,请始终做好重新发出交易的准备。死锁并不危险。再试一次。
4).保持交易规模小且持续时间短,以使其不易发生冲突。
5).在进行一组相关更改后立即提交事务,以使其不易发生冲突。特别是,不要让交互式 mysql会话长时间打开且未提交事务。
6).如果您使用锁定读取( SELECT ... FOR UPDATE 或 SELECT ... FORSHARE ),请尝试使用较低的隔离级别,例如 READ COMMITTED .
7).当修改事务中的多个表或同一表中的不同行集时,每次都以一致的顺序执行这些操作。然后事务形成明确定义的队列并且不会死锁。例如,将数据库操作组织到应用程序内的函数中,或调用存储的例程,而不是 在不同位置 编写多个相似的 INSERT 、 UPDATE 和 语句序列。 DELETE
8).将精心选择的索引添加到表中,以便您的查询扫描更少的索引记录并设置更少的锁。用于 EXPLAIN SELECT 确定 MySQL 服务器认为哪些索引最适合您的查询。
9).少用锁定。如果您有能力允许 a SELECT 从旧快照返回数据,请不要向其中添加 FOR UPDATE or 子句。 FOR SHARE 此处使用 READ COMMITTED 隔离级别很好,因为同一事务中的每个一致读取都从其自己
的新快照中读取。 

性能优化

1 业务优化

限流、降级、削峰

2 架构优化

引入缓存;主从、集群部署;读写分离;分库分表

2 配置优化

数据库连接池优化

表字段设计

索引数量最佳实践

3 SQL 优化

4 实战

执行计划缓存

复杂SQL拆分

综合问题

1. MySQL如何实现读的高性能?

核心两点:

1. 把磁盘的读变成了内存,引入了缓存,磁盘的性能跟缓存没法比。

2. 索引

2. MySQL如何实现写的高性能与安全性之间的平衡?

Mysql支持ACID,只要提交成功了, 就一定得落盘持久化。如果要持久化,就得去写磁盘,但是写磁盘的话效率又比较低,那我就需要去提升写的性能,那我就写缓存,但写缓存的话,那就意味着数据可能丢失,它的安全性得不到保证。在这两之间,它是如何做到平衡的。

顺序IO的性能是比内存的实际IO都要快的。磁盘的顺序IO的性能是比内存的随机IO都要快的,因为我们的日志是追加的形式,所以它是顺序IO。
而如果我们去更新磁盘里面的数据的话,它是要去寻址的,它是随机IO,主要是通过这样的一个方式去解决他写的一个性能。

3. 为什么MySQL的LRU算法跟常规LRU算法不一样?

InnoDB有预读机制,只是猜测会用,但是不一定真的会用到,那么假如如果用传统的LRU实现,那么我们会发现,会预加载很多页到bufferpool,但是可能用户根本不适用,但是又淘汰不了,既占用了内存,也没有得到很大的性能提升!
Mysql里面采用了LRU算法的变种,来解决预读导致的大量无效页淘汰不了的问题。但是实现还是采用链表的方式。既然可能这个页可能用不到,那么我能不能先把这些页放到用了的页的后面。当真正用到的时候才提升到链表头部。
所以LRU列表分为2段 ,链表的前面8分之5是新页列表、后面的8分之3是老页;然后淘汰页面从后面尾部进行淘汰。

4. 有redo log了为什么还需要doublewrite buffer?

redolog的设计之初,是“账本的作用”,是一种操作日志,用于MySQL异常崩溃恢复使用,是InnoDB引擎特有的日志,本质上是物理日志,记录的是 “ 在某个数据页上做了什么修改 ” ,但如果数据页本身已经发生了损坏,redolog来恢复已经损坏的数据块是无效的,数据块的本身已经损坏,再次重做依然是一个坏块。

所以此时需要一个数据块的副本来还原该损坏的数据块,再利用重做日志进行其他数据块的重做操作,这就是double write buffer的原因作用。

5.设置冷区锁定时间为了解决什么问题?

InnoDB对于加载到冷区然后被访问的数据,设置一个时间窗口, 只有超过这个时间之后被访问,我们才认为它是有效的访问。由参数innodb_old_blocks_time参数控制,默认是1秒。这样就可以从很大程度上避免全表扫描或者预读的数据污染真正的热数据。

6. binlog, redolog,doublewrite buffer 都是用于异常恢复,有什么区别?

Redo Log

  • 目的:Redo Log 主要用于保证InnoDB存储引擎的事务持久性。它确保在系统崩溃的情况下,已经提交的事务不会丢失,这是通过预写日志(Write-Ahead Logging, WAL)机制实现的。
  • 工作方式:Redo Log 记录的是事务对数据页所做的物理修改。当事务提交时,这些修改可能还没有写入磁盘上的数据文件,但Redo Log会确保这些修改被写入日志文件中。
  • 恢复机制:在发生崩溃后,InnoDB可以利用Redo Log来重做(redo)事务的修改,以此恢复到崩溃前的状态。

如果数据库发生崩溃,InnoDB 在重启时会检查 Redo Log。通过 Redo Log,InnoDB 可以重做(redo)在崩溃前已经提交但可能未写入到数据文件中的事务,确保这些事务的修改得到持久化。

Binlog

  • 目的:Binlog 主要用于记录所有修改了数据库数据的SQL语句,以便用于复制和数据恢复。它是MySQL服务器层的功能,与存储引擎无关。
  • 工作方式:Binlog 记录的是逻辑日志,即实际执行的SQL语句或者在RBR模式下记录的行事件。
  • 复制和恢复:Binlog 使得MySQL可以实现主从复制,主服务器上的操作可以通过复制Binlog到从服务器并重放来同步数据。同时,Binlog也可以用于点时间恢复(Point-in-Time Recovery),恢复到特定时间点的数据库状态。

为什么两者都需要

  • 不同的恢复和复制需求:Redo Log 主要用于崩溃恢复,而Binlog用于数据复制和点时间恢复。
  • 性能和优化:Redo Log 的物理日志对于崩溃恢复来说性能更优,因为它直接关联到数据页的状态。Binlog 的逻辑日志则更适合于复制和审计,因为它记录了实际执行的操作。
  • 隔离存储引擎与服务器层:Redo Log 是InnoDB存储引擎特有的,而Binlog 是MySQL服务器层的功能,这种设计使得Binlog可以用于多种存储引擎。

7. MVCC在RR 和RC级别下的区别?

8. 那么关系型数据库相比非关系型数据的区别:

1.关系型数据都是用表来进行维护,所以格式一致,可以统一用sql语言来进行操作。
2.关系型数据都是表结构,所以灵活度不够,操作复杂的海量数据性能比较差,所以我们才会有表结构、索引以及索引优化。
3.虽然性能可能会比较慢,但是能做复杂的关联查询操作。 比如一对一,一对多等等。

9. 我们主键一般建议趋势递增的原因在哪?

首先,我们知道我们每个表都会去有一个主键索引树,是根据主键进行排序的B+树,树的特性是会加快查询速度,但是在添加数据的时候,是要去维护这个树的,如果是递增的,我们只需要往树上添加节点,那么假如如果不是趋势递增的,那么我们会引发树的分裂与合并。然后索引树中的叶
子节点的page里的数据也是排序好的,也会导致页的分裂与合并。

10. 我们在做InnoDB表结构设计的时候,一些大字段,比如varchar(5000),会独立成附表,为什么? 

我们都知道,我们的数据都是存在我们主键索引的叶子结点的,叶子结点也是一个一个page页的链表,那么如果字段过大,每个page页存的数据也就越少,同样多的数据就需要更多的page页来保存。叶子结点的页越多,那么层级就越高。层级越高,在同样的表数量的情况下,磁盘IO次数就越多,性能也就会越慢。 

11. 我们innodb中索引结构为啥要用B+树的结构?

InnoDB索引肯定会有1个clustered索引,clustered引默认是主键,然后是非空的唯一索引,最后是隐藏列rowid。聚集索引的存储方式为叶子节点有完整的数据,而非叶子节点,只存有索引值。
那么每页存的数据也就更多,内存跟磁盘交互的单位为页。每页的数据越多,那么就能减少跟磁盘的交互次数,整体上提升速度。
同时,因为真实数据都在叶子节点,所以sql语句的查询路径都是一样长。查询稳定。
为什么不用二叉查找树? 因为二叉树会有斜树的情况出现,会退化成链表,不够平衡。
为什么不用红黑树?同样的,路数比较少,深度会随着数据量的提升而提升。速度会越来越慢。同时也不够平衡。
为什么不用B- tree?B-树跟B+最大的一个区别,B树每个节点都有真实数据,那么每页存的数据就越少,查询数据跟磁盘的交互也就越多,同时,索引树的高度也就越高,查询的链路也会越长,整体查询会慢。还有每个节点都有真实数据,查询数据就不稳定,有些在索引第一层就能查到,有
些要查到索引最后一层。

12 .三层的B+树结构,能支撑多大的数据量 ?

如果根节点和内节点Page页里面的一个逻辑单元大小是16byte,一行完整的数据是1KB,请问,三层的B+树可以放多少条数据

一个Page是16KB,一个逻辑单元是16Byte,那么一个根节点就可以放16384/16 =1024个,就有1024个分叉,第二层就有1024*1024=1048576个逻辑单元,叶子节点一行数据1KB,那么一个Page就可以放16条,那么第三层就可以放 大约1048576 * 16=16777216条

  • 26
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值