以 Compact 行格式为例:
总结
删除一条记录,数据原有的被废弃,记录头发生变化,主要是打上了删除标记。也就是原有的数据 deleted_flag 变成 1,代表数据被删除。但是数据没有被清空,在新一行数据大小小于这一行的时候,可能会占用这一行。这样其实就是存储碎片,要想减少存储碎片,可以通过重建表来实现(例如对于高并发大数据量表,除了归档,还可以通过利用无锁算法Alter
修改字段来重建表增加表性能)。
Compact 行格式存储
我们来创建一个包含几乎所有基本数据类型的表,其他的例如 geometry,timestamp 等等,也是基于 double 还有 bigint 而来的, text、json、blob等类型,一般不与行数据一起存储,我们之后再说:
create table record_test_1 ( id bigint, score double, name char(4), content varchar(8), extra varchar(16) )row_format=compact;
插入如下几条记录:
INSERT INTO `record_test_1`(`id`, `score`, `name`, `content`, `extra`) VALUES (1, 78.5, 'hash', 'wodetian', 'nidetiantadetian'); INSERT INTO `record_test_1`(`id`, `score`, `name`, `content`, `extra`) VALUES (65536, 17983.9812, 'zhx', 'shin', 'nosuke'); INSERT INTO `record_test_1`(`id`, `score`, `name`, `content`, `extra`) VALUES (NULL, -669.996, 'aa', NULL, NULL); INSERT INTO `record_test_1`(`id`, `score`, `name`, `content`, `extra`) VALUES (2048, NULL, NULL, 'c', 'jun');
目前表结构:
+-------+------------+------+----------+------------------+ | id | score | name | content | extra | +-------+------------+------+----------+------------------+ | 1 | 78.5 | hash | wodetian | nidetiantadetian | | 65536 | 17983.9812 | zhx | shin | nosuke | | NULL | -669.996 | aa | NULL | NULL | | 2048 | NULL | NULL | c | jun | +-------+------------+------+----------+------------------+
查看底层存储文件:record_test_1.ibd
,用16进制编辑器打开,我这里使用的是Notepad++
和他的HEX-Editor
插件。可以找到如下的数据域(可能会有其中 mysql 生成的行数据不一样,但是我们创建的行数据内容应该是一样的,而且数据长度应该是一摸一样的,可以搜索其中的字符找到这些数据):
我们这里先直接给出这些数据代表的意义,让大家直观感受下:
变长字段长度列表:10 08 Null值列表:00 记录头信息:00 00 10 00 47 隐藏列DB_ROW_ID:00 00 00 00 08 0c 隐藏列DB_TRX_ID:00 00 00 03 c9 4d 隐藏列DB_ROLL_PTR:b9 00 00 01 2d 01 10 列数据id(1):80 00 00 00 00 00 00 01 列数据score(78.5):00 00 00 00 00 a0 53 40 列数据name(hash):68 61 73 68 列数据content(wodetian):77 6f 64 65 74 69 61 6e 列数据extra(nidetiantadetian):6e 69 64 65 74 69 61 6e 74 61 64 65 74 69 61 6e 变长字段长度列表:06 04 Null值列表:00 记录头信息:00 00 18 00 37 隐藏列DB_ROW_ID:00 00 00 00 08 0d 隐藏列DB_TRX_ID:00 00 00 03 c9 4e 隐藏列DB_ROLL_PTR:ba 00 00 01 2f 01 10 列数据id(65536):80 00 00 00 00 01 00 00 列数据score(17983.9812):b5 15 fb cb fe 8f d1 40 列数据name(zhx):7a 68 78 20 列数据content(shin):73 68 69 6e 列数据extra(nosuke):6e 6f 73 75 6b 65 Null值列表:19 记录头信息:00 00 00 00 27 隐藏列DB_ROW_ID:00 00 00 00 08 0e 隐藏列DB_TRX_ID:00 00 00 03 c9 51 隐藏列DB_ROLL_PTR:bc 00 00 01 33 01 10 列数据score(-669.996):87 16 d9 ce f7 ef 84 c0 列数据name(aa):61 61 20 20 变长字段长度列表:03 01 Null值列表:06 记录头信息:00 00 28 ff 4b 隐藏列DB_ROW_ID:00 00 00 00 08 0f 隐藏列DB_TRX_ID:00 00 00 03 c9 54 隐藏列DB_ROLL_PTR:be 00 00 01 3d 01 10 列数据id(2048):80 00 00 00 00 00 08 00 列数据content(c):63 列数据extra(jun):6a 75 6e
可以看出,在 Compact 行记录格式下,一条 InnoDB 记录,其结构如下图所示:
Compact 行格式存储 - 变长字段长度列表
对于像 varchar, varbinary,text,blob,json以及他们的各种类型的可变长度字段,需要将他们到底占用多少字节存储起来,这样就省去了列数据之间的边界定义,MySQL 就可以分清楚哪些数据属于这一列,那些不属于。Compact行格式存储,开头就是变长字段长度列表,这个列表包括数据不为NULL的每个可变长度字段的长度,并按照列的顺序逆序排列。
例如上面的第一条数据:
+-------+------------+------+----------+------------------+ | id | score | name | content | extra | +-------+------------+------+----------+------------------+ | 1 | 78.5 | hash | wodetian | nidetiantadetian | +-------+------------+------+----------+------------------+
有两个数据不为NULL的字段content
和extra
,长度分别是 8 和 16,转换为 16 进制分别是:0x08,0x10。倒序的顺序排列就是10 08
这是对于长度比较短的情况,用一字节表示长度即可。如果变长列的内容占用的字节数比较多,可能就需要用2个字节来表示。那么什么时候用一个字节,什么时候用两个字节呢?
我们给这张表加一列来测试下:
alter table `record_test_1` add column `large_content` varchar(1024) null after `extra`;
这时候行数据部分并没有变化。
- 如果 字符集的最大字节长度(我们这里字符集是latin,所以长度就是1)乘以 字段最大字符个数(就是varchar里面的参数,我们这里的
large_content
就是1024) < 255,那么就用一个字节表示。这里对于large_content
,已经超过了255. - 如果超过255,那么:
- 如果 字段真正占用字节数 < 128,就用一个字节
- 如果 字段真正占用字节数 >= 128,就用两个字节
问题一:那么为什么用 128 作为分界线呢? 一个字节可以最多表示255,但是 MySQL 设计长度表示时,为了区分是否是一个字节表示长度,规定,如果最高位为1,那么就是两个字节表示长度,否则就是一个字节。例如,01111111,这个就代表长度为 127,而如果长度是 128,就需要两个字节,就是 10000000 10000000,首个字节的最高位为1,那么这就是两个字节表示长度的开头,第二个字节可以用所有位表示长度,并且需要注意的是,MySQL采取 Little Endian 的计数方式,低位在前,高位在后,所以 129 就是 10000001 10000000。同时,这种标识方式,最大长度就是 2^15 - 1 = 32767,也就是32 KB。问题二:如果两个字节也不够表示的长度,该怎么办? innoDB 页大小默认为 16KB,对于一些占用字节数非常多的字段,比方说某个字段长度大于了16KB,那么如果该记录在单个页面中无法存储时,InnoDB会把一部分数据存放到所谓的溢出页中,在变长字段长度列表处只存储留在本页面中的长度,所以使用两个字节也可以存放下来。这个溢出页机制,我们后面和Text字段一起再说。
然后对第一行数据填充large_content
字段,对于第二行,将新字段更新为空字符串。
update `record_test_1` set `large_content` = 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz' where id = 1; update `record_test_1` set `large_content` = '' where id = 1;