InnoDB 存储引擎之表存储

本文主要是针对《Mysql技术内幕:InnoDB 存储引擎》一书中第四章关于表相关概念的概括和总结,主要包括组织索引表,InnoDB 逻辑存储结构,InnoDB 行记录格式,InnoDB 数据页结构,表相关的约束问题,视图,表分区。

一、索引组织表

索引组织表是指按照主键的顺序组织存放数据的表,InnoDB 存储引擎创建的表都是索引组织表。InnoDB 主键定义规则如下:

  • 如果定义表时,显式指定了 Primary Key,Primary Key 便是主键。
  • 如果没有使用 Primary Key 指定主键,判断表中是否有非空的唯一索引,如果有则该列即为主键,如果表中有多个非空的唯一索引,会选择第一个非空唯一索引为主键,这里是索引的定义顺序,并非列的定义顺序。
  • 如果不存在非空的唯一索引,InnoDB 会主动创建一个 6 个字节大小的指针为主键。
  • _rowid 在单个列为主键的情况下可以查看主键对应的值,多个列则无能为力。

二、InnoDB 逻辑存储结构

表空间

InnoDB 所有数据都存放在一个空间中,称之为表空间,表空间由段,区,页组成。

innodb_file_per_table 参数:

  • InnoDB 默认情况下所有表的数据都存放在共享表空间 ibdata1 中,该参数决定了是否每为张表内的数据单独设置一个表空间。
  • 如果开启该功能,单独表空间中只存放数据、索引、插入缓存 Bitmap 页,其它数据包括回滚信息,插入缓存索引页,系统事务信息等还是存放在原来的共享表空间中。

表空间由各个段组成,主要分为索引段,数据段以及回滚段。其中数据段存放在 B+ 树的叶子节点,索引段存放在 B+ 树的非叶子节点。

区由连续的页组成:

  • 每个区的大小是固定的:1MB
  • 默认页的大小是 16KB,所以一个区中有 64 个连续页。
  • innodb_page_size 参数,在 1.2.x 版本以后可以设置对页的大小进行设置。
  • 当创建一个表时,并不是直接使用连续的 64 个页存放数据,而是先用每个段开始时的 32 个碎片页来存放数据,等使用完这些碎片页才开始申请 64 个连续页,这样做的主要目的对于一些小表,刚开始可以申请较少的空间,节省磁盘容量的开销。

InnoDB 存储引擎中,常见的页的类型有:

  • 数据和索引页
  • undo 页
  • 系统页
  • 事务数据页
  • 插入缓存 Bitmap 页
  • 插入缓存空闲列表页
  • 未压缩的二进制大对象页
  • 压缩的二进制大对象页

InnoDB 存储引擎中,数据是按照行进行存放的,最多可以存放 7992 条行记录

三、InnoDB 行记录格式

InnoDB 中行记录格式通过在建表语句中关键字 ROW_FORMAT=xxx 来指定。

create Table mytest (
    t1 varchar(10),
    t2 varchar(10)
) engine=INNODB ROW_FORMAT=COMPACT

在 InnoDB 1.0.x 版本之前,提供了 Compact 和 Redundant 两种格式来存放行记录数据。

在 InnoDB 1.0.x 版本之后,引入了新的两种行记录格式:Compressed 和 Dynamic。

Compact

存放顺序:

  • 变长字段列表:按照列的顺序逆序存放每个非 NULL 变长字段所占的长度
  • NULL 标志位:存放哪些字段是NULL值
  • 记录头信息:固定5个字节,记录一些关于行是否被删除,下一行记录的相对位置等信息
  • 列1数据,列2数据…:存放每一列的数据

不管是 CHAR 类型还是 VARCHAR 类型,NULL 在 Compact 存储格式下都不占用存储空间。

Redundant

存放顺序:

  • 字段长度偏移列表:逆序存放每个字段的偏移量
  • 记录头信息:固定6个字节,记录一些关于行是否被删除,下一行记录的相对位置等信息
  • 列1数据,列2数据…:存放每一列的数据

对于 VARCHAR 类型的 NULL 值同样不占用存储空间,但是 CHAR 类型的 NULL 值会占用存储空间。

Compressed 和 Dynamic
  • 对于存放在 BLOB 中的数据,完全采用行溢出数据存放
  • Compressed 存放的行数据会采用 zlib 算法进行压缩
