提起MySQL,其实网上已经有一大把教程了,为什么我还要写这篇文章呢,大概是因为网上很多网站都是比较零散,而且描述不够直观,不能系统对MySQL相关知识有一个系统的学习,导致不能形成知识体系。为此我撰写了这篇文章,试图让这些底层架构相关知识更加直观易懂:
- 尽量以
图文
的方式描述技术原理; - 涉及到关键的技术,附加
官网
或者技术书籍
来源,方便大家进一步扩展学习; - 涉及到的
背景知识
尽可能做一个交代,比如讨论到log buffer的刷盘方式,延伸一下IO写磁盘相关知识点。
好了,MySQL从不会到精通系列马上就要开始了(看完之后还是不会的话..请忽略这句话)。
可能会有同学问:为啥不直接学更加先进的TiDB,或者是强大的OceanBase。
其实,MySQL作为老牌的应用场景广泛的关系型开源数据库,其底层架构是很值得我们学习的,吸收其设计精华,那么我们在平时的方案设计工作中也可以借鉴,如果项目中用的是MySQL,那么就能够把数据库用的更好了,了解了MySQL底层的执行原理,对于调优工作也是有莫大帮助的。本文我重点讲述MySQL底层架构,涉及到:
-
内存结构:
buffer pool
、log buffer
、change buffer
,buffer pool的页淘汰机制是怎样的; -
磁盘结构:
系统表空间
、独立表空间
、通用表空间
、undo表空间
、redo log
; - 以及
IO
相关底层原理、查询SQL执行流程
、数据页结构
和行结构
描述、聚集索引
和辅助索引
的底层数据组织方式、MVCC
多版本并发控制的底层实现原理,以及可重复读
、读已提交
是怎么通过MVCC实现的。
看完文本文,您将了解到:
- 整体架构:InnoDB存储架构是怎样的 (1、MySQL架构)
- 工作原理:查询语句的底层执行流程是怎样的 (2、查询SQL执行流程)
-
IO性能:文件
IO操作
写磁盘有哪几种方式,有什么IO优化方式 (3.1.2、关于磁盘IO的方式) -
缓存:
InnoDB缓存
(buffer pool, log buffer)的刷新方式有哪些(3.1.2.2、innodb_flush_method) - 缓存:log buffer是在什么时候写入到磁盘的(3.10.2、如何保证数据不丢失 - 其中第四步log buffer持久化到磁盘的时机为)
- 缓存:为什么redo log prepare状态也要写磁盘?(3.10.2、如何保证数据不丢失 - 为什么第二步redo log prepare状态也要写磁盘?)
- 缓存:脏页写盘一般发生在什么时候(3.10.2、如何保证数据不丢失 - 其中第五步:脏页刷新到磁盘的时机为)
- 缓存:为什么唯一索引的更新不可以借助change buffer(3.2、Change Buffer)
-
缓存:
log buffer
的日志刷盘控制参数innodb_flush_log_at_trx_commit
对写性能有什么影响(3.4.1、配置参数) - 缓存:buffer pool的LRU是如何实现的,为什么要这样实现(3.1.1、缓冲池LRU算法)
-
表存储:系统表空间的结构,MySQL InnoDB磁盘存储格式,各种
表空间
(系统表空间,独立表空间,通用表空间)的作用和优缺点是什么,ibdata
、ibd
、frm
文件分别是干嘛的(3.5、表空间) - 行字段存储:底层页和行的存储格式(3.6、InnoDB底层逻辑存储结构)
-
行字段存储:
varchar
,null
底层是如何存储的,最大可用存储多大的长度(3.6.3.1、MySQL中varchar最大长度是多少) - 行字段存储:行记录太长了,一页存不下,该怎么存储?(3.6.3.2、行记录超过页大小如何存储)
-
索引:数据库
索引
的组织方式是怎样的,明白为什么要采用B+树
,而不是哈希表、二叉树或者B树(3.7、索引 - 为什么MySQL使用B+树) -
索引:索引组织方式是怎样的,为什么
大字段
会影响表性能(查询性能,更新性能)(3.7、索引) -
索引:
覆盖索引
、联合索引
什么情况下会生效(3.7.2、辅助索引) -
索引:什么是
索引下推
,索引下推减少了哪方面的开销?(3.7.2、辅助索引 - 索引条件下推) -
索引:
Change Buffer
对二级索引DML语句有什么优化(3.2、Change Buffer) -
数据完整性:MySQL是如何保证数据完整性的,
redo log
、undo log
和buffer pool
数据完整性的关键作用分别是什么(3.10.2、如何保证数据不丢失) -
MVCC:
MVCC
底层是怎么实现的,可重复读和读已提交是怎么实现的(3.11.2、MVCC实现原理) - 双写缓冲区有什么作用(3.9、Doublewrite Buffer)
- Redo Log在一个事务中是在什么时候写入的?binlog和Redo Log有什么区别?(3.10.1、Redo Log在事务中的写入时机)
1、MySQL架构
如下图为MySQL架构涉及到的常用组件:
2、查询SQL执行流程
有如下表格:
我们执行以下sql:
select * from t_user where user_id=10000;
2.1、MySQL客户端与服务器建立连接
如下图,建立过程:
- 客户端通过mysql命令发起连接请求;
- 经过三次握手后与服务端建立TCP连接;
- 连接器接收到请求之后使用用户密码进行身份验证;
- 验证通过之后,获取用户的权限信息缓存起来,该连接后面都是基于该缓存中的权限执行sql;
对于Java应用程序来说,一般会把建立好的连接放入数据库连接池中进行复用,只要这个连接不关闭,就会一直在MySQL服务端保持着,可以通过show processlist
命令查看,如下:
注意,这里有个Time,表示这个连接多久没有动静了,上面例子是656秒没有动静,默认地,如果超过8个小时还没有动静,连接器就会自动断开连接,可以通过wait_timeout
参数进行控制。
2.2、执行SQL
如下图,执行sql:
- 服务端接收到客户端的查询sql之后,先尝试从查询缓存中查询该sql是否已经有缓存的结果了,如果有则直接返回结果,如果没有则执行下一步;
- 分析器拿到sql之后会尝试对sql语句进行词法分析和语法分析,校验语法的正确性,通过之后继续往下执行;
- 优化器拿到分析器的sql之后,开始继续解析sql,判断到需要走什么索引,根据实际情况重写sql,最终生成执行计划;
- 执行器根据执行计划执行sql,执行之前会先进行操作权限校验;然后根据表存储引擎调用对饮接口进行查询数据,这里的扫描行数就是指的接口返回的记录数,执行器拿到返回记录之后进一步加工,如本例子:
- 执行器拿到select * from t_user where user_id=10000的所有记录,在依次判断user_name是不是等于"arthinking",获取到匹配的记录。
3、InnoDB引擎架构
如下图,为存储引擎的架构:
其实内存中的结构不太好直接观察到,不过磁盘的还是可以看到的,我们找到磁盘中MySQL的数据文件夹看看:
cd innodb_data_home_dir
查看MySQL 数据目录:
|- ib_buffer_pool // 保存缓冲池中页面的表空间ID和页面ID,用于重启恢复缓冲池
|- ib_logfile0 // redo log 磁盘文件1
|- ib_logfile1 // redo log 磁盘文件2,默认情况下,重做日志存在磁盘的这两个文件中,循环的方式写入重做日志
|- ibdata1 // 系统表空间文件
|- ibtmp1 // 默认临时表空间文件,可通过innodb_temp_data_file_path属性指定文件位置
|- mysql/
|- mysql-bin.000001 // bin log文件
|- mysql-bin.000001 // bin log文件
...
|- mysql-bin.index // bin log文件索引
|- mysqld.local.err // 错误日志
|- mysqld.local.pid // mysql进程号
|- performance_schema/ // performance_schema数据库
|- sys/ // sys数据库
|- test/ // 数据库文件夹
|- db.opt // test数据库配置文件,包含数据库字符集属性
|- t.frm // 数据表元数据文件,不管是使用独立表空间还是系统表空间,每个表都对应有一个
|- t.ibd // 数据库表独立表空间文件,如果使用的是独立表空间,则一个表对应一个ibd文件,否则保存在系统表空间文件中
接下来我们逐一来介绍。
3.1、buffer pool
buffer pool
(缓冲池
)是主内存
中的一个区域,在InnoDB访问表数据
和索引数据
的时候,会顺便把对应的数据页缓存到缓冲池中。如果直接从缓冲池中直接读取数据将会加快处理速度。在专用服务器上,通常将80%左右的物理内存分配给缓冲池。
为了提高缓存管理效率,缓冲池把页面链接为列表,使用改进版的LRU算法
将很少使用的数据从缓存中老化淘汰掉。
3.1.1、缓冲池LRU算法
通过使用改进版的LRU算法来管理缓冲池列表。
当需要把新页面存储到缓冲池中的时候,将淘汰最近最少使用的页面,并将新页面添加到旧子列表的头部。
该算法运行方式:
- 默认 3/8缓冲池用于旧子列表;
- 当新页面如缓冲池时,首先将其插入旧子列表头部;
- 重复访问旧子列表的页面,将使其移动至新子列表的头部;
- 随着数据库的运行,页面逐步移至列表尾部,缓冲池中未被方位的页面最终将被老化淘汰。
相关优化参数:
-
innodb_old_blocks_pct
:控制LRU列表中旧子列表的百分比,默认是37,也就是3/8,可选范围为5~95; -
innodb_old_blocks_time
:指定第一次访问页面后的时间窗口,该时间窗口内访问页面不会使其移动到LRU列表的最前面。默认是1000,也就是1秒。
innodb_old_blocks_time很重要,有了这1秒,对于全表扫描,由于是顺序扫描的,一般同一个数据页的数据都是在一秒内访问完成的,不会升级到新子列表中,一直在旧子列表淘汰数据,所以不会影响到新子列表的缓存。
3.1.2、关于磁盘IO的方式
O_DIRECT
是innodb_flush_method
参数的一个可选值。
这里先介绍下和数据库性能密切相关的文件IO操作方法
3.1.2.1、文件IO操作方法
数据库系统是基于文件系统的,其性能和设备读写的机制有密切的关系。
open:打开文件[6]
int open(const char *pathname, int flags);
系统调用Open会为该进程一个文件描述符fd,常用的flags如下:
-
O_WRONLY
:表示我们以"写"的方式打开,告诉内核我们需要向文件中写入数据; -
O_DSYNC
:每次write都等待物理I/O完成,但是如果写操作不影响读取刚写入的数据,则不等待文件属性更新; -
O_SYNC
:每次write都等到物理I/O完成,包括write引起的文件属性的更新; -
O_DIRECT
:执行磁盘IO时绕过缓冲区高速缓存(内核缓冲区),从用户空间直接将数据传递到文件或磁盘设备,称为直接IO(direct IO)。因为没有了OS cache,所以会O_DIRECT降低文件的顺序读写的效率。
write:写文件[7]
ssize_t write(int fd, const void *buf, size_t count);
使用open打开文件获取到文件描述符之后,可以调用write函数来写文件,具体表现根据open函数参数的不同而不同弄。
fsync & fdatasync:刷新文件[8]
#include <unistd.h>
int fsync(int fd);
int fdatasync(int fd);
-
fdatasync
:操作完write之后,我们可以调用fdatasync将文件数据块flush到磁盘,只要fdatasync返回成功,则可以认为数据已经写到磁盘了; -
fsync
:与O_SYNC参数类似,fsync还会更新文件metadata到磁盘; -
sync
:sync只是将修改过的块缓冲区写入队列,然后就返回,不等实际写磁盘操作完成;
为了保证文件更新成功持久化到硬盘,除了调用write方法,还需要调用fsync。
大致交互流程如下图:
更多关于磁盘IO的相关内容,可以阅读:On Disk IO, Part 1: Flavors of IO[9]
fsync性能问题:除了刷脏页到磁盘,fsync还会同步文件metadata,而文件数据和metadata通常存放在磁盘不同地方,所以fsync至少需要两次IO操作。
对fsync性能的优化建议:由于以上性能问题,如果能够减少metadata的更新,那么就可以使用fdatasync了。因此需要确保文件的尺寸在write前后没有发生变化。为此,可以创建固定大小的文件进行写,写完则开启新的文件继续写。
3.1.2.2、innodb_flush_method
innodb_flush_method
定义用于将数据刷新到InnoDB
数据文件和日志文件的方法,这可能会影响I/O吞吐量。
以下是具体参数说明:
属性 | 值 |
---|---|
命令行格式 | --innodb-flush-method=value |
系统变量 | innodb_flush_method |
范围 | 全局 |
默认值(Windows) | unbuffered |
默认值(Unix) | fsync |
有效值(Windows) | unbuffered, normal |
有效值(Unix) | fsync, O_DSYNC, littlesync, nosync, O_DIRECT, O_DIRECT_NO_FSYNC |
比较常用的是这三种:
fsync
默认值,使用fsync()
系统调用来flush数据文件和日志文件到磁盘;
O_DSYNC
由于open函数的O_DSYNC参数在许多Unix系统上都存中问题,因此InnoDB不直接使用O_DSYNC。
InnoDB
用于O_SYNC
打开和刷新日志文件,fsync()
刷新数据文件。
表现为:写日志操作是在write函数完成,数据文件写入是通过fsync()
系统调用来完成;
O_DIRECT
使用O_DIRECT
(在Solaris上对应为directio()
)打开数据文件,并用于fsync()
刷新数据文件和日志文件。此选项在某些GNU/Linux版本,FreeBSD和Solaris上可用。
表现为:数据文件写入直接从buffer pool到磁盘,不经过操作系统缓冲,日志还是需要经过操作系统缓存;
O_DIRECT_NO_FSYNC
在刷新I/O期间InnoDB
使用O_DIRECT
,并且每次write操作后跳过fsync()
系统调用。
此设置适用于某些类型的文件系统,但不适用于其他类型的文件系统。例如,它不适用于XFS。如果不确定所使用的文件系统是否需要fsync()(例如保留所有文件元数据),请改用O_DIRECT。
如下图所示:
为什么使用了O_DIRECT配置后还需要调用fsync()?
参考MySQL的这个bug:Innodb calls fsync for writes with innodb_flush_method=O_DIRECT[10]
Domas进行的一些测试表明,如果没有fsync,某些文件系统(XFS)不会同步元数据。如果元数据会更改,那么您仍然需要使用fsync(或O_SYNC来打开文件)。
例如,如果在启用O_DIRECT的情况下增大文件大小,它仍将写入文件的新部分,但是由于元数据不能反映文件的新大小,因此如果此刻系统发生崩溃,文件尾部可能会丢失。
为此:当重要的元数据发生更改时,请继续使用fsync或除O_DIRECT之外,也可以选择使用O_SYNC。
MySQL从v5.6.7起提供了O_DIRECT_NO_FSYNC
选项来解决此类问题。
3.2、Change Buffer
change buffer是一种特殊的数据结构,当二级索引页(非唯一索引)不在缓冲池中时,它们会缓存这些更改 。当页面通过其他读取操作加载到缓冲池中时,再将由INSERT
,UPDATE
或DELETE
操作(DML)产生的change buffer合并到buffer pool的数据页中。
为什么唯一索引不可以使用chage buffer?
针对唯一索引,如果buffer pool不存在对应的数据页,还是需要先去磁盘加载数据页,才能判断记录是否重复,这一步避免不了。
而普通索引是非唯一的,插入的时候以相对随机的顺序发生,删除和更新也会影响索引树中不相邻的二级索引树,通过使用合并缓冲,避免了在磁盘产生大量的随机IO访问获取普通索引页。
问题
当有许多受影响的行和许多辅助索引要更新时,change buffer合并可能需要几个小时,在此期间,I/O会增加,可能会导致查询效率大大降低,即使在事务提交之后,或者服务器重启之后,change buffer合并操作也会继续发生。相关阅读:Section 14.22.2, “Forcing InnoDB Recovery”
3.3、自适应哈希索引
自适应哈希索引功能由innodb_adaptive_hash_index
变量启用 ,或在服务器启动时由--skip-innodb-adaptive-hash-index
禁用。
3.4、Log Buffer
log buffer(日志缓冲区)用于保存要写入磁盘上的log file(日志文件)的数据。日志缓存区的内容会定期刷新到磁盘。
日志缓冲区大小由innodb_log_buffer_size
变量定义 。默认大小为16MB。较大的日志缓冲区可以让大型事务在提交之前无需将redo log写入磁盘。
如果您有更新,插入或者删除多行的事务,尝试增大日志缓冲区的大小可以节省磁盘I/O。
3.4.1、配置参数
innodb_flush_log_at_trx_commit
innodb_flush_log_at_trx_commit
变量控制如何将日志缓冲区的内容写入并刷新到磁盘。
该参数控制是否严格存储ACID还是尝试获取更高的性能,可以通过该参数获取更好的性能,但是会导致在系统崩溃的过程中导致数据丢失。
可选参数:
- 0,事务提交之后,日志只记录到log buffer中,每秒写一次日志到缓存并刷新到磁盘,尚未刷新的日志可能会丢失;
- 1,要完全符合ACID,必须使用该值,表示日志在每次事务提交时写入缓存并刷新到磁盘;
- 2,每次事务提交之后,日志写到page cache,每秒刷一次到磁盘,尚未刷新的日志可能会丢失;
innodb_flush_log_at_timeout
innodb_flush_log_at_timeout
变量控制日志刷新频率。可让您将日志刷新频率设置为N
秒(其中N
为1 ... 2700
,默认值为1)
为了保证数据不丢失,请执行以下操作:
- 如果启用了binlog,则设置:sync_binlog=1;
- innodb_flush_log_at_trx_commit=1;
配置效果如下图所示:
3.5、表空间
一个InnoDB
表及其索引可以在建在系统表空间中,或者是在一个 独立表空间 中,或在 通用表空间。
- 当
innodb_file_per_table
启用时,通常是将表存放在独立表空间中,这是默认配置; - 当
innodb_file_per_table
禁用时,则会在系统表空间中创建表; - 要在通用表空间中创建表,请使用
CREATE TABLE ... TABLESPACE
语法。有关更多信息,请参见官方文档 14.6.3.3 General Tablespaces。
表空间概览图:
表空间涉及的文件
相关文件默认在磁盘中的innodb_data_home_dir
目录下:
|- ibdata1 // 系统表空间文件
|- ibtmp1 // 默认临时表空间文件,可通过innodb_temp_data_file_path属性指定文件位置
|- test/ // 数据库文件夹
|- db.opt // test数据库配置文件,包含数据库字符集属性
|- t.frm // 数据表元数据文件,不管是使用独立表空间还是系统表空间,每个表都对应有一个
|- t.ibd // 数据库表独立表空间文件,如果使用的是独立表空间,则一个表对应一个ibd文件,否则保存在系统表空间文件中
frm文件
创建一个InnoDB
表时,MySQL 在数据库目录中创建一个.frm文件。frm文件包含MySQL表的元数据(如表定义)。每个InnoDB表都有一个.frm文件。
与其他MySQL存储引擎不同, InnoDB
它还在系统表空间
内的自身内部数据字典中编码有关表的信息。MySQL删除表或数据库时,将删除一个或多个.frm
文件以及InnoDB
数据字典中的相应条目。
因此,在InnoDB中,您不能仅通过移动.frm
文件来移动表。有关移动InnoDB
表的信息,请参见官方文档14.6.1.4 Moving or Copying InnoDB Tables。
ibd文件
对于在独立表空间创建的表,还会在数据库目录中生成一个 .ibd表空间文件。
在通用表空间
中创建的表在现有的常规表空间 .ibd文件中创建。常规表空间文件可以在MySQL数据目录内部或外部创建。有关更多信息,请参见官方文档14.6.3.3 General Tablespaces。
ibdata文件
系统表空间文件,在 InnoDB
系统表空间中创建的表在ibdata中创建。
3.5.1、系统表空间
系统表空间由一个或多个数据文件(ibdata文件)组成。其中包含与InnoDB
相关对象有关的元数据(InnoDB
数据字典 data dictionary),以及更改缓冲区(change buffer), 双写缓冲区(doublewrite buffer)和撤消日志(undo logs)的存储区 。
InnoDB
如果表是在系统表空间中创建的,则系统表空间中也包含表的表数据和索引数据。
系统表空间的问题
在MySQL 5.6.7之前,默认设置是将所有InnoDB
表和索引保留 在系统表空间内,这通常会导致该文件变得非常大。因为系统表空间永远不会缩小,所以如果先加载然后删除大量临时数据,则可能会出现存储问题。
在MySQL 5.7中,默认设置为 独立表空间模式,其中每个表及其相关索引存储在单独的 .ibd文件中。此默认设置使使用Barracuda文件格式的InnoDB
功能更容易使用,例如表压缩,页外列的有效存储以及大索引键前缀(innodb_large_prefix
)。
将所有表数据保留在系统表空间或单独的 .ibd
文件中通常会对存储管理产生影响。
InnoDB
在MySQL 5.7.6中引入了通用表空间[11],这些表空间也由.ibd
文件表示 。通用表空间是使用CREATE TABLESPACE
语法创建的共享表空间。它们可以在MySQL数据目录之外创建,能够容纳多个表,并支持所有行格式的表。
3.5.2、独立表空间
MySQL 5.7中,配置参数:innodb_file_per_table
,默认处于启用状态,这是一个重要的配置选项,会影响InnoDB
文件存储,功能的可用性和I/O特性等。
启用之后,每个表的数据和索引是存放在单独的.ibd文件中的,而不是在系统表空间的共享ibdata文件中。
优点
- 您可以更加灵活的选择
数据压缩
[12]的行格式,如:- 默认情况下(innodb_page_size=16K),
前缀索引
[13]最多包含768个字节。如果开启innodb_large_prefix,且Innodb表的存储行格式为 DYNAMIC 或 COMPRESSED,则前缀索引最多可包含3072个字节,前缀索引也同样适用;
- 默认情况下(innodb_page_size=16K),
-
TRUNCATE TABLE
执行的更快,并且回收的空间不会继续保留,而是让操作系统使用; - 可以在单独的存储设备上创建每表文件表空间数据文件,以进行I / O优化,空间管理或备份。请参见 14.6.1.2 Creating Tables Externally;
缺点
- 独立表空间中的未使用空间只能由同一个表使用,如果管理不当,会造成空间浪费;
- 多个表需要刷盘,只能执行多次fsync,无法合并多个表的写操作,这可能会导致更多的fsync操作总数;
- mysqld必须为每个表文件空间保留一个打开的文件句柄,如果表数量多,可能会影响性能;
- 每个表都需要自己的数据文件,需要更多的文件描述符;
即使启用了innodb_file_per_table参数,每张表空间存放的只是数据、索引和插入缓存Bitmap页,其他数据如回滚信息、插入缓冲索引页、系统事务信息、二次写缓冲等还是存放在原来的共享表空间中。
3.5.3、通用表空间
通用表空间使用CREATE TABLESPACE
语法创建。
类似于系统表空间,通用表空间是共享表空间,可以存储多个表的数据。
通用表空间比独立表空间具有潜在的内存优势,服务器在表空间的生存期内将表空间元数据保留在内存中。一个通用表空间通常可以存放多个表数据,消耗更少的表空间元数据内存。
数据文件可以放置在MySQL数据目录或独立于MySQL数据目录。
3.5.4、undo表空间
undo表空间包含undo log。
innodb_rollback_segments
变量定义分配给每个撤消表空间的回滚段的数量。
undo log可以存储在一个或多个undo表空间中,而不是系统表空间中。
在默认配置中,撤消日志位于系统表空间中。SSD存储更适合undo log的I/O模式,为此,可以把undo log存放在有别于系统表空间的ssd硬盘中。
innodb_undo_tablespaces
配置选项控制undo表空间的数量。
3.5.5、临时表空间
由用户创建的非压缩临时表和磁盘内部临时表是在共享临时表空间中创建的。
innodb_temp_data_file_path
配置选项指定零时表空间文件的路径,如果未指定,则默认在 innodb_data_home_dir
目录中创建一个略大于12MB 的自动扩展数据文件ibtmp1
。
使用ROW_FORMAT=COMPRESSED
属性创建的压缩临时表,是在独立表空间中的临时文件目录中创建的 。
服务启动的时候创建临时表空间,关闭的时候销毁临时表空间。如果临时表空间创建失败,则意味着服务启动失败。
3.6、InnoDB底层逻辑存储结构
在介绍索引之前,我们有必要了解一下InnoDB底层的逻辑存储结构,因为索引是基于这个底层逻辑存储结构创建的。截止到目前,我们所展示的都仅仅是物理磁盘中的逻辑视图,接下来我们就来看看底层的视图。
3.6.1、ibd文件组织结构
现在我们打开一个表空间ibd文件,看看里面都是如何组织数据的?
如下图,表空间由段(segment)、区(extent)、页(page)组成。
InnoDB最小的存储单位是页,默认每个页大小是16k。
而InnoDB存储引擎是面向行的(row-oriented),数据按行进行存放,每个页规定最多允许存放的行数=16k/2 - 200,即7992行。
段:如数据段、索引段、回滚段等。InnoDB存储引擎是B+树索引组织的,所以数据即索引,索引即数据。B+树的叶子节点存储的都是数据段的数据。
3.6.2、数据页结构[14]
名称 | 占用空间 | 描述 |
---|---|---|
Fil Header | 38 byte | 页的基本信息,如所属表空间,上一页和下一页指针。 |
Page Header | 56 byte | 数据页专有的相关信息 |
Infimun + Supremum | 26 byte | 两个虚拟的行记录,用于限定记录的边界 |
User Records | 动态分配 | 实际存储的行记录内容 |
Free Space | 动态调整 | 尚未使用的页空间 |
Page Directory | 动态调整 | 页中某些记录的相对位置 |
Fil Trailer | 8 byte | 校验页是否完整 |
关于Infimun和Supremum:首次创建索引时,InnoDB会在根页面中自动设置一个最小记录和一个最高记录,并且永远不会删除它们。最低记录和最高记录可以视为索引页开销的一部分。最初,它们都存在于根页面上,但是随着索引的增长,最低记录将存在于第一或最低叶子页上,最高记录将出现在最后或最大关键字页上。
3.6.3、行记录结构描述[15]
先来讲讲Compact行记录格式,Compact是MySQL5.0引入的,设计目标是高效的存储数据,让一个页能够存放更多的数据,从而实现更快的B+树查找。
名称 | 描述 |
---|---|
变长字段长度列表 | 字段大小最多用2个字节表示,也就是最多限制长度:2^16=65535个字节;字段大小小于255字节,则用1个字节表示; |
NULL标志位 | 记录该行哪些位置的字段是null值 |
记录头信息 | 记录头信息信息,固定占用5个字节 |
列1数据 | 实际的列数据,NULL不占用该部分的空间 |
列2数据 | |
... |
记录头用于将连续的记录链接在一起,并用于行级锁定。
每行数据除了用户定义的列外,还有两个隐藏列:
- 6个字节的事务ID列;
- 7个字节的回滚指针列;
- 如果InnoDB没有指定主键,还会增加一个6个字节的rowid列;
而记录头信息包[16]含如下内容:
名称 | 大小(bit) | 描述 |
---|---|---|
() | 1 | 未知 |
() | 1 | 未知 |
deleted_flag | 1 | 该行是否已被删除 |
min_rec_flag | 1 | 如果该记录是预定义的最小记录,则为1 |
n_owned | 4 | 该记录拥有的记录数 |
heap_no | 13 | 索引堆中该条记录的排序号 |
record_type | 3 | 记录类型:000 普通,001 B+树节点指针,010 Infimum,011 Supremum,1xx 保留 |
next_record | 16 | 指向页中下一条记录 |
更详细的页结构参考官网:22.2 InnoDB Page Structure
更详细的行结构参考官网:22.1 InnoDB Record Structure
更详细的行格式参考官网:14.11 InnoDB Row Formats
根据以上格式,可以得出数据页内的记录组织方式:
3.6.3.1、MySQL中varchar最大长度是多少
上面表格描述我们知道,一个字段最长限制是65535个字节,这是存储长度的限制。
而MySQL中对存储是有限制的,
- MySQL对每个表有4096列的硬限制,但是对于给定的表,有效最大值可能会更少;
- MySQL表的每行行最大限制为65,535字节,这是逻辑的限制;实际存储的时候,表的物理最大行大小略小于页面的一半。如果一行的长度少于一页的一半,则所有行都将存储在本地页面内。如果它超过一页的一半,那么将选择可变长度列用于外部页外存储,直到该行大小控制在半页之内为止。