Files
rikako-note/mysql/mysql文档/mysql_表.md
2025-02-08 01:47:49 +08:00

22 KiB
Raw Blame History

索引组织表

innodb存储引擎中表都是根据主键顺序组织存放的这种存储方式被称为索引组织表index organized table。在innodb存储引擎表中每张表都有主键primary key如果在创建表时没有显式指定主键那么innodb会按照如下方式创建主键

  • 首先判断表中是否存在非空的唯一索引unique not null字段如果有则其为主键
  • 如果不存在非空唯一索引那么innodb会自动创建一个6字节大小的指针作为主键

如果有多个非空唯一索引innodb存储引擎将会选择第一个定义的非空唯一索引作为主键。

innodb逻辑存储结构

在innodb的存储逻辑结构中所有的数据都被逻辑存放在表空间table space中。表空间则由segementextentpage组成。

组成如图所示:

表空间

表空间为innodb存储引擎逻辑结构的最高层所有数据都存放于表空间中。innodb存在一个默认的共享表空间ibdata1,在开启innodb_file_per_table参数后,每张表内的数据可以单独存放到一个表空间。

innodb_file_per_table

innodb_file_per_table参数启用会导致每张表的数据、索引、插入缓冲bitmap页存放到单独的文件中;但是其他数据,例如回滚undo信息插入缓冲页系统事务信息double write buffer等还是存放在默认的共享表空间中。

segment

如上图所示表空间是由段segment所组成的常见的段分为数据段,索引段,回滚段等。

在innodb存储引擎中数据即索引索引即数据。数据段即为B+树的叶子节点(Leaf node segment),索引段即为B+树的非叶子节点(Non-leaf node segment)

Extent

区是由连续页组成的空间在任何情况下每个区的大小都为1MB。为了保证区中页的连续性innodb存储引擎会一次性从磁盘申请4~5个区。在默认情况下innodb存储引擎中页的大小为16KB,一个区中包含64个页。

在新建表时新建表的大小为96KB小于一个Extent的大小1MB因为每个段Segmenet开始时都会有至多32个页大小的碎片页等使用完这些页后才会申请64个连续页作为Extent。

都与一些小表或是undo这样的段可以在开始时申请较少的空间节省磁盘容量开销。

Page

innodb存储引擎中页的大小默认为16KB,默认的页大小可以通过innodb_page_size参数进行修改。通过该参数可以将innodb的默认页大小设置为4K8K。

页是innodb磁盘管理的最小单位在innodb中常见的页有:

  • 数据页B-tree node
  • undo页undo log page
  • 系统页system page
  • 事务数据页transaction system page
  • 插入缓冲位图页insert buffer bitmap
  • 插入缓冲空闲列表页insert buffer free list
  • 未压缩的二进制大对象页uncompressed blob page
  • 压缩的二进制大对象页compressed blob page

innodb存储引擎是面向行的数据按行进行存放。每个页中至多可以存放16KB/2 - 200行的记录即7992行记录。

innodb行记录格式

innodb存储引擎以行的形式进行存储可以通过show table status like '{table_name}'的语句来查询表的行格式,示例如下:

show table status like 'demo_t1'
Name Engine Version Row_format Rows Avg_row_length Data_length Max_data_length Index_length Data_free Auto_increment Create_time Update_time Check_time Collation Checksum Create_options Comment
demo_t1 InnoDB 10 Dynamic 0 0 16384 0 32768 0 1 2025-01-30 15:18:57 null null utf8mb4_0900_ai_ci null

上述示例中表的row_format为dynamic。

Compact

在使用Compact行记录格式时一个页中存放数据越多其性能越高。

Compact格式下行记录的存储格式如下

变长字段长度列表 NULL标志位 记录头信息 列1数据 列2数据 ......

变长字段长度列表

Compact行记录格式其首部是一个非Null变长字段长度列表,其按照列的顺序逆序放置,变长列的长度为:

  • 如果列的长度小于255字节用1字节表示
  • 如果列的长度大于255字节用2个字节表示

