当前位置:   article > 正文

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

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 文件中)

  1. bool serialize(const Json_dom *dom, String *dest)
  2. {
  3. // Reset the destination buffer.
  4. dest->length(0);
  5. dest->set_charset(&my_charset_bin);
  6. // Reserve space (one byte) for the type identifier.
  7. if (dest->append('\0'))
  8. return true; /* purecov: inspected */
  9. return serialize_json_value(dom, 0, dest, 0, false) != OK;
  10. }

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

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

  1. static enum_serialization_result
  2. serialize_json_value(const Json_dom *dom, size_t type_pos, String *dest,
  3. size_t depth, bool small_parent)
  4. {
  5. const size_t start_pos= dest->length();
  6. assert(type_pos < start_pos);
  7. enum_serialization_result result;
  8. switch (dom->json_type())
  9. {
  10. case Json_dom::J_ARRAY:
  11. {
  12. const Json_array *array= down_cast<const Json_array*>(dom);
  13. (*dest)[type_pos]= JSONB_TYPE_SMALL_ARRAY;
  14. result= serialize_json_array(array, dest, false, depth);
  15. if (result == VALUE_TOO_BIG)
  16. {
  17. // If the parent uses the small storage format, it needs to grow too.
  18. if (small_parent)
  19. return VALUE_TOO_BIG;
  20. dest->length(start_pos);
  21. (*dest)[type_pos]= JSONB_TYPE_LARGE_ARRAY;
  22. result= serialize_json_array(array, dest, true, depth);
  23. }
  24. break;
  25. }

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

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

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

ce1c5a131a753f771dd36783fb897da0.png

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

  1. static enum_serialization_result
  2. serialize_json_array(const Json_array *array, String *dest, bool large,
  3. size_t depth)
  4. {
  5. const size_t start_pos= dest->length();
  6. const size_t size= array->size();
  7. if (is_too_big_for_json(size, large))
  8.     return VALUE_TOO_BIG;
  9. // First write the number of elements in the array.
  10. if (append_offset_or_size(dest, size, large))
  11. return FAILURE; /* purecov: inspected */
  12. // Reserve space for the size of the array in bytes. To be filled in later.
  13. const size_t size_pos= dest->length();
  14. // 这里只是先用2/4个字节占位, size=0,方法的最后会更新这几个字节
  15. if (append_offset_or_size(dest, 0, large))
  16. return FAILURE; /* purecov: inspected */
  17. size_t entry_pos= dest->length();
  18. // Reserve space for the value entries at the beginning of the array.
  19. const size_t entry_size=
  20. large ? VALUE_ENTRY_SIZE_LARGE : VALUE_ENTRY_SIZE_SMALL;
  21. // 预留 entry 的空间,元素个数 * entry_size, 先填充0
  22. if (dest->fill(dest->length() + size * entry_size, 0))
  23. return FAILURE; /* purecov: inspected */
  24. // 遍历 json 数组
  25. for (uint32 i= 0; i < size; i++)
  26. {
  27. //拿到当前数组元素
  28. const Json_dom *elt= (*array)[i];
  29. // 继续向目标字符串 dest 追加(上面已经追加到了entry),
  30. // start_pos 从 1 开始,第0个字节被json对象类型占用
  31. // entry_pos 从 5 开始, 第1、2个字节存储元素个数,第3、4个字节存储整个json数组占用的字节数
  32. enum_serialization_result res= append_value(dest, elt, start_pos,
  33. entry_pos, large, depth + 1);
  34. if (res != OK)
  35. return res;
  36. // 下次循环加 3
  37. entry_pos+= entry_size;
  38. }
  39. // Finally, write the size of the object in bytes.
  40. size_t bytes= dest->length() - start_pos;
  41. if (is_too_big_for_json(bytes, large))
  42. return VALUE_TOO_BIG; /* purecov: inspected */
  43. insert_offset_or_size(dest, size_pos, bytes, large);
  44. return OK;
  45. }

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

53de484cabec17d884944d210775e1de.png

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

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

a7e0399ec0a420d64407ab277f3e8031.png

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

来到方法  append_value 。

  1. static enum_serialization_result
  2. append_value(String *dest, const Json_dom *value, size_t start_pos,
  3. size_t entry_pos, bool large, size_t depth)
  4. {
  5. if (depth >= JSON_DOCUMENT_MAX_DEPTH)
  6. {
  7. my_error(ER_JSON_DOCUMENT_TOO_DEEP, MYF(0));
  8. return FAILURE;
  9. }
  10. // start_pos 从 1 开始,第0个字节被json对象类型占用
  11.   // entry_pos 从 5 开始, 第1、2个字节存储元素个数,第3、4个字节存储整个json数组占用的字节数
  12. uint8 element_type;
  13. int32 inlined_value;
  14. if (should_inline_value(value, large, &inlined_value, &element_type))
  15. { // 追加 42、true 这两个个数组元素的时候会走到这个分支
  16. (*dest)[entry_pos]= element_type;
  17. insert_offset_or_size(dest, entry_pos + 1, inlined_value, large);
  18. return OK;
  19. }
  20. // 追加字符串 xy , offset = 14 - 1 = 13
  21. size_t offset= dest->length() - start_pos;
  22. if (is_too_big_for_json(offset, large))
  23. return VALUE_TOO_BIG;
  24. // dest, 9, 13, false
  25. // 把 offset 插入到 entry 中
  26. insert_offset_or_size(dest, entry_pos + 1, offset, large);
  27. // value, 8, dest, 1, false
  28. return serialize_json_value(value, entry_pos, dest, depth, !large);
  29. }

第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: 分支。

  1. case Json_dom::J_OBJECT:
  2. {
  3. const Json_object *object= down_cast<const Json_object*>(dom);
  4. (*dest)[type_pos]= JSONB_TYPE_SMALL_OBJECT;
  5.       result= serialize_json_object(object, dest, false, depth);
  6. if (result == VALUE_TOO_BIG)
  7. {
  8. // If the parent uses the small storage format, it needs to grow too.
  9. if (small_parent)
  10. return VALUE_TOO_BIG;
  11. dest->length(start_pos);
  12. (*dest)[type_pos]= JSONB_TYPE_LARGE_OBJECT;
  13. result= serialize_json_object(object, dest, true, depth);
  14. }
  15. break;
  16. }

还是先设置类型 SMALL_OBJECT 00

a0f462b05605b433597a7a5ffe1c1579.png

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

  1. static enum_serialization_result
  2. serialize_json_object(const Json_object *object, String *dest, bool large,
  3. size_t depth)
  4. {
  5. const size_t start_pos= dest->length();
  6. const size_t size= object->cardinality();
  7. if (is_too_big_for_json(size, large))
  8. return VALUE_TOO_BIG;
  9.   // First write the number of members in the object.
  10.   // 对象内的元素个数
  11. if (append_offset_or_size(dest, size, large))
  12. return FAILURE;
  13. // Reserve space for the size of the object in bytes. To be filled in later.
  14.   // 占位填充 占用的总字节数
  15. const size_t size_pos= dest->length();
  16. if (append_offset_or_size(dest, 0, large))
  17. return FAILURE;

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

d0c52c6f69fd2daf0c6d995acc41dabb.png

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

  1. // 计算 key_entry、value_entry所需要的字节数
  2. const size_t key_entry_size=
  3. large ? KEY_ENTRY_SIZE_LARGE : KEY_ENTRY_SIZE_SMALL;
  4. const size_t value_entry_size=
  5. large ? VALUE_ENTRY_SIZE_LARGE : VALUE_ENTRY_SIZE_SMALL;
  6. /*
  7. Calculate the offset of the first key relative to the start of the
  8. object. The first key comes right after the value entries.
  9. {"x":33, "n":"name","ab":true, "a":"测试"}
  10. key_entry_size=4, value_entry_size=3,dest.length=5, 5+4(4个元素)*(4+3)-1=32=0x20
  11. */
  12. size_t offset= dest->length() +
  13. size * (key_entry_size + value_entry_size) - start_pos;
  14.   
  15. #ifndef NDEBUG
  16. const std::string *prev_key= NULL;
  17. #endif
  18. // Add the key entries.
  19. for (Json_object::const_iterator it= object->begin();
  20. it != object->end(); ++it)
  21. {
  22. const std::string *key= &it->first;
  23.     // 拿到 key 的长度
  24. size_t len= key->length();
  25. #ifndef NDEBUG
  26. // Check that the DOM returns the keys in the correct order.
  27.     // 对 key 进行字典排序,先按长度排序;长度相同,再按内容排序。
  28. if (prev_key)
  29.     {
  30. assert(prev_key->length() <= len);
  31. if (len == prev_key->length())
  32. assert(memcmp(prev_key->data(), key->data(), len) < 0);
  33. }
  34. prev_key= key;
  35. #endif
  36. // We only have two bytes for the key size. Check if the key is too big.
  37.     // 只有2字节来存储 key 的长度,所以 key 的长度 < 2^16-1
  38. if (len > UINT_MAX16)
  39. {
  40. my_error(ER_JSON_KEY_TOO_BIG, MYF(0));
  41. return FAILURE;
  42. }
  43. if (is_too_big_for_json(offset, large))
  44. return VALUE_TOO_BIG;
  45.     // 追加 key 的 offset, 追加 key 的长度
  46. if (append_offset_or_size(dest, offset, large) ||
  47. append_int16(dest, static_cast<int16>(len)))
  48. return FAILURE;
  49. offset+= len;
  50. }

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

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

  1. /*
  2. The size of key entries for objects when using the small storage
  3. format or the large storage format. In the small format it is 4
  4. bytes (2 bytes for key length and 2 bytes for key offset). In the
  5. large format it is 6 (2 bytes for length, 4 bytes for offset).
  6. */
  7. #define KEY_ENTRY_SIZE_SMALL (2 + SMALL_OFFSET_SIZE)
  8. #define KEY_ENTRY_SIZE_LARGE (2 + LARGE_OFFSET_SIZE)
  9. /*
  10. The size of value entries for objects or arrays. When using the
  11. small storage format, the entry size is 3 (1 byte for type, 2 bytes
  12. for offset). When using the large storage format, it is 5 (1 byte
  13. for type, 4 bytes for offset).
  14. */
  15. #define VALUE_ENTRY_SIZE_SMALL (1 + SMALL_OFFSET_SIZE)
  16. #define VALUE_ENTRY_SIZE_LARGE (1 + LARGE_OFFSET_SIZE)

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

f51aff06a3c2adf7b0c3f217e206b1b6.png

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

下面会继续填充占位 value_entrys

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

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

8003aabced39076eb25bca49143e721d.png

8c1e4cf498015cc0803df828d9760c7f.png

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

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

追加后的效果

2bc113f586dd9c11d45451d1748e0ad4.png

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

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

追加后的效果

1373c31c727d361d6f5293fc136c1001.png

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

  1. // Finally, write the size of the object in bytes.
  2. size_t bytes= dest->length() - start_pos;
  3. if (is_too_big_for_json(bytes, large))
  4. return VALUE_TOO_BIG;
  5. insert_offset_or_size(dest, size_pos, bytes, large);
  6. return OK;
  7. }

来一张全图看看效果。

b87e3a5cca10350762836a9c911411de.png

e809e2b4cff06945e65c315b2d03e5ef.png

4、疑问

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

问题涉及到的是这段代码

  1. case Json_dom::J_STRING:
  2. {
  3. const Json_string *jstr= down_cast<const Json_string*>(dom);
  4. size_t size= jstr->size();
  5. if (append_variable_length(dest, size) ||
  6. dest->append(jstr->value().c_str(), size))
  7. return FAILURE;
  8. (*dest)[type_pos]= JSONB_TYPE_STRING;
  9. result= OK;
  10. break;
  11. }
  1. static bool append_variable_length(String *dest, size_t length)
  2. {
  3. do
  4. {
  5. // Filter out the seven least significant bits of length.
  6. uchar ch= (length & 0x7F);
  7. /*
  8. Right-shift length to drop the seven least significant bits. If there
  9. is more data in length, set the high bit of the byte we're writing
  10. to the String.
  11. */
  12. length>>= 7;
  13. if (length != 0)
  14. ch|= 0x80;
  15. if (dest->append(ch))
  16. return true; /* purecov: inspected */
  17. }
  18. while (length != 0);
  19. // Successfully appended the length.
  20. return false;
  21. }

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

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

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

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

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

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

扯两句

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

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

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/158047
推荐阅读
相关标签
  

闽ICP备14008679号