如何深入理解并应用MySQL JSON数据类型?
- 内容介绍
- 文章标签
- 相关推荐
本文共计9973个文字,预计阅读时间需要40分钟。
JSON数据类型是MySQL 5.7.8及以上版本开始支持的。在此之前,只能通过字符类型(CHAR、VARCHAR或TEXT)来存储JSON文档。对于字符类型,原生JSON类型具有以下优势:在插入时能自动校验文档。
JSON 数据类型是 MySQL 5.7.8 开始支持的。在此之前,只能通过字符类型(CHAR,VARCHAR 或 TEXT )来保存 JSON 文档。
相对字符类型,原生的 JSON 类型具有以下优势:
- 在插入时能自动校验文档是否满足 JSON 格式的要求。
- 优化了存储格式。无需读取整个文档就能快速访问某个元素的值。
在 JSON 类型引入之前,如果我们想要获取 JSON 文档中的某个元素,必须首先读取整个 JSON 文档,然后在客户端将其转换为 JSON 对象,最后再通过对象获取指定元素的值。
下面是 Python 中的获取方式。
importjson
#JSON字符串:
x='{"name":"John","age":30,"city":"NewYork"}'
#将JSON字符串转换为JSON对象:
y=json.loads(x)
#读取JSON对象中指定元素的值:
print(y["age"])
这种方式有两个弊端:一、消耗磁盘 IO,二、消耗网络带宽,如果 JSON 文档比较大,在高并发场景,有可能会打爆网卡。
如果使用的是 JSON 类型,相同的需求,直接使用 SQL 命令就可搞定。不仅能节省网络带宽,结合后面提到的函数索引,还能降低磁盘 IO 消耗。
mysql>createtablet(c1json);
QueryOK,0rowsaffected(0.09sec)
mysql>insertintotvalues('{"name":"John","age":30,"city":"NewYork"}');
QueryOK,1rowaffected(0.01sec)
mysql>selectc1->"$.age"fromt;
+-------------+
|c1->"$.age"|
+-------------+
|30|
+-------------+
1rowinset(0.00sec)
本文将从以下几个方面展开:
- 什么是 JSON。
- JSON 字段的增删改查操作。
- 如何对 JSON 字段创建索引。
- 如何将存储 JSON 字符串的字符字段升级为 JSON 字段。
- 使用 JSON 时的注意事项。
- Partial Updates。
- 其它 JSON 函数。
JSON 是 JavaScript Object Notation(JavaScript 对象表示法)的缩写,是一个轻量级的,基于文本的,跨语言的数据交换格式。易于阅读和编写。
JSON 的基本数据类型如下:
-
数值:十进制数,不能有前导 0,可以为负数或小数,还可以为 e 或 E 表示的指数。
-
字符串:字符串必须用双引号括起来。
-
布尔值:true,false。
-
数组:一个由零或多个值组成的有序序列。每个值可以为任意类型。数组使用方括号
[]括起来,元素之间用逗号,分隔。譬如,[1,"abc",null,true,"10:27:06.000000",{"id":1}] -
对象:一个由零或者多个键值对组成的无序集合。其中键必须是字符串,值可以为任意类型。
对象使用花括号
{}括起来,键值对之间使用逗号,分隔,键与值之间用冒号:分隔。譬如,{"db":["mysql","oracle"],"id":123,"info":{"age":20}} -
空值:null。
下面我们看看 JSON 字段常见的增删改查操作:
2.1 插入操作可直接插入 JSON 格式的字符串。
mysql>createtablet(c1json);
QueryOK,0rowsaffected(0.03sec)
mysql>insertintotvalues('[1,"abc",null,true,"08:45:06.000000"]');
QueryOK,1rowaffected(0.01sec)
mysql>insertintotvalues('{"id":87,"name":"carrot"}');
QueryOK,1rowaffected(0.01sec)
也可使用函数,常用的有 JSON_ARRAY() 和 JSON_OBJECT(),前者用于构造 JSON 数组,后者用于构造 JSON 对象。如,
mysql>selectjson_array(1,"abc",null,true,curtime());
+--------------------------------------------+
|json_array(1,"abc",null,true,curtime())|
+--------------------------------------------+
|[1,"abc",null,true,"10:12:25.000000"]|
+--------------------------------------------+
1rowinset(0.01sec)
mysql>selectjson_object('id',87,'name','carrot');
+-----------------------------------------+
|json_object('id',87,'name','carrot')|
+-----------------------------------------+
|{"id":87,"name":"carrot"}|
+-----------------------------------------+
1rowinset(0.00sec)
对于 JSON 文档,KEY 名不能重复。
如果插入的值中存在重复 KEY,在 MySQL 8.0.3 之前,遵循 first duplicate key wins 原则,会保留第一个 KEY,后面的将被丢弃掉。
从 MySQL 8.0.3 开始,遵循的是 last duplicate key wins 原则,只会保留最后一个 KEY。
下面通过一个具体的示例来看看两者的区别。
MySQL 5.7.36
mysql>selectjson_object('key1',10,'key2',20,'key1',30);
+--------------------------------------------+
|json_object('key1',10,'key2',20,'key1',30)|
+--------------------------------------------+
|{"key1":10,"key2":20}|
+--------------------------------------------+
1rowinset(0.02sec)
MySQL 8.0.27
mysql>selectjson_object('key1',10,'key2',20,'key1',30);
+--------------------------------------------+
|json_object('key1',10,'key2',20,'key1',30)|
+--------------------------------------------+
|{"key1":30,"key2":20}|
+--------------------------------------------+
1rowinset(0.00sec)
2.2 查询操作
JSON_EXTRACT(json_doc, path[, path] ...)
其中,json_doc 是 JSON 文档,path 是路径。该函数会从 JSON 文档提取指定路径(path)的元素。如果指定 path 不存在,会返回 NULL。可指定多个 path,匹配到的多个值会以数组形式返回。
下面我们结合一些具体的示例来看看 path 及 JSON_EXTRACT 的用法。
首先我们看看数组。
数组的路径是通过下标来表示的。第一个元素的下标是 0。
mysql>selectjson_extract('[10,20,[30,40]]','$[0]');
+--------------------------------------------+
|json_extract('[10,20,[30,40]]','$[0]')|
+--------------------------------------------+
|10|
+--------------------------------------------+
1rowinset(0.00sec)
mysql>selectjson_extract('[10,20,[30,40]]','$[0]','$[1]','$[2][0]');
+--------------------------------------------------------------+
|json_extract('[10,20,[30,40]]','$[0]','$[1]','$[2][0]')|
+--------------------------------------------------------------+
|[10,20,30]|
+--------------------------------------------------------------+
1rowinset(0.00sec)
除此之外,还可通过[M to N]获取数组的子集。
mysql>selectjson_extract('[10,20,[30,40]]','$[0to1]');
+-------------------------------------------------+
|json_extract('[10,20,[30,40]]','$[0to1]')|
+-------------------------------------------------+
|[10,20]|
+-------------------------------------------------+
1rowinset(0.00sec)
#这里的last代表最后一个元素的下标
mysql>selectjson_extract('[10,20,[30,40]]','$[last-1tolast]');
+---------------------------------------------------------+
|json_extract('[10,20,[30,40]]','$[last-1tolast]')|
+---------------------------------------------------------+
|[20,[30,40]]|
+---------------------------------------------------------+
1rowinset(0.00sec)
也可通过[*]获取数组中的所有元素。
mysql>selectjson_extract('[10,20,[30,40]]','$[*]');
+--------------------------------------------+
|json_extract('[10,20,[30,40]]','$[*]')|
+--------------------------------------------+
|[10,20,[30,40]]|
+--------------------------------------------+
1rowinset(0.00sec)
接下来,我们看看对象。
对象的路径是通过 KEY 来表示的。
mysql>set@j='{"a":1,"b":[2,3],"ac":4}';
QueryOK,0rowsaffected(0.00sec)
#如果 KEY 在路径表达式中不合法(譬如存在空格),则在引用这个 KEY 时,需用双引号括起来。
mysql>selectjson_extract(@j,'$.a'),json_extract(@j,'$."ac"'),json_extract(@j,'$.b[1]');
+-------------------------+-----------------------------+----------------------------+
|json_extract(@j,'$.a')|json_extract(@j,'$."ac"')|json_extract(@j,'$.b[1]')|
+-------------------------+-----------------------------+----------------------------+
|1|4|3|
+-------------------------+-----------------------------+----------------------------+
1rowinset(0.00sec)
除此之外,还可通过.*获取对象中的所有元素。
mysql>selectjson_extract('{"a":1,"b":[2,3],"ac":4}','$.*');
+--------------------------------------------------------+
|json_extract('{"a":1,"b":[2,3],"ac":4}','$.*')|
+--------------------------------------------------------+
|[1,[2,3],4]|
+--------------------------------------------------------+
1rowinset(0.00sec)
#这里的$**.b匹配$.a.b和$.c.b
mysql>selectjson_extract('{"a":{"b":1},"c":{"b":2}}','$**.b');
+---------------------------------------------------------+
|json_extract('{"a":{"b":1},"c":{"b":2}}','$**.b')|
+---------------------------------------------------------+
|[1,2]|
+---------------------------------------------------------+
1rowinset(0.00sec)
column->path
column->path,包括后面讲到的 column->>path,都是语法糖,在实际使用的时候都会转化为 JSON_EXTRACT。
column->path 等同于 JSON_EXTRACT(column, path) ,只能指定一个path。
createtablet(c2json);
insertintotvalues('{"empno":1001,"ename":"jack"}'),('{"empno":1002,"ename":"mark"}');
mysql>selectc2,c2->"$.ename"fromt;
+----------------------------------+---------------+
|c2|c2->"$.ename"|
+----------------------------------+---------------+
|{"empno":1001,"ename":"jack"}|"jack"|
|{"empno":1002,"ename":"mark"}|"mark"|
+----------------------------------+---------------+
2rowsinset(0.00sec)
mysql>select*fromtwherec2->"$.empno"=1001;
+------+----------------------------------+
|c1|c2|
+------+----------------------------------+
|1|{"empno":1001,"ename":"jack"}|
+------+----------------------------------+
1rowinset(0.00sec)
column->>path
同 column->path 类似,只不过其返回的是字符串。以下三者是等价的。
- JSON_UNQUOTE( JSON_EXTRACT(column, path) )
- JSON_UNQUOTE(column -> path)
- column->>path
mysql>selectc2->'$.ename',json_extract(c2,"$.ename"),json_unquote(c2->'$.ename'),c2->>'$.ename'fromt;
+---------------+-----------------------------+-----------------------------+----------------+
|c2->'$.ename'|json_extract(c2,"$.ename")|json_unquote(c2->'$.ename')|c2->>'$.ename'|
+---------------+-----------------------------+-----------------------------+----------------+
|"jack"|"jack"|jack|jack|
|"mark"|"mark"|mark|mark|
+---------------+-----------------------------+-----------------------------+----------------+
2rowsinset(0.00sec)
2.3 修改操作
JSON_INSERT(json_doc, path, val[, path, val] ...)
插入新值。
仅当指定位置或指定 KEY 的值不存在时,才执行插入操作。另外,如果指定的 path 是数组下标,且 json_doc 不是数组,该函数首先会将 json_doc 转化为数组,然后再插入新值。
下面我们看几个示例。
mysql>selectjson_insert('1','$[0]',"10");
+------------------------------+
|json_insert('1','$[0]',"10")|
+------------------------------+
|1|
+------------------------------+
1rowinset(0.00sec)
mysql>selectjson_insert('1','$[1]',"10");
+------------------------------+
|json_insert('1','$[1]',"10")|
+------------------------------+
|[1,"10"]|
+------------------------------+
1rowinset(0.01sec)
mysql>selectjson_insert('["1","2"]','$[2]',"10");
+--------------------------------------+
|json_insert('["1","2"]','$[2]',"10")|
+--------------------------------------+
|["1","2","10"]|
+--------------------------------------+
1rowinset(0.00sec)
mysql>set@j='{"a":1,"b":[2,3]}';
QueryOK,0rowsaffected(0.00sec)
mysql>selectjson_insert(@j,'$.a',10,'$.c','[true,false]');
+----------------------------------------------------+
|json_insert(@j,'$.a',10,'$.c','[true,false]')|
+----------------------------------------------------+
|{"a":1,"b":[2,3],"c":"[true,false]"}|
+----------------------------------------------------+
1rowinset(0.00sec)
JSON_SET(json_doc, path, val[, path, val] ...)
插入新值,并替换已经存在的值。
换言之,如果指定位置或指定 KEY 的值不存在,会执行插入操作,如果存在,则执行更新操作。
mysql>set@j='{"a":1,"b":[2,3]}';
QueryOK,0rowsaffected(0.00sec)
mysql>selectjson_set(@j,'$.a',10,'$.c','[true,false]');
+-------------------------------------------------+
|json_set(@j,'$.a',10,'$.c','[true,false]')|
+-------------------------------------------------+
|{"a":10,"b":[2,3],"c":"[true,false]"}|
+-------------------------------------------------+
1rowinset(0.00sec)
JSON_REPLACE(json_doc, path, val[, path, val] ...)
替换已经存在的值。
mysql>set@j='{"a":1,"b":[2,3]}';
QueryOK,0rowsaffected(0.00sec)
mysql>selectjson_replace(@j,'$.a',10,'$.c','[true,false]');
+-----------------------------------------------------+
|json_replace(@j,'$.a',10,'$.c','[true,false]')|
+-----------------------------------------------------+
|{"a":10,"b":[2,3]}|
+-----------------------------------------------------+
1rowinset(0.00sec)
2.4 删除操作
JSON_REMOVE(json_doc, path[, path] ...)
删除 JSON 文档指定位置的元素。
mysql>set@j='{"a":1,"b":[2,3]}';
QueryOK,0rowsaffected(0.00sec)
mysql>selectjson_remove(@j,'$.a');
+------------------------+
|JSON_REMOVE(@j,'$.a')|
+------------------------+
|{"b":[2,3]}|
+------------------------+
1rowinset(0.00sec)
mysql>set@j='["a",["b","c"],"d","e"]';
QueryOK,0rowsaffected(0.00sec)
mysql>selectjson_remove(@j,'$[1]');
+-------------------------+
|JSON_REMOVE(@j,'$[1]')|
+-------------------------+
|["a","d","e"]|
+-------------------------+
1rowinset(0.00sec)
mysql>selectjson_remove(@j,'$[1]','$[2]');
+--------------------------------+
|JSON_REMOVE(@j,'$[1]','$[2]')|
+--------------------------------+
|["a","d"]|
+--------------------------------+
1rowinset(0.00sec)
mysql>selectjson_remove(@j,'$[1]','$[1]');
+--------------------------------+
|JSON_REMOVE(@j,'$[1]','$[1]')|
+--------------------------------+
|["a","e"]|
+--------------------------------+
1rowinset(0.00sec)
最后一个查询,虽然两个 path 都是 '$[1]' ,但作用对象不一样,第一个 path 的作用对象是 '["a", ["b", "c"], "d", "e"]' ,第二个 path 的作用对象是删除了 '$[1]' 后的数组,即 '["a", "d", "e"]' 。
三、如何对 JSON 字段创建索引同 TEXT,BLOB 字段一样,JSON 字段不允许直接创建索引。
mysql>createtablet(c1json,index(c1));
ERROR3152(42000):JSONcolumn'c1'supportsindexingonlyviageneratedcolumnsonaspecifiedJSONpath.
即使支持,实际意义也不大,因为我们一般是基于文档中的元素进行查询,很少会基于整个 JSON 文档。
对文档中的元素进行查询,就需要用到 MySQL 5.7 引入的虚拟列及函数索引。
下面我们来看一个具体的示例。
#C2即虚拟列
# index (c2)对虚拟列添加索引。
createtablet(c1json,c2varchar(10)as(JSON_UNQUOTE(c1->"$.name")),index(c2));
insertintot(c1)values('{"id":1,"name":"a"}'),('{"id":2,"name":"b"}'),('{"id":3,"name":"c"}'),('{"id":4,"name":"d"}');
mysql>explainselect*fromtwherec2='a';
+----+-------------+-------+------------+------+---------------+------+---------+-------+------+----------+-------+
|id|select_type|table|partitions|type|possible_keys|key|key_len|ref|rows|filtered|Extra|
+----+-------------+-------+------------+------+---------------+------+---------+-------+------+----------+-------+
|1|SIMPLE|t|NULL|ref|c2|c2|43|const|1|100.00|NULL|
+----+-------------+-------+------------+------+---------------+------+---------+-------+------+----------+-------+
1rowinset,1warning(0.00sec)
mysql>explainselect*fromtwherec1->'$.name'='a';
+----+-------------+-------+------------+------+---------------+------+---------+-------+------+----------+-------+
|id|select_type|table|partitions|type|possible_keys|key|key_len|ref|rows|filtered|Extra|
+----+-------------+-------+------------+------+---------------+------+---------+-------+------+----------+-------+
|1|SIMPLE|t|NULL|ref|c2|c2|43|const|1|100.00|NULL|
+----+-------------+-------+------------+------+---------------+------+---------+-------+------+----------+-------+
1rowinset,1warning(0.00sec)
可以看到,无论是使用虚拟列,还是文档中的元素来查询,都可以利用上索引。
注意,在创建虚拟列时需指定 JSON_UNQUOTE,将 c1 -> "$.name" 的返回值转换为字符串。
四、如何将存储 JSON 字符串的字符字段升级为 JSON 字段在 MySQL 支持 JSON 类型之前,对于 JSON 文档,一般是以字符串的形式存储在字符类型(VARCHAR 或 TEXT)中。
在 JSON 类型出来之后,如何将这些字符字段升级为 JSON 字段呢?
为方便演示,这里首先构建测试数据。
createtablet(idintauto_incrementprimarykey,c1text);
insertintot(c1)values('{"id":"1","name":"a"}'),('{"id":"2","name":"b"}'),('{"id":"3","name":"c"}'),('{"id","name":"d"}');
注意,最后一个文档有问题,不是合格的 JSON 文档。
如果使用 DDL 直接修改字段的数据类型,会报错。
mysql>altertabletmodifyc1json;
ERROR3140(22032):InvalidJSONtext:"Missingacolonafteranameofobjectmember."atposition5invalueforcolumn'#sql-7e1c_1f6.c1'.
下面,我们看看具体的升级步骤。
(1)使用 json_valid 函数找出不满足 JSON 格式要求的文档。
mysql>select*fromtwherejson_valid(c1)=0;
+----+---------------------+
|id|c1|
+----+---------------------+
|4|{"id","name":"d"}|
+----+---------------------+
1rowinset(0.00sec)
(2)处理不满足 JSON 格式要求的文档。
mysql>updatetsetc1='{"id":"4","name":"d"}'whereid=4;
QueryOK,1rowaffected(0.01sec)
Rowsmatched:1Changed:1Warnings:0
(3)将 TEXT 字段修改为 JSON 字段。
mysql>select*fromtwherejson_valid(c1)=0;
五、使用 JSON 时的注意事项
Emptyset(0.00sec)
mysql>altertabletmodifyc1json;
QueryOK,4rowsaffected(0.13sec)
Records:4Duplicates:0Warnings:0
对于 JSON 类型,有以下几点需要注意:
-
在 MySQL 8.0.13 之前,不允许对 BLOB,TEXT,GEOMETRY,JSON 字段设置默认值。从 MySQL 8.0.13 开始,取消了这个限制。
设置时,注意默认值需通过小括号
()括起来,否则的话,还是会提示 JSON 字段不允许设置默认值。mysql>createtablet(c1jsonnotnulldefault(''));
QueryOK,0rowsaffected(0.03sec)
mysql>createtablet(c1jsonnotnulldefault'');
ERROR1101(42000):BLOB,TEXT,GEOMETRYorJSONcolumn'c1'can'thaveadefaultvalue -
不允许直接创建索引,可创建函数索引。
-
JSON 列的最大大小和 LONGBLOB(LONGTEXT)一样,都是 4G。
-
插入时,单个文档的大小受到 max_allowed_packet 的限制,该参数最大是 1G。
在 MySQL 5.7 中,对 JSON 文档进行更新,其处理策略是,删除旧的文档,再插入新的文档。即使这个修改很微小,只涉及几个字节,也会替换掉整个文档。很显然,这种处理方式的效率较为低下。
在 MySQL 8.0 中,针对 JSON 文档,引入了一项新的特性-Partial Updates(部分更新),支持 JSON 文档的原地更新。得益于这个特性,JSON 文档的处理性能得到了极大提升。
下面我们具体来看看。
6.1 使用 Partial Updates 的条件为方便阐述,这里先构造测试数据。
createtablet(idintauto_incrementprimarykey,c1json);
insertintot(c1)values('{"id":1,"name":"a"}'),('{"id":2,"name":"b"}'),('{"id":3,"name":"c"}'),('{"id":4,"name":"d"}');
mysql>select*fromt;
+----+------------------------+
|id|c1|
+----+------------------------+
|1|{"id":1,"name":"a"}|
|2|{"id":2,"name":"b"}|
|3|{"id":3,"name":"c"}|
|4|{"id":4,"name":"d"}|
+----+------------------------+
4rowsinset(0.00sec)
使用 Partial Updates 需满足以下条件:
-
被更新的列是 JSON 类型。
-
使用 JSON_SET,JSON_REPLACE,JSON_REMOVE 进行 UPDATE 操作,如,
updatetsetc1=json_remove(c1,'$.id')whereid=1;不使用这三个函数,而显式赋值,就不会进行部分更新,如,
updatetsetc1='{"id":1,"name":"a"}'whereid=1; -
输入列和目标列必须是同一列,如,
updatetsetc1=json_replace(c1,'$.id',10)whereid=1;否则的话,就不会进行部分更新,如,
updatetsetc1=json_replace(c2,'$.id',10)whereid=1; -
变更前后,JSON 文档的空间使用不会增加。
关于最后一个条件,我们看看下面这个示例。
mysql>select*,json_storage_size(c1),json_storage_free(c1)fromtwhereid=1;
+----+------------------------+-----------------------+-----------------------+
|id|c1|json_storage_size(c1)|json_storage_free(c1)|
+----+------------------------+-----------------------+-----------------------+
|1|{"id":1,"name":"a"}|27|0|
+----+------------------------+-----------------------+-----------------------+
1rowinset(0.00sec)
mysql>updatetsetc1=json_remove(c1,'$.id')whereid=1;
QueryOK,1rowaffected(0.01sec)
Rowsmatched:1Changed:1Warnings:0
mysql>select*,json_storage_size(c1),json_storage_free(c1)fromtwhereid=1;
+----+---------------+-----------------------+-----------------------+
|id|c1|json_storage_size(c1)|json_storage_free(c1)|
+----+---------------+-----------------------+-----------------------+
|1|{"name":"a"}|27|9|
+----+---------------+-----------------------+-----------------------+
1rowinset(0.00sec)
mysql>updatetsetc1=json_set(c1,'$.id',3306)whereid=1;
QueryOK,1rowaffected(0.01sec)
Rowsmatched:1Changed:1Warnings:0
mysql>select*,json_storage_size(c1),json_storage_free(c1)fromtwhereid=1;
+----+---------------------------+-----------------------+-----------------------+
|id|c1|json_storage_size(c1)|json_storage_free(c1)|
+----+---------------------------+-----------------------+-----------------------+
|1|{"id":3306,"name":"a"}|27|0|
+----+---------------------------+-----------------------+-----------------------+
1rowinset(0.00sec)
mysql>updatetsetc1=json_set(c1,'$.id','mysql')whereid=1;
QueryOK,1rowaffected(0.01sec)
Rowsmatched:1Changed:1Warnings:0
mysql>select*,json_storage_size(c1),json_storage_free(c1)fromtwhereid=1;
+----+------------------------------+-----------------------+-----------------------+
|id|c1|json_storage_size(c1)|json_storage_free(c1)|
+----+------------------------------+-----------------------+-----------------------+
|1|{"id":"mysql","name":"a"}|33|0|
+----+------------------------------+-----------------------+-----------------------+
1rowinset(0.00sec)
示例中,用到了两个函数:JSON_STORAGE_SIZE 和 JSON_STORAGE_FREE ,前者用来获取 JSON 文档的空间使用情况,后者用来获取 JSON 文档在执行原地更新后的空间释放情况。
这里一共执行了三次 UPDATE 操作,前两次是原地更新,第三次不是。同样是 JSON_SET 操作,为什么第一次是原地更新,而第二次不是呢?
因为第一次的 JSON_SET 复用了 JSON_REMOVE 释放的空间。而第二次的 JSON_SET 执行的是更新操作,且 'mysql' 比 3306 需要更多的存储空间。
6.2 如何在 binlog 中开启 Partial Updates
Partial Updates 不仅仅适用于存储引擎层,还可用于主从复制场景。
主从复制开启 Partial Updates,只需将参数 binlog_row_value_options(默认为空)设置为 PARTIAL_JSON。
下面具体来看看,同一个 UPDATE 操作,开启和不开启 Partial Updates,在 binlog 中的记录有何区别。
updatetsetc1=json_replace(c1,'$.id',10)whereid=1;
不开启
###UPDATE`slowtech`.`t`
###WHERE
###@1=1
###@2='{"id":"1","name":"a"}'
###SET
###@1=1
###@2='{"id":10,"name":"a"}'
开启
###UPDATE`slowtech`.`t`
###WHERE
###@1=1
###@2='{"id":1,"name":"a"}'
###SET
###@1=1
###@2=JSON_REPLACE(@2,'$.id',10)
对比 binlog 的内容,可以看到,不开启,无论是修改前的镜像(before_image)还是修改后的镜像(after_image),记录的都是完整文档。而开启后,对于修改后的镜像,记录的是命令,而不是完整文档,这样可节省近一半的空间。
在将 binlog_row_value_options 设置为 PARTIAL_JSON 后,对于可使用 Partial Updates 的操作,在 binlog 中,不再通过 ROWS_EVENT 来记录,而是新增了一个 PARTIAL_UPDATE_ROWS_EVENT 的事件类型。
需要注意的是,binlog 中使用 Partial Updates,只需满足存储引擎层使用 Partial Updates 的前三个条件,无需考虑变更前后,JSON 文档的空间使用是否会增加。
6.3 关于 Partial Updates 的性能测试首先构造测试数据,t 表一共有 16 个文档,每个文档近 10 MB。
createtablet(idintauto_incrementprimarykey,
json_coljson,
namevarchar(100)as(json_col->>'$.name'),
ageintas(json_col->'$.age'));
insertintot(json_col)values
(json_object('name','Joe','age',24,
'data',repeat('x',10*1000*1000))),
(json_object('name','Sue','age',32,
'data',repeat('y',10*1000*1000))),
(json_object('name','Pete','age',40,
'data',repeat('z',10*1000*1000))),
(json_object('name','Jenny','age',27,
'data',repeat('w',10*1000*1000)));
insertintot(json_col)selectjson_colfromt;
insertintot(json_col)selectjson_colfromt;
接下来,测试下述 SQL
updatetsetjson_col=json_set(json_col,'$.age',age+1);
在以下四种场景下的执行时间:
- MySQL 5.7.36
- MySQL 8.0.27
- MySQL 8.0.27,binlog_row_value_options=PARTIAL_JSON
- MySQL 8.0.27,binlog_row_value_options=PARTIAL_JSON + binlog_row_image=MINIMAL
分别执行 10 次,去掉最大值和最小值后求平均值。
最后的测试结果如下:
以 MySQL 5.7.36 的查询时间作为基准:
- MySQL 8.0 只开启存储引擎层的 Partial Updates,查询时间比 MySQL 5.7 快 1.94 倍。
- MySQL 8.0 同时开启存储引擎层和 binlog 中的 Partial Updates,查询时间比 MySQL 5.7 快 4.87 倍。
- 如果在 2 的基础上,同时将 binlog_row_image 设置为 MINIMAL,查询时间更是比 MySQL 5.7 快 102.22 倍。
当然,在生产环境,我们一般很少将 binlog_row_image 设置为 MINIMAL。
但即使如此,只开启存储引擎层和 binlog 中的 Partial Updates,查询时间也比 MySQL 5.7 快 4.87 倍,性能提升还是比较明显的。
七、其它 JSON 函数 7.1 查询相关JSON_CONTAINS(target, candidate[, path])
判断 target 文档是否包含 candidate 文档,如果包含,则返回 1,否则是 0。
mysql>set@j='{"a":[1,2],"b":3,"c":{"d":4}}';
QueryOK,0rowsaffected(0.00sec)
mysql>selectjson_contains(@j,'1','$.a'),json_contains(@j,'1','$.b');
+-------------------------------+-------------------------------+
|json_contains(@j,'1','$.a')|json_contains(@j,'1','$.b')|
+-------------------------------+-------------------------------+
|1|0|
+-------------------------------+-------------------------------+
1rowinset(0.00sec)
mysql>selectjson_contains(@j,'{"d":4}','$.a'),json_contains(@j,'{"d":4}','$.c');
+------------------------------------+------------------------------------+
|json_contains(@j,'{"d":4}','$.a')|json_contains(@j,'{"d":4}','$.c')|
+------------------------------------+------------------------------------+
|0|1|
+------------------------------------+------------------------------------+
1rowinset(0.00sec)
JSON_CONTAINS_PATH(json_doc, one_or_all, path[, path] ...)
判断指定的 path 是否存在,存在,则返回 1,否则是 0。
函数中的 one_or_all 可指定 one 或 all,one 是任意一个路径存在就返回 1,all 是所有路径都存在才返回 1。
mysql>set@j='{"a":[1,2],"b":3,"c":{"d":4}}';
QueryOK,0rowsaffected(0.00sec)
mysql>selectjson_contains_path(@j,'one','$.a','$.e'),json_contains_path(@j,'all','$.a','$.e');
+---------------------------------------------+---------------------------------------------+
|json_contains_path(@j,'one','$.a','$.e')|json_contains_path(@j,'all','$.a','$.e')|
+---------------------------------------------+---------------------------------------------+
|1|0|
+---------------------------------------------+---------------------------------------------+
1rowinset(0.00sec)
mysql>selectjson_contains_path(@j,'one','$.c.d'),json_contains_path(@j,'one','$.a.d');
+----------------------------------------+----------------------------------------+
|json_contains_path(@j,'one','$.c.d')|json_contains_path(@j,'one','$.a.d')|
+----------------------------------------+----------------------------------------+
|1|0|
+----------------------------------------+----------------------------------------+
1rowinset(0.00sec)
JSON_SEARCH(json_doc, one_or_all, search_str[, escape_char[, path] ...])
返回某个字符串(search_str)在 JSON 文档中的位置,其中,
- one_or_all:匹配的次数,one 是只匹配一次,all 是匹配所有。如果匹配到多个,结果会以数组的形式返回。
- search_str:子串,支持模糊匹配:
%和_。 - escape_char:转义符,如果该参数不填或为 NULL,则取默认转义符
\。 - path:查找路径。
mysql>set@j='["abc",[{"k":"10"},"def"],{"x":"abc"},{"y":"bcd"}]';
QueryOK,0rowsaffected(0.00sec)
mysql>selectjson_search(@j,'one','abc'),json_search(@j,'all','abc'),json_search(@j,'all','ghi');
+-------------------------------+-------------------------------+-------------------------------+
|json_search(@j,'one','abc')|json_search(@j,'all','abc')|json_search(@j,'all','ghi')|
+-------------------------------+-------------------------------+-------------------------------+
|"$[0]"|["$[0]","$[2].x"]|NULL|
+-------------------------------+-------------------------------+-------------------------------+
1rowinset(0.00sec)
mysql>selectjson_search(@j,'all','%b%',NULL,'$[1]'),json_search(@j,'all','%b%',NULL,'$[3]');
+---------------------------------------------+---------------------------------------------+
|json_search(@j,'all','%b%',NULL,'$[1]')|json_search(@j,'all','%b%',NULL,'$[3]')|
+---------------------------------------------+---------------------------------------------+
|NULL|"$[3].y"|
+---------------------------------------------+---------------------------------------------+
1rowinset(0.00sec)
JSON_KEYS(json_doc[, path])
返回 JSON 文档最外层的 key,如果指定了 path,则返回该 path 对应元素最外层的 key。
mysql>selectjson_keys('{"a":1,"b":{"c":30}}');
+---------------------------------------+
|json_keys('{"a":1,"b":{"c":30}}')|
+---------------------------------------+
|["a","b"]|
+---------------------------------------+
1rowinset(0.00sec)
mysql>selectjson_keys('{"a":1,"b":{"c":30}}','$.b');
+----------------------------------------------+
|json_keys('{"a":1,"b":{"c":30}}','$.b')|
+----------------------------------------------+
|["c"]|
+----------------------------------------------+
1rowinset(0.00sec)
JSON_VALUE(json_doc, path)
8.0.21 引入的,从 JSON 文档提取指定路径(path)的元素。
该函数的完整语法如下:
JSON_VALUE(json_doc,path[RETURNINGtype][on_empty][on_error])
on_empty:
{NULL|ERROR|DEFAULTvalue}ONEMPTY
on_error:
{NULL|ERROR|DEFAULTvalue}ONERROR
其中:
- RETURNING type:返回值的类型,不指定,则默认是 VARCHAR(512)。不指定字符集,则默认是 utf8mb4,且区分大小写。
- on_empty:如果指定路径没有值,会触发 on_empty 子句, 默认是返回 NULL,也可指定 ERROR 抛出错误,或者通过 DEFAULT value 返回默认值。
- on_error:三种情况下会触发 on_error 子句:从数组或对象中提取元素时,会解析到多个值;类型转换错误,譬如将 "abc" 转换为 unsigned 类型;值被 truncate 了。默认是返回 NULL。
mysql>selectjson_value('{"item":"shoes","price":"49.95"}','$.item');
+-------------------------------------------------------------+
|json_value('{"item":"shoes","price":"49.95"}','$.item')|
+-------------------------------------------------------------+
|shoes|
+-------------------------------------------------------------+
1rowinset(0.00sec)
mysql>selectjson_value('{"item":"shoes","price":"49.95"}','$.price'returningdecimal(4,2))asprice;
+-------+
|price|
+-------+
|49.95|
+-------+
1rowinset(0.00sec)
mysql>selectjson_value('{"item":"shoes","price":"49.95"}','$.price1'erroronempty);
ERROR3966(22035):Novaluewasfoundby'json_value'onthespecifiedpath.
mysql>selectjson_value('[1,2,3]','$[1to2]'erroronerror);
ERROR3967(22034):Morethanonevaluewasfoundby'json_value'onthespecifiedpath.
mysql>selectjson_value('{"item":"shoes","price":"49.95"}','$.item'returningunsignederroronerror)asprice;
ERROR1690(22003):UNSIGNEDvalueisoutofrangein'json_value'
value MEMBER OF(json_array)
判断 value 是否是 JSON 数组的一个元素,如果是,则返回 1,否则是 0。
mysql>select17memberof('[23,"abc",17,"ab",10]');
+-------------------------------------------+
|17memberof('[23,"abc",17,"ab",10]')|
+-------------------------------------------+
|1|
+-------------------------------------------+
1rowinset(0.00sec)
mysql>selectcast('[4,5]'asjson)memberof('[[3,4],[4,5]]');
+--------------------------------------------------+
|cast('[4,5]'asjson)memberof('[[3,4],[4,5]]')|
+--------------------------------------------------+
|1|
+--------------------------------------------------+
1rowinset(0.00sec)
JSON_OVERLAPS(json_doc1, json_doc2)
MySQL 8.0.17 引入的,用来比较两个 JSON 文档是否有相同的键值对或数组元素,如果有,则返回 1,否则是 0。如果两个参数都是标量,则判断这两个标量是否相等。
mysql>selectjson_overlaps('[1,3,5,7]','[2,5,7]'),json_overlaps('[1,3,5,7]','[2,6,8]');
+---------------------------------------+---------------------------------------+
|json_overlaps('[1,3,5,7]','[2,5,7]')|json_overlaps('[1,3,5,7]','[2,6,8]')|
+---------------------------------------+---------------------------------------+
|1|0|
+---------------------------------------+---------------------------------------+
1rowinset(0.00sec)
mysql>selectjson_overlaps('{"a":1,"b":2}','{"c":3,"d":4,"b":2}');
+-------------------------------------------------------+
|json_overlaps('{"a":1,"b":2}','{"c":3,"d":4,"b":2}')|
+-------------------------------------------------------+
|1|
+-------------------------------------------------------+
1rowinset(0.00sec)
mysql>selectjson_overlaps('{"a":1,"b":2}','{"c":3,"d":4,"b":10}');
+--------------------------------------------------------+
|json_overlaps('{"a":1,"b":2}','{"c":3,"d":4,"b":10}')|
+--------------------------------------------------------+
|0|
+--------------------------------------------------------+
1rowinset(0.00sec)
mysql>selectjson_overlaps('5','5'),json_overlaps('5','6');
+-------------------------+-------------------------+
|json_overlaps('5','5')|json_overlaps('5','6')|
+-------------------------+-------------------------+
|1|0|
+-------------------------+-------------------------+
1rowinset(0.00sec)
从 MySQL 8.0.17 开始,InnoDB 支持多值索引,可用在 JSON 数组中。当我们使用 JSON_CONTAINS、MEMBER OF、JSON_OVERLAPS 进行数组相关的操作时,可使用多值索引来加快查询。
7.2 修改相关
JSON_ARRAY_APPEND(json_doc, path, val[, path, val] ...)
向数组指定位置追加元素。如果指定 path 不存在,则不添加。
mysql>set@j='["a",["b","c"],"d"]';
QueryOK,0rowsaffected(0.00sec)
mysql>selectjson_array_append(@j,'$[0]',1,'$[1][0]',2,'$[3]',3);
+-----------------------------------------------------------+
|json_array_append(@j,'$[0]',1,'$[1][0]',2,'$[3]',3)|
+-----------------------------------------------------------+
|[["a",1],[["b",2],"c"],"d"]|
+-----------------------------------------------------------+
1rowinset(0.00sec)
mysql>set@j='{"a":1,"b":[2,3],"c":4}';
QueryOK,0rowsaffected(0.00sec)
mysql>selectjson_array_append(@j,'$.b','x','$','z');
+---------------------------------------------+
|json_array_append(@j,'$.b','x','$','z')|
+---------------------------------------------+
|[{"a":1,"b":[2,3,"x"],"c":4},"z"]|
+---------------------------------------------+
1rowinset(0.00sec)
JSON_ARRAY_INSERT(json_doc, path, val[, path, val] ...)
向数组指定位置插入元素。
mysql>set@j='["a",["b","c"],{"d":"e"}]';
QueryOK,0rowsaffected(0.00sec)
mysql>selectjson_array_insert(@j,'$[0]',1);
+----------------------------------+
|json_array_insert(@j,'$[0]',1)|
+----------------------------------+
|[1,"a",["b","c"],{"d":"e"}]|
+----------------------------------+
1rowinset(0.00sec)
mysql>selectjson_array_insert(@j,'$[1]',cast('[1,2]'asjson));
+------------------------------------------------------+
|json_array_insert(@j,'$[1]',cast('[1,2]'asjson))|
+------------------------------------------------------+
|["a",[1,2],["b","c"],{"d":"e"}]|
+------------------------------------------------------+
1rowinset(0.00sec)
mysql>selectjson_array_insert(@j,'$[5]',2);
+----------------------------------+
|json_array_insert(@j,'$[5]',2)|
+----------------------------------+
|["a",["b","c"],{"d":"e"},2]|
+----------------------------------+
1rowinset(0.00sec)
JSON_MERGE_PATCH(json_doc, json_doc[, json_doc] ...)
MySQL 8.0.3 引入的,用来合并多个 JSON 文档。其合并规则如下:
- 如果两个文档不全是 JSON 对象,则合并后的结果是第二个文档。
- 如果两个文档都是 JSON 对象,且不存在着同名 KEY,则合并后的文档包括两个文档的所有元素,如果存在着同名 KEY,则第二个文档的值会覆盖第一个。
mysql>selectjson_merge_patch('[1,2]','[3,4]'),json_merge_patch('[1,2]','{"a":123}');
+--------------------------------------+------------------------------------------+
|json_merge_patch('[1,2]','[3,4]')|json_merge_patch('[1,2]','{"a":123}')|
+--------------------------------------+------------------------------------------+
|[3,4]|{"a":123}|
+--------------------------------------+------------------------------------------+
1rowinset(0.00sec)
mysql>selectjson_merge_patch('{"a":1}','{"b":2}'),json_merge_patch('{"a":1,"b":2}','{"a":3,"c":4}');
+------------------------------------------+-----------------------------------------------------------+
|json_merge_patch('{"a":1}','{"b":2}')|json_merge_patch('{"a":1,"b":2}','{"a":3,"c":4}')|
+------------------------------------------+-----------------------------------------------------------+
|{"a":1,"b":2}|{"a":3,"b":2,"c":4}|
+------------------------------------------+-----------------------------------------------------------+
1rowinset(0.00sec)
#如果第二个文档存在 null 值,文档合并后不会输出对应的 KEY。
mysql>selectjson_merge_patch('{"a":1,"b":2}','{"a":3,"b":null}');
+---------------------------------------------------------+
|json_merge_patch('{"a":1,"b":2}','{"a":3,"b":null}')|
+---------------------------------------------------------+
|{"a":3}|
+---------------------------------------------------------+
1rowinset(0.00sec)
JSON_MERGE_PRESERVE(json_doc, json_doc[, json_doc] ...)
MySQL 8.0.3 引入的,用来代替 JSON_MERGE。也是用来合并文档,但合并规则与 JSON_MERGE_PATCH 有所不同。
- 两个文档中,只要有一个文档是数组,则另外一个文档会合并到该数组中。
- 两个文档都是 JSON 对象,若存在着同名 KEY ,第二个文档并不会覆盖第一个,而是会将值 append 到第一个文档中。
mysql>selectjson_merge_preserve('1','2'),json_merge_preserve('[1,2]','[3,4]');
+------------------------------+-----------------------------------------+
|json_merge_preserve('1','2')|json_merge_preserve('[1,2]','[3,4]')|
+------------------------------+-----------------------------------------+
|[1,2]|[1,2,3,4]|
+------------------------------+-----------------------------------------+
1rowinset(0.00sec)
mysql>selectjson_merge_preserve('[1,2]','{"a":123}'),json_merge_preserve('{"a":123}','[3,4]');
+---------------------------------------------+--------------------------------------------+
|json_merge_preserve('[1,2]','{"a":123}')|json_merge_preserve('{"a":123}','[3,4]')|
+---------------------------------------------+--------------------------------------------+
|[1,2,{"a":123}]|[{"a":123},3,4]|
+---------------------------------------------+--------------------------------------------+
1rowinset(0.00sec)
mysql>selectjson_merge_preserve('{"a":1}','{"b":2}'),json_merge_preserve('{"a":1,"b":2}','{"a":3,"c":4}');
+---------------------------------------------+--------------------------------------------------------------+
|json_merge_preserve('{"a":1}','{"b":2}')|json_merge_preserve('{"a":1,"b":2}','{"a":3,"c":4}')|
+---------------------------------------------+--------------------------------------------------------------+
|{"a":1,"b":2}|{"a":[1,3],"b":2,"c":4}|
+---------------------------------------------+--------------------------------------------------------------+
1rowinset(0.00sec)
JSON_MERGE(json_doc, json_doc[, json_doc] ...)
与 JSON_MERGE_PRESERVE 作用一样,从 MySQL 8.0.3 开始不建议使用,后续会移除。
7.3 其它辅助函数
JSON_QUOTE(string)
生成有效的 JSON 字符串,主要是对一些特殊字符(如双引号)进行转义。
mysql>selectjson_quote('null'),json_quote('"null"'),json_quote('[1,2,3]');
+--------------------+----------------------+-------------------------+
|json_quote('null')|json_quote('"null"')|json_quote('[1,2,3]')|
+--------------------+----------------------+-------------------------+
|"null"|"\"null\""|"[1,2,3]"|
+--------------------+----------------------+-------------------------+
1rowinset(0.00sec)
除此之外,也可通过 CAST(value AS JSON) 进行类型转换。
JSON_UNQUOTE(json_val)
将 JSON 转义成字符串输出。
mysql>selectc2->'$.ename',json_unquote(c2->'$.ename'),
->json_valid(c2->'$.ename'),json_valid(json_unquote(c2->'$.ename'))fromt;
+---------------+-----------------------------+---------------------------+-----------------------------------------+
|c2->'$.ename'|json_unquote(c2->'$.ename')|json_valid(c2->'$.ename')|json_valid(json_unquote(c2->'$.ename'))|
+---------------+-----------------------------+---------------------------+-----------------------------------------+
|"jack"|jack|1|0|
|"mark"|mark|1|0|
+---------------+-----------------------------+---------------------------+-----------------------------------------+
2rowsinset(0.00sec)
直观地看,没加 JSON_UNQUOTE 字符串会用双引号引起来,加了 JSON_UNQUOTE 就没有。但本质上,前者是 JSON 中的 STRING 类型,后者是 MySQL 中的字符类型,这一点可通过 JSON_VALID 来判断。
JSON_OBJECTAGG(key, value)
取表中的两列作为参数,其中,第一列是 key,第二列是 value,返回 JSON 对象。如,
mysql>select*fromemp;
+--------+----------+--------+
|deptno|ename|sal|
+--------+----------+--------+
|10|emp_1001|100.00|
|10|emp_1002|200.00|
|20|emp_1003|300.00|
|20|emp_1004|400.00|
+--------+----------+--------+
4rowsinset(0.00sec)
mysql>selectjson_objectagg(ename,sal)fromemp;
+----------------------------------------------------------------------------------+
|json_objectagg(ename,sal)|
+----------------------------------------------------------------------------------+
|{"emp_1001":100.00,"emp_1002":200.00,"emp_1003":300.00,"emp_1004":400.00}|
+----------------------------------------------------------------------------------+
1rowinset(0.00sec)
mysql>selectdeptno,json_objectagg(ename,sal)fromempgroupbydeptno;
+--------+------------------------------------------+
|deptno|json_objectagg(ename,sal)|
+--------+------------------------------------------+
|10|{"emp_1001":100.00,"emp_1002":200.00}|
|20|{"emp_1003":300.00,"emp_1004":400.00}|
+--------+------------------------------------------+
2rowsinset(0.00sec)
JSON_ARRAYAGG(col_or_expr)
将列的值聚合成 JSON 数组,注意,JSON 数组中元素的顺序是随机的。
mysql>selectjson_arrayagg(ename)fromemp;
+--------------------------------------------------+
|json_arrayagg(ename)|
+--------------------------------------------------+
|["emp_1001","emp_1002","emp_1003","emp_1004"]|
+--------------------------------------------------+
1rowinset(0.00sec)
mysql>selectdeptno,json_arrayagg(ename)fromempgroupbydeptno;
+--------+--------------------------+
|deptno|json_arrayagg(ename)|
+--------+--------------------------+
|10|["emp_1001","emp_1002"]|
|20|["emp_1003","emp_1004"]|
+--------+--------------------------+
2rowsinset(0.00sec)
JSON_PRETTY(json_val)
将 JSON 格式化输出。
mysql>selectjson_pretty("[1,3,5]");
+------------------------+
|json_pretty("[1,3,5]")|
+------------------------+
|[
1,
3,
5
]|
+------------------------+
1rowinset(0.00sec)
mysql>selectjson_pretty('{"a":"10","b":"15","x":"25"}');
+---------------------------------------------+
|json_pretty('{"a":"10","b":"15","x":"25"}')|
+---------------------------------------------+
|{
"a":"10",
"b":"15",
"x":"25"
}|
+---------------------------------------------+
1rowinset(0.00sec)
JSON_STORAGE_FREE(json_val)
MySQL 8.0 新增的,与 Partial Updates 有关,用于计算 JSON 文档在进行部分更新后的剩余空间。
JSON_STORAGE_SIZE(json_val)
MySQL 5.7.22 引入的,用于计算 JSON 文档的空间使用情况。
JSON_DEPTH(json_doc)
返回 JSON 文档的最大深度。对于空数组,空对象,标量值,其深度为 1。
mysql>selectjson_depth('{}'),json_depth('[10,20]'),json_depth('[10,{"a":20}]');
+------------------+------------------------+-------------------------------+
|json_depth('{}')|json_depth('[10,20]')|json_depth('[10,{"a":20}]')|
+------------------+------------------------+-------------------------------+
|1|2|3|
+------------------+------------------------+-------------------------------+
1rowinset(0.00sec)
JSON_LENGTH(json_doc[, path])
返回 JSON 文档的长度,其计算规则如下:
- 如果是标量值,其长度为 1。
- 如果是数组,其长度为数组元素的个数。
- 如果是对象,其长度为对象元素的个数。
- 不包括嵌套数据和嵌套对象的长度。
mysql>selectjson_length('"abc"');
+----------------------+
|json_length('"abc"')|
+----------------------+
|1|
+----------------------+
1rowinset(0.00sec)
mysql>selectjson_length('[1,2,{"a":3}]');
+---------------------------------+
|json_length('[1,2,{"a":3}]')|
+---------------------------------+
|3|
+---------------------------------+
1rowinset(0.00sec)
mysql>selectjson_length('{"a":1,"b":{"c":30}}');
+-----------------------------------------+
|json_length('{"a":1,"b":{"c":30}}')|
+-----------------------------------------+
|2|
+-----------------------------------------+
1rowinset(0.00sec)
mysql>selectjson_length('{"a":1,"b":{"c":30}}','$.a');
+------------------------------------------------+
|json_length('{"a":1,"b":{"c":30}}','$.a')|
+------------------------------------------------+
|1|
+------------------------------------------------+
1rowinset(0.00sec)
JSON_TYPE(json_val)
返回 JSON 值的类型。
mysql>selectjson_type('123');
+------------------+
|json_type('123')|
+------------------+
|INTEGER|
+------------------+
1rowinset(0.00sec)
mysql>selectjson_type('"abc"');
+--------------------+
|json_type('"abc"')|
+--------------------+
|STRING|
+--------------------+
1rowinset(0.00sec)
mysql>selectjson_type(cast(now()asjson));
+--------------------------------+
|json_type(cast(now()asjson))|
+--------------------------------+
|DATETIME|
+--------------------------------+
1rowinset(0.00sec)
mysql>selectjson_type(json_extract('{"a":[10,true]}','$.a'));
+-----------------------------------------------------+
|json_type(json_extract('{"a":[10,true]}','$.a'))|
+-----------------------------------------------------+
|ARRAY|
+-----------------------------------------------------+
1rowinset(0.00sec)
JSON_VALID(val)
判断给定值是否是有效的 JSON 文档。
mysql>selectjson_valid('hello'),json_valid('"hello"');
+---------------------+-----------------------+
|json_valid('hello')|json_valid('"hello"')|
+---------------------+-----------------------+
|0|1|
+---------------------+-----------------------+
1rowinset(0.00sec)
JSON_TABLE(expr, path COLUMNS (column_list) [AS] alias)
从 JSON 文档中提取数据并以表格的形式返回。
该函数的完整语法如下:
JSON_TABLE(
expr,
pathCOLUMNS(column_list)
)[AS]alias
column_list:
column[,column][,...]
column:
nameFORORDINALITY
|nametypePATHstring_path[on_empty][on_error]
|nametypeEXISTSPATHstring_path
|NESTED[PATH]pathCOLUMNS(column_list)
on_empty:
{NULL|DEFAULTjson_string|ERROR}ONEMPTY
on_error:
{NULL|DEFAULTjson_string|ERROR}ONERROR
其中,
- expr:可以返回 JSON 文档的表达式。可以是一个标量( JSON 文档 ),列名或者一个函数调用( JSON_EXTRACT(t1.json_data,'$.post.comments') )。
- path:JSON 的路径表达式,
- column:列的类型,支持以下四种类型:
- name FOR ORDINALITY:序号。name 是列名。
- name type PATH string_path [on_empty] [on_error]:提取指定路径( string_path )的元素。name 是列名,type 是 MySQL 中的数据类型。
- name type EXISTS PATH string_path:指定路径( string_path )的元素是否存在。
- NESTED [PATH] path COLUMNS (column_list):将嵌套对象或数组与来自父对象或数组的 JSON 值扁平化为一行输出。
select*
from
json_table(
'[{"x":2,"y":"8","z":9,"b":[1,2,3]},{"x":"3","y":"7"},{"x":"4","y":6,"z":10}]',
"$[*]"columns(
idforordinality,
xvalvarchar(100)path"$.x",
yvalvarchar(100)path"$.y",
z_existintexistspath"$.z",
nestedpath'$.b[*]'columns(bINTPATH'$')
)
)ast;
+------+------+------+---------+------+
|id|xval|yval|z_exist|b|
+------+------+------+---------+------+
|1|2|8|1|1|
|1|2|8|1|2|
|1|2|8|1|3|
|2|3|7|0|NULL|
|3|4|6|1|NULL|
+------+------+------+---------+------+
5rowsinset(0.00sec)
JSON_SCHEMA_VALID(schema,document)
判断 document ( JSON 文档 )是否满足 schema ( JSON 对象)定义的规范要求。完整的规范要求可参考 Draft 4 of the JSON Schema specification 。如果不满足,可通过 JSON_SCHEMA_VALIDATION_REPORT() 获取具体的原因。
以下面这个 schema 为例。
set@schema='{
"type":"object",
"properties":{
"latitude":{
"type":"number",
"minimum":-90,
"maximum":90
},
"longitude":{
"type":"number",
"minimum":-180,
"maximum":180
}
},
"required":["latitude","longitude"]
}';
它的要求如下:
- document 必须是 JSON 对象。
- JSON 对象必需的两个属性是 latitude 和 longitude。
- latitude 和 longitude 必须是数值类型,且两者的大小分别在 -90 ~ 90,-180 ~ 180 之间。
下面通过具体的 document 来测试一下。
mysql>set@document='{"latitude":63.444697,"longitude":10.445118}';
八、总结
QueryOK,0rowsaffected(0.00sec)
mysql>selectjson_schema_valid(@schema,@document);
+---------------------------------------+
|json_schema_valid(@schema,@document)|
+---------------------------------------+
|1|
+---------------------------------------+
1rowinset(0.00sec)
mysql>set@document='{"latitude":63.444697}';
QueryOK,0rowsaffected(0.00sec)
mysql>selectjson_schema_valid(@schema,@document);
+---------------------------------------+
|json_schema_valid(@schema,@document)|
+---------------------------------------+
|0|
+---------------------------------------+
1rowinset(0.00sec)
mysql>selectjson_pretty(json_schema_validation_report(@schema,@document))\G
***************************1.row***************************
json_pretty(json_schema_validation_report(@schema,@document)):{
"valid":false,
"reason":"TheJSONdocumentlocation'#'failedrequirement'required'atJSONSchemalocation'#'",
"schema-location":"#",
"document-location":"#",
"schema-failed-keyword":"required"
}
1rowinset(0.00sec)
mysql>set@document='{"latitude":91,"longitude":0}';
QueryOK,0rowsaffected(0.00sec)
mysql>selectjson_schema_valid(@schema,@document);
+---------------------------------------+
|json_schema_valid(@schema,@document)|
+---------------------------------------+
|0|
+---------------------------------------+
1rowinset(0.00sec)
mysql>selectjson_pretty(json_schema_validation_report(@schema,@document))\G
***************************1.row***************************
json_pretty(json_schema_validation_report(@schema,@document)):{
"valid":false,
"reason":"TheJSONdocumentlocation'#/latitude'failedrequirement'maximum'atJSONSchemalocation'#/properties/latitude'",
"schema-location":"#/properties/latitude",
"document-location":"#/latitude",
"schema-failed-keyword":"maximum"
}
1rowinset(0.00sec)
如果要使用 JSON 类型,推荐使用 MySQL 8.0。相比于 MySQL 5.7,Partial update 带来的性能提升还是十分明显的。
Partial update 在存储引擎层是默认开启的,binlog 中是否开启取决于 binlog_row_value_options 。该参数默认为空,不会开启 Partial update,建议设置为 PARTIAL_JSON。
注意使用 Partial update 的前提条件。
当我们使用 JSON_CONTAINS、MEMBER OF、JSON_OVERLAPS 进行数组相关的操作时,可使用 MySQL 8.0.17 引入的多值索引来加快查询。
九、参考资料-
JSON
-
The JSON Data Type
-
JSON Functions
-
Upgrading JSON data stored in TEXT columns
-
Indexing JSON documents via Virtual Columns
-
Partial update of JSON values
-
MySQL 8.0: InnoDB Introduces LOB Index For Faster Updates
本文共计9973个文字,预计阅读时间需要40分钟。
JSON数据类型是MySQL 5.7.8及以上版本开始支持的。在此之前,只能通过字符类型(CHAR、VARCHAR或TEXT)来存储JSON文档。对于字符类型,原生JSON类型具有以下优势:在插入时能自动校验文档。
JSON 数据类型是 MySQL 5.7.8 开始支持的。在此之前,只能通过字符类型(CHAR,VARCHAR 或 TEXT )来保存 JSON 文档。
相对字符类型,原生的 JSON 类型具有以下优势:
- 在插入时能自动校验文档是否满足 JSON 格式的要求。
- 优化了存储格式。无需读取整个文档就能快速访问某个元素的值。
在 JSON 类型引入之前,如果我们想要获取 JSON 文档中的某个元素,必须首先读取整个 JSON 文档,然后在客户端将其转换为 JSON 对象,最后再通过对象获取指定元素的值。
下面是 Python 中的获取方式。
importjson
#JSON字符串:
x='{"name":"John","age":30,"city":"NewYork"}'
#将JSON字符串转换为JSON对象:
y=json.loads(x)
#读取JSON对象中指定元素的值:
print(y["age"])
这种方式有两个弊端:一、消耗磁盘 IO,二、消耗网络带宽,如果 JSON 文档比较大,在高并发场景,有可能会打爆网卡。
如果使用的是 JSON 类型,相同的需求,直接使用 SQL 命令就可搞定。不仅能节省网络带宽,结合后面提到的函数索引,还能降低磁盘 IO 消耗。
mysql>createtablet(c1json);
QueryOK,0rowsaffected(0.09sec)
mysql>insertintotvalues('{"name":"John","age":30,"city":"NewYork"}');
QueryOK,1rowaffected(0.01sec)
mysql>selectc1->"$.age"fromt;
+-------------+
|c1->"$.age"|
+-------------+
|30|
+-------------+
1rowinset(0.00sec)
本文将从以下几个方面展开:
- 什么是 JSON。
- JSON 字段的增删改查操作。
- 如何对 JSON 字段创建索引。
- 如何将存储 JSON 字符串的字符字段升级为 JSON 字段。
- 使用 JSON 时的注意事项。
- Partial Updates。
- 其它 JSON 函数。
JSON 是 JavaScript Object Notation(JavaScript 对象表示法)的缩写,是一个轻量级的,基于文本的,跨语言的数据交换格式。易于阅读和编写。
JSON 的基本数据类型如下:
-
数值:十进制数,不能有前导 0,可以为负数或小数,还可以为 e 或 E 表示的指数。
-
字符串:字符串必须用双引号括起来。
-
布尔值:true,false。
-
数组:一个由零或多个值组成的有序序列。每个值可以为任意类型。数组使用方括号
[]括起来,元素之间用逗号,分隔。譬如,[1,"abc",null,true,"10:27:06.000000",{"id":1}] -
对象:一个由零或者多个键值对组成的无序集合。其中键必须是字符串,值可以为任意类型。
对象使用花括号
{}括起来,键值对之间使用逗号,分隔,键与值之间用冒号:分隔。譬如,{"db":["mysql","oracle"],"id":123,"info":{"age":20}} -
空值:null。
下面我们看看 JSON 字段常见的增删改查操作:
2.1 插入操作可直接插入 JSON 格式的字符串。
mysql>createtablet(c1json);
QueryOK,0rowsaffected(0.03sec)
mysql>insertintotvalues('[1,"abc",null,true,"08:45:06.000000"]');
QueryOK,1rowaffected(0.01sec)
mysql>insertintotvalues('{"id":87,"name":"carrot"}');
QueryOK,1rowaffected(0.01sec)
也可使用函数,常用的有 JSON_ARRAY() 和 JSON_OBJECT(),前者用于构造 JSON 数组,后者用于构造 JSON 对象。如,
mysql>selectjson_array(1,"abc",null,true,curtime());
+--------------------------------------------+
|json_array(1,"abc",null,true,curtime())|
+--------------------------------------------+
|[1,"abc",null,true,"10:12:25.000000"]|
+--------------------------------------------+
1rowinset(0.01sec)
mysql>selectjson_object('id',87,'name','carrot');
+-----------------------------------------+
|json_object('id',87,'name','carrot')|
+-----------------------------------------+
|{"id":87,"name":"carrot"}|
+-----------------------------------------+
1rowinset(0.00sec)
对于 JSON 文档,KEY 名不能重复。
如果插入的值中存在重复 KEY,在 MySQL 8.0.3 之前,遵循 first duplicate key wins 原则,会保留第一个 KEY,后面的将被丢弃掉。
从 MySQL 8.0.3 开始,遵循的是 last duplicate key wins 原则,只会保留最后一个 KEY。
下面通过一个具体的示例来看看两者的区别。
MySQL 5.7.36
mysql>selectjson_object('key1',10,'key2',20,'key1',30);
+--------------------------------------------+
|json_object('key1',10,'key2',20,'key1',30)|
+--------------------------------------------+
|{"key1":10,"key2":20}|
+--------------------------------------------+
1rowinset(0.02sec)
MySQL 8.0.27
mysql>selectjson_object('key1',10,'key2',20,'key1',30);
+--------------------------------------------+
|json_object('key1',10,'key2',20,'key1',30)|
+--------------------------------------------+
|{"key1":30,"key2":20}|
+--------------------------------------------+
1rowinset(0.00sec)
2.2 查询操作
JSON_EXTRACT(json_doc, path[, path] ...)
其中,json_doc 是 JSON 文档,path 是路径。该函数会从 JSON 文档提取指定路径(path)的元素。如果指定 path 不存在,会返回 NULL。可指定多个 path,匹配到的多个值会以数组形式返回。
下面我们结合一些具体的示例来看看 path 及 JSON_EXTRACT 的用法。
首先我们看看数组。
数组的路径是通过下标来表示的。第一个元素的下标是 0。
mysql>selectjson_extract('[10,20,[30,40]]','$[0]');
+--------------------------------------------+
|json_extract('[10,20,[30,40]]','$[0]')|
+--------------------------------------------+
|10|
+--------------------------------------------+
1rowinset(0.00sec)
mysql>selectjson_extract('[10,20,[30,40]]','$[0]','$[1]','$[2][0]');
+--------------------------------------------------------------+
|json_extract('[10,20,[30,40]]','$[0]','$[1]','$[2][0]')|
+--------------------------------------------------------------+
|[10,20,30]|
+--------------------------------------------------------------+
1rowinset(0.00sec)
除此之外,还可通过[M to N]获取数组的子集。
mysql>selectjson_extract('[10,20,[30,40]]','$[0to1]');
+-------------------------------------------------+
|json_extract('[10,20,[30,40]]','$[0to1]')|
+-------------------------------------------------+
|[10,20]|
+-------------------------------------------------+
1rowinset(0.00sec)
#这里的last代表最后一个元素的下标
mysql>selectjson_extract('[10,20,[30,40]]','$[last-1tolast]');
+---------------------------------------------------------+
|json_extract('[10,20,[30,40]]','$[last-1tolast]')|
+---------------------------------------------------------+
|[20,[30,40]]|
+---------------------------------------------------------+
1rowinset(0.00sec)
也可通过[*]获取数组中的所有元素。
mysql>selectjson_extract('[10,20,[30,40]]','$[*]');
+--------------------------------------------+
|json_extract('[10,20,[30,40]]','$[*]')|
+--------------------------------------------+
|[10,20,[30,40]]|
+--------------------------------------------+
1rowinset(0.00sec)
接下来,我们看看对象。
对象的路径是通过 KEY 来表示的。
mysql>set@j='{"a":1,"b":[2,3],"ac":4}';
QueryOK,0rowsaffected(0.00sec)
#如果 KEY 在路径表达式中不合法(譬如存在空格),则在引用这个 KEY 时,需用双引号括起来。
mysql>selectjson_extract(@j,'$.a'),json_extract(@j,'$."ac"'),json_extract(@j,'$.b[1]');
+-------------------------+-----------------------------+----------------------------+
|json_extract(@j,'$.a')|json_extract(@j,'$."ac"')|json_extract(@j,'$.b[1]')|
+-------------------------+-----------------------------+----------------------------+
|1|4|3|
+-------------------------+-----------------------------+----------------------------+
1rowinset(0.00sec)
除此之外,还可通过.*获取对象中的所有元素。
mysql>selectjson_extract('{"a":1,"b":[2,3],"ac":4}','$.*');
+--------------------------------------------------------+
|json_extract('{"a":1,"b":[2,3],"ac":4}','$.*')|
+--------------------------------------------------------+
|[1,[2,3],4]|
+--------------------------------------------------------+
1rowinset(0.00sec)
#这里的$**.b匹配$.a.b和$.c.b
mysql>selectjson_extract('{"a":{"b":1},"c":{"b":2}}','$**.b');
+---------------------------------------------------------+
|json_extract('{"a":{"b":1},"c":{"b":2}}','$**.b')|
+---------------------------------------------------------+
|[1,2]|
+---------------------------------------------------------+
1rowinset(0.00sec)
column->path
column->path,包括后面讲到的 column->>path,都是语法糖,在实际使用的时候都会转化为 JSON_EXTRACT。
column->path 等同于 JSON_EXTRACT(column, path) ,只能指定一个path。
createtablet(c2json);
insertintotvalues('{"empno":1001,"ename":"jack"}'),('{"empno":1002,"ename":"mark"}');
mysql>selectc2,c2->"$.ename"fromt;
+----------------------------------+---------------+
|c2|c2->"$.ename"|
+----------------------------------+---------------+
|{"empno":1001,"ename":"jack"}|"jack"|
|{"empno":1002,"ename":"mark"}|"mark"|
+----------------------------------+---------------+
2rowsinset(0.00sec)
mysql>select*fromtwherec2->"$.empno"=1001;
+------+----------------------------------+
|c1|c2|
+------+----------------------------------+
|1|{"empno":1001,"ename":"jack"}|
+------+----------------------------------+
1rowinset(0.00sec)
column->>path
同 column->path 类似,只不过其返回的是字符串。以下三者是等价的。
- JSON_UNQUOTE( JSON_EXTRACT(column, path) )
- JSON_UNQUOTE(column -> path)
- column->>path
mysql>selectc2->'$.ename',json_extract(c2,"$.ename"),json_unquote(c2->'$.ename'),c2->>'$.ename'fromt;
+---------------+-----------------------------+-----------------------------+----------------+
|c2->'$.ename'|json_extract(c2,"$.ename")|json_unquote(c2->'$.ename')|c2->>'$.ename'|
+---------------+-----------------------------+-----------------------------+----------------+
|"jack"|"jack"|jack|jack|
|"mark"|"mark"|mark|mark|
+---------------+-----------------------------+-----------------------------+----------------+
2rowsinset(0.00sec)
2.3 修改操作
JSON_INSERT(json_doc, path, val[, path, val] ...)
插入新值。
仅当指定位置或指定 KEY 的值不存在时,才执行插入操作。另外,如果指定的 path 是数组下标,且 json_doc 不是数组,该函数首先会将 json_doc 转化为数组,然后再插入新值。
下面我们看几个示例。
mysql>selectjson_insert('1','$[0]',"10");
+------------------------------+
|json_insert('1','$[0]',"10")|
+------------------------------+
|1|
+------------------------------+
1rowinset(0.00sec)
mysql>selectjson_insert('1','$[1]',"10");
+------------------------------+
|json_insert('1','$[1]',"10")|
+------------------------------+
|[1,"10"]|
+------------------------------+
1rowinset(0.01sec)
mysql>selectjson_insert('["1","2"]','$[2]',"10");
+--------------------------------------+
|json_insert('["1","2"]','$[2]',"10")|
+--------------------------------------+
|["1","2","10"]|
+--------------------------------------+
1rowinset(0.00sec)
mysql>set@j='{"a":1,"b":[2,3]}';
QueryOK,0rowsaffected(0.00sec)
mysql>selectjson_insert(@j,'$.a',10,'$.c','[true,false]');
+----------------------------------------------------+
|json_insert(@j,'$.a',10,'$.c','[true,false]')|
+----------------------------------------------------+
|{"a":1,"b":[2,3],"c":"[true,false]"}|
+----------------------------------------------------+
1rowinset(0.00sec)
JSON_SET(json_doc, path, val[, path, val] ...)
插入新值,并替换已经存在的值。
换言之,如果指定位置或指定 KEY 的值不存在,会执行插入操作,如果存在,则执行更新操作。
mysql>set@j='{"a":1,"b":[2,3]}';
QueryOK,0rowsaffected(0.00sec)
mysql>selectjson_set(@j,'$.a',10,'$.c','[true,false]');
+-------------------------------------------------+
|json_set(@j,'$.a',10,'$.c','[true,false]')|
+-------------------------------------------------+
|{"a":10,"b":[2,3],"c":"[true,false]"}|
+-------------------------------------------------+
1rowinset(0.00sec)
JSON_REPLACE(json_doc, path, val[, path, val] ...)
替换已经存在的值。
mysql>set@j='{"a":1,"b":[2,3]}';
QueryOK,0rowsaffected(0.00sec)
mysql>selectjson_replace(@j,'$.a',10,'$.c','[true,false]');
+-----------------------------------------------------+
|json_replace(@j,'$.a',10,'$.c','[true,false]')|
+-----------------------------------------------------+
|{"a":10,"b":[2,3]}|
+-----------------------------------------------------+
1rowinset(0.00sec)
2.4 删除操作
JSON_REMOVE(json_doc, path[, path] ...)
删除 JSON 文档指定位置的元素。
mysql>set@j='{"a":1,"b":[2,3]}';
QueryOK,0rowsaffected(0.00sec)
mysql>selectjson_remove(@j,'$.a');
+------------------------+
|JSON_REMOVE(@j,'$.a')|
+------------------------+
|{"b":[2,3]}|
+------------------------+
1rowinset(0.00sec)
mysql>set@j='["a",["b","c"],"d","e"]';
QueryOK,0rowsaffected(0.00sec)
mysql>selectjson_remove(@j,'$[1]');
+-------------------------+
|JSON_REMOVE(@j,'$[1]')|
+-------------------------+
|["a","d","e"]|
+-------------------------+
1rowinset(0.00sec)
mysql>selectjson_remove(@j,'$[1]','$[2]');
+--------------------------------+
|JSON_REMOVE(@j,'$[1]','$[2]')|
+--------------------------------+
|["a","d"]|
+--------------------------------+
1rowinset(0.00sec)
mysql>selectjson_remove(@j,'$[1]','$[1]');
+--------------------------------+
|JSON_REMOVE(@j,'$[1]','$[1]')|
+--------------------------------+
|["a","e"]|
+--------------------------------+
1rowinset(0.00sec)
最后一个查询,虽然两个 path 都是 '$[1]' ,但作用对象不一样,第一个 path 的作用对象是 '["a", ["b", "c"], "d", "e"]' ,第二个 path 的作用对象是删除了 '$[1]' 后的数组,即 '["a", "d", "e"]' 。
三、如何对 JSON 字段创建索引同 TEXT,BLOB 字段一样,JSON 字段不允许直接创建索引。
mysql>createtablet(c1json,index(c1));
ERROR3152(42000):JSONcolumn'c1'supportsindexingonlyviageneratedcolumnsonaspecifiedJSONpath.
即使支持,实际意义也不大,因为我们一般是基于文档中的元素进行查询,很少会基于整个 JSON 文档。
对文档中的元素进行查询,就需要用到 MySQL 5.7 引入的虚拟列及函数索引。
下面我们来看一个具体的示例。
#C2即虚拟列
# index (c2)对虚拟列添加索引。
createtablet(c1json,c2varchar(10)as(JSON_UNQUOTE(c1->"$.name")),index(c2));
insertintot(c1)values('{"id":1,"name":"a"}'),('{"id":2,"name":"b"}'),('{"id":3,"name":"c"}'),('{"id":4,"name":"d"}');
mysql>explainselect*fromtwherec2='a';
+----+-------------+-------+------------+------+---------------+------+---------+-------+------+----------+-------+
|id|select_type|table|partitions|type|possible_keys|key|key_len|ref|rows|filtered|Extra|
+----+-------------+-------+------------+------+---------------+------+---------+-------+------+----------+-------+
|1|SIMPLE|t|NULL|ref|c2|c2|43|const|1|100.00|NULL|
+----+-------------+-------+------------+------+---------------+------+---------+-------+------+----------+-------+
1rowinset,1warning(0.00sec)
mysql>explainselect*fromtwherec1->'$.name'='a';
+----+-------------+-------+------------+------+---------------+------+---------+-------+------+----------+-------+
|id|select_type|table|partitions|type|possible_keys|key|key_len|ref|rows|filtered|Extra|
+----+-------------+-------+------------+------+---------------+------+---------+-------+------+----------+-------+
|1|SIMPLE|t|NULL|ref|c2|c2|43|const|1|100.00|NULL|
+----+-------------+-------+------------+------+---------------+------+---------+-------+------+----------+-------+
1rowinset,1warning(0.00sec)
可以看到,无论是使用虚拟列,还是文档中的元素来查询,都可以利用上索引。
注意,在创建虚拟列时需指定 JSON_UNQUOTE,将 c1 -> "$.name" 的返回值转换为字符串。
四、如何将存储 JSON 字符串的字符字段升级为 JSON 字段在 MySQL 支持 JSON 类型之前,对于 JSON 文档,一般是以字符串的形式存储在字符类型(VARCHAR 或 TEXT)中。
在 JSON 类型出来之后,如何将这些字符字段升级为 JSON 字段呢?
为方便演示,这里首先构建测试数据。
createtablet(idintauto_incrementprimarykey,c1text);
insertintot(c1)values('{"id":"1","name":"a"}'),('{"id":"2","name":"b"}'),('{"id":"3","name":"c"}'),('{"id","name":"d"}');
注意,最后一个文档有问题,不是合格的 JSON 文档。
如果使用 DDL 直接修改字段的数据类型,会报错。
mysql>altertabletmodifyc1json;
ERROR3140(22032):InvalidJSONtext:"Missingacolonafteranameofobjectmember."atposition5invalueforcolumn'#sql-7e1c_1f6.c1'.
下面,我们看看具体的升级步骤。
(1)使用 json_valid 函数找出不满足 JSON 格式要求的文档。
mysql>select*fromtwherejson_valid(c1)=0;
+----+---------------------+
|id|c1|
+----+---------------------+
|4|{"id","name":"d"}|
+----+---------------------+
1rowinset(0.00sec)
(2)处理不满足 JSON 格式要求的文档。
mysql>updatetsetc1='{"id":"4","name":"d"}'whereid=4;
QueryOK,1rowaffected(0.01sec)
Rowsmatched:1Changed:1Warnings:0
(3)将 TEXT 字段修改为 JSON 字段。
mysql>select*fromtwherejson_valid(c1)=0;
五、使用 JSON 时的注意事项
Emptyset(0.00sec)
mysql>altertabletmodifyc1json;
QueryOK,4rowsaffected(0.13sec)
Records:4Duplicates:0Warnings:0
对于 JSON 类型,有以下几点需要注意:
-
在 MySQL 8.0.13 之前,不允许对 BLOB,TEXT,GEOMETRY,JSON 字段设置默认值。从 MySQL 8.0.13 开始,取消了这个限制。
设置时,注意默认值需通过小括号
()括起来,否则的话,还是会提示 JSON 字段不允许设置默认值。mysql>createtablet(c1jsonnotnulldefault(''));
QueryOK,0rowsaffected(0.03sec)
mysql>createtablet(c1jsonnotnulldefault'');
ERROR1101(42000):BLOB,TEXT,GEOMETRYorJSONcolumn'c1'can'thaveadefaultvalue -
不允许直接创建索引,可创建函数索引。
-
JSON 列的最大大小和 LONGBLOB(LONGTEXT)一样,都是 4G。
-
插入时,单个文档的大小受到 max_allowed_packet 的限制,该参数最大是 1G。
在 MySQL 5.7 中,对 JSON 文档进行更新,其处理策略是,删除旧的文档,再插入新的文档。即使这个修改很微小,只涉及几个字节,也会替换掉整个文档。很显然,这种处理方式的效率较为低下。
在 MySQL 8.0 中,针对 JSON 文档,引入了一项新的特性-Partial Updates(部分更新),支持 JSON 文档的原地更新。得益于这个特性,JSON 文档的处理性能得到了极大提升。
下面我们具体来看看。
6.1 使用 Partial Updates 的条件为方便阐述,这里先构造测试数据。
createtablet(idintauto_incrementprimarykey,c1json);
insertintot(c1)values('{"id":1,"name":"a"}'),('{"id":2,"name":"b"}'),('{"id":3,"name":"c"}'),('{"id":4,"name":"d"}');
mysql>select*fromt;
+----+------------------------+
|id|c1|
+----+------------------------+
|1|{"id":1,"name":"a"}|
|2|{"id":2,"name":"b"}|
|3|{"id":3,"name":"c"}|
|4|{"id":4,"name":"d"}|
+----+------------------------+
4rowsinset(0.00sec)
使用 Partial Updates 需满足以下条件:
-
被更新的列是 JSON 类型。
-
使用 JSON_SET,JSON_REPLACE,JSON_REMOVE 进行 UPDATE 操作,如,
updatetsetc1=json_remove(c1,'$.id')whereid=1;不使用这三个函数,而显式赋值,就不会进行部分更新,如,
updatetsetc1='{"id":1,"name":"a"}'whereid=1; -
输入列和目标列必须是同一列,如,
updatetsetc1=json_replace(c1,'$.id',10)whereid=1;否则的话,就不会进行部分更新,如,
updatetsetc1=json_replace(c2,'$.id',10)whereid=1; -
变更前后,JSON 文档的空间使用不会增加。
关于最后一个条件,我们看看下面这个示例。
mysql>select*,json_storage_size(c1),json_storage_free(c1)fromtwhereid=1;
+----+------------------------+-----------------------+-----------------------+
|id|c1|json_storage_size(c1)|json_storage_free(c1)|
+----+------------------------+-----------------------+-----------------------+
|1|{"id":1,"name":"a"}|27|0|
+----+------------------------+-----------------------+-----------------------+
1rowinset(0.00sec)
mysql>updatetsetc1=json_remove(c1,'$.id')whereid=1;
QueryOK,1rowaffected(0.01sec)
Rowsmatched:1Changed:1Warnings:0
mysql>select*,json_storage_size(c1),json_storage_free(c1)fromtwhereid=1;
+----+---------------+-----------------------+-----------------------+
|id|c1|json_storage_size(c1)|json_storage_free(c1)|
+----+---------------+-----------------------+-----------------------+
|1|{"name":"a"}|27|9|
+----+---------------+-----------------------+-----------------------+
1rowinset(0.00sec)
mysql>updatetsetc1=json_set(c1,'$.id',3306)whereid=1;
QueryOK,1rowaffected(0.01sec)
Rowsmatched:1Changed:1Warnings:0
mysql>select*,json_storage_size(c1),json_storage_free(c1)fromtwhereid=1;
+----+---------------------------+-----------------------+-----------------------+
|id|c1|json_storage_size(c1)|json_storage_free(c1)|
+----+---------------------------+-----------------------+-----------------------+
|1|{"id":3306,"name":"a"}|27|0|
+----+---------------------------+-----------------------+-----------------------+
1rowinset(0.00sec)
mysql>updatetsetc1=json_set(c1,'$.id','mysql')whereid=1;
QueryOK,1rowaffected(0.01sec)
Rowsmatched:1Changed:1Warnings:0
mysql>select*,json_storage_size(c1),json_storage_free(c1)fromtwhereid=1;
+----+------------------------------+-----------------------+-----------------------+
|id|c1|json_storage_size(c1)|json_storage_free(c1)|
+----+------------------------------+-----------------------+-----------------------+
|1|{"id":"mysql","name":"a"}|33|0|
+----+------------------------------+-----------------------+-----------------------+
1rowinset(0.00sec)
示例中,用到了两个函数:JSON_STORAGE_SIZE 和 JSON_STORAGE_FREE ,前者用来获取 JSON 文档的空间使用情况,后者用来获取 JSON 文档在执行原地更新后的空间释放情况。
这里一共执行了三次 UPDATE 操作,前两次是原地更新,第三次不是。同样是 JSON_SET 操作,为什么第一次是原地更新,而第二次不是呢?
因为第一次的 JSON_SET 复用了 JSON_REMOVE 释放的空间。而第二次的 JSON_SET 执行的是更新操作,且 'mysql' 比 3306 需要更多的存储空间。
6.2 如何在 binlog 中开启 Partial Updates
Partial Updates 不仅仅适用于存储引擎层,还可用于主从复制场景。
主从复制开启 Partial Updates,只需将参数 binlog_row_value_options(默认为空)设置为 PARTIAL_JSON。
下面具体来看看,同一个 UPDATE 操作,开启和不开启 Partial Updates,在 binlog 中的记录有何区别。
updatetsetc1=json_replace(c1,'$.id',10)whereid=1;
不开启
###UPDATE`slowtech`.`t`
###WHERE
###@1=1
###@2='{"id":"1","name":"a"}'
###SET
###@1=1
###@2='{"id":10,"name":"a"}'
开启
###UPDATE`slowtech`.`t`
###WHERE
###@1=1
###@2='{"id":1,"name":"a"}'
###SET
###@1=1
###@2=JSON_REPLACE(@2,'$.id',10)
对比 binlog 的内容,可以看到,不开启,无论是修改前的镜像(before_image)还是修改后的镜像(after_image),记录的都是完整文档。而开启后,对于修改后的镜像,记录的是命令,而不是完整文档,这样可节省近一半的空间。
在将 binlog_row_value_options 设置为 PARTIAL_JSON 后,对于可使用 Partial Updates 的操作,在 binlog 中,不再通过 ROWS_EVENT 来记录,而是新增了一个 PARTIAL_UPDATE_ROWS_EVENT 的事件类型。
需要注意的是,binlog 中使用 Partial Updates,只需满足存储引擎层使用 Partial Updates 的前三个条件,无需考虑变更前后,JSON 文档的空间使用是否会增加。
6.3 关于 Partial Updates 的性能测试首先构造测试数据,t 表一共有 16 个文档,每个文档近 10 MB。
createtablet(idintauto_incrementprimarykey,
json_coljson,
namevarchar(100)as(json_col->>'$.name'),
ageintas(json_col->'$.age'));
insertintot(json_col)values
(json_object('name','Joe','age',24,
'data',repeat('x',10*1000*1000))),
(json_object('name','Sue','age',32,
'data',repeat('y',10*1000*1000))),
(json_object('name','Pete','age',40,
'data',repeat('z',10*1000*1000))),
(json_object('name','Jenny','age',27,
'data',repeat('w',10*1000*1000)));
insertintot(json_col)selectjson_colfromt;
insertintot(json_col)selectjson_colfromt;
接下来,测试下述 SQL
updatetsetjson_col=json_set(json_col,'$.age',age+1);
在以下四种场景下的执行时间:
- MySQL 5.7.36
- MySQL 8.0.27
- MySQL 8.0.27,binlog_row_value_options=PARTIAL_JSON
- MySQL 8.0.27,binlog_row_value_options=PARTIAL_JSON + binlog_row_image=MINIMAL
分别执行 10 次,去掉最大值和最小值后求平均值。
最后的测试结果如下:
以 MySQL 5.7.36 的查询时间作为基准:
- MySQL 8.0 只开启存储引擎层的 Partial Updates,查询时间比 MySQL 5.7 快 1.94 倍。
- MySQL 8.0 同时开启存储引擎层和 binlog 中的 Partial Updates,查询时间比 MySQL 5.7 快 4.87 倍。
- 如果在 2 的基础上,同时将 binlog_row_image 设置为 MINIMAL,查询时间更是比 MySQL 5.7 快 102.22 倍。
当然,在生产环境,我们一般很少将 binlog_row_image 设置为 MINIMAL。
但即使如此,只开启存储引擎层和 binlog 中的 Partial Updates,查询时间也比 MySQL 5.7 快 4.87 倍,性能提升还是比较明显的。
七、其它 JSON 函数 7.1 查询相关JSON_CONTAINS(target, candidate[, path])
判断 target 文档是否包含 candidate 文档,如果包含,则返回 1,否则是 0。
mysql>set@j='{"a":[1,2],"b":3,"c":{"d":4}}';
QueryOK,0rowsaffected(0.00sec)
mysql>selectjson_contains(@j,'1','$.a'),json_contains(@j,'1','$.b');
+-------------------------------+-------------------------------+
|json_contains(@j,'1','$.a')|json_contains(@j,'1','$.b')|
+-------------------------------+-------------------------------+
|1|0|
+-------------------------------+-------------------------------+
1rowinset(0.00sec)
mysql>selectjson_contains(@j,'{"d":4}','$.a'),json_contains(@j,'{"d":4}','$.c');
+------------------------------------+------------------------------------+
|json_contains(@j,'{"d":4}','$.a')|json_contains(@j,'{"d":4}','$.c')|
+------------------------------------+------------------------------------+
|0|1|
+------------------------------------+------------------------------------+
1rowinset(0.00sec)
JSON_CONTAINS_PATH(json_doc, one_or_all, path[, path] ...)
判断指定的 path 是否存在,存在,则返回 1,否则是 0。
函数中的 one_or_all 可指定 one 或 all,one 是任意一个路径存在就返回 1,all 是所有路径都存在才返回 1。
mysql>set@j='{"a":[1,2],"b":3,"c":{"d":4}}';
QueryOK,0rowsaffected(0.00sec)
mysql>selectjson_contains_path(@j,'one','$.a','$.e'),json_contains_path(@j,'all','$.a','$.e');
+---------------------------------------------+---------------------------------------------+
|json_contains_path(@j,'one','$.a','$.e')|json_contains_path(@j,'all','$.a','$.e')|
+---------------------------------------------+---------------------------------------------+
|1|0|
+---------------------------------------------+---------------------------------------------+
1rowinset(0.00sec)
mysql>selectjson_contains_path(@j,'one','$.c.d'),json_contains_path(@j,'one','$.a.d');
+----------------------------------------+----------------------------------------+
|json_contains_path(@j,'one','$.c.d')|json_contains_path(@j,'one','$.a.d')|
+----------------------------------------+----------------------------------------+
|1|0|
+----------------------------------------+----------------------------------------+
1rowinset(0.00sec)
JSON_SEARCH(json_doc, one_or_all, search_str[, escape_char[, path] ...])
返回某个字符串(search_str)在 JSON 文档中的位置,其中,
- one_or_all:匹配的次数,one 是只匹配一次,all 是匹配所有。如果匹配到多个,结果会以数组的形式返回。
- search_str:子串,支持模糊匹配:
%和_。 - escape_char:转义符,如果该参数不填或为 NULL,则取默认转义符
\。 - path:查找路径。
mysql>set@j='["abc",[{"k":"10"},"def"],{"x":"abc"},{"y":"bcd"}]';
QueryOK,0rowsaffected(0.00sec)
mysql>selectjson_search(@j,'one','abc'),json_search(@j,'all','abc'),json_search(@j,'all','ghi');
+-------------------------------+-------------------------------+-------------------------------+
|json_search(@j,'one','abc')|json_search(@j,'all','abc')|json_search(@j,'all','ghi')|
+-------------------------------+-------------------------------+-------------------------------+
|"$[0]"|["$[0]","$[2].x"]|NULL|
+-------------------------------+-------------------------------+-------------------------------+
1rowinset(0.00sec)
mysql>selectjson_search(@j,'all','%b%',NULL,'$[1]'),json_search(@j,'all','%b%',NULL,'$[3]');
+---------------------------------------------+---------------------------------------------+
|json_search(@j,'all','%b%',NULL,'$[1]')|json_search(@j,'all','%b%',NULL,'$[3]')|
+---------------------------------------------+---------------------------------------------+
|NULL|"$[3].y"|
+---------------------------------------------+---------------------------------------------+
1rowinset(0.00sec)
JSON_KEYS(json_doc[, path])
返回 JSON 文档最外层的 key,如果指定了 path,则返回该 path 对应元素最外层的 key。
mysql>selectjson_keys('{"a":1,"b":{"c":30}}');
+---------------------------------------+
|json_keys('{"a":1,"b":{"c":30}}')|
+---------------------------------------+
|["a","b"]|
+---------------------------------------+
1rowinset(0.00sec)
mysql>selectjson_keys('{"a":1,"b":{"c":30}}','$.b');
+----------------------------------------------+
|json_keys('{"a":1,"b":{"c":30}}','$.b')|
+----------------------------------------------+
|["c"]|
+----------------------------------------------+
1rowinset(0.00sec)
JSON_VALUE(json_doc, path)
8.0.21 引入的,从 JSON 文档提取指定路径(path)的元素。
该函数的完整语法如下:
JSON_VALUE(json_doc,path[RETURNINGtype][on_empty][on_error])
on_empty:
{NULL|ERROR|DEFAULTvalue}ONEMPTY
on_error:
{NULL|ERROR|DEFAULTvalue}ONERROR
其中:
- RETURNING type:返回值的类型,不指定,则默认是 VARCHAR(512)。不指定字符集,则默认是 utf8mb4,且区分大小写。
- on_empty:如果指定路径没有值,会触发 on_empty 子句, 默认是返回 NULL,也可指定 ERROR 抛出错误,或者通过 DEFAULT value 返回默认值。
- on_error:三种情况下会触发 on_error 子句:从数组或对象中提取元素时,会解析到多个值;类型转换错误,譬如将 "abc" 转换为 unsigned 类型;值被 truncate 了。默认是返回 NULL。
mysql>selectjson_value('{"item":"shoes","price":"49.95"}','$.item');
+-------------------------------------------------------------+
|json_value('{"item":"shoes","price":"49.95"}','$.item')|
+-------------------------------------------------------------+
|shoes|
+-------------------------------------------------------------+
1rowinset(0.00sec)
mysql>selectjson_value('{"item":"shoes","price":"49.95"}','$.price'returningdecimal(4,2))asprice;
+-------+
|price|
+-------+
|49.95|
+-------+
1rowinset(0.00sec)
mysql>selectjson_value('{"item":"shoes","price":"49.95"}','$.price1'erroronempty);
ERROR3966(22035):Novaluewasfoundby'json_value'onthespecifiedpath.
mysql>selectjson_value('[1,2,3]','$[1to2]'erroronerror);
ERROR3967(22034):Morethanonevaluewasfoundby'json_value'onthespecifiedpath.
mysql>selectjson_value('{"item":"shoes","price":"49.95"}','$.item'returningunsignederroronerror)asprice;
ERROR1690(22003):UNSIGNEDvalueisoutofrangein'json_value'
value MEMBER OF(json_array)
判断 value 是否是 JSON 数组的一个元素,如果是,则返回 1,否则是 0。
mysql>select17memberof('[23,"abc",17,"ab",10]');
+-------------------------------------------+
|17memberof('[23,"abc",17,"ab",10]')|
+-------------------------------------------+
|1|
+-------------------------------------------+
1rowinset(0.00sec)
mysql>selectcast('[4,5]'asjson)memberof('[[3,4],[4,5]]');
+--------------------------------------------------+
|cast('[4,5]'asjson)memberof('[[3,4],[4,5]]')|
+--------------------------------------------------+
|1|
+--------------------------------------------------+
1rowinset(0.00sec)
JSON_OVERLAPS(json_doc1, json_doc2)
MySQL 8.0.17 引入的,用来比较两个 JSON 文档是否有相同的键值对或数组元素,如果有,则返回 1,否则是 0。如果两个参数都是标量,则判断这两个标量是否相等。
mysql>selectjson_overlaps('[1,3,5,7]','[2,5,7]'),json_overlaps('[1,3,5,7]','[2,6,8]');
+---------------------------------------+---------------------------------------+
|json_overlaps('[1,3,5,7]','[2,5,7]')|json_overlaps('[1,3,5,7]','[2,6,8]')|
+---------------------------------------+---------------------------------------+
|1|0|
+---------------------------------------+---------------------------------------+
1rowinset(0.00sec)
mysql>selectjson_overlaps('{"a":1,"b":2}','{"c":3,"d":4,"b":2}');
+-------------------------------------------------------+
|json_overlaps('{"a":1,"b":2}','{"c":3,"d":4,"b":2}')|
+-------------------------------------------------------+
|1|
+-------------------------------------------------------+
1rowinset(0.00sec)
mysql>selectjson_overlaps('{"a":1,"b":2}','{"c":3,"d":4,"b":10}');
+--------------------------------------------------------+
|json_overlaps('{"a":1,"b":2}','{"c":3,"d":4,"b":10}')|
+--------------------------------------------------------+
|0|
+--------------------------------------------------------+
1rowinset(0.00sec)
mysql>selectjson_overlaps('5','5'),json_overlaps('5','6');
+-------------------------+-------------------------+
|json_overlaps('5','5')|json_overlaps('5','6')|
+-------------------------+-------------------------+
|1|0|
+-------------------------+-------------------------+
1rowinset(0.00sec)
从 MySQL 8.0.17 开始,InnoDB 支持多值索引,可用在 JSON 数组中。当我们使用 JSON_CONTAINS、MEMBER OF、JSON_OVERLAPS 进行数组相关的操作时,可使用多值索引来加快查询。
7.2 修改相关
JSON_ARRAY_APPEND(json_doc, path, val[, path, val] ...)
向数组指定位置追加元素。如果指定 path 不存在,则不添加。
mysql>set@j='["a",["b","c"],"d"]';
QueryOK,0rowsaffected(0.00sec)
mysql>selectjson_array_append(@j,'$[0]',1,'$[1][0]',2,'$[3]',3);
+-----------------------------------------------------------+
|json_array_append(@j,'$[0]',1,'$[1][0]',2,'$[3]',3)|
+-----------------------------------------------------------+
|[["a",1],[["b",2],"c"],"d"]|
+-----------------------------------------------------------+
1rowinset(0.00sec)
mysql>set@j='{"a":1,"b":[2,3],"c":4}';
QueryOK,0rowsaffected(0.00sec)
mysql>selectjson_array_append(@j,'$.b','x','$','z');
+---------------------------------------------+
|json_array_append(@j,'$.b','x','$','z')|
+---------------------------------------------+
|[{"a":1,"b":[2,3,"x"],"c":4},"z"]|
+---------------------------------------------+
1rowinset(0.00sec)
JSON_ARRAY_INSERT(json_doc, path, val[, path, val] ...)
向数组指定位置插入元素。
mysql>set@j='["a",["b","c"],{"d":"e"}]';
QueryOK,0rowsaffected(0.00sec)
mysql>selectjson_array_insert(@j,'$[0]',1);
+----------------------------------+
|json_array_insert(@j,'$[0]',1)|
+----------------------------------+
|[1,"a",["b","c"],{"d":"e"}]|
+----------------------------------+
1rowinset(0.00sec)
mysql>selectjson_array_insert(@j,'$[1]',cast('[1,2]'asjson));
+------------------------------------------------------+
|json_array_insert(@j,'$[1]',cast('[1,2]'asjson))|
+------------------------------------------------------+
|["a",[1,2],["b","c"],{"d":"e"}]|
+------------------------------------------------------+
1rowinset(0.00sec)
mysql>selectjson_array_insert(@j,'$[5]',2);
+----------------------------------+
|json_array_insert(@j,'$[5]',2)|
+----------------------------------+
|["a",["b","c"],{"d":"e"},2]|
+----------------------------------+
1rowinset(0.00sec)
JSON_MERGE_PATCH(json_doc, json_doc[, json_doc] ...)
MySQL 8.0.3 引入的,用来合并多个 JSON 文档。其合并规则如下:
- 如果两个文档不全是 JSON 对象,则合并后的结果是第二个文档。
- 如果两个文档都是 JSON 对象,且不存在着同名 KEY,则合并后的文档包括两个文档的所有元素,如果存在着同名 KEY,则第二个文档的值会覆盖第一个。
mysql>selectjson_merge_patch('[1,2]','[3,4]'),json_merge_patch('[1,2]','{"a":123}');
+--------------------------------------+------------------------------------------+
|json_merge_patch('[1,2]','[3,4]')|json_merge_patch('[1,2]','{"a":123}')|
+--------------------------------------+------------------------------------------+
|[3,4]|{"a":123}|
+--------------------------------------+------------------------------------------+
1rowinset(0.00sec)
mysql>selectjson_merge_patch('{"a":1}','{"b":2}'),json_merge_patch('{"a":1,"b":2}','{"a":3,"c":4}');
+------------------------------------------+-----------------------------------------------------------+
|json_merge_patch('{"a":1}','{"b":2}')|json_merge_patch('{"a":1,"b":2}','{"a":3,"c":4}')|
+------------------------------------------+-----------------------------------------------------------+
|{"a":1,"b":2}|{"a":3,"b":2,"c":4}|
+------------------------------------------+-----------------------------------------------------------+
1rowinset(0.00sec)
#如果第二个文档存在 null 值,文档合并后不会输出对应的 KEY。
mysql>selectjson_merge_patch('{"a":1,"b":2}','{"a":3,"b":null}');
+---------------------------------------------------------+
|json_merge_patch('{"a":1,"b":2}','{"a":3,"b":null}')|
+---------------------------------------------------------+
|{"a":3}|
+---------------------------------------------------------+
1rowinset(0.00sec)
JSON_MERGE_PRESERVE(json_doc, json_doc[, json_doc] ...)
MySQL 8.0.3 引入的,用来代替 JSON_MERGE。也是用来合并文档,但合并规则与 JSON_MERGE_PATCH 有所不同。
- 两个文档中,只要有一个文档是数组,则另外一个文档会合并到该数组中。
- 两个文档都是 JSON 对象,若存在着同名 KEY ,第二个文档并不会覆盖第一个,而是会将值 append 到第一个文档中。
mysql>selectjson_merge_preserve('1','2'),json_merge_preserve('[1,2]','[3,4]');
+------------------------------+-----------------------------------------+
|json_merge_preserve('1','2')|json_merge_preserve('[1,2]','[3,4]')|
+------------------------------+-----------------------------------------+
|[1,2]|[1,2,3,4]|
+------------------------------+-----------------------------------------+
1rowinset(0.00sec)
mysql>selectjson_merge_preserve('[1,2]','{"a":123}'),json_merge_preserve('{"a":123}','[3,4]');
+---------------------------------------------+--------------------------------------------+
|json_merge_preserve('[1,2]','{"a":123}')|json_merge_preserve('{"a":123}','[3,4]')|
+---------------------------------------------+--------------------------------------------+
|[1,2,{"a":123}]|[{"a":123},3,4]|
+---------------------------------------------+--------------------------------------------+
1rowinset(0.00sec)
mysql>selectjson_merge_preserve('{"a":1}','{"b":2}'),json_merge_preserve('{"a":1,"b":2}','{"a":3,"c":4}');
+---------------------------------------------+--------------------------------------------------------------+
|json_merge_preserve('{"a":1}','{"b":2}')|json_merge_preserve('{"a":1,"b":2}','{"a":3,"c":4}')|
+---------------------------------------------+--------------------------------------------------------------+
|{"a":1,"b":2}|{"a":[1,3],"b":2,"c":4}|
+---------------------------------------------+--------------------------------------------------------------+
1rowinset(0.00sec)
JSON_MERGE(json_doc, json_doc[, json_doc] ...)
与 JSON_MERGE_PRESERVE 作用一样,从 MySQL 8.0.3 开始不建议使用,后续会移除。
7.3 其它辅助函数
JSON_QUOTE(string)
生成有效的 JSON 字符串,主要是对一些特殊字符(如双引号)进行转义。
mysql>selectjson_quote('null'),json_quote('"null"'),json_quote('[1,2,3]');
+--------------------+----------------------+-------------------------+
|json_quote('null')|json_quote('"null"')|json_quote('[1,2,3]')|
+--------------------+----------------------+-------------------------+
|"null"|"\"null\""|"[1,2,3]"|
+--------------------+----------------------+-------------------------+
1rowinset(0.00sec)
除此之外,也可通过 CAST(value AS JSON) 进行类型转换。
JSON_UNQUOTE(json_val)
将 JSON 转义成字符串输出。
mysql>selectc2->'$.ename',json_unquote(c2->'$.ename'),
->json_valid(c2->'$.ename'),json_valid(json_unquote(c2->'$.ename'))fromt;
+---------------+-----------------------------+---------------------------+-----------------------------------------+
|c2->'$.ename'|json_unquote(c2->'$.ename')|json_valid(c2->'$.ename')|json_valid(json_unquote(c2->'$.ename'))|
+---------------+-----------------------------+---------------------------+-----------------------------------------+
|"jack"|jack|1|0|
|"mark"|mark|1|0|
+---------------+-----------------------------+---------------------------+-----------------------------------------+
2rowsinset(0.00sec)
直观地看,没加 JSON_UNQUOTE 字符串会用双引号引起来,加了 JSON_UNQUOTE 就没有。但本质上,前者是 JSON 中的 STRING 类型,后者是 MySQL 中的字符类型,这一点可通过 JSON_VALID 来判断。
JSON_OBJECTAGG(key, value)
取表中的两列作为参数,其中,第一列是 key,第二列是 value,返回 JSON 对象。如,
mysql>select*fromemp;
+--------+----------+--------+
|deptno|ename|sal|
+--------+----------+--------+
|10|emp_1001|100.00|
|10|emp_1002|200.00|
|20|emp_1003|300.00|
|20|emp_1004|400.00|
+--------+----------+--------+
4rowsinset(0.00sec)
mysql>selectjson_objectagg(ename,sal)fromemp;
+----------------------------------------------------------------------------------+
|json_objectagg(ename,sal)|
+----------------------------------------------------------------------------------+
|{"emp_1001":100.00,"emp_1002":200.00,"emp_1003":300.00,"emp_1004":400.00}|
+----------------------------------------------------------------------------------+
1rowinset(0.00sec)
mysql>selectdeptno,json_objectagg(ename,sal)fromempgroupbydeptno;
+--------+------------------------------------------+
|deptno|json_objectagg(ename,sal)|
+--------+------------------------------------------+
|10|{"emp_1001":100.00,"emp_1002":200.00}|
|20|{"emp_1003":300.00,"emp_1004":400.00}|
+--------+------------------------------------------+
2rowsinset(0.00sec)
JSON_ARRAYAGG(col_or_expr)
将列的值聚合成 JSON 数组,注意,JSON 数组中元素的顺序是随机的。
mysql>selectjson_arrayagg(ename)fromemp;
+--------------------------------------------------+
|json_arrayagg(ename)|
+--------------------------------------------------+
|["emp_1001","emp_1002","emp_1003","emp_1004"]|
+--------------------------------------------------+
1rowinset(0.00sec)
mysql>selectdeptno,json_arrayagg(ename)fromempgroupbydeptno;
+--------+--------------------------+
|deptno|json_arrayagg(ename)|
+--------+--------------------------+
|10|["emp_1001","emp_1002"]|
|20|["emp_1003","emp_1004"]|
+--------+--------------------------+
2rowsinset(0.00sec)
JSON_PRETTY(json_val)
将 JSON 格式化输出。
mysql>selectjson_pretty("[1,3,5]");
+------------------------+
|json_pretty("[1,3,5]")|
+------------------------+
|[
1,
3,
5
]|
+------------------------+
1rowinset(0.00sec)
mysql>selectjson_pretty('{"a":"10","b":"15","x":"25"}');
+---------------------------------------------+
|json_pretty('{"a":"10","b":"15","x":"25"}')|
+---------------------------------------------+
|{
"a":"10",
"b":"15",
"x":"25"
}|
+---------------------------------------------+
1rowinset(0.00sec)
JSON_STORAGE_FREE(json_val)
MySQL 8.0 新增的,与 Partial Updates 有关,用于计算 JSON 文档在进行部分更新后的剩余空间。
JSON_STORAGE_SIZE(json_val)
MySQL 5.7.22 引入的,用于计算 JSON 文档的空间使用情况。
JSON_DEPTH(json_doc)
返回 JSON 文档的最大深度。对于空数组,空对象,标量值,其深度为 1。
mysql>selectjson_depth('{}'),json_depth('[10,20]'),json_depth('[10,{"a":20}]');
+------------------+------------------------+-------------------------------+
|json_depth('{}')|json_depth('[10,20]')|json_depth('[10,{"a":20}]')|
+------------------+------------------------+-------------------------------+
|1|2|3|
+------------------+------------------------+-------------------------------+
1rowinset(0.00sec)
JSON_LENGTH(json_doc[, path])
返回 JSON 文档的长度,其计算规则如下:
- 如果是标量值,其长度为 1。
- 如果是数组,其长度为数组元素的个数。
- 如果是对象,其长度为对象元素的个数。
- 不包括嵌套数据和嵌套对象的长度。
mysql>selectjson_length('"abc"');
+----------------------+
|json_length('"abc"')|
+----------------------+
|1|
+----------------------+
1rowinset(0.00sec)
mysql>selectjson_length('[1,2,{"a":3}]');
+---------------------------------+
|json_length('[1,2,{"a":3}]')|
+---------------------------------+
|3|
+---------------------------------+
1rowinset(0.00sec)
mysql>selectjson_length('{"a":1,"b":{"c":30}}');
+-----------------------------------------+
|json_length('{"a":1,"b":{"c":30}}')|
+-----------------------------------------+
|2|
+-----------------------------------------+
1rowinset(0.00sec)
mysql>selectjson_length('{"a":1,"b":{"c":30}}','$.a');
+------------------------------------------------+
|json_length('{"a":1,"b":{"c":30}}','$.a')|
+------------------------------------------------+
|1|
+------------------------------------------------+
1rowinset(0.00sec)
JSON_TYPE(json_val)
返回 JSON 值的类型。
mysql>selectjson_type('123');
+------------------+
|json_type('123')|
+------------------+
|INTEGER|
+------------------+
1rowinset(0.00sec)
mysql>selectjson_type('"abc"');
+--------------------+
|json_type('"abc"')|
+--------------------+
|STRING|
+--------------------+
1rowinset(0.00sec)
mysql>selectjson_type(cast(now()asjson));
+--------------------------------+
|json_type(cast(now()asjson))|
+--------------------------------+
|DATETIME|
+--------------------------------+
1rowinset(0.00sec)
mysql>selectjson_type(json_extract('{"a":[10,true]}','$.a'));
+-----------------------------------------------------+
|json_type(json_extract('{"a":[10,true]}','$.a'))|
+-----------------------------------------------------+
|ARRAY|
+-----------------------------------------------------+
1rowinset(0.00sec)
JSON_VALID(val)
判断给定值是否是有效的 JSON 文档。
mysql>selectjson_valid('hello'),json_valid('"hello"');
+---------------------+-----------------------+
|json_valid('hello')|json_valid('"hello"')|
+---------------------+-----------------------+
|0|1|
+---------------------+-----------------------+
1rowinset(0.00sec)
JSON_TABLE(expr, path COLUMNS (column_list) [AS] alias)
从 JSON 文档中提取数据并以表格的形式返回。
该函数的完整语法如下:
JSON_TABLE(
expr,
pathCOLUMNS(column_list)
)[AS]alias
column_list:
column[,column][,...]
column:
nameFORORDINALITY
|nametypePATHstring_path[on_empty][on_error]
|nametypeEXISTSPATHstring_path
|NESTED[PATH]pathCOLUMNS(column_list)
on_empty:
{NULL|DEFAULTjson_string|ERROR}ONEMPTY
on_error:
{NULL|DEFAULTjson_string|ERROR}ONERROR
其中,
- expr:可以返回 JSON 文档的表达式。可以是一个标量( JSON 文档 ),列名或者一个函数调用( JSON_EXTRACT(t1.json_data,'$.post.comments') )。
- path:JSON 的路径表达式,
- column:列的类型,支持以下四种类型:
- name FOR ORDINALITY:序号。name 是列名。
- name type PATH string_path [on_empty] [on_error]:提取指定路径( string_path )的元素。name 是列名,type 是 MySQL 中的数据类型。
- name type EXISTS PATH string_path:指定路径( string_path )的元素是否存在。
- NESTED [PATH] path COLUMNS (column_list):将嵌套对象或数组与来自父对象或数组的 JSON 值扁平化为一行输出。
select*
from
json_table(
'[{"x":2,"y":"8","z":9,"b":[1,2,3]},{"x":"3","y":"7"},{"x":"4","y":6,"z":10}]',
"$[*]"columns(
idforordinality,
xvalvarchar(100)path"$.x",
yvalvarchar(100)path"$.y",
z_existintexistspath"$.z",
nestedpath'$.b[*]'columns(bINTPATH'$')
)
)ast;
+------+------+------+---------+------+
|id|xval|yval|z_exist|b|
+------+------+------+---------+------+
|1|2|8|1|1|
|1|2|8|1|2|
|1|2|8|1|3|
|2|3|7|0|NULL|
|3|4|6|1|NULL|
+------+------+------+---------+------+
5rowsinset(0.00sec)
JSON_SCHEMA_VALID(schema,document)
判断 document ( JSON 文档 )是否满足 schema ( JSON 对象)定义的规范要求。完整的规范要求可参考 Draft 4 of the JSON Schema specification 。如果不满足,可通过 JSON_SCHEMA_VALIDATION_REPORT() 获取具体的原因。
以下面这个 schema 为例。
set@schema='{
"type":"object",
"properties":{
"latitude":{
"type":"number",
"minimum":-90,
"maximum":90
},
"longitude":{
"type":"number",
"minimum":-180,
"maximum":180
}
},
"required":["latitude","longitude"]
}';
它的要求如下:
- document 必须是 JSON 对象。
- JSON 对象必需的两个属性是 latitude 和 longitude。
- latitude 和 longitude 必须是数值类型,且两者的大小分别在 -90 ~ 90,-180 ~ 180 之间。
下面通过具体的 document 来测试一下。
mysql>set@document='{"latitude":63.444697,"longitude":10.445118}';
八、总结
QueryOK,0rowsaffected(0.00sec)
mysql>selectjson_schema_valid(@schema,@document);
+---------------------------------------+
|json_schema_valid(@schema,@document)|
+---------------------------------------+
|1|
+---------------------------------------+
1rowinset(0.00sec)
mysql>set@document='{"latitude":63.444697}';
QueryOK,0rowsaffected(0.00sec)
mysql>selectjson_schema_valid(@schema,@document);
+---------------------------------------+
|json_schema_valid(@schema,@document)|
+---------------------------------------+
|0|
+---------------------------------------+
1rowinset(0.00sec)
mysql>selectjson_pretty(json_schema_validation_report(@schema,@document))\G
***************************1.row***************************
json_pretty(json_schema_validation_report(@schema,@document)):{
"valid":false,
"reason":"TheJSONdocumentlocation'#'failedrequirement'required'atJSONSchemalocation'#'",
"schema-location":"#",
"document-location":"#",
"schema-failed-keyword":"required"
}
1rowinset(0.00sec)
mysql>set@document='{"latitude":91,"longitude":0}';
QueryOK,0rowsaffected(0.00sec)
mysql>selectjson_schema_valid(@schema,@document);
+---------------------------------------+
|json_schema_valid(@schema,@document)|
+---------------------------------------+
|0|
+---------------------------------------+
1rowinset(0.00sec)
mysql>selectjson_pretty(json_schema_validation_report(@schema,@document))\G
***************************1.row***************************
json_pretty(json_schema_validation_report(@schema,@document)):{
"valid":false,
"reason":"TheJSONdocumentlocation'#/latitude'failedrequirement'maximum'atJSONSchemalocation'#/properties/latitude'",
"schema-location":"#/properties/latitude",
"document-location":"#/latitude",
"schema-failed-keyword":"maximum"
}
1rowinset(0.00sec)
如果要使用 JSON 类型,推荐使用 MySQL 8.0。相比于 MySQL 5.7,Partial update 带来的性能提升还是十分明显的。
Partial update 在存储引擎层是默认开启的,binlog 中是否开启取决于 binlog_row_value_options 。该参数默认为空,不会开启 Partial update,建议设置为 PARTIAL_JSON。
注意使用 Partial update 的前提条件。
当我们使用 JSON_CONTAINS、MEMBER OF、JSON_OVERLAPS 进行数组相关的操作时,可使用 MySQL 8.0.17 引入的多值索引来加快查询。
九、参考资料-
JSON
-
The JSON Data Type
-
JSON Functions
-
Upgrading JSON data stored in TEXT columns
-
Indexing JSON documents via Virtual Columns
-
Partial update of JSON values
-
MySQL 8.0: InnoDB Introduces LOB Index For Faster Updates

