InnoDB物理行中null值的存储的推断与验证

InnoDB物理行中null值的存储的推断与验证

前言

想写这边文章,是因为之前想写一个解析innodb ibd文件的工具,在写这个工具的过程中,发现逻辑记录转物理记录的转换中,最难的有两部分,一是每行每字段null值占用的字节和存储,二是变长字段占用的字节和存储的格式。本文中重点针对第一种情况。第二种情况之后会专门写一篇
之前看有关介绍compact行记录格式:

变长字段之后的第二个部分是NULL标志位,该位指示了该行数据中是否有NULL值,有则用1表示。该部分所占字节为1字节
-----《InnoDB存储引擎》103页,作者:姜承尧

之后便思考是否不管有多少个列都是NULL,该部分都只占1个字节呢?
便有了如下测试

本文约定

逻辑记录:record (元组)
物理记录:row(行)
只讨论compact行格式

所用工具

自己python写的工具innodb_extract

测试数据

表结构

localhost.test>desc null_test;
+------------------+--------------+------+-----+---------+----------------+
| Field            | Type         | Null | Key | Default | Extra          |
+------------------+--------------+------+-----+---------+----------------+
| id               | bigint(20)   | NO   | PRI | NULL    | auto_increment | 
| name             | varchar(20)  | YES  |     | NULL    |                | 
| legalname        | varchar(25)  | YES  |     | NULL    |                | 
| industry         | varchar(10)  | YES  |     | NULL    |                | 
| province         | varchar(10)  | YES  |     | NULL    |                | 
| city             | varchar(15)  | YES  |     | NULL    |                | 
| size             | varchar(15)  | YES  |     | NULL    |                | 
| admin_department | varchar(128) | YES  |     | NULL    |                | 
+------------------+--------------+------+-----+---------+----------------+
8 rows in set (0.00 sec)


表内数据

+----+------+-----------+----------+----------+------+------+------------------+
| id | name | legalname | industry | province | city | size | admin_department |
+----+------+-----------+----------+----------+------+------+------------------+
|  1 | NULL | NULL      | NULL     | NULL     | NULL | NULL | NULL             | 
|  2 | TOM  | NULL      | NULL     | NULL     | NULL | NULL | NULL             | 
|  3 | ALEX | NULL      | NULL     | NULL     | NULL | NULL | HR               | 
+----+------+-----------+----------+----------+------+------+------------------+
3 rows in set (0.00 sec)

分析数据

通过工具看三行数据

#  python innodb_extract.py null_test.ibd
infimum
7f 000010001c 8000000000000001 0000f1e27b17 b5000001680084
1          
7e 0000180020 8000000000000002 0000f1e27b17 b5000001680094 544f4d

2   TOM       
3e 000020ffb6 8000000000000003 0000f1e27b17 b50000016800a4 414c4558 4852

3   ALEX      HR 

第一行:
null标志位:0x7f (01111111)
说明:从右向左方向写,一共7个null值
record header:000010001c
Transaction Id:0000f1e27b17
Roll Pointer:b5000001680084
数据:

第二行:
null标志位:0x7e (01111110)
说明:除第二列,其余均是null值
record header:0000180020
Transaction Id:0000f1e27b17
Roll Pointer:b5000001680084
数据:
第二列:544f4d => TOM

第三行:
null标志位:0x3e (00111110)
说明:除了第2列和第8列,其余均是null值
record header:000020ffb6
Transaction Id:0000f1e27b17
Roll Pointer:b5000001680084
数据:
第二列:414c4558 => ALEX
第八列:4852 => HR

假设

继续上面,如果包含Null值的字段是8个,或者9个会是怎样?

深度剖析

代码片段,该函数将物理记录转化为逻辑记录,版本5.5.31,源文件rem0rec.c,