变长字段的长度不能小于2字节因为varchar类型最长长度限制为65535。

NULL标志位

NULL标志位为bitmap代表每一列是否为空。如果行中存在n个字段可为空那么NULL标志位部分的长度为ceiling(n/8)。

记录头信息

record header固定占用5字节其中record header的各bit含义如下所示

名称 大小(bit)
() 1 未知
() 1 未知
deleted_flag 1 该行是否已经被删除
min_rec_flag 1 该行是否为预订被定义的最小记录行
n_owned 4 该记录拥有的记录数
heap_no 13 索引堆中该条记录的排序记录
record_type 3 记录类型000代表普通001代表B+树节点指针, 010代表infimum011代表supremum1xx保留
next_record 16 页中下一条记录相对位置

除了上述3个部分之外其他部分就是各个列的实际值。

在Compact格式中NULL除了占有NULL标志位外不占用任何实际空间。

每行数据中,除了有用户自定义的列外,还存在两个隐藏列,即事务id列回滚指针列长度分别为6字节和7字节。

如果innodb表没有自定义主键每行还会增加一个rowid列。

行溢出数据

innodb存储引擎可能将一条记录中某些数据存储在真正的存储数据页面之外。一般来说blob、lob这类大对象的存储位于数据页面之外。

当行数据的大小特别大导致一个页无法存放2条行数据时innodb会自动将占用空间大的blob字段或varchar字段值放到额外的uncompressed blob page中。

dynamic

dynamic格式几乎和compact格式相同但是对于每个blob字段其存储只消耗20字节用于存储指针。

而对于Compact格式其会在blob格式中存储768字节的前缀字节。

char存储结构

char(n)字段中n代表字符长度而非字节长度故而在不同字符集下char类型字段的内部存储可能不是定长的。

例如在utf8字符集下ab我们两个字符串其字符数都是2个但是ab其占用2字节我们占用4字节即使同样是char(2)类型的字符串,其占用字节数量仍然有可能不同。

innodb数据页结构

innodb中页是磁盘管理的最小结构页类型为B-tree Node的页存放的即是表中行的实际数据。

innodb数据页由如下7个部分组成

  • File Header文件头
  • Page Header页头
  • Infimum和Supremum Records
  • user records用户记录即行记录
  • free space空闲空间
  • page directory页目录
  • file trailer文件结尾信息

file headerpage headerfile trailer的大小是固定的分别为38568字节这些空间用于标记页的一些信息例如checksum数据页所在B+树的层数等。

image.png

File Header

File Header用于记录页的一些头信息由8个部分组成共占38字节

名称 大小 说明
FIL_PAGE_SPACE_OR_CHKSUM 4 代表页的checksum值
FIL_PAGE_OFFSET 4 表空间中页的偏移位置
FIL_PAGE_PREV 4 当前页的上一个页B+树决定叶子节点为双向列表
FIL_PAGE_NEXT 4 当前页的下一个页
FIL_PAGE_LSN 8 代表该页最后被修改的日志序列位置LSN
FIL_PAGE_TYPE 2 存储引擎页类型
FIL_PAGE_FILE_FLUSH_LSN 8 该值仅在系统表空间的一个页中定义代表文件至少被更新到了该LSN值对于独立的表空间该值为0
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID 4 代表页属于哪个表空间

Infimum和Supremum record

在innodb存储引擎中每个数据页都有两行虚拟的行记录用于限定记录边界。Infimum是比该页中所有主键值都要小的值Supremum是比任何可能值都要大的值。这两个值在页创建时被建立,并且在任何情况下都不会被删除。

user record 和 free space

user reocrd代表实际存储行记录中的内容free space则是代表空闲空间同样是链表数据结构在一条记录被删除后该空间会被加入到空闲列表中。

page directory