行溢出数据
  • InnoDB 会将一些大对象数据存放在数据页之外的 BLOB 页中
  • 要不要将数据放在 BLOB 页中,取决于当前页中是否可以存放下至少两行数据
  • VARCHAR 类型的数据也有可能被存放在 BLOB 页中,而 BLOB 类型和 TEXT 类型的数据也有可能不被存放在 BLOB 页中
  • 对于 Compact 和 Redundant 格式存放的数据,采用的是部分行溢出存储,前 768 字节还是会存放在数据页中的
  • 对于 Compressed 和 Dynamic 格式存放的数据,采用的完全行溢出存储,只用 20 个字节存放指针,其余所有数据都放在行溢出数据中
CHAR 类型的存储
  • 对于单字节的字符编码,CHAR 类型是固定长度的字符串
  • 对于多字节的字符编码,CHAR 类型不再代表固定长度的字符串了,innoDB 将其看作变长字符存放,例如对于 UTF-8 下的 CHAR(10) 类型,最小可以存放10字节的字符,最大可以存放30字节的字符

四、InnoDB 数据页结构

InnoDB 数据页由以下七个部分组成:

  • File Header 文件头
  • Page Header 页头
  • Infimun 和 Supremum Records
  • User Records 行记录
  • Free Space 空闲空间
  • Page Directory 页目录
  • File Trailer 文件结尾信息
File Header

File Header 用来记录页的一些头信息,共占用 38 个字节,主要存放关于当前页的一些信息,包括下面一些信息:

  • 当前页在表空间的偏移量
  • 上一页和下一页的位置,方便查找下一页和上一页数据
  • 页的类型,包括索引页,数据页,系统页,插入缓存位图等
Page Header

Page Header 用来记录数据页的状态信息,主要包括下面一些信息:

  • 指向可重用空间的首指针
  • 已删除的记录数
  • 页中记录的数量
  • 当前页在索引树中的位置
  • 索引ID,表示当前页属于那些索引
  • B+ 树所在的段信息,只有 Root 节点才会定义
Infimun 和 Supremum Records

Infimun 和 Supremum Records 是两个虚拟的行记录,用来存放记录的边界, Infimun 记录比所有主键值都要小的记录,Supremum Records 记录比所有主键值都要大的值,这两个值在页创建时被创建,任何情况下都不会删除。

User Records 和 Free Space

User Records 存放行记录数据,Free Space 指的是空闲空间,当一条记录被删除时就加入到空闲链表结构里

Page Directory

Page Directory 存放了记录的相对位置,称为槽,并不是每行记录一个槽,是一个稀疏目录,一个槽里面可能有多个记录,按照主键值顺序索引存放。

Page Directory 主要用于在页中根据索引查询某条记录时,通过二分查找法查到一个粗略的位置,最后在通过行记录中的 next_record 来继续查找相关记录。

所以整个数据查询的逻辑如下:

- 通过 B+ 树找到该记录所在的页,数据库把该页载入内存
- 然后再根据  Page Directory 的二分查找法找到一个粗略的记录位置
- 最后再根据行记录中的 next_record 来继续查找相关记录
File Trailer

File Trailer 为了检查页是否被完整的写入磁盘或者是否完整从磁盘读取,可以通过以下两个参数对其进行控制:

  • innodb_checksums: 是否开启页面完整性检查
  • innodb_checksum_algorithm:设置页面完整性检查算法,默认是 crc32

五、约束

数据的完整性包括下面三个部分:

  • 实体完整性:主键约束,Primary Key 和 Unique Key
  • 域完整性:保证每列的值满足一些条件,通过默认值,NOT NULL 等来约束
  • 参照完整性:保证两张表之间的关系,通过外键和触发器来约束
触发器
  • 触发器用于在执行 INSERT,DELETE,UPDATE 之前或者之后自动调用的 sql 语句或者存储过程
  • 创建触发器命令是 CREATE TRIGGER,只有具备 Super 权限的 MYSQL 数据库用户才能执行该命令
  • 一个表最多创建 6 个触发器,分别是 before insert/before delete/before update/after insert/after delete/after update
  • 触发器可以用来实现对数据完整性约束做检查,物化视图,高级复制,审计等功能
外键约束
  • InnoDB 在创建外键时,自动会给该列加上索引
  • 外键关系的两个表的列必须是数据类型相似,也就是可以相互转换类型的列,比如int和tinyint可以,而int和char则不可以
  • 外键通过 FOREIGN KEY … REFERENCES 来定义:
