史上最详讲解,MySQL 的 json 数据类型的存储结构(源码+图)

1、前情交代

本篇文章以 MySQL 5.7 版本为例。

官方文档:https://dev.mysql.com/doc/refman/5.7/en/json.html

5.7.8 及以后的版本才支持,遵循 RFC7159 标准(https://datatracker.ietf.org/doc/html/rfc7159)。

json  内容只能是 json object 或者  json array。

支持的 json 元素的数据类型

类型对应的十六进制编码
SMALL_OBJECT0x0
LARGE_OBJECT0x1
SMALL_ARRAY0x2
LARGE_ARRAY0x3
LITERAL  0x4
INT16    0x5
UINT16  0x6
INT32    0x7
UINT32  0x8
INT64    0x9
UINT64  0xA
DOUBLE  0xB
STRING  0xC
OPAQUE  0xF
其中 object 和 array 各自细分为大小两种类型,为的是尽量使用更少的空间存储内容。小类型的很多结构采用 2 个字节存储,而大类型的才用 4 个字节存储,后面会讲到。

其中 LITERAL  类型比较特殊,对应以下 3 种值:

NULL\x00
TRUE\x01
FALSE\x02

关于 json 数据的序列化、反序列化,主要逻辑都在 mysql-5.7.44\sql\json_binary.cc 文件中,另外 3 个文件也可以了解一下。

mysql-5.7.44\sql\json_binary.h

mysql-5.7.44\sql\json_dom.h

mysql-5.7.44\sql\json_dom.cc

2、存储结构

json 的存储由这几部分构成:

类型占1个字节
元素个数

小类型占2个字节

大类型占4个字节

占用字节数

小类型占2个字节

大类型占4个字节

key_entrys

json object 专有

由 offset + size 组成

offset (表示每个 key 所在的脚标位置),

size (表示每个 key 本身占的字节数)

小类型占2个字节

大类型占4个字节

value_entrys

由type + inlined_value/offset 组成

type 占1个字节和首字节类似

J_NULL、J_BOOLEAN、J_INT、J_UINT 这几种数据类型属于 inlined_value,存储的是真实的值;其他的数据类型则存储每个 value 所在的脚标位置

小类型占2个字节

大类型占4个字节

keys

json object 专有

依次存储每个key的内容

values

依次存储每个value的内容

由 size + value 组成

size 表示每个value占用的字节数,size最多占1~4个字节

value 表示真实的value 内容

也就是说,如果存储的是 json object,就是上面的结构。

大小类型的判断依据是下面这段逻辑:

static bool is_too_big_for_json(size_t offset_or_size, bool large)

{

  if (offset_or_size > UINT_MAX16)

  {

    if (!large)

      return true;

    if (offset_or_size > UINT_MAX32)

    {

      my_error(ER_JSON_VALUE_TOO_BIG, MYF(0));

      return true;

    }

  }

概括来说就是,数组元素个数 / 对象key的个数 / 脚标位置 / 内容本身的size 小于 65535(2^16 - 1) 都是小类型, 65535 <    < 4294967295 (2^32 - 1) 的都是大类型,再大的就不支持了。

这就是存储组成中,小类型的用 2 个字节,大类型的用 4 个字节的原因。

如果是 json array,结构如下:

类型
元素个数
占用字节数
value_entrys
values

3、实例 + 图解

先建一张表

CREATE TABLE facts (sentence JSON);

3.1 存储一个 json array

插入一条 json arrsy 数据

INSERT INTO facts VALUES('[42, "xy", true]');

接下来就跟随代码逻辑,一步一步看看插入的 json 内容是如何序列化到磁盘上的。

从 serialize 这个方法作为切入口(下文如无特殊说明,都是在 json_binary.cc 文件中)

bool serialize(const Json_dom *dom, String *dest)
{
  // Reset the destination buffer.
  dest->length(0);
  dest->set_charset(&my_charset_bin);


  // Reserve space (one byte) for the type identifier.
  if (dest->append('\0'))
    return true;                              /* purecov: inspected */
  return serialize_json_value(dom, 0, dest, 0, false) != OK;
}

这个方法主要是把待序列化的 *dom 序列化后,存储在 String 类型的 *dest 的中。

没啥可说的,主要逻辑在z下面这个方法 serialize_json_value 中

static enum_serialization_result
serialize_json_value(const Json_dom *dom, size_t type_pos, String *dest,
                     size_t depth, bool small_parent)
{
  const size_t start_pos= dest->length();
  assert(type_pos < start_pos);


  enum_serialization_result result;


  switch (dom->json_type())
  {
  case Json_dom::J_ARRAY:
    {
      const Json_array *array= down_cast<const Json_array*>(dom);
      (*dest)[type_pos]= JSONB_TYPE_SMALL_ARRAY;
      result= serialize_json_array(array, dest, false, depth);


      if (result == VALUE_TOO_BIG)
      {
        // If the parent uses the small storage format, it needs to grow too.
        if (small_parent)
          return VALUE_TOO_BIG;
        dest->length(start_pos);
        (*dest)[type_pos]= JSONB_TYPE_LARGE_ARRAY;
        result= serialize_json_array(array, dest, true, depth);
      }
      break;
    }

这个方法会根据不同的 json 类型,执行相应的序列化逻辑。

我们本次插入的是数组,会执行 case Json_dom::J_ARRAY: 里的逻辑。

首次进来会在脚标 0 的位置设置上类型 SMALL_ARRAY,即十六进制的 02。

ce1c5a131a753f771dd36783fb897da0.png

下面会继续进入这个方法 serialize_json_array 。

static enum_serialization_result
serialize_json_array(const Json_array *array, String *dest, bool large,
                     size_t depth)
{
  const size_t start_pos= dest->length();
  const size_t size= array->size();


  if (is_too_big_for_json(size, large))
    return VALUE_TOO_BIG;
  // First write the number of elements in the array.
  if (append_offset_or_size(dest, size, large))
    return FAILURE;                             /* purecov: inspected */


  // Reserve space for the size of the array in bytes. To be filled in later.
  const size_t size_pos= dest->length();
  // 这里只是先用2/4个字节占位, size=0,方法的最后会更新这几个字节
  if (append_offset_or_size(dest, 0, large))
    return FAILURE;                             /* purecov: inspected */


  size_t entry_pos= dest->length();


  // Reserve space for the value entries at the beginning of the array.
  const size_t entry_size=
    large ? VALUE_ENTRY_SIZE_LARGE : VALUE_ENTRY_SIZE_SMALL;
  // 预留 entry 的空间,元素个数 * entry_size, 先填充0
  if (dest->fill(dest->length() + size * entry_size, 0))
    return FAILURE;                             /* purecov: inspected */


// 遍历 json 数组
  for (uint32 i= 0; i < size; i++)
  {
    //拿到当前数组元素
    const Json_dom *elt= (*array)[i];
  // 继续向目标字符串 dest 追加(上面已经追加到了entry),
  // start_pos 从 1 开始,第0个字节被json对象类型占用
  // entry_pos 从 5 开始, 第1、2个字节存储元素个数,第3、4个字节存储整个json数组占用的字节数
    enum_serialization_result res= append_value(dest, elt, start_pos,
                                                entry_pos, large, depth + 1);
    if (res != OK)
      return res;
  // 下次循环加 3 
    entry_pos+= entry_size;
  }


  // Finally, write the size of the object in bytes.
  size_t bytes= dest->length() - start_pos;
  if (is_too_big_for_json(bytes, large))
    return VALUE_TOO_BIG;                     /* purecov: inspected */
  insert_offset_or_size(dest, size_pos, bytes, large);


  return OK;
}

上面这段代码,会先把数组元素的个数(3个)追加到 *dest 上,就变成下图的结构。

53de484cabec17d884944d210775e1de.png

图中灰色虚线框,为大类型占用的字节数,小类型的忽略,后面所有图中都是此效果,不再赘述。

接下来追加的是整个 json 数据占用的字节数、value_entrys,因为这 2 部分占用的字节数可以提前算出来,计算逻辑见代码注释,这里只是追加对应的字节数占位,用 0 填充,后面会逐步更新为正确的内容。就变成下图的结构。

a7e0399ec0a420d64407ab277f3e8031.png

接下来就会遍历数组里的每个元素,逐个追加。

来到方法  append_value 。

static enum_serialization_result
append_value(String *dest, const Json_dom *value, size_t start_pos,
             size_t entry_pos, bool large, size_t depth)
{
  if (depth >= JSON_DOCUMENT_MAX_DEPTH)
  {
    my_error(ER_JSON_DOCUMENT_TOO_DEEP, MYF(0));
    return FAILURE;
  }
  // start_pos 从 1 开始,第0个字节被json对象类型占用
  // entry_pos 从 5 开始, 第1、2个字节存储元素个数,第3、4个字节存储整个json数组占用的字节数
  uint8 element_type;
  int32 inlined_value;
  if (should_inline_value(value, large, &inlined_value, &element_type))
  { // 追加 42、true 这两个个数组元素的时候会走到这个分支
    (*dest)[entry_pos]= element_type;
    insert_offset_or_size(dest, entry_pos + 1, inlined_value, large);
    return OK;
  }


  // 追加字符串 xy , offset = 14 - 1 = 13
  size_t offset= dest->length() - start_pos;
  if (is_too_big_for_json(offset, large))
    return VALUE_TOO_BIG;
  // dest, 9, 13, false
  // 把 offset 插入到 entry 中
  insert_offset_or_size(dest, entry_pos + 1, offset, large);
  // value, 8, dest, 1, false
  return serialize_json_value(value, entry_pos, dest, depth, !large);
}

第1个元素是数字 42,它属于 inlined_value,类型(05)和内容(2A) 直接存储在 value_entry 中,位置是由 start_pos、entry_pos 决定。

就变成下图的结构。

ccdb1875729b07e9b2d3a8a5f129fa06.png

第2个元素是字符串 xy,不属于 inlined_value,所以 value_entry 中会存放真实 value 的脚标位置 -1(读取的时候会读第 脚标+1 的字节)。

追加类型(0C),补充 offset 后就变成下图的结构。

9b0addffd18fc3feb30bf698207df674.png

追加完类型后,继续追加 value,value 会追加在当前字符串的最后,也就是 脚标 13 + 1 =14 的位置。xy 对应的十六进制是 78、79。如下图所示。

2f6d9ed866de6553adc0cdeb1623c03a.png

这里说一下字符的编码转换。

如果字符属于 ASCII 编码范围,就会用一个字节来存储;

如果是其他字符,采用3个字节来存储;

如果是 emoji 表情等字符,采用4个字节来存储。

之所以前面逻辑中占用字节数、offset 用0占位填充,就是当时还不能确定真实 value 占用的字节数,必须等每个 value 遍历后就知道了 offset,所有 value 遍历完后就知道了占用字节数,只需要一次循环就可以。

第3个元素是布尔值 true,属于 inlined_value,类型(04)和内容(01) 直接存储在 value_entry 中,就变成下图的结构。

e4cbc3d0e4fd0f3280d4be24d4be54db.png

到此为止,我们的 json  array 就算序列化完成了。

我们看一下 facts.ibd 文件中的内容,和我们通过代码推导的是否一致。

202d1e0098b34af901d92376b42152e9.png

可以看到,和我们代码推导的完全一致。

我们趁热再存一条中文的数据,["中华人民共和国"],过程不再赘述,直接看结果。

23ba1673dfd85f4a528625a28a41eb26.png

bbc96299e1434c46025b5405d5fe468e.png

3.2 存储一个 json object

object 的序列化,整体流程和 array 类似,多了处理 key_entrys、keys 的逻辑。

插入一条 json arrsy 数据

INSERT INTO facts VALUES('{"x":33, "n":"name","ab":true, "a":"测试"} ');

09643e3e1a9470d5a2e4926accb51353.png

注意我们插入时和查询出的 key 的顺序是不一样的,查询出的 key 显然是经过排序的,到底是插入时排的序,还是查询时排的序,这个后面代码中会看到,这里先有个印象。

同样还是跟着代码逻辑,看一下 object 的序列化过程。

还是 serialize_json_value 方法,这次是 case Json_dom::J_OBJECT: 分支。

case Json_dom::J_OBJECT:
    {
      const Json_object *object= down_cast<const Json_object*>(dom);
      (*dest)[type_pos]= JSONB_TYPE_SMALL_OBJECT;
      result= serialize_json_object(object, dest, false, depth);
      if (result == VALUE_TOO_BIG)
      {
        // If the parent uses the small storage format, it needs to grow too.
        if (small_parent)
          return VALUE_TOO_BIG;
        dest->length(start_pos);
        (*dest)[type_pos]= JSONB_TYPE_LARGE_OBJECT;
        result= serialize_json_object(object, dest, true, depth);
      }
      break;
    }

还是先设置类型 SMALL_OBJECT 00

a0f462b05605b433597a7a5ffe1c1579.png

接下来就会进入 serialize_json_object 方法。这个方法很长,分开来讲。

static enum_serialization_result
serialize_json_object(const Json_object *object, String *dest, bool large,
                      size_t depth)
{
  const size_t start_pos= dest->length();
  const size_t size= object->cardinality();


  if (is_too_big_for_json(size, large))
    return VALUE_TOO_BIG;


  // First write the number of members in the object.
  // 对象内的元素个数
  if (append_offset_or_size(dest, size, large))
    return FAILURE;


  // Reserve space for the size of the object in bytes. To be filled in later.
  // 占位填充 占用的总字节数
  const size_t size_pos= dest->length();
  if (append_offset_or_size(dest, 0, large))
    return FAILURE;

上面这段代码会追加 object 内的元素个数,以及占用总字节数的占位填充,和 array 的类似,直接看结果。

d0c52c6f69fd2daf0c6d995acc41dabb.png

接下来就是非常关键的  key_entrys 的序列化

// 计算 key_entry、value_entry所需要的字节数
  const size_t key_entry_size=
    large ? KEY_ENTRY_SIZE_LARGE : KEY_ENTRY_SIZE_SMALL;
  const size_t value_entry_size=
    large ? VALUE_ENTRY_SIZE_LARGE : VALUE_ENTRY_SIZE_SMALL;
  /*
    Calculate the offset of the first key relative to the start of the
    object. The first key comes right after the value entries.
   {"x":33, "n":"name","ab":true, "a":"测试"}
  key_entry_size=4, value_entry_size=3,dest.length=5, 5+4(4个元素)*(4+3)-1=32=0x20
  */
  size_t offset= dest->length() +
    size * (key_entry_size + value_entry_size) - start_pos;
  
#ifndef NDEBUG
  const std::string *prev_key= NULL;
#endif


  // Add the key entries.
  for (Json_object::const_iterator it= object->begin();
       it != object->end(); ++it)
  {
    const std::string *key= &it->first;
    // 拿到 key 的长度
    size_t len= key->length();


#ifndef NDEBUG
    // Check that the DOM returns the keys in the correct order.
    // 对 key 进行字典排序,先按长度排序;长度相同,再按内容排序。
    if (prev_key)
    {
      assert(prev_key->length() <= len);
      if (len == prev_key->length())
        assert(memcmp(prev_key->data(), key->data(), len) < 0);
    }
    prev_key= key;
#endif


    // We only have two bytes for the key size. Check if the key is too big.
    // 只有2字节来存储 key 的长度,所以 key 的长度 < 2^16-1
    if (len > UINT_MAX16)
    {
      my_error(ER_JSON_KEY_TOO_BIG, MYF(0));
      return FAILURE;
    }


    if (is_too_big_for_json(offset, large))
      return VALUE_TOO_BIG;
    // 追加 key 的 offset, 追加 key 的长度
    if (append_offset_or_size(dest, offset, large) ||
        append_int16(dest, static_cast<int16>(len)))
      return FAILURE;
    offset+= len;
  }

上面这段代码,首先根据大、小类型,计算出每个 key_entry 和 value_entry 所需要的字节数。

计算规则在 mysql-5.7.44\sql\json_binary.cc 文件中的头部有如下的定义。官方注释已经写的很清楚了。

/*
  The size of key entries for objects when using the small storage
  format or the large storage format. In the small format it is 4
  bytes (2 bytes for key length and 2 bytes for key offset). In the
  large format it is 6 (2 bytes for length, 4 bytes for offset).
*/
#define KEY_ENTRY_SIZE_SMALL      (2 + SMALL_OFFSET_SIZE)
#define KEY_ENTRY_SIZE_LARGE      (2 + LARGE_OFFSET_SIZE)


/*
  The size of value entries for objects or arrays. When using the
  small storage format, the entry size is 3 (1 byte for type, 2 bytes
  for offset). When using the large storage format, it is 5 (1 byte
  for type, 4 bytes for offset).
*/
#define VALUE_ENTRY_SIZE_SMALL    (1 + SMALL_OFFSET_SIZE)
#define VALUE_ENTRY_SIZE_LARGE    (1 + LARGE_OFFSET_SIZE)

继续看 key_entrys 序列化的代码,紧接着就会遍历对象的元素,遍历的过程会对 key 按字典排序,然后把计算出的 key 的 offset 和 key 的长度追加,依次循环,直至所有 key 都处理完成。key 追加完成后的效果如下图所示。

f51aff06a3c2adf7b0c3f217e206b1b6.png

排序的原因是为了更快的检索,根据 key 检索时就可以采用二分法检索。

下面会继续填充占位 value_entrys

const size_t start_of_value_entries= dest->length();


  // Reserve space for the value entries. Will be filled in later.
  // 21+4*3=33
  dest->fill(dest->length() + size * value_entry_size, 0);

填充后的效果如下图所示(由于图片太长,分2张图展示)

8003aabced39076eb25bca49143e721d.png

8c1e4cf498015cc0803df828d9760c7f.png

接下来就是追加 key 的真实内容。

// Add the actual keys.
  for (Json_object::const_iterator it= object->begin(); it != object->end();
       ++it)
  {
    if (dest->append(it->first.c_str(), it->first.length()))
      return FAILURE;
  }

追加后的效果

2bc113f586dd9c11d45451d1748e0ad4.png

继续追加 value 的真实内容,并更新对应的 类型、inlined_value / offset

// Add the values, and update the value entries accordingly.
  size_t entry_pos= start_of_value_entries;
  for (Json_object::const_iterator it= object->begin(); it != object->end();
       ++it)
  {
    enum_serialization_result res= append_value(dest, it->second,
                                                start_pos, entry_pos, large,
                                                depth + 1);
    if (res != OK)
      return res;
    entry_pos+= value_entry_size;
  }

追加后的效果

1373c31c727d361d6f5293fc136c1001.png

最后再更新整个 json object 所占用的字节数

// Finally, write the size of the object in bytes.
  size_t bytes= dest->length() - start_pos;
  if (is_too_big_for_json(bytes, large))
    return VALUE_TOO_BIG;
  insert_offset_or_size(dest, size_pos, bytes, large);


  return OK;
}

来一张全图看看效果。

b87e3a5cca10350762836a9c911411de.png

e809e2b4cff06945e65c315b2d03e5ef.png

4、疑问

4.1 value 的 size 为什么采用变长的形式存储?读的时候怎么知道要读几个字节?

问题涉及到的是这段代码

case Json_dom::J_STRING:
    {
      const Json_string *jstr= down_cast<const Json_string*>(dom);
      size_t size= jstr->size();
      if (append_variable_length(dest, size) ||
          dest->append(jstr->value().c_str(), size))
        return FAILURE;
      (*dest)[type_pos]= JSONB_TYPE_STRING;
      result= OK;
      break;
    }
static bool append_variable_length(String *dest, size_t length)
{
  do
  {
    // Filter out the seven least significant bits of length.
    uchar ch= (length & 0x7F);


    /*
      Right-shift length to drop the seven least significant bits. If there
      is more data in length, set the high bit of the byte we're writing
      to the String.
    */
    length>>= 7;
    if (length != 0)
      ch|= 0x80;


    if (dest->append(ch))
      return true;                            /* purecov: inspected */
  }
  while (length != 0);


  // Successfully appended the length.
  return false;
}

个人觉得可以根据元素的个数,调整一下存储结构。

如果元素个数少,可以固定 4 个字节存储 size,有浪费也不会太多,省去了存、取 时的移位比较操作。

如果元素多,还采用这种变长存储结构,可以节省不少空间。

4.2 json object 存储要 3 次遍历,是否能减少遍历次数?

这段代码在前面已经讲的很详细了。

个人觉得也可以分场景,第 2 次遍历需要的内容可以在第 1 次遍历时缓存,以空间换时间。

扯两句

好的存储结构,就要抠每一个字节。

一旦数据量大了,就得研究底层的存储结构。合理设计业务内容的长度可能成倍的节省存储空间,因为除了内容本身还要有很多空间来支撑这个数据结构。

原创不易,多多关注,一键三连,感谢支持!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值