page directory页目录中存放了记录的相对位置这些记录指针被称为目录槽directory slotsinnodb中槽是一个稀疏目录一个槽中可能包含多个记录记录Infimum的n_owned总为1记录Supremum的n_owned取值范围为[1,8]其他用户记录的n_owned为[4,8]。当记录被插入或删除时,需要对槽进行分裂或平衡的维护操作。

在slots中记录按照索引键值顺序存放可以通过二叉查询迅速找到记录的指针。

由于在innodb存储引擎中page directory是稀疏目录二叉查找结果只是一个粗略的结果innodb存储引擎必须通过record header中的next_record来继续查找相关记录。

B+树索引

如上图所示innodb中的数据是按照索引来进行组织的。但是通过B+树索引,其无法直接查询到指定的记录,其只能查询到记录所位于的页。

例如,在上图示例中,其在查询ID=32的时只能查询到该记录位于page 17中。

在B+树的叶子节点每个页的File Header中都存有指向前一个页和后一个页的指针故而每个页之间是通过双向列表结构来维护的;但是对于页中的记录,记录与记录之间维维护的时单向列表的关系

对于Compact或Dynamic行格式的页其每条记录的record header中都包含一个next_record字段指向下一条记录。故而位于同一个页中的记录可以单向访问。

但是在一个页内查找某条记录时沿着单向链表进行查找其效率很低。故而page中的数据时机被分为了多个组被分为的组构成了一个subdirectory故而通过子目录能够缩小查询范围提高查询性能。

page directory是一个能够存储多个slots的部分每个slot中存储了group中最后一条记录的相对位置。假设slot中最大一条记录为r那么group中记录的条数被存储在r记录record header的n_owned字段中。

group中record的数量约束如下

  • infimum group中records数量限制为[1,8]
  • supremum group中records数量限制为[1,8]
  • 其他group中records限制为[4,8]

page directory生成过程如下所示

  • 最开始时页中只有infimum和supremum两条记录它们分别属于两个group。page directory中有两个slots指向这两条记录两个slot的n_owned都为1
  • 后续当新的记录被插入页时系统会查找page directory中主键值大于待插入记录的第一个slot。slot对应最大记录的n_owned字段会增加1代表group中新插入了记录直到group中的记录数量到达8
  • 当新纪录被插入到的group中记录数大于8时group中的记录被分为2个group一个group包含4条记录另一个group包含5条记录。该过程将会向page directory中新增一个新的slot
  • 当记录被删除时slot最最大一条记录的n_owned将会减少1当n_owned字段小于4时会触发再平衡操作平衡后的page directory满足上述要求

page directory中的slots数量如page header中的PAGE_N_DIR_SLOTS所示。

一旦innodb中页包含page directory后其会通过二分查找快速的定位slot并且从group中最小记录开始通过next_record指针来遍历页中的记录列表。这样能够快速的定位记录位置。

File Trailer

完整性校验

在innodb中页的大小为16KB可能和磁盘的扇区大小不同。通常磁盘的扇区大小为512字节故而在写入一个页到磁盘中时需要分32个扇区进行写入。

在写入一个页时可能会发生宕机场景这时一个页只写入了一部分可能会发生脏写。此时可以通过double write buffer机制对脏写的页进行恢复。

在一个页被写入到磁盘中时首先会被写入的是File Header的FIL_PAGE_SPACE_OR_CHKSUM,该部分是page的checksum。

innodb设置了File Trailer部分来校验page是否被完全写入到磁盘中File Trailer中只包含一个FIL_PAGE_END_LSN部分占用8字节前4字节代表该页的checksum值后4字节和File Header中的FIL_PAGE_LSN相同,代表最后一次修改该页关联的LSN位置。将上述两个字段和File Header中的FIL_PAGE_SPACE_OR_CHKSUMFIL_PAGE_LSN值进行比较,查看是否一致,从而保证页的完整性。

默认情况下在innodb每次从磁盘读取page时都会检查页面的完整性。

分区表

innodb存储引擎支持分区功能分区过程将一个表或索引分为多个更小、更可管理的部分。