CREATE TABLE `dage` (
  `id` int(11) NOT NULL auto_increment,
  `name` varchar(32) default '',
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

CREATE TABLE `xiaodi` (
  `id` int(11) NOT NULL auto_increment,
  `dage_id` int(11) default NULL,
  `name` varchar(32) default '',
  PRIMARY KEY  (`id`),
  KEY `dage_id` (`dage_id`),
  CONSTRAINT `xiaodi_ibfk_1` FOREIGN KEY (`dage_id`) REFERENCES `dage` (`id`) 
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
  • 定义外键时可以指定 ON DELETE 和 ON UPDATE,表示对外键进行 DELETE 和 UPDATE时,子表的行为:
- SET NULL:表示外键发生变化时,子表相应字段被设置为 NULL
- NO ACTION:表示外键发生变化时,抛出错误,不允许该类操作
- SET DEFAULT:表示外键发生变化时,设置成相应的默认值
- RESTRICT:表示父表发生变化时,抛出错误,不允许该类操作,和 NO ACTION 类似
  • 创建外键约束时,每次插入和更新会花费大量时间去检查相关依赖,对于性能有一定的消耗,通过 set foreign_key_checks = 0 或者 = 1 来关闭或者开启外键约束检查
对错误数据的约束

默认情况下,如果向 mysql 中插入不符合约束规范的数据,mysql 会给予警告提示,并且将不符合规范的数据自动转换为符合符合规范的数据,并不会报错。

可以通过参数 sql_mode = “STRICT_TRANS_TABLES” 强制对输入值进行合法性检查,如果不满足条件则报错。

六、视图

  • 视图没有实际的物理存储
  • 视图可以被更新数据,本质上是对基表进行更新的
  • 视图的优点是简化查询,数据安全(只查询部分数据),数据独立
  • 创建视图时如果添加 WITH CHECK OPTION 选项,则会对插入视图的数据作检查,不满足视图定义的数据会报错
  • SHOW TABLES 命令查看到所有基表及视图,如果只想看到视图或者基表,可以查看 information_schema 下的 Table 和 Views 表查看
  • mysql 不支持物化视图,不过可以通过触发器的方式来模拟物化视图,每次更新时,把表中的数据清空,重新导入一遍数据
  • 创建视图通过 CREATE VIEW AS … 语句实现:
create view v_match
    as 
    select a.PLAYERNO,a.NAME,MATCHNO,WON,LOST,c.TEAMNO,c.DIVISION
    from 
    PLAYERS a,MATCHES b,TEAMS c
    where a.PLAYERNO=b.PLAYERNO and b.TEAMNO=c.TEAMNO;

七、分区表

区分特性:

  • 分区功能并不是在存储引擎层完成的,但并不是所有存储引擎都支持分区功能。
  • MYSQL 数据库不支持垂直分区,支持水平分区,也就是按照行进行分区。
  • MYSQL 数据库的分区是局部分区索引,即一个分区中既存放了数据又存放了索引。
  • have_partion 参数用来设置数据库是否开启分区功能
  • 不管创建何种分区,如果表中存在唯一主键或者索引时,分区列必须是唯一索引的一个组成部分
  • 通过 explain partition 可以查看当前 SQL 的分区命中情况
  • 可以通过查看 information_schema 下的 PARTITIONS 表来查看每个分区的具体信息
  • 插入值应该严格遵守分区的定义,如果插入的值不在分区定义范围内,MYSQL 数据库会抛出一个异常
  • RANGE,LIST,HASH,KEY 分区的结果必须是整型,如果不是整型,需要根据函数计算转换为整型

分区优点:

  • 和单个磁盘或文件系统分区相比,可以存储更多的数据。
  • where 子句中包含分区条件时,可以只扫描必要的分区,提高查询效率。
  • 涉及聚合函数的查询时,可以容易的在每个分区上并行处理,最终只需汇总得到结果。
  • 对于已经过期或者不需要保存的数据,可以通过删除与这些数据有关的分区来快速删除数据。

分区分类:

RANGE 分区
  • 行数据基于属于一个给定的连续区间的列值被放入分区
  • 对于某些具有连续意义的列进行分区可能会提高查询效率,一般用于日期列的分区
  • 如果是根据日期函数对某列的计算结果进行分区,分区优化器只能对 YEAR(),TO_DAYS(),TO_SECONDS(),UNIX_TIMESTAMP() 这类函数进行查询优化
  • 特殊值 MAXVALUE 表示正无穷值
create table t(
id int
) ENGINE=INNODB
PARTITION BY RANGE (id)(
    PARTITION p0 values less then (10),
    PARTITION p1 values less then (20),
    PARTITION p1 values less then MAXVALUE  - MAXVALUE 表示正无穷
)
LIST 分区
  • 和 RANGE 分区类似, LIST 分区是面向离散的值,Range 分区用 values less then 定义分区,而 List 分区用 values in 定义分区
  • 在用 INSERT 插入多行数据的过程中遇到分区未定义的情况,MyISAM 引擎会将之前的数据都插入,之后的数据不会插入,而 InnoDB 引擎会将其视为一个事务,不插入任何数据
create table t(
id int
) ENGINE=INNODB
PARTITION BY RANGE (id)(
    PARTITION p0 values in  (10, 20),
    PARTITION p1 values in (30, 40, 50)
)

HASH 分区
  • 根据用户自定义的表达式的返回值进行分区,返回值是正整数
  • 通过 PARTITION BY HASH(expr) 来指定分区,expr 必须返回一个整数的表达式
  • 通过 PARTITIONS 字段指定分区的个数,系统会根据 HASH(expr) 表达式返回的结果使用取余方法将数据拆分为多个分区
create table t(
id int,
b DATETIME
) ENGINE=INNODB
PARTITION BY HASH (YEAR(b))
PARTITIONS 4;    - 表示设置4个分区

KEY 分区
  • 和 HASH 分区类似,只是根据 mysql 存储引擎自己提供的哈希函数来进行分区
create table t(
id int,
b DATETIME
) ENGINE=INNODB
PARTITION BY KEY (b)
PARTITIONS 4;    - 表示设置4个分区
COLUMNS 分区
  • 和其它四种分区相比,COLUMNS 分区可以直接使用非整数类型的数据进行分区,根据数据类型直接比较,不需要转换为整型
  • COLUMNS 分区分为 RANGE COLUMNS 和 LIST COLUMNS 两种
  • 对于 RANGE COLUMNS 可以使用多个列进行分区
create table rcx (
    a int,
    b int,
    c char(3),
    d int
) engine=innoDB
partition by range columns(a,d,c)(
    partition p0 values less than (5, 10, 'ggg'),
    partition p1 values less than (10, 20, 'mmm'),
    partition p3 values less than (maxvalue, maxvalue, maxvalue),
)

子分区:

  • 在已经分区的基础上再进行分区,也称为复合分区
  • 可以在 LIST 和 RANGE 的基础上再进行 HASH 和 KEY 分区
  • 每个子分区的数量必须相同,子分区的名字必须唯一
  • 要在任何一个分区表上明确定义子分区,就必须在所有分区上都明确定义子分区
  • 创建子分区的两种方式:
--  不定义每个子分区
CREATE TABLE tb_sub (id INT, purchased DATE)
    PARTITION BY RANGE( YEAR(purchased) )
    SUBPARTITION BY HASH( TO_DAYS(purchased) )
    SUBPARTITIONS 2 (
        PARTITION p0 VALUES LESS THAN (1990),
        PARTITION p1 VALUES LESS THAN (2000),
        PARTITION p2 VALUES LESS THAN MAXVALUE
    );

-- 定义每个子分区
CREATE TABLE tb_sub_ev (id INT, purchased DATE)
    PARTITION BY RANGE( YEAR(purchased) )
    SUBPARTITION BY HASH( TO_DAYS(purchased) ) (
        PARTITION p0 VALUES LESS THAN (1990) (
            SUBPARTITION s0,
            SUBPARTITION s1
        ),
        PARTITION p1 VALUES LESS THAN (2000) (
            SUBPARTITION s2,
            SUBPARTITION s3
        ),
        PARTITION p2 VALUES LESS THAN MAXVALUE (
            SUBPARTITION s4,
            SUBPARTITION s5
        )
    );

分区中 NULL 值处理:

  • mysql 不禁止在分区键值上使用 NULL
  • range 分区对于 NULL 值的处理是将 NULL 值视为小于任何一个非 NULL 值
  • 对于 list 分区如果插入 NULL 值会报错
  • hash 和 key 分区对于 NULL 值的处理将含有 NULL 值的记录都返回为 0

分区与性能:

并不是所有启动了分区的表,数据库查询就会变快。一般对于 OLTP(在线事务处理) 的应用不建议使用分区,建议对于 OLAP(在线分析处理)使用分区。一般 OLTP 表数据量不是特别大,而且大部分查询会根据索引查询,采用分区反而会增加 IO 的读写次数使性能下降。

表和分区交换数据:

通过 ALTER TABLE … EXCHANGE PARTITION 语法可以将某个分区的数据和另外一个非分区表中的数据进行交换,必须满足以下条件:

  • 表结构必须相同
  • 非分区表中的数据必须在交换分区定义内
  • 被交换的表中不能含有外键或者其他对该表的外键引用
  • 使用该语句时不会触发交换表和被交换表上的触发器
  • AUTO_INCREMENT 列会自动被重置
  • 用户需要有 DROP 的权限
alter table e exchange partition p0 with table e2;

你可能感兴趣的:(数据库)