本章节从InnoDB存储引擎表的逻辑存储及实现开始进行介绍,然后将重点分析表的物理存储特征,即数据在表中是如何组织和存放的。简单来说,表就是关于特定实体的数据集合,这也是关系型数据库模型的核心。
在InnoDB存储引擎中,表都是根据主键顺序组织存放的,这种存储方式的表称为索引组织表(index organized table)。在InnoDB存储引擎表中,每张表都有个主键(primary key),如果在创建表时没有显式的定义主键,则InnoDB存储引擎会按如下方式选择或创建主键:
当表中有多个非空唯一索引时,InnoDB存储引擎将选择建表时第一个定义的非空唯一索引作为主键。如下所示:
案例一:
# 1.创建表uniq,字段依次是a,b,c,d
# 索引顺序依次是b,c,d
mysql> create table uniq (
-> a int not null,
-> b int null,
-> c int not null,
-> d int not null,
-> unique key (b),
-> unique key (c),
-> unique key (d)
-> );
Query OK, 0 rows affected (0.14 sec)
# 2.插入数据
mysql> insert into uniq values(1,2,3,4),(5,6,7,8),(9,10,11,12);
Query OK, 3 rows affected (0.01 sec)
Records: 3 Duplicates: 0 Warnings: 0
# 3._rowid可以显示表的主键
mysql> select a,b,c,d,_rowid from uniq;
+---+------+----+----+--------+
| a | b | c | d | _rowid |
+---+------+----+----+--------+
| 1 | 2 | 3 | 4 | 3 |
| 5 | 6 | 7 | 8 | 7 |
| 9 | 10 | 11 | 12 | 11 |
+---+------+----+----+--------+
3 rows in set (0.00 sec)
案例二:
# 1.创建表uniq2,字段依次是a,b,c,d
# 索引依次是b,d,c
mysql> create table uniq2 (
-> a int not null,
-> b int null,
-> c int not null,
-> d int not null,
-> unique key (b),
-> unique key (d),
-> unique key (c)
-> );
Query OK, 0 rows affected (0.06 sec)
# 2.插入数据
mysql> insert into uniq2 values(1,2,3,4),(5,6,7,8),(9,10,11,12);
Query OK, 3 rows affected (0.02 sec)
Records: 3 Duplicates: 0 Warnings: 0
# 3._rowid可以显示表的主键
mysql> select a,b,c,d,_rowid from uniq2;
+---+------+----+----+--------+
| a | b | c | d | _rowid |
+---+------+----+----+--------+
| 1 | 2 | 3 | 4 | 4 |
| 5 | 6 | 7 | 8 | 8 |
| 9 | 10 | 11 | 12 | 12 |
+---+------+----+----+--------+
3 rows in set (0.00 sec)
通过案例一和案例二结果对比,我们发现案例一的主键是字段c
,也就是在uniq表
中字段c
是第一个非空唯一索引
字段。案例二的主键是字段d
,也就是在uniq2表
中字段d
是第一个非空唯一索引
字段。两则建表语句中,字段顺序是相同的,但是索引的顺序是相同的。所以证实了主键的选择是根据定义索引的顺序,而不是建表时列的顺序
。
除此之外,需要注意的是通过_rowid
查看主键的方法,只适用于单列为主键的情况,比如:当多列作为主键时,就无法查询。
# 1.创建表uniq3,a,b作为联合主键
mysql> create table uniq3 (
-> a int ,
-> b int ,
-> primary key (a,b)
-> ) engine=InnoDB;
Query OK, 0 rows affected (0.04 sec)
# 2.插入顺序
mysql> insert into uniq3 values(1,1),(2,2);
Query OK, 2 rows affected (0.01 sec)
Records: 2 Duplicates: 0 Warnings: 0
# 3.查询主键失败
mysql> select a,b,_rowid from uniq3;
ERROR 1054 (42S22): Unknown column '_rowid' in 'field list'
从InnoDB存储引擎的逻辑存储结构看,所有数据都被逻辑地存放在一个空间中,称之为表空间(tablespace)。表空间又由段(segment)、区(extent)、页(page)组成。页在一些文档中有时也称为块(block),InnoDB存储引擎的逻辑存储结构大致如下。
表空间可以看做是InnoDB存储引擎逻辑结构的最高层,所有的数据都存放在表空间中。在默认情况下InnoDB存储引擎有一个共享表空间ibdata1,即所有数据都存放在这个表空间内。如果用户启用了参数innodb_file_per_table
,则每张表的数据可以单独放到一个表空间内。
如果启用了innodb_file_per_table
的参数,需要注意的是每张表的表空间内存放的只是数据、索引和插入缓存Bitmap页,其他类的数据,如回滚(undo)信息,插入缓存索引页、系统事务信息,二次写缓存(Double write buffer)等还是存放在原来的共享表空间内。
可以使用py_innodb_page_info
小工具,来查看表空间中各页的类型和信息。在使用时需要注意的是,总共有三个文件,分别是include.py
、mylib.py
、py_innodb_page_info.py
,放在同一目录即可。
GitHub下载
# 1.python文件
[root@zxy_master py_innodb_page_type]# ll
total 12
-rw-r--r-- 1 root root 1257 Feb 23 10:06 include.py
-rw-r--r-- 1 root root 2179 Feb 23 10:07 mylib.py
-rw-r--r-- 1 root root 234 Feb 23 09:59 py_innodb_page_info.py
# 2.表空间统计查看
[root@zxy_master py_innodb_page_type]# python py_innodb_page_info.py /var/lib/mysql/ibdata1
# 总页数
Total number of page: 4864:
# 插入缓存
Insert Buffer Bitmap: 1
# 系统页
System Page: 123
# 事务系统页
Transaction system Page: 2
# 可用页
Freshly Allocated Page: 4326
# Undo页
Undo Log Page: 288
# segment节点页
File Segment inode: 8
# 数据页
B-tree Node: 114
# 表空间标题页
File Space Header: 2
# 3.详细表空间内容查看
[root@zxy_master py_innodb_page_type]# python -v py_innodb_page_info.py /var/lib/mysql/ibdata1
......
表空间是由各个段组成,常见的段有数据段、索引段、回滚段等。
其中,leaf node segment
是B+树的叶子节点,即数据段。Non-leaf node segment
是B+树的非索引节点,即索引段。Roolback segment
是回滚段。
在InnoDB引擎中,对段的管理都是由引擎自身所完成的,DBA不能也没有必要对其进行控制。这和Oracle对数据库中自动段的管理(ASSM)类似,从一定程度上简化了DBA对段的管理。
区由连续页组成的空间,在任何情况下每个区的大小都为1MB。为保证页的连续性,InnoDB存储引擎一次从磁盘申请4-5个区。在默认情况下,InnoDB存储引擎的页大小为16KB,即一个区共有64个连续的页。
在InnoDB 1.2.x版本新增参数innodb_page_size
后,通过该参数可以将默认页的大小设置为4K、8K,但是页中的数据库不是压缩。这时区中的页的数量通用页为256、128。总之,不论页的大小怎么变化,区的大小总是1M。
mysql> show variables like 'innodb_page_size';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| innodb_page_size | 16384 |
+------------------+-------+
1 row in set (0.00 sec)
当启用参数innodb_file_per_table
后,创建的表默认是96KB。
这时就引起了思考?
一个区有64个页,一个页16KB,那么创建的表至少应该是1MB才对?为什么这里创建的表才96KB。
# 1.为方便测试,t2字段类型为varchar(7000),能保证一个页最多存放2条记录
mysql> create table page_test(
-> t1 int not null auto_increment,
-> t2 varchar(7000),
-> primary key (t1)
-> ) engine=innodb;
Query OK, 0 rows affected (0.04 sec)
# 2.通过查看表空间文件,可以发现初始表空间大小默认96KB
mysql> system ls -lh /var/lib/mysql/zxy/page_test.ibd
-rw-r----- 1 mysql mysql 96K Feb 23 11:00 /var/lib/mysql/zxy/page_test.ibd
其实是每个段开始时,先用32个页大小的碎片页(fragement page)来存放数据,在使用完这些页后才开始64个连续页的申请。这样做的目的是对于一些小表,或者是undo这类的段,可以在开始申请较少的空间,节省磁盘容量的开销。
测试一:插入两条数据,表空间大小、B+树变化
结论1:经过测试发现表大小未发生变化
# 1.插入第一条
mysql> insert into page_test select null,repeat('a',7000);
Query OK, 1 row affected (0.01 sec)
Records: 1 Duplicates: 0 Warnings: 0
# 2.插入第二条
mysql> insert into page_test select null,repeat('a',7000);
Query OK, 1 row affected (0.01 sec)
Records: 1 Duplicates: 0 Warnings: 0
# 3.查看大小
mysql> system ls -lh /var/lib/mysql/zxy/page_test.ibd
-rw-r----- 1 mysql mysql 96K Feb 23 11:26 /var/lib/mysql/zxy/page_test.ibd
结论2:
通过py_innodb_page_info.py
工具,参数-v
查看表空间详情。可以看到page offset
是3的页,这个是数据页。page level
表示所在索引层,0表示叶子节点。因为两条数据未满16KB,所以当前所有记录都在一个页中,因此没有非叶节点。但是如果再插入一条数据,就会产生非叶节点。
[root@zxy_master py_innodb_page_type]# python py_innodb_page_info.py -v /var/lib/mysql/zxy/page_test.ibd
page offset 00000000, page type <File Space Header>
page offset 00000001, page type <Insert Buffer Bitmap>
page offset 00000002, page type <File Segment inode>
page offset 00000003, page type <B-tree Node>, page level <0000>
page offset 00000000, page type <Freshly Allocated Page>
page offset 00000000, page type <Freshly Allocated Page>
Total number of page: 6:
Freshly Allocated Page: 2
Insert Buffer Bitmap: 1
File Space Header: 1
B-tree Node: 1
File Segment inode: 1
测试二:再次插入一条数据后,表空间大小、B+树变化
结论1:经过测试发现表大小未发生变化
mysql> insert into page_test select null,repeat('a',7000);
Query OK, 1 row affected (0.01 sec)
Records: 1 Duplicates: 0 Warnings: 0
mysql> system ls -lh /var/lib/mysql/zxy/page_test.ibd
-rw-r----- 1 mysql mysql 96K Feb 23 11:34 /var/lib/mysql/zxy/page_test.ibd
结论2:可以看到page offset
为3的页的page level由之前的0变为了1,这时虽然新增的记录导致了B+树的分裂操作,但这个页的类型还是B-tree Node。
[root@zxy_master py_innodb_page_type]# python py_innodb_page_info.py -v /var/lib/mysql/zxy/page_test.ibd
page offset 00000000, page type <File Space Header>
page offset 00000001, page type <Insert Buffer Bitmap>
page offset 00000002, page type <File Segment inode>
page offset 00000003, page type <B-tree Node>, page level <0001>
page offset 00000004, page type <B-tree Node>, page level <0000>
page offset 00000005, page type <B-tree Node>, page level <0000>
Total number of page: 6:
Insert Buffer Bitmap: 1
File Space Header: 1
B-tree Node: 3
File Segment inode: 1
测试三:再次插入60条数据后,表空间大小、B+树变化
结论1:表空间大小变化
mysql> DELIMITER &&
mysql> create procedure load_t1(count int unsigned)
-> begin
-> declare s int unsigned default 1;
-> declare c varchar(7000) default repeat('a',7000);
-> while s <= count DO
-> insert into page_test select null,c;
-> set s = s+1;
-> end while;
-> end;
-> &&
Query OK, 0 rows affected (0.01 sec)
mysql> delimiter ;
mysql> call load_t1(60);
Query OK, 1 row affected (0.67 sec)
# 可以看到现在有63条数据后,表空间的大小还是小于1MB,即表示数据空间的申请还是通过碎片页,而不是通过64个连续页的区。
mysql> system ls -lh /var/lib/mysql/zxy/page_test.ibd;
-rw-r----- 1 mysql mysql 592K Feb 23 13:50 /var/lib/mysql/zxy/page_test.ibd
结论2:
可以观察到,B-tree Node
页一共有33个,除去一个page level为1的非叶节点,一共有32个page level为0的页,也就是说,对于数据段,已经有32个碎片页了。之后如果再申请空间,肯定就是按照64个页大小进行增长了。
[root@zxy_master py_innodb_page_type]# python py_innodb_page_info.py -v /var/lib/mysql/zxy/page_test.ibd
page offset 00000000, page type <File Space Header>
page offset 00000001, page type <Insert Buffer Bitmap>
page offset 00000002, page type <File Segment inode>
page offset 00000003, page type <B-tree Node>, page level <0001>
page offset 00000004, page type <B-tree Node>, page level <0000>
page offset 00000005, page type <B-tree Node>, page level <0000>
page offset 00000006, page type <B-tree Node>, page level <0000>
page offset 00000007, page type <B-tree Node>, page level <0000>
page offset 00000008, page type <B-tree Node>, page level <0000>
page offset 00000009, page type <B-tree Node>, page level <0000>
page offset 0000000a, page type <B-tree Node>, page level <0000>
page offset 0000000b, page type <B-tree Node>, page level <0000>
page offset 0000000c, page type <B-tree Node>, page level <0000>
page offset 0000000d, page type <B-tree Node>, page level <0000>
page offset 0000000e, page type <B-tree Node>, page level <0000>
page offset 0000000f, page type <B-tree Node>, page level <0000>
page offset 00000010, page type <B-tree Node>, page level <0000>
page offset 00000011, page type <B-tree Node>, page level <0000>
page offset 00000012, page type <B-tree Node>, page level <0000>
page offset 00000013, page type <B-tree Node>, page level <0000>
page offset 00000014, page type <B-tree Node>, page level <0000>
page offset 00000015, page type <B-tree Node>, page level <0000>
page offset 00000016, page type <B-tree Node>, page level <0000>
page offset 00000017, page type <B-tree Node>, page level <0000>
page offset 00000018, page type <B-tree Node>, page level <0000>
page offset 00000019, page type <B-tree Node>, page level <0000>
page offset 0000001a, page type <B-tree Node>, page level <0000>
page offset 0000001b, page type <B-tree Node>, page level <0000>
page offset 0000001c, page type <B-tree Node>, page level <0000>
page offset 0000001d, page type <B-tree Node>, page level <0000>
page offset 0000001e, page type <B-tree Node>, page level <0000>
page offset 0000001f, page type <B-tree Node>, page level <0000>
page offset 00000020, page type <B-tree Node>, page level <0000>
page offset 00000021, page type <B-tree Node>, page level <0000>
page offset 00000022, page type <B-tree Node>, page level <0000>
page offset 00000023, page type <B-tree Node>, page level <0000>
page offset 00000000, page type <Freshly Allocated Page>
Total number of page: 37:
Freshly Allocated Page: 1
Insert Buffer Bitmap: 1
File Space Header: 1
B-tree Node: 33
File Segment inode: 1
测试四:继续插入60条数据后,表空间大小、B+树变化
结论1:表空间大小增加明细
mysql> call load_t1(60);
Query OK, 1 row affected (0.68 sec)
mysql> system ls -lh /var/lib/mysql/zxy/page_test.ibd;
-rw-r----- 1 mysql mysql 9.0M Feb 23 13:59 /var/lib/mysql/zxy/page_test.ibd
结论2:出现较多freshly allocated page
空闲页。
[root@zxy_master py_innodb_page_type]# python py_innodb_page_info.py -v /var/lib/mysql/zxy/page_test.ibd
.......
Total number of page: 576:
Freshly Allocated Page: 510
Insert Buffer Bitmap: 1
File Space Header: 1
B-tree Node: 63
File Segment inode: 1
同大多数数据库一样,InnoDB有页(Page)的概念(也可以称为块),页是InnoDB磁盘管理的最小单位。在InnoDB存储引擎中,默认每个页的大小为16KB。从InnoDB 1.2.x版本开始,可以通过参数innodb_page_size
将页的大小设置为4K、8K、16K。若设置完成,则所有表中页的大小都为innodb_page_size
,不可以对其进行再次修改。除非通过mysqldump导入和导出操作产生新的库。
在InnoDB存储引擎中,常见的页类型有:
InnoDB存储引擎是面向列的,也就是说数据是按行存放的。每个页存放的行记录也是有硬性定义的饿,最多存放16KB/2-200行的记录。
每页最少存储2行记录,用链表连接气力啊,否则会失去B+树的意义。数据大的行记录,如:大字符串、TEXT、BLOB对象,都是采用行溢出数据存储。不同的行格式、存储方式不同。
网上各种说法不一,只能简单测试一下,结果如下:
MySQL的varchar(n),无论是英文字符还是中文,都可以插入n个字符,但是实际存储的大小不同。
当字符集是utf8时,一个英文占1个字节,一个中文占3个字节。
当字符集是gbk时,一个英文占1个字节,一个中文占2个字节。
当字符是latin1时,一个英文占1个字节。
一、UTF-8
# 1.innodb引擎,utf8字符集
mysql> create table char_utf8(
-> t1 int not null auto_increment,
-> t2 varchar(10) null,
-> primary key (t1)
-> ) engine=innodb default charset=utf8;
Query OK, 0 rows affected (0.05 sec)
# 2.字段t2插入10个英文字符 成功
mysql> insert into char_utf8(t2) values('aaaaaaaaaa');
Query OK, 1 row affected (0.01 sec)
# 3.字段t2插入11个英文字符 失败
mysql> insert into char_utf8(t2) values('aaaaaaaaaaa');
ERROR 1406 (22001): Data too long for column 't2' at row 1
# 4.字段t2插入10个中文字符 成功
mysql> insert into char_utf8(t2) values('测试测试测试测试测试');
Query OK, 1 row affected (0.01 sec)
# 5.字段t2插入11个中文字符 失败
mysql> insert into char_utf8(t2) values('测试测试测试测试测试测');
ERROR 1406 (22001): Data too long for column 't2' at row 1
# 6.一个英文字符占1个字节,一个中文字符占3个字节
mysql> select t2,length(t2) from char_utf8 ;
+--------------------------------+------------+
| t2 | length(t2) |
+--------------------------------+------------+
| aaaaaaaaaa | 10 |
| 测试测试测试测试测试 | 30 |
+--------------------------------+------------+
2 rows in set (0.00 sec)
二、GBK
# 1.innodb引擎,gbk字符集
mysql> create table char_gbk(
-> t1 int not null auto_increment,
-> t2 varchar(10) null,
-> primary key (t1)
-> ) engine=innodb default charset=gbk;
Query OK, 0 rows affected (0.04 sec)
# 2.插入10个英文字符 成功
mysql> insert into char_gbk(t2) values('aaaaaaaaaa');
Query OK, 1 row affected (0.02 sec)
# 3.插入11个英文字符 失败
mysql> insert into char_gbk(t2) values('aaaaaaaaaaa');
ERROR 1406 (22001): Data too long for column 't2' at row 1
# 4.插入10个中文字符 成功
mysql> insert into char_gbk(t2) values('测试测试测试测试测试');
Query OK, 1 row affected (0.01 sec)
# 5.插入11个中文字符 失败
mysql> insert into char_gbk(t2) values('测试测试测试测试测试测');
ERROR 1406 (22001): Data too long for column 't2' at row 1
# 6.一个英文字符占1个字节,一个中文字符占2个字节
mysql> select t2,length(t2) from char_gbk;
+--------------------------------+------------+
| t2 | length(t2) |
+--------------------------------+------------+
| aaaaaaaaaa | 10 |
| 测试测试测试测试测试 | 20 |
+--------------------------------+------------+
2 rows in set (0.00 sec)
三、Latin1
# 1.Innodb引擎,Latin1字符集
mysql> create table char_latin1(
-> t1 int not null auto_increment,
-> t2 varchar(10) null,
-> primary key (t1)
-> ) engine=innodb default charset=latin1;
Query OK, 0 rows affected (0.06 sec)
# 2.插入10个英文字符 成功
mysql> insert into char_latin1(t2) values('aaaaaaaaaa');
Query OK, 1 row affected (0.01 sec)
# 3.插入11个英文字符 失败
mysql> insert into char_latin1(t2) values('aaaaaaaaaaa');
ERROR 1406 (22001): Data too long for column 't2' at row 1
# 4.插入中文字符 失败 不支持
mysql> insert into char_latin1(t2) values('测试测试测试测试测试');
ERROR 1366 (HY000): Incorrect string value: '\xE6\xB5\x8B\xE8\xAF\x95...' for column 't2' at row 1
# 5.一个英文字符占1字节
mysql> select t2,length(t2) from char_latin1;
+------------+------------+
| t2 | length(t2) |
+------------+------------+
| aaaaaaaaaa | 10 |
+------------+------------+
1 row in set (0.00 sec)
InnoDB存储引擎和大多数数据库意义(如Oracle和Micrisoft SQL Server数据库),记录是以行的形式存储的。这意味着页中保存着表中一行行的数据。InnoDB存储引擎设计了4种不同类型的行格式,分别是Compact,Redundant,Dynamic,Compressed
行格式。
可以通过命令select @@innodb_default_row_format;
查看默认的行格式:
mysql> select @@innodb_default_row_format;
+-----------------------------+
| @@innodb_default_row_format |
+-----------------------------+
| dynamic |
+-----------------------------+
1 row in set (0.00 sec)
也可以查看当前某表的行格式:
# 可以看到ROW_format行是Dynamic
mysql> show table status like 'zxy'\G;
*************************** 1. row ***************************
Name: zxy
Engine: InnoDB
Version: 10
Row_format: Dynamic
Rows: 12
Avg_row_length: 1365
Data_length: 16384
Max_data_length: 0
Index_length: 0
Data_free: 0
Auto_increment: NULL
Create_time: 2022-10-08 16:51:21
Update_time: 2023-02-08 09:55:59
Check_time: NULL
Collation: latin1_swedish_ci
Checksum: NULL
Create_options:
Comment:
1 row in set (0.00 sec)
也可以修改表的行格式:
# 1.将表的行格式修改为compact
mysql> alter table zxy row_format=compact;
Query OK, 0 rows affected (0.12 sec)
Records: 0 Duplicates: 0 Warnings: 0
# 2.查看zxy表的存储格式,ROW_format是Compact
mysql> show table status like 'zxy'\G;
*************************** 1. row ***************************
Name: zxy
Engine: InnoDB
Version: 10
Row_format: Compact
Rows: 12
Avg_row_length: 1365
Data_length: 16384
Max_data_length: 0
Index_length: 0
Data_free: 0
Auto_increment: NULL
Create_time: 2023-02-24 09:43:32
Update_time: NULL
Check_time: NULL
Collation: latin1_swedish_ci
Checksum: NULL
Create_options: row_format=COMPACT
Comment:
1 row in set (0.00 sec)
ERROR:
No query specified
参数inodb_file_format
用来指定文件格式,可以通过下面的方式来查看当前所使用的InnoDB存储引擎的文件格式。
mysql> select @@version\G;
*************************** 1. row ***************************
@@version: 5.7.38-log
1 row in set (0.00 sec)
ERROR:
No query specified
mysql> show variables like 'innodb_version'\G;
*************************** 1. row ***************************
Variable_name: innodb_version
Value: 5.7.38
1 row in set (0.00 sec)
ERROR:
No query specified
mysql> show variables like 'innodb_file_format'\G;
*************************** 1. row ***************************
Variable_name: innodb_file_format
Value: Barracuda
1 row in set (0.01 sec)
ERROR:
No query specified
参数innodb_file_format_check
用来检测当前InnoDB存储引擎文件格式的支持度,该值默认为ON,如果出现不支持的文件格式,用户可能在错误日志文件中看到类似如下错误:
# 1.查看支持度
mysql> show variables like 'innodb_file_format_check';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| innodb_file_format_check | ON |
+--------------------------+-------+
1 row in set (0.00 sec)
# 2.错误信息
Innodb: Warning: the system tablespace is in a file format
that this version doesn't support.
关系型数据库系统和文件系统的一个不同点是,关系数据库本身能保证存储数据的完整性,不需要应用程序的控制,而文件系统一般需要在程序端进行控制。当前几乎所有的关系型数据库都提供约束(constraint)机制,该机制提供了一条强大而简易的途径来保证数据库中数据的完整性。一般来说,数据完整性有以下三种形式:
1.实体完整性
**保证表中有一个主键。在InnoDB存储引擎表中,用户可以通过定义Primary Key和Unique Key约束来保证实体的完整性。用户还可以通过编写一个触发器来保证数据完整性。2.域完整性
**保证数据每列的值满足特定的条件。在InnoDB存储引擎表中,域完整性可以通过以下几种途径保证。
3.参照完整性
**保证两张表之间的关系。InnoDB存储引擎支持外键,因此允许用户定义外键以强制参照完整性,也可以通过编写触发器以强制执行。对于InnoDB引擎本身,提供了以下几种约束:
约束的创建可以采用以下两种方式:
对于Unique Key(唯一索引)的约束,用户还可以通过命令create unique index
来建立。
Primary Key
## 1.创建表,id是Primary Key
mysql> create table constraint_test(
-> id int,
-> primary key (id)
-> ) engine=innodb;
Query OK, 0 rows affected (0.05 sec)
## 2.查询元数据信息可以发现
## Primary Key的约束名是Primary
mysql> select constraint_name,constraint_type
-> from information_schema.table_constraints
-> where table_schema='zxy' and table_name='constraint_test'\G;
*************************** 1. row ***************************
constraint_name: PRIMARY
constraint_type: PRIMARY KEY
Unique Key
方式一:alter table...add unique key...
添加
# 1.添加字段name
mysql> alter table constraint_test add column name varchar(25);
Query OK, 0 rows affected (0.08 sec)
Records: 0 Duplicates: 0 Warnings: 0
# 2.对name添加unique key,并指定约束名为uk_name
mysql> alter table constraint_test add unique key uk_name (name);
Query OK, 0 rows affected (0.09 sec)
Records: 0 Duplicates: 0 Warnings: 0
# 3.查询元数据
mysql> select constraint_name,constraint_type
-> from information_schema.table_constraints
-> where table_schema='zxy' and table_name='constraint_test'\G;
*************************** 1. row ***************************
constraint_name: PRIMARY
constraint_type: PRIMARY KEY
*************************** 2. row ***************************
constraint_name: uk_name
constraint_type: UNIQUE
方式二:create unique index ...
添加
mysql> alter table constraint_test add column sex varchar(25) ;
Query OK, 0 rows affected (0.07 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> create unique index un_sex on constraint_test(sex);
Query OK, 0 rows affected (0.04 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> select constraint_name,constraint_type
-> from information_schema.table_constraints
-> where table_schema='zxy' and table_name='constraint_test'\G;
*************************** 1. row ***************************
constraint_name: PRIMARY
constraint_type: PRIMARY KEY
*************************** 2. row ***************************
constraint_name: uk_name
constraint_type: UNIQUE
*************************** 3. row ***************************
constraint_name: un_sex
constraint_type: UNIQUE
3 rows in set (0.00 sec)
Foregin Key
## 1.创建外键表constraint_foreign
# id为primary key
# test_id为constraint_test的外键
mysql> create table constraint_foreign(
-> id int ,
-> test_id int ,
-> name varchar(25) ,
-> primary key (id),
-> foreign key (test_id) references constraint_test(id)
-> );
# 2.查看元数据信息
mysql> select constraint_name,constraint_type
-> from information_schema.table_constraints
-> where table_schema='zxy' and table_name='constraint_foreign'\G;
*************************** 1. row ***************************
constraint_name: PRIMARY
constraint_type: PRIMARY KEY
*************************** 2. row ***************************
constraint_name: constraint_foreign_ibfk_1
constraint_type: FOREIGN KEY
2 rows in set (0.00 sec)
# 3.用户还可以通过information_constraints查看表的外键属性
mysql> select *
-> from information_schema.referential_constraints
-> where constraint_schema='zxy' and table_name = 'constraint_foreign'\G;
*************************** 1. row ***************************
CONSTRAINT_CATALOG: def
CONSTRAINT_SCHEMA: zxy
CONSTRAINT_NAME: constraint_foreign_ibfk_1
UNIQUE_CONSTRAINT_CATALOG: def
UNIQUE_CONSTRAINT_SCHEMA: zxy
UNIQUE_CONSTRAINT_NAME: PRIMARY
MATCH_OPTION: NONE
UPDATE_RULE: RESTRICT
DELETE_RULE: RESTRICT
TABLE_NAME: constraint_foreign
REFERENCED_TABLE_NAME: constraint_test
1 row in set (0.00 sec)
我们可以看到在使用Primary Key和Unique Key的时候,就是正常创建索引的方法。那么约束和索引有什么区别呢?
用户创建一个唯一索引就是创建了一个唯一的约束。但是约束和索引的概念有所不同,约束更像是一个逻辑的概念,用户保证数据的完整性,而索引是一个数据结构,既有逻辑上的概念,在数据库中还代表着物理存储的方式。
在某些默认设置下,MySQL数据库允许非法的或不正确的数据的插入或更新,又或者可以在数据库内部将其转化为一个合法的值。比如向NOT NULL的字段插入一个NULL值,MySQL数据库会将其更改为0再进行插入,因此数据库本身没有对数据的正确性进行约束。
但是在插入非法的数据或不正确的数据的时候,会根据数据库的sql_mode
来判断是报错还是警告。
# 1.查看sql_mode类型
mysql> select @@sql_mode\G;
*************************** 1. row ***************************
@@sql_mode: ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
1 row in set (0.00 sec)
# 2.查看当前会话的sql_mode类型
mysql> select @@session.sql_mode\G;
*************************** 1. row ***************************
@@session.sql_mode: ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
1 row in set (0.00 sec)
# 3.查看全局的sql_mode类型
mysql> select @@global.sql_mode\G;
*************************** 1. row ***************************
@@global.sql_mode: ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
1 row in set (0.00 sec)
此时设置有sql_mode,因此插入非法或不正确的数据会直接报错
mysql> create table constraint_null (
-> id int not null,
-> date date not null
-> );
Query OK, 0 rows affected (0.04 sec)
mysql> insert into constraint_null select null,'2023-02-30';
ERROR 1048 (23000): Column 'id' cannot be null
设置sql_mode为空,此时向NOT NULL的列插入了一个NULL值,同时向date列插入一个不合法的日期。这时候就没有报错信息,只是显示了警告(warning)。通过指令show warnings
可以查看警告的信息。
mysql> set session sql_mode = '';
Query OK, 0 rows affected, 1 warning (0.00 sec)
mysql> insert into constraint_null select null,'2023-02-30';
Query OK, 1 row affected, 2 warnings (0.01 sec)
Records: 1 Duplicates: 0 Warnings: 2
mysql> show warnings\G;
*************************** 1. row ***************************
Level: Warning
Code: 1048
Message: Column 'id' cannot be null
*************************** 2. row ***************************
Level: Warning
Code: 1264
Message: Out of range value for column 'date' at row 1
2 rows in set (0.00 sec)
mysql> select * from constraint_null;
+----+------------+
| id | date |
+----+------------+
| 0 | 0000-00-00 |
+----+------------+
1 row in set (0.00 sec)
MySQL数据库不支持传统的CHECK约束,但是通过ENUM和SET类型可以解决部分这样的约束需求。例如表上有一个性别类型,规定域的范围只能是male或female,在这种情况下用户可以通过ENUM类型来进行约束。
mysql> create table enum_test (
-> id int ,
-> sex enum('male','female')
-> );
Query OK, 0 rows affected (0.02 sec)
mysql> insert into enum_test values(1,'male'),(2,'female');
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0
mysql> insert into enum_test values(1,'test');
ERROR 1265 (01000): Data truncated for column 'sex' at row 1
如上所示,对非法的输入值进行了约束,但是只限于对离散数值的约束,对于传统check约束支持的连续值的范围或更复杂的约束,ENUM和SET类型还是无能为力,这时用户需要通过触发器来实现对于值域的约束。
触发器的作用是在执行INSERT、UPDATE、DELETE命令前后自动调用SQL命令或存储过程。
触发器的创建命令是CREATE TRIGGER,只有具备Super权限的MySQL数据库用户才可以执行这条命令。
CREATE
[DEFINER = {user | current_user}]
TRIGGER trigger_name DEFORE|AFTER INSERT|UPDATE|DELETE
ON table_name FOR EACH ROW
最多可以为一个表建立6个触发器,即分别是INSERT、UPDATE、DELETE的BEFORE和AFTER各定义一个。BEFORE和AFTER代表触发器发生的时间,表示在每行操作的之前还是之后发生。MySQL只支持行级触发器for each row
,不支持语句级触发器for each statement
。
通过触发器,用户可以实现MySQL数据库本身并不支持的一些特性,比如对于传统check约束的支持,物化视图、高级复制、审计等特性。这里先关注触发器对于约束的支持。
案例
加入有张用户消费表,每次用户购买一样物品后其金额都是减的,若这时残生了负值的操作,这样用户的金额不减反增
mysql> create table usercash(
-> userid int not null,
-> cash int unsigned not null
-> );
Query OK, 0 rows affected (0.02 sec)
mysql> insert into usercash values(1,1000);
Query OK, 1 row affected (0.01 sec)
mysql> update usercash
-> set cash=cash-(-20)
-> where userid=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from usercash;
+--------+------+
| userid | cash |
+--------+------+
| 1 | 1020 |
+--------+------+
1 row in set (0.00 sec)
如上所示,SQL语句对于数据库来说是没有任何问题的,可以正常的运行,不会报错。但是从业务逻辑上讲这是绝对错误的。所以可以通过触发器来约束这个行为:
# 1.创建存储错误业务数据表
mysql> create table usercash_err_log (
-> userid int not null,
-> old_cash int unsigned not null,
-> new_cash int unsigned not null,
-> user varchar(30),
-> time datetime
-> );
Query OK, 0 rows affected (0.02 sec)
# 2.创建触发器
mysql> delimiter &&
mysql> create trigger trigger_user_cash_updatebefore before update
-> on usercash
-> for each row
-> begin
-> if new.cash - old.cash > 0 then
-> insert into usercash_err_log
-> select old.userid,old.cash,new.cash,user(),now();
-> set new.cash = old.cash;
-> end if;
-> end;
-> &&
Query OK, 0 rows affected (0.01 sec)
mysql> delimiter ;
# 3.业务数据负数情况
mysql> update usercash
-> set cash = cash - (-20)
-> where userid = 1;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 1 Changed: 0 Warnings: 0
# 4.查看错误业务数据是否影响业务
mysql> select * from usercash;
+--------+------+
| userid | cash |
+--------+------+
| 1 | 1020 |
+--------+------+
1 row in set (0.00 sec)
# 5.查看错误日志表
mysql> select * from usercash_err_log;
+--------+----------+----------+----------------+---------------------+
| userid | old_cash | new_cash | user | time |
+--------+----------+----------+----------------+---------------------+
| 1 | 1020 | 1040 | root@localhost | 2023-03-09 16:44:36 |
+--------+----------+----------+----------------+---------------------+
1 row in set (0.00 sec)
可以看到对于异常的数据更新通过触发器将其保存到usercash_err_log
。此外该触发器还记录了操作该SQL语句的用户及时间。通过上述例子可以发现,创建触发器也是实现约束的一种手段和方法。
外键用来保证参照完整性,MySQL数据库的MyISAM存储引擎本身并不支持外键,对于外键的定义只是起到一个注释的作用。而InnoDB存储引擎则完整支持外键约束。
[CONSTRAINT [symbol]] FOREIGN KEY
[index_name] (index_col_name,...)
REFERENCES tb1_name (index_col_name,...)
[ON DELETE reference_option]
[ON UPDATE reference_option]
referece_optio:
RESTRICT | CASCADE |SET NULL|NO ACTION
用户可以在执行CREATE TABLE时就添加外键,也可以在表创建后ALTER TABLE命令来添加。一个简单的外键的创建示例如下:
mysql> create table parent(
-> id int not null,
-> primary key (id)
-> ) engine=innodb;
Query OK, 0 rows affected (0.02 sec)
mysql> create table child(
-> id int ,
-> parent_id int,
-> foreign key (parent_id) references parent(id)
-> ) engine=innodb;
Query OK, 0 rows affected (0.02 sec)
一般来说,称被引用的表为父表,引用的表为子表。外键定义时的ON DELETE和ON UPDATE表示在对父表进行DELETE和UPDATE操作时,对子表所做的操作,可定义的子表操作有:
CASCADE
CASCADE表示当父表发生DELETE或UPDATE操作时,对相应的子表中数据进行DELETE或UPDATE操作。
SET NULL
SET NULL表示当父表发生DELETE或UPDATE操作时,相应的子表中的数据被更新为NULL值。
NO ACTION
NO ACTION表示当父表发生DELETE或UPDATE操作时,抛出错误,不允许这类操作发生
RESTRICT
RESTRICT表示当父表发生DELETE或UPDATE操作时,抛出错误,不允许这类操作发生。
如果定义外键没有指定ON DELETE或ON UPDATE,RESTRICT就是默认的外键设置。
在其他数据库中,如Oracle数据库,有一种称为延时检查(deferred check)的外键约束,即检查在SQL语句运行完成后再进行。而目前MySQL数据库的外键约束都是即时检查(immediate check),因此从上面的定义可以看出,在MySQL数据库中NO ACTION和RESTRICT功能都是相同的。
在Oracle数据库中,对于建立外键的列,一定不要忘记给这个列加上一个索引。而InnoDB存储引擎在外键建立时会自动对该列加一个索引,这和SQL Server数据库做法一样。因此可以很好的避免外键列上无索引而导致死锁的问题。例如上述的例子,表child创建时只定义了外键,并没有手动指定parent_id列为索引,但是通过命令show create table可以发现InnoDB引擎自动为外键约束的列parent_id添加了索引:
mysql> show create table child\G;
*************************** 1. row ***************************
Table: child
Create Table: CREATE TABLE `child` (
`id` int(11) DEFAULT NULL,
`parent_id` int(11) DEFAULT NULL,
KEY `parent_id` (`parent_id`),
CONSTRAINT `child_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `parent` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
对于参照完整性约束,外键能起到一个非常好的作用。但是对数据的导入操作时,外键往往导致在外键约束的检查上花费大量时间。因为MySQL数据库的外键是即时检查的,所以对导入的每一行都会进行外键检查。但是在导入过程中忽视外键的检查,如:
mysql> set foreign_key_checks = 0;
Query OK, 0 rows affected (0.00 sec)
...
加载数据
...
mysql> set foreign_key_checks = 1;
Query OK, 0 rows affected (0.00 sec)
在MySQL数据库中,视图是作为一个虚表存在,是由一个SQL查询定义的,可以当作表使用。与持久表不同的是,视图中的数据没有实际的物理存储。
视图在数据库中,可以被作为一个抽象的装置,特别是对于一些应用程序,程序本身是不需要关注基础表的结构,只需要按照视图定义来取数据或更新数据。
视图虽然是基于基础表的虚拟表,但是用户可以对某些视图进行更新操作,其本质还是通过更新视图来更新基础表。
在创建视图时,如果添加with check option
,则当不满足修改条件时,修改视图会报错。
# 1.查询基础表
mysql> select * from zxy;
+------+------+------------+
| id | name | testtime |
+------+------+------------+
| 1 | zxy | 2023-03-10 |
| 2 | zxy | 2023-03-11 |
+------+------+------------+
2 rows in set (0.00 sec)
# 2.创建视图,with check option;
mysql> create view v_zxy as select * from zxy with check option;
Query OK, 0 rows affected (0.00 sec)
# 3.修改视图
mysql> update v_zxy set name='ZXY' where id = 1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
# 4.检查视图修改结果
mysql> select * from v_zxy;
+------+------+------------+
| id | name | testtime |
+------+------+------------+
| 1 | ZXY | 2023-03-10 |
| 2 | zxy | 2023-03-11 |
+------+------+------------+
2 rows in set (0.00 sec)
# 5.检查基础表修改结果
mysql> select * from zxy;
+------+------+------------+
| id | name | testtime |
+------+------+------------+
| 1 | ZXY | 2023-03-10 |
| 2 | zxy | 2023-03-11 |
+------+------+------------+
2 rows in set (0.00 sec)
如果想要查看基础表和视图的元数据信息,可以访问information_schema
下的TABLES
和VIEWS
表
TABLES
select *
from information_schema.TABLES
where table_type='BASE TABLE'
and table_schema=database()
and table_name='zxy'\G;
mysql> select *
-> from information_schema.TABLES
-> where table_type='BASE TABLE'
-> and table_schema=database()
-> and table_name='zxy'\G;
*************************** 1. row ***************************
TABLE_CATALOG: def
TABLE_SCHEMA: zxy
TABLE_NAME: zxy
TABLE_TYPE: BASE TABLE
ENGINE: InnoDB
VERSION: 10
ROW_FORMAT: Compact
TABLE_ROWS: 3
AVG_ROW_LENGTH: 5461
DATA_LENGTH: 16384
MAX_DATA_LENGTH: 0
INDEX_LENGTH: 0
DATA_FREE: 0
AUTO_INCREMENT: NULL
CREATE_TIME: 2023-02-26 15:21:16
UPDATE_TIME: 2023-03-13 16:52:22
CHECK_TIME: NULL
TABLE_COLLATION: latin1_swedish_ci
CHECKSUM: NULL
CREATE_OPTIONS: row_format=COMPACT
TABLE_COMMENT:
1 row in set (0.00 sec)
VIEWS
select *
from information_schema.VIEWS
where table_schema=database()
and table_name='v_zxy'\G;
mysql> select *
-> from information_schema.VIEWS
-> where table_schema=database()
-> and table_name='v_zxy'\G;
*************************** 1. row ***************************
TABLE_CATALOG: def
TABLE_SCHEMA: zxy
TABLE_NAME: v_zxy
VIEW_DEFINITION: select `zxy`.`zxy`.`id` AS `id`,`zxy`.`zxy`.`name` AS `name`,`zxy`.`zxy`.`testtime` AS `testtime` from `zxy`.`zxy`
CHECK_OPTION: CASCADED
IS_UPDATABLE: YES
DEFINER: root@%
SECURITY_TYPE: DEFINER
CHARACTER_SET_CLIENT: utf8
COLLATION_CONNECTION: utf8_general_ci
1 row in set (0.00 sec)
有些数据库支持物化视图,比如Oracle、postgres。物化视图不是虚表,而是基于基础表实际存在的实表。物化视图可以预先计算并保存多表连接的SQL结果。
在Oracle数据库中,物化视图的创建方式包括以下两种:
build immediate
默认的方式,在创建物化视图时就生成数据
build deferred
创建物化视图时不生成数据,以后根据需求再生成
物化视图的刷新的条件是,当基础表发生了DML操作后,物化视图何时采用何种方式进行同步更新,刷新的模式有两种:
on demand
用户需要的时候刷新
on commit
基础表DML提交的同时刷新
刷新的方法有四种:
fast
fast刷新采用增量刷新,只刷新自上次刷新后进行的修改
complete
对物化视图完全刷新
force
数据后在刷新时判断是否可以进行快速刷新,如果可以则采用fast,否则采用complete方式
never
物化视图永不刷新
MySQL只有虚表视图,如果想实现物化视图的功能。全量刷新的话,可以创建实表作为物化视图,通过写存储过程,定时刷新。增量刷新的话,需要利用触发器,记录基础表的更改操作,然后进行增量刷新。
分区功能不是在存储引擎层完成的,常见的InnoDB、MyISAM、NDB都支持,CSV、MERGE、FEDORATED这些不支持。在使用分区功能之前,应了解当前选择的存储引擎对分区功能的支持怎么样。
MySQL5.1开始支持分区。分区的过程是将一个表或索引分解为多个更小、更可管理的部分。就访问数据库的应用而言,从逻辑上来讲,只有一个表或一个索引,但是在物理上这个表可能由数十个物理分区组成。每个分区都是独立的对象,可以独自处理,也可以作为 一个更大对象的一部分处理。
MySQL支持水平分区,不支持垂直分区。此外MySQL数据库分区是局部分区索引,一个分区既存放数据又存放索引。
tips:
水平分区:将同一个表中不同行分配到不同物理文件中
垂直分区:将同一个表不同列的记录分配到不同物理文件中
局部分区:一个分区既存放数据又存放索引
全局分区:数据存放在各个分区中,但是所有数据的索引放在一个对象中
MySQL数据库支持以下类型的分区:
不论创建何种类型的分区,如果表中存在主键或唯一索引时,分区列必须是唯一索引的一个组成部分。
mysql> create table sales_p(
-> money int unsigned not null,
-> date datetime
-> ) engine=innodb
-> partition by range(year(date)) (
-> partition p2020 values less than (2021),
-> partition p2021 values less than (2022),
-> partition pothers values less than maxvalue
-> );
Query OK, 0 rows affected (0.06 sec)
mysql> insert into sales_p values(100,'2020-01-01'),(200,'2021-01-01'),(300,'2022-01-01');
Query OK, 3 rows affected (0.00 sec)
Records: 3 Duplicates: 0 Warnings: 0
mysql> select * from information_schema.partitions
-> where table_schema=database() and table_name='sales_p'\G;
*************************** 1. row ***************************
TABLE_CATALOG: def
TABLE_SCHEMA: zxy
TABLE_NAME: sales_p
PARTITION_NAME: p2020
SUBPARTITION_NAME: NULL
PARTITION_ORDINAL_POSITION: 1
SUBPARTITION_ORDINAL_POSITION: NULL
PARTITION_METHOD: RANGE
SUBPARTITION_METHOD: NULL
PARTITION_EXPRESSION: year(date)
SUBPARTITION_EXPRESSION: NULL
PARTITION_DESCRIPTION: 2021
TABLE_ROWS: 1
AVG_ROW_LENGTH: 16384
DATA_LENGTH: 16384
MAX_DATA_LENGTH: NULL
INDEX_LENGTH: 0
DATA_FREE: 0
CREATE_TIME: 2023-03-14 10:15:57
UPDATE_TIME: 2023-03-14 10:17:01
CHECK_TIME: NULL
CHECKSUM: NULL
PARTITION_COMMENT:
NODEGROUP: default
TABLESPACE_NAME: NULL
*************************** 2. row ***************************
TABLE_CATALOG: def
TABLE_SCHEMA: zxy
TABLE_NAME: sales_p
PARTITION_NAME: p2021
SUBPARTITION_NAME: NULL
PARTITION_ORDINAL_POSITION: 2
SUBPARTITION_ORDINAL_POSITION: NULL
PARTITION_METHOD: RANGE
SUBPARTITION_METHOD: NULL
PARTITION_EXPRESSION: year(date)
SUBPARTITION_EXPRESSION: NULL
PARTITION_DESCRIPTION: 2022
TABLE_ROWS: 1
AVG_ROW_LENGTH: 16384
DATA_LENGTH: 16384
MAX_DATA_LENGTH: NULL
INDEX_LENGTH: 0
DATA_FREE: 0
CREATE_TIME: 2023-03-14 10:15:57
UPDATE_TIME: 2023-03-14 10:17:01
CHECK_TIME: NULL
CHECKSUM: NULL
PARTITION_COMMENT:
NODEGROUP: default
TABLESPACE_NAME: NULL
*************************** 3. row ***************************
TABLE_CATALOG: def
TABLE_SCHEMA: zxy
TABLE_NAME: sales_p
PARTITION_NAME: pothers
SUBPARTITION_NAME: NULL
PARTITION_ORDINAL_POSITION: 3
SUBPARTITION_ORDINAL_POSITION: NULL
PARTITION_METHOD: RANGE
SUBPARTITION_METHOD: NULL
PARTITION_EXPRESSION: year(date)
SUBPARTITION_EXPRESSION: NULL
PARTITION_DESCRIPTION: MAXVALUE
TABLE_ROWS: 1
AVG_ROW_LENGTH: 16384
DATA_LENGTH: 16384
MAX_DATA_LENGTH: NULL
INDEX_LENGTH: 0
DATA_FREE: 0
CREATE_TIME: 2023-03-14 10:15:57
UPDATE_TIME: 2023-03-14 10:17:01
CHECK_TIME: NULL
CHECKSUM: NULL
PARTITION_COMMENT:
NODEGROUP: default
TABLESPACE_NAME: NULL
3 rows in set (0.00 sec)
# 1.因为是按照日期分区的,所有按照money去查询的话,还是会查询所有的分区数据
mysql> explain partitions select * from sales_p where money = 100\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: sales_p
partitions: p2020,p2021,pothers
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 3
filtered: 33.33
Extra: Using where
1 row in set, 2 warnings (0.00 sec)
ERROR:
No query specified
# 2.当根据日期查询的时候,自动会去查询p2021分区的数据,提高查询速度
mysql> explain partitions select * from sales_p where date = '2021-01-01'\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: sales_p
partitions: p2021
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 1
filtered: 100.00
Extra: Using where
1 row in set, 2 warnings (0.00 sec)
ERROR:
No query specified
# 使用分区后,ibd文件也分为了如下几个
[root@zxy zxy]# ls | grep sales_p
sales_p.frm
sales_p#P#p2020.ibd
sales_p#P#p2021.ibd
sales_p#P#pothers.ibd
对于Range分区的查询,优化器只能对YEAR()
、TO_DAYS()
、TO_SECONDS()
、UNIX_TIMESTAMP()
这类函数进行优化选择
mysql> create table list_p(
-> a int,
-> b int
-> )engine=innodb
-> partition by list(b) (
-> partition p0 values in (0,2,4,6,8),
-> partition p1 values in (1,3,5,7,9)
-> );
Query OK, 0 rows affected (0.03 sec)
mysql> insert into list_p values(1,1),(2,2);
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0
mysql> select table_name,partition_name,table_rows
-> from information_schema.partitions
-> where table_name='list_p' and table_schema=database()\G;
*************************** 1. row ***************************
table_name: list_p
partition_name: p0
table_rows: 1
*************************** 2. row ***************************
table_name: list_p
partition_name: p1
table_rows: 1
2 rows in set (0.00 sec)
# 因为innodb引擎是事务的,所以一旦有一个任务失败,为保持事务一致,会回滚。
mysql> insert into list_p values(3,3),(4,4),(10,10),(5,5);
ERROR 1526 (HY000): Table has no partition for value 10
mysql> select * from list_p;
+------+------+
| a | b |
+------+------+
| 2 | 2 |
| 1 | 1 |
+------+------+
2 rows in set (0.00 sec)
# 1.创建myisam分区表
mysql> create table list_myisam_p(
-> a int,
-> b int
-> ) engine=myisam
-> partition by list(b) (
-> partition p0 values in (0,2,4,6,8),
-> partition p1 values in (1,3,5,7,9)
-> );
Query OK, 0 rows affected, 1 warning (0.01 sec)
# 2.模拟插入数据
mysql> insert into list_myisam_p values(3,3),(4,4),(10,10),(5,5);
ERROR 1526 (HY000): Table has no partition for value 10
# 3.可以发现就算其中一条插入失败了,之前的数据还是能正常插入表中
mysql> select * from list_myisam_p;
+------+------+
| a | b |
+------+------+
| 4 | 4 |
| 3 | 3 |
+------+------+
2 rows in set (0.01 sec)
hash分区的目的是将数据均匀的分布到预先定义的各个分区中,保证各分区的数据量大致一样。在range和list分区中,必须明确指定一个给定的列值或列值集合应该保存在哪个分区。而Hash分区,MySQL会自动完成这些工作,用户所要做的只是基于将要进行哈希分区的列值指定一个列值或表达式,以及指定被分区的表将要被分割的分区数量。
创建Hash分区,可以在创建表的时候添加partition by hash(expr)
子句,其中expr
可以是字段的列明。如果需要指定有多少个分区,可以使用partitions num
来表示要分割成分区的数量。如果不指定的话,默认分区数量是1。
1.Hash分区
MOD(data,num),data是分区的依据值,num是分区的数量
创建hash分区
mysql> create table hash_p(
-> a int ,
-> b datetime
-> )engine=innodb
-> partition by hash (year(b))
-> partitions 4;
Query OK, 0 rows affected (0.07 sec)
插入数据
mysql> insert into hash_p values(1,'2022-01-01');
Query OK, 1 row affected (0.01 sec)
预测分区
使用计算规则:MOD(year(‘2022-01-01’),4) = MOD(2022,4)=2
通过计算可知,该条数据应该会被分配到p2分区
查看分区
通过元数据结果查询可知,该数据被分配到p2分区
mysql> select table_name,partition_name,table_rows
-> from information_schema.partitions
-> where table_schema=database() and table_name='hash_p'\G;
*************************** 1. row ***************************
table_name: hash_p
partition_name: p0
table_rows: 0
*************************** 2. row ***************************
table_name: hash_p
partition_name: p1
table_rows: 0
*************************** 3. row ***************************
table_name: hash_p
partition_name: p2
table_rows: 1
*************************** 4. row ***************************
table_name: hash_p
partition_name: p3
table_rows: 0
4 rows in set (0.00 sec)
2.linear hash分区
V = power(2,ceiling(log(2,um)))
N = data&(V-1)
linear hash分区的优点在于,增加、删除、合并和拆分分区将变得更加快捷,有利于处理含有大量数据的表
创建分区
mysql> create table linear_hash_p(
-> a int,
-> b datetime
-> ) engine=innodb
-> partition by linear hash (year(b))
-> partitions 4;
Query OK, 0 rows affected (0.07 sec)
插入数据
mysql> insert into linear_hash_p values(1,'2022-01-01');
Query OK, 1 row affected (0.01 sec)
预测分区
V = power(2,ceiling(log(2,4))) = power(2,2) = 4
N = year('2022-01-01')&(V-1) = 2022 & 3 = 2
所有该数据应该被分配到p2分区
查看分区
mysql> select table_name,partition_name,table_rows
-> from information_schema.partitions
-> where table_schema=database() and table_name='linear_hash_p'\G;
*************************** 1. row ***************************
table_name: linear_hash_p
partition_name: p0
table_rows: 0
*************************** 2. row ***************************
table_name: linear_hash_p
partition_name: p1
table_rows: 0
*************************** 3. row ***************************
table_name: linear_hash_p
partition_name: p2
table_rows: 1
*************************** 4. row ***************************
table_name: linear_hash_p
partition_name: p3
table_rows: 0
4 rows in set (0.00 sec)
key分区和hash分区相似,不同之处在于hash分区使用用户定义的函数进行分区,key分区使用MySQL数据库提供的函数进行分区。对于NDB Cluster引擎,MySQL数据库使用MD5函数来分区。对于其他存储引擎,MySQL数据库使用其内部的哈希函数,这些函数基于与pasword()
一样的运算法则。如:
mysql> create table key_p(
-> a int,
-> b datetime
-> ) engine=innodb
-> partition by key (b)
-> partitions 4;
Query OK, 0 rows affected (0.07 sec)
range、list、hash和key这四种分区,分区的条件是:数据必须是整型,如果不是整型,那么应该转换为整型,如year(),to_days(),month()
等函数。columns分区可以视为是range分区和list分区的进化。columns分区可以直接使用非整型的数据进行分区,分区根据类型直接比较而得,不需要转换为整型。除此之外,range columns可以对多个列值进行分区。
columns分区支持以下数据类型:
int,smallint,tinyint,bigint。
而 float和decimal
不支持。date和datetime
。其余得日期类型不支持char,varhcar,binary,varbinary
。blob和text类型不予支持。
所以对于日期类型得分区,不需要再使用year(),to_days()
这些函数转换了,直接可以使用columns。如:
# range columns分区
mysql> create table range_columns_p (
-> a int,
-> b datetime
-> ) engine=innodb
-> partition by range columns(b) (
-> partition p1 values less than ('2022-01-01'),
-> partition p2 values less than ('2023-01-01')
-> );
Query OK, 0 rows affected (0.04 sec)
# list columns分区
mysql> create table list_columns_p (
-> a int,
-> b varchar(25)
-> ) engine=innodb
-> partition by list columns(b) (
-> partition p1 values in ('hello','world'),
-> partition p2 values in ('ni','hao')
-> )
-> ;
Query OK, 0 rows affected (0.03 sec)
子分区是在分区的基础上再进行分区,有时也称这种分区为复合分区。MySQL数据库允许在range和list分区的基础上再进行hash和key分区,如:
父分区使用range按照年分区,子分区使用hash按照天分区
mysql> create table sub_p (
-> a int ,
-> b date
-> ) engine=innodb
-> partition by range(year(b))
-> subpartition by hash(to_days(b))
-> subpartitions 2 (
-> partition p0 values less than (2020),
-> partition p1 values less than (2021),
-> partition p2 values less than maxvalue
-> );
Query OK, 0 rows affected (0.09 sec)
父分区三个,子分区两个,则共有分区六个
[root@zxy zxy]# ls | grep sub_p
sub_p.frm
sub_p#P#p0#SP#p0sp0.ibd
sub_p#P#p0#SP#p0sp1.ibd
sub_p#P#p1#SP#p1sp0.ibd
sub_p#P#p1#SP#p1sp1.ibd
sub_p#P#p2#SP#p2sp0.ibd
sub_p#P#p2#SP#p2sp1.ibd
子分区还可以写成如下格式,不过需要注意的是,六个分区的名称必须是唯一的。
create table sub_p2 (
a int,
b date
) engine=innodb
partition by range(year(b))
subpartition by hash(to_days(b)) (
partition p0 values less than(2020)(
subpartition s0,
subpartition s1
),
partition p1 values less than(2021)(
subpartition s2,
subpartition s3
),
partition p2 values less than maxvalue(
subpartition s4,
subpartition s5
)
);
MySQL数据库允许null值做分区,但是处理方法与其他数据库可能完全不同。MySQL数据库的分区总是视NULL值小于任何一个非NULL的值,这和MySQL数据库处理NULL值得order by操作是一样得。因此对于不同得分区类型,MySQL数据库对于NULL值得处理也是各不相同。
range分区
如果插入NULL值,则放置在最左边的分区。
list分区
如果要插入NULL值,需要指定分区存储NULL值
例如:partition p0 values in (1,3,5,7,NULL)
hash分区
任何分区函数都会将含有NULL的值记录返回为0
key分区
任何分区函数都会将含有NULL的值记录返回为0
对表设计分区是否会提升性能?
首先数据库根据应用主要分为两类,一类是OLTP事务处理。一类是OLAP分析处理。不过在实际的应用中,也可能存在既有OLTP又有OLAP的情况。
对于OLAP而言,通常是数据仓库、数据集市这些,往往存储大量的数据,那么通过分区确实可以提高查询性能。
然而对于OLTP,能否提高性能却不一定。OLTP对应的系统,通常是通过索引查询某些数据,不会经常的扫描表。而根据B+树索引的原理可知,对于一张大表,一般的B+树需要2-3次的磁盘IO。因此即使不在分区的帮助下,也能很好的完成操作。相反的是,如果没有合理的建立分区,也会影响查询的性能。
比如一个storage表有1000万数据,我根据id进行hash分区,分了10个区。每个区平均有100万数据。如果我根据id进行查询某条数据,那么只需要检索对于的分区即可。但是如果我根据其他字段查询某条数据,他就会检索所有分区,也就会造成20-30次磁盘IO,影响查询效率。
MySQL5.6后支持alter table ... exchange partition
语法,可以进行分区表和非分区表的数据交换。
不过使用这个语法需要满足以下条件:
alter,insert,create
权限外,还需要drop
权限,还需要注意的是:
auto_increment
列将被重置1.创建分区表
mysql> create table exchange_p1 (
-> a int,
-> b date
-> ) engine=innodb
-> partition by range(year(b)) (
-> partition p0 values less than (2021),
-> partition p1 values less than (2022),
-> partition p2 values less than maxvalue
-> );
Query OK, 0 rows affected (0.06 sec)
2.分区表插入数据
mysql> insert into exchange_p1 values(1,'2020-01-01'),(2,'2021-01-01'),(3,'2022-01-01');
Query OK, 3 rows affected (0.00 sec)
Records: 3 Duplicates: 0 Warnings: 0
3.检查各分区数据
mysql> select table_name,partition_name,table_rows
-> from information_schema.partitions
-> where table_schema=database() and table_name = 'exchange_p1'\G;
*************************** 1. row ***************************
table_name: exchange_p1
partition_name: p0
table_rows: 1
*************************** 2. row ***************************
table_name: exchange_p1
partition_name: p1
table_rows: 1
*************************** 3. row ***************************
table_name: exchange_p1
partition_name: p2
table_rows: 1
3 rows in set (0.00 sec)
4.创建相同结构非分区表
# 1.创建相同结构的表
mysql> create table exchange_p2 like exchange_p1;
Query OK, 0 rows affected (0.06 sec)
# 2.删除该表上分区
mysql> alter table exchange_p2 remove partitioning;
Query OK, 0 rows affected (0.08 sec)
Records: 0 Duplicates: 0 Warnings: 0
5.将分区表p2分区数据导入非分区表
# 1.将分区表的p2分区的数据导入到非分区表中
mysql> alter table exchange_p1 exchange partition p2 with table exchange_p2;
Query OK, 0 rows affected (0.03 sec)
mysql> select * from exchange_p2;
+------+------------+
| a | b |
+------+------------+
| 3 | 2022-01-01 |
+------+------------+
1 row in set (0.00 sec)
1.创建非分区表
mysql> create table exchange_p3 (
-> a int,
-> b date
-> ) engine=innodb;
Query OK, 0 rows affected (0.02 sec)
2.插入数据
mysql> insert into exchange_p3 values(3,'2022-01-01');
Query OK, 1 rows affected (0.00 sec)
Records: 1 Duplicates: 0 Warnings: 0
mysql> select * from exchange_p3;
+------+------------+
| a | b |
+------+------------+
| 3 | 2022-01-01 |
+------+------------+
1 rows in set (0.00 sec)
3.创建分区表
mysql> create table exchange_p4 (
-> a int,
-> b date
-> ) engine=innodb
-> partition by range(year(b)) (
-> partition p0 values less than (2021),
-> partition p1 values less than (2022),
-> partition p2 values less than maxvalue
-> );
Query OK, 0 rows affected (0.05 sec)
4.导入数据到分区表
# 1.导入数据到分区表
mysql> alter table exchange_p4 exchange partition p2 with table exchange_p3;
Query OK, 0 rows affected (0.03 sec)
# 2.查看分区表数据,已经导入成功,只有符合分区条件时,才能导入成功
mysql> select * from exchange_p4;
+------+------------+
| a | b |
+------+------------+
| 3 | 2022-01-01 |
+------+------------+
1 row in set (0.00 sec)
# 3.查看分区表分区情况
mysql> select table_name,partition_name,table_rows from information_schema.partitions where table_schema=database() and table_name = 'exchange_p4'\G
*************************** 1. row ***************************
table_name: exchange_p4
partition_name: p0
table_rows: 0
*************************** 2. row ***************************
table_name: exchange_p4
partition_name: p1
table_rows: 0
*************************** 3. row ***************************
table_name: exchange_p4
partition_name: p2
table_rows: 1
3 rows in set (0.00 sec)