对于访问数据库的应用而言,逻辑上的一个表或索引,其物理上可能由多个物理分区组成,每个分区都是独立的对象,既可以独自进行处理,又可以作为一个更大对象的一部分进行处理。

mysql仅支持水平分区将同一张表中的不同行记录分配到不同的物理文件中并不支持垂直分区将同一张表中的不同列分配到不同的物理文件中

目前mysql支持如下几种类型的分区

  • RANGE: 行数据基于一个给定连续区间的列值被放入分区
  • LIST: 和RANGE类似,但LIST分区面对的是更加离散的值
  • HASH: 根据用户自定义的表达式返回值来进行分区,返回值不能为负数
  • KEY:根据mysql提供的哈希函数来进行分区

不论创建何种类型的分区,如果表中存在唯一索引或主键,分区列必须是唯一索引的一个组成部分。

partition keys & primary keys & unique keys

对于分区表而言,所有在partition expression中使用到的col其必须被包含在分区表所有的唯一索引中

所有唯一索引中包含主键索引。

正确建立分区表的声明示例如下:

CREATE TABLE t1 (
    col1 INT NOT NULL,
    col2 DATE NOT NULL,
    col3 INT NOT NULL,
    col4 INT NOT NULL,
    UNIQUE KEY (col1, col2, col3)
)
PARTITION BY HASH(col3)
PARTITIONS 4;

CREATE TABLE t2 (
    col1 INT NOT NULL,
    col2 DATE NOT NULL,
    col3 INT NOT NULL,
    col4 INT NOT NULL,
    UNIQUE KEY (col1, col3)
)
PARTITION BY HASH(col1 + col3)
PARTITIONS 4;


CREATE TABLE t7 (
    col1 INT NOT NULL,
    col2 DATE NOT NULL,
    col3 INT NOT NULL,
    col4 INT NOT NULL,
    PRIMARY KEY(col1, col2)
)
PARTITION BY HASH(col1 + YEAR(col2))
PARTITIONS 4;

CREATE TABLE t8 (
    col1 INT NOT NULL,
    col2 DATE NOT NULL,
    col3 INT NOT NULL,
    col4 INT NOT NULL,
    PRIMARY KEY(col1, col2, col4),
    UNIQUE KEY(col2, col1)
)
PARTITION BY HASH(col1 + YEAR(col2))
PARTITIONS 4;


create table t2_partition (
	col1 int ,
	col2 date,
	col3 int null,
	col4 int null,
	unique index `idx_t2_partition_cols` (col1, col2,col3),
	primary key (col2, col1)
) 
partition by hash(col1) 
partitions 4;

表中不存在唯一索引

如果表中没有唯一索引也未定义primary key那么上述要求并不会生效可以在partition expression中使用任意cols。

后续向分区表添加唯一索引

如果想要向分区表中添加唯一索引那么新增的唯一索引中必须包含partition expression中所有的列。

对非分区表进行分区

可以参照如下示例对非分区表进行分区:

CREATE TABLE np_pk (
        id INT NOT NULL AUTO_INCREMENT,
        name VARCHAR(50),
        added DATE,
        PRIMARY KEY (id)
);

可以按id进行分区

ALTER TABLE np_pk
        PARTITION BY HASH(id)
        PARTITIONS 4;

分区类型

RANGE

RANGE为较常见的分区类型如下示例为创建RANGE类型分区表的示例

create table t1 (
    id bigint auto_increment not null,
    tran_date datetime(6) not null default now(6) on update now(6),
    primary key (id, tran_date)
) engine=innodb
partition by range (year(tran_date)) (
    partition part_y_let_2024 values less than (2025),
    partition part_y_eq_2025 values less than (2026)
    );

上述创建了表t1t1主键为(id, tran_date),且按照tran_date字段的年部分进行分区分区类型为RANGE。

