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

前言

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

变长字段之后的第二个部分是NULL标志位,该位指示了该行数据中是否有NULL值,有则用1表示。该部分所占字节为1字节

之后便思考是否不管有多少个列都是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标志位:0x7e (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

#  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的字段

之后会专门再介绍下物理行中的变长字段是如何存储的

时间: 2024-10-13 13:51:33

InnoDB物理行中null值的存储的推断与验证的相关文章

Sql与oracle中null值的区别

原贴链接请点击: 1 null值的介绍 NULL 是数据库中特有的数据类型,当一条记录的某个列为 NULL ,则表示这个列的值是未知的.是不确定的.既然是未知的,就有无数种的可能性.因此, NULL 并不是一个确定的值. 这是 NULL 的由来.也是 NULL 的基础,所有和 NULL 相关的操作的结果都可以从 NULL 的概念推导出来. 2 oracle中的null值介绍 在不知道具体有什么数据的时候,即未知,可以用NULL, 称它为空,ORACLE中,含有空值的表列长度为零.允许任何一种数据

微软BI 之SSIS 系列 - 对于平面文件中 NULL 值处理过程中容易极易混淆的几个细节

最近有人问我 OLE DB Destination 中的 Keep Nulls 如何控制 NULL 值的显示,为什么选中了 Keep Nulls 但是数据库中没有 NULL 值? 为什么在 Flat File Source 中勾选上了 Retain null values from the source as null values in the data flow 但是为什么目标表上显示的是一个当前日期,而不是 NULL 值等等,单开此文来解释这些非常容易混淆的概念. 在比较纯粹的 ETL 项

SQL 中 Null 值使用时需要注意的地方

一.Null不支持大小/相等判断 1.下面的2个查询,不管表 users 中有多少条记录,返回的记录都是0行 select * from  users where deleted_at = null; select * from  users where deleted_at != null; 用常规的比较操作符(normal conditional operators)来将 null 与其他值比较是没有意义的. Null 也不等于 Null 2.将某个值与 null 进行比较的正确方法是使用

hive 配置文件以及join中null值的处理

一.Hive的参数设置 1.  三种设定方式:配置文件 ·   用户自定义配置文件:$HIVE_CONF_DIR/hive-site.xml ·   默认配置文件:$HIVE_CONF_DIR/hive-default.xml 用户自定义配置会覆盖默认配置.另外,Hive也会读入Hadoop的配置,因为Hive是作为Hadoop的客户端启动的,Hadoop的配置文件包括 ·   $HADOOP_CONF_DIR/hive-site.xml ·   $HADOOP_CONF_DIR/hive-de

细说Oracle中NULL值

1.NULL是什么? NULL表示UNKNOW(未知),其不代表不论什么值. 比如一行中某列没有不论什么值即为NULL. ORACLE同意不论什么一种数据类型的字段为空,除了下面两种情况: 1)主键字段(primary key), 2)定义时已经加了NOT NULL限制条件的字段 2.NULL有什么用? 1)NULL可用于条件推断: SELECT * FROM EMP WHERE COMM IS NULL; 或 SELECT * FROM EMP WHERE COMM IS NOT NULL;

Javascript中null值,特别注意的两点

null 是一个javascript字面量,表示空值,就是没有对象被呈现.他是javascript原始值之一.null值常被放在期望一个对象上,但是不引用任何对象的参数位置,也就是说对象的初始化. 我们看下面的一个例子 Javascript var obj = null; console.log(typeof obj === "object"); // true 我们通过对null的命名描述来理解,null是被期望在一个对象上,可能在javascript的规范中设这么设定的.所以返回一

SQL 中 null 值的处理方式

1.查询时,如果列中的值为 null,如何为列赋默认值. SELECT ISNULL([Column],'默认值') AS [Column] FROM [DataTable]

SQL——处理列中NULL值

处理NULL值 - 数据库中某列为NULL值,使用函数在列值为NULL时返回固定值.    SQLServer:ISNULL(col,value)        示例:SELECT ISNULL(col,value) FROM tableName    Oracle:NVL(col,value)        示例:SELECT NVL(col,value) FROM tableName    MySQL:IFNULL(col,value).COALESCE(col,value)       

MYSQL中NULL值的运算

今天更新MYSQL数据库一个的表的某个字段,涉及到子查询 UPDATE t1 SET points = ( points - ( SELECT sum(point) FROM t2 WHERE t2_id NOT IN (1, 2, 3, 4) ) ); 结果是求和的子查询得到的值是NULL,然后t1表的points都成了NULL了 在MYSQL里测试了一下,在MYSQL中,任何值和NULL的运算得到的值都是NULL SELECT 100+NULL; SELECT 100-NULL; SELEC