rec_convert_dtuple_to_rec_comp(
/*===========================*/
    rec_t*          rec,    /*!< in: origin of record */
    const dict_index_t* index,  /*!< in: record descriptor */
    const dfield_t*     fields, /*!< in: array of data fields */
    ulint           n_fields,/*!< in: number of data fields */
    ulint           status, /*!< in: status bits of the record */
    ibool           temp)   /*!< in: whether to use the
                    format for temporary files in
                    index creation */
{
    const dfield_t* field;
    const dtype_t*  type;
    byte*       end;
    byte*       nulls;
    byte*       lens;
    ulint       len;
    ulint       i;
    ulint       n_node_ptr_field;
    ulint       fixed_len;
    ulint       null_mask   = 1;
    ut_ad(temp || dict_table_is_comp(index->table));
    ut_ad(n_fields > 0);

    if (temp) {
        ut_ad(status == REC_STATUS_ORDINARY);
        ut_ad(n_fields <= dict_index_get_n_fields(index));
        n_node_ptr_field = ULINT_UNDEFINED;
        nulls = rec - 1;
        if (dict_table_is_comp(index->table)) {
            /* No need to do adjust fixed_len=0. We only
            need to adjust it for ROW_FORMAT=REDUNDANT. */
            temp = FALSE;
        }
    } else {
        nulls = rec - (REC_N_NEW_EXTRA_BYTES + 1);

        switch (UNIV_EXPECT(status, REC_STATUS_ORDINARY)) {
        case REC_STATUS_ORDINARY:
            ut_ad(n_fields <= dict_index_get_n_fields(index));
            n_node_ptr_field = ULINT_UNDEFINED;
            break;
        case REC_STATUS_NODE_PTR:
            ut_ad(n_fields
                  == dict_index_get_n_unique_in_tree(index) + 1);
            n_node_ptr_field = n_fields - 1;
            break;
        case REC_STATUS_INFIMUM:
        case REC_STATUS_SUPREMUM:
            ut_ad(n_fields == 1);
            n_node_ptr_field = ULINT_UNDEFINED;
            break;
        default:
            ut_error;
            return;
        }
    }

    end = rec;
    lens = nulls - UT_BITS_IN_BYTES(index->n_nullable);
    /* clear the SQL-null flags */
    memset(lens + 1, 0, nulls - lens);
    
    

结合COMPACT row格式来看:

row记录格式如下:

|---------------------extra_size-----------------------------------------|---------fields_data------------|
|--columns_lens---|---null lens----|------fixed_extrasize(5)-------------|--col1---|---col2---|---col2----|
|end<--------begin|end<-------beign|-------------------------------------|orgin---------------------------|

  • 先看nulls = rec - (REC_N_NEW_EXTRA_BYTES + 1)
    rec为记录开始的offset,也就是,extrasize也就是固定长度的record header的长度。注意null标志位和变长字段长度列表是从右->左的方向写的(原因可参见下部分代码)。所以nulls指向的是null lens后一字节开始的位置。
  • 再看lens = nulls - UT_BITS_IN_BYTES(index->n_nullable)
    index->n_nullable指的是表结构中定义can be null的字段的个数,一个字段用一个bit来标记,UT_BITS_IN_BYTES将占用bit数转为占用的字节数。所以lens指向的是column_lens后面一个字节的位置,即跳过了Null标志的占用的空间,同样在写入值的时候也是从后面向前面写。
  • memset(lens + 1, 0, nulls - lens) 将nulls空间清零。

之后就是遍历每一个字段,先对定义了can be null字段进行处理

/* Store the data and the offsets */

    for (i = 0, field = fields; i < n_fields; i++, field++) {
        const dict_field_t* ifield;

        type = dfield_get_type(field);
        len = dfield_get_len(field);

        if (UNIV_UNLIKELY(i == n_node_ptr_field)) {
            ut_ad(dtype_get_prtype(type) & DATA_NOT_NULL);
            ut_ad(len == REC_NODE_PTR_SIZE);
            memcpy(end, dfield_get_data(field), len);
            end += REC_NODE_PTR_SIZE;
            break;
        }

        if (!(dtype_get_prtype(type) & DATA_NOT_NULL)) {
            /* nullable field */
            ut_ad(index->n_nullable > 0);

            if (UNIV_UNLIKELY(!(byte) null_mask)) {
                nulls--;
                null_mask = 1;
            }
            
            

因为方向是从右向左写,也就是从后往前写,如果该字段为null,则将null标志位设为1并向前移1位,如果满了8个,也就是有8个字段都为null则offset向左移1位,并将null_mask置为1

==从这段代码看出之前的猜想,也就是并不是Null标志位只固定占用1个字节==,而是以8为单位,满8个null字段就多1个字节,不满8个也占用1个字节,高位用0补齐

            ut_ad(*nulls < null_mask);

            /* set the null flag if necessary */
            if (dfield_is_null(field)) {
                *nulls |= null_mask;
                null_mask <<= 1;
                continue;
            }

            null_mask <<= 1;
        }
        

这段代码是就是设置null字段与null标志位的映射关系,如果字段为null,则设置标志位为1。

栗子验证

翻过来再看之前的例子,我们逐步的添加字段并设置default null看下null标志位的变化

  • step 1,添加两个并设置default null
localhost.test>alter table null_test add column `kind` varchar(15) DEFAULT NULL after `size`;
Query OK, 3 rows affected (0.09 sec)
Records: 3  Duplicates: 0  Warnings: 0

localhost.test>alter table null_test add column licenseno varchar(15) DEFAULT NULL after `kind`;
Query OK, 3 rows affected (0.11 sec)
Records: 3  Duplicates: 0  Warnings: 0.11

那么理论来讲,第一行数据有9个null列了。满8个null列之后,继续向左写移,写1个bit之后开始占据两个字节。我们通过工具解析之后看下

#  python innodb_extract.py null_test.ibd
01ff 000010001d 8000000000000001 0000f1e27c81 980000028c0084
1            
01fe 0000180021 8000000000000002 0000f1e27c81 980000028c0094 544f4d
2   TOM         
00fe 000020ffb3 8000000000000003 0000f1e27c81 980000028c00a4 414c455848
3   ALEX        HR 

第一行null标志位变为0x01ff,即00000001 11111111一共有9个null字段,满了8位之后,继续向前占1个字节从右往左继续写
同理,第二行0x01fe,即00000001 11111110
第三行0x00fe,00000000 11111110

再继续添加8个字段并设置default null

localhost.test>desc null_test;
+------------------+--------------+------+-----+---------+----------------+
| Field            | Type         | Null | Key | Default | Extra          |
+------------------+--------------+------+-----+---------+----------------+
| id               | bigint(20)   | NO   | PRI | NULL    | auto_increment | 
| name             | varchar(20)  | YES  |     | NULL    |                | 
| legalname        | varchar(25)  | YES  |     | NULL    |                | 
| industry         | varchar(10)  | YES  |     | NULL    |                | 
| province         | varchar(10)  | YES  |     | NULL    |                | 
| city             | varchar(15)  | YES  |     | NULL    |                | 
| size             | varchar(15)  | YES  |     | NULL    |                | 
| kind             | varchar(15)  | YES  |     | NULL    |                | 
| licenseno        | varchar(15)  | YES  |     | NULL    |                | 
| admin_department | varchar(128) | YES  |     | NULL    |                | 
| null_col1        | varchar(15)  | YES  |     | NULL    |                | 
| null_col2        | varchar(15)  | YES  |     | NULL    |                | 
| null_col3        | varchar(15)  | YES  |     | NULL    |                | 
| null_col4        | varchar(15)  | YES  |     | NULL    |                | 
| null_col5        | varchar(15)  | YES  |     | NULL    |                | 
| null_col6        | varchar(15)  | YES  |     | NULL    |                | 
| null_col7        | varchar(15)  | YES  |     | NULL    |                | 
| null_col8        | varchar(15)  | YES  |     | NULL    |                | 
+------------------+--------------+------+-----+---------+----------------+
18 rows in set (0.00 sec)


最多Null字段的第一行目前有个17个null字段,对应17个Null bit

root@hebe211 ibd]#  python innodb_extract.py null_test.ibd

01ffff 000010001e 8000000000000001 0000f1e27cce c60000017600840301fffe0000
1                    
01fffe 0000180022 8000000000000002 0000f1e27cce c6000001760094 544f4d
2   TOM                 
01fefe 000020ffb0 8000000000000003 0000f1e27cce c60000017600a4 414c45 5848
3   ALEX        HR         

第一行null标志位变为0x01ff,即00000001 11111111 11111111 一共有17个null字段,满了两个8位之后,继续向前占1个字节从右往左继续写
同理,第二行0x01fe,即00000001 11111111 11111110
第三行0x00fe,00000001 11111110 11111110

结论

允许null的字段需要额外的空间来保存字段Null到null标志位映射的对应关系,所以保存这个映射关系的null标志位长度并不是固定的。也就是null字段越多并不是越省空间。实际生产环境中应尽量减少can be null的字段。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容