表t1的分区为两部分分区1为tran_date位于2024或之前年份的分区part_y_let_2024分区2是tran_date位于2025年的分区part_y_eq_2025

information_schema.partitions

如果要查看表的分区情况,可以查询information_schema.partitions表,示例如下

select * from information_schema.partitions where table_schema = 'learn_innodb' and table_name = 't1';

查询结果如下所示:

TABLE_CATALOG TABLE_SCHEMA TABLE_NAME PARTITION_NAME SUBPARTITION_NAME PARTITION_ORDINAL_POSITION SUBPARTITION_ORDINAL_POSITION PARTITION_METHOD SUBPARTITION_METHOD PARTITION_EXPRESSION SUBPARTITION_EXPRESSION PARTITION_DESCRIPTION TABLE_ROWS AVG_ROW_LENGTH DATA_LENGTH MAX_DATA_LENGTH INDEX_LENGTH DATA_FREE CREATE_TIME UPDATE_TIME CHECK_TIME CHECKSUM PARTITION_COMMENT NODEGROUP TABLESPACE_NAME
def learn_innodb t1 part_y_eq_2025 null 2 null RANGE null year(`tran_date`) null 2026 0 0 16384 0 0 0 2025-02-08 01:05:53 null null null default null
def learn_innodb t1 part_y_let_2024 null 1 null RANGE null year(`tran_date`) null 2025 0 0 16384 0 0 0 2025-02-08 01:05:53 null null null default null

为分区表预置如下数据

insert into t1(tran_date) values ('2023-01-01'), ('2024-12-31'), ('2025-01-01');

可以看到分区表中数据分布如下:

select partition_name, table_rows from information_schema.partitions where table_schema = 'learn_innodb' and table_name = 't1';
PARTITION_NAME TABLE_ROWS
part_y_eq_2025 1
part_y_let_2024 2

易知('2023-01-01'), ('2024-12-31')数据位于part_y_let_2024分区,故而该分区存在两条数据;而('2025-01-01')数据位于part_y_let_2024分区,该分区存在一条数据。

看select语句查询了哪些分区

数据分布可以通过如下方式验证:

explain  select * from t1 where tran_date >= '2023-01-01' and tran_date <= '2024-12-31';
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE t1 part_y_let_2024 index PRIMARY PRIMARY 16 null 2 50 Using where; Using index
explain select * from t1 where tran_date >= '2025-01-01'
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE t1 part_y_eq_2025 index PRIMARY PRIMARY 16 null 1 100 Using where; Using index
插入超过分区范围的数据

如果尝试向分区表中插入超过所有分区范围的数据,会执行失败,示例如下:

insert into t1(tran_date) values ('2026-01-01');

由于分区表t1中现有分区最大只支持2025年的分区故而当尝试插入2026年分区时会抛出如下错误

[2025-02-08 01:32:51] [HY000][1526] Table has no partition for value 2026
向分区表中添加分区

mysql支持向分区表中添加分区语法如下

alter table t1 add partition (
    partition part_year_egt_2026 values less than maxvalue
    );

添加分区后mysql支持tran_date大于等于2026的分区,执行如下语句

insert into t1(tran_date) values ('2026-01-01'), ('2027-12-31'), ('2028-01-01');

之后,再次分区分区中的数据分区,结果如下

PARTITION_NAME TABLE_ROWS
part_y_eq_2025 1
part_y_let_2024 2
part_year_egt_2026 3

information.partitions表中,table_rows字段可能存在不准确的情况,如果想要获取准确的值,需要执行analyze table {schema}.{table_name}语句

向分区表中删除分区

当想要删除分区表中现存分区时,可以通过执行如下语句

alter table t1 drop partition part_year_egt_2026;

在删除分区后,分区中的数据也会被删除,执行查询语句后

select * from t1;

可知part_year_egt_2026被删除后分区中数据全部消失

id tran_date
1 2023-01-01 00:00:00.000000
2 2024-12-31 00:00:00.000000
3 2025-01-01 00:00:00.000000