MySQL JSON
本文最后更新于:2024年3月18日 凌晨
MySQL JSON
- JSON 数据类型是 MySQL 5.7.8 开始支持的。在此之前,只能通过字符类型(CHAR,VARCHAR 或 TEXT )来保存 JSON 文档。
- 相对字符类型,原生的 JSON 类型具有以下优势:
- 在插入时能自动校验文档是否满足 JSON 格式的要求。
- 优化了存储格式。无需读取整个文档就能快速访问某个元素的值。
- 在 JSON 类型引入之前,如果我们想要获取 JSON 文档中的某个元素,必须首先读取整个 JSON 文档,然后在客户端将其转换为 JSON 对象,最后再通过对象获取指定元素的值。
CRUD
插入操作
- 可直接插入 JSON 格式的字符串。
1 |
|
JSON_ARRAY()和 JSON_OBJECT()
- 前者用于构造 JSON 数组,后者用于构造 JSON 对象。如
1 |
|
- 对于 JSON 文档,KEY 名不能重复。
- 如果插入的值中存在重复 KEY,在 MySQL 8.0.3 之前,遵循 first duplicate key wins 原则,会保留第一个 KEY,后面的将被丢弃掉。从 MySQL 8.0.3 开始,遵循的是 last duplicate key wins 原则,只会保留最后一个 KEY。
查询操作
JSON_EXTRACT(json_doc, path[, path] …)
- 其中,json_doc 是 JSON 文档,path 是路径。该函数会从 JSON 文档提取指定路径(path)的元素。如果指定 path 不存在,会返回 NULL。可指定多个 path,匹配到的多个值会以数组形式返回。
数组
- 首先我们看看数组,数组的路径是通过下标来表示的。第一个元素的下标是 0。
1 |
|
- 除此之外,还可通过
[M to N]
获取数组的子集。
1 |
|
- 也可通过
[*]
获取数组中的所有元素。
1 |
|
对象
- 对象的路径是通过 KEY 来表示的。
1 |
|
- 除此之外,还可通过
.*
获取对象中的所有元素。
1 |
|
column->path
- column->path,包括后面讲到的 column->>path,都是语法糖,在实际使用的时候都会转化为 JSON_EXTRACT。
- column->path 等同于 JSON_EXTRACT(column, path) ,只能指定一个path。
1 |
|
column->>path
-
同 column->path 类似,只不过其返回的是字符串。以下三者是等价的。
-
JSON_UNQUOTE( JSON_EXTRACT(column, path) )
-
JSON_UNQUOTE(column -> path)
-
column->>path
-
1 |
|
修改操作
JSON_INSERT(json_doc, path, val[, path, val] …)
- 插入新值。仅当指定位置或指定 KEY 的值不存在时,才执行插入操作。另外,如果指定的 path 是数组下标,且 json_doc 不是数组,该函数首先会将 json_doc 转化为数组,然后再插入新值。
1 |
|
JSON_SET(json_doc, path, val[, path, val] …)
- 插入新值,并替换已经存在的值。换言之,如果指定位置或指定 KEY 的值不存在,会执行插入操作,如果存在,则执行更新操作。
1 |
|
JSON_REPLACE(json_doc, path, val[, path, val] …)
- 替换已经存在的值。
1 |
|
删除操作
JSON_REMOVE(json_doc, path[, path] …)
- 删除 JSON 文档指定位置的元素。
1 |
|
- 最后一个查询,虽然两个 path 都是
'$[1]'
,但作用对象不一样,第一个 path 的作用对象是'["a", ["b", "c"], "d", "e"]'
,第二个 path 的作用对象是删除了'$[1]'
后的数组,即'["a", "d", "e"]'
。
创建索引
- 同 TEXT,BLOB 字段一样,JSON 字段不允许直接创建索引。即使支持,实际意义也不大,因为我们一般是基于文档中的元素进行查询,很少会基于整个 JSON 文档。
1 |
|
- 对文档中的元素进行查询,就需要用到 MySQL 5.7 引入的虚拟列及函数索引。
1 |
|
- 可以看到,无论是使用虚拟列,还是文档中的元素来查询,都可以利用上索引。
- 注意,在创建虚拟列时需指定 JSON_UNQUOTE,将
c1 -> "$.name"
的返回值转换为字符串。
如何将存储 JSON 字符串的字符字段升级为 JSON 字段
- 在 MySQL 支持 JSON 类型之前,对于 JSON 文档,一般是以字符串的形式存储在字符类型(VARCHAR 或 TEXT)中。在 JSON 类型出来之后,如何将这些字符字段升级为 JSON 字段呢?
- 为方便演示,这里首先构建测试数据。
1 |
|
- 注意,最后一个文档有问题,不是合格的 JSON 文档。
- 如果使用 DDL 直接修改字段的数据类型,会报错。
1 |
|
- 使用 json_valid 函数找出不满足 JSON 格式要求的文档。
1 |
|
- 处理不满足 JSON 格式要求的文档。
1 |
|
- 将 TEXT 字段修改为 JSON 字段。
1 |
|
使用 JSON 时的注意事项
-
在 MySQL 8.0.13 之前,不允许对 BLOB,TEXT,GEOMETRY,JSON 字段设置默认值。从 MySQL 8.0.13 开始,取消了这个限制。
设置时,注意默认值需通过小括号
()
括起来,否则的话,还是会提示 JSON 字段不允许设置默认值。1
2
3
4
5mysql> create table t(c1 json not null default (''));
Query OK, 0 rows affected (0.03 sec)
mysql> create table t(c1 json not null default '');
ERROR 1101 (42000): BLOB, TEXT, GEOMETRY or JSON column 'c1' can't have a default value -
不允许直接创建索引,可创建函数索引。
-
JSON 列的最大大小和 LONGBLOB(LONGTEXT)一样,都是 4G。
-
插入时,单个文档的大小受到 max_allowed_packet 的限制,该参数最大是 1G。
Partial Updates
- 在 MySQL 5.7 中,对 JSON 文档进行更新,其处理策略是,删除旧的文档,再插入新的文档。即使这个修改很微小,只涉及几个字节,也会替换掉整个文档。很显然,这种处理方式的效率较为低下。
- 在 MySQL 8.0 中,针对 JSON 文档,引入了一项新的特性-Partial Updates(部分更新),支持 JSON 文档的原地更新。得益于这个特性,JSON 文档的处理性能得到了极大提升。
使用 Partial Updates 的条件
- 为方便阐述,这里先构造测试数据。
1 |
|
- 使用 Partial Updates 需满足以下条件:
-
被更新的列是 JSON 类型。
-
使用 JSON_SET,JSON_REPLACE,JSON_REMOVE 进行 UPDATE 操作,如
1
update t set c1=json_remove(c1,'$.id') where id=1;
不使用这三个函数,而显式赋值,就不会进行部分更新,如
1
update t set c1='{"id": 1, "name": "a"}' where id=1;
-
输入列和目标列必须是同一列,如
1
update t set c1=json_replace(c1,'$.id',10) where id=1;
否则的话,就不会进行部分更新,如
1
update t set c1=json_replace(c2,'$.id',10) where id=1;
-
变更前后,JSON 文档的空间使用不会增加。
- 关于最后一个条件,我们看看下面这个示例。
1 |
|
- 示例中,用到了两个函数:JSON_STORAGE_SIZE 和 JSON_STORAGE_FREE ,前者用来获取 JSON 文档的空间使用情况,后者用来获取 JSON 文档在执行原地更新后的空间释放情况。
- 这里一共执行了三次 UPDATE 操作,前两次是原地更新,第三次不是。同样是 JSON_SET 操作,为什么第一次是原地更新,而第二次不是呢?
- 因为第一次的 JSON_SET 复用了 JSON_REMOVE 释放的空间。而第二次的 JSON_SET 执行的是更新操作,且 ‘mysql’ 比 3306 需要更多的存储空间。
如何在 binlog 中开启 Partial
- Partial Updates 不仅仅适用于存储引擎层,还可用于主从复制场景。
- 主从复制开启 Partial Updates,只需将参数 binlog_row_value_options(默认为空)设置为 PARTIAL_JSON。
- 下面具体来看看,同一个 UPDATE 操作,开启和不开启 Partial Updates,在 binlog 中的记录有何区别。
1 |
|
- 不开启。
1 |
|
- 开启。
1 |
|
- 对比 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 文档的空间使用是否会增加。
关于 Partial Updates 的性能测试
- 以 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 函数
查询相关
JSON_CONTAINS(target, candidate[, path])
- 判断 target 文档是否包含 candidate 文档,如果包含,则返回 1,否则是 0。
1 |
|
JSON_CONTAINS_PATH(json_doc, one_or_all, path[, path] …)
- 判断指定的 path 是否存在,存在,则返回 1,否则是 0。
- 函数中的 one_or_all 可指定 one 或 all,one 是任意一个路径存在就返回 1,all 是所有路径都存在才返回 1。
1 |
|
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:查找路径。
1 |
|
JSON_KEYS(json_doc[, path])
- 返回 JSON 文档最外层的 key,如果指定了 path,则返回该 path 对应元素最外层的 key。
1 |
|
JSON_VALUE(json_doc, path)
- 8.0.21 引入的,从 JSON 文档提取指定路径(path)的元素。
1 |
|
- RETURNING type:返回值的类型,不指定,则默认是 VARCHAR(512)。不指定字符集,则默认是 utf8mb4,且区分大小写。
- on_empty:如果指定路径没有值,会触发 on_empty 子句, 默认是返回 NULL,也可指定 ERROR 抛出错误,或者通过 DEFAULT value 返回默认值。
- on_error:三种情况下会触发 on_error 子句:从数组或对象中提取元素时,会解析到多个值;类型转换错误,譬如将 “abc” 转换为 unsigned 类型;值被 truncate 了。默认是返回 NULL。
1 |
|
value MEMBER OF(json_array)
- 判断 value 是否是 JSON 数组的一个元素,如果是,则返回 1,否则是 0。
1 |
|
JSON_OVERLAPS(json_doc1, json_doc2)
- MySQL 8.0.17 引入的,用来比较两个 JSON 文档是否有相同的键值对或数组元素,如果有,则返回 1,否则是 0。如果两个参数都是标量,则判断这两个标量是否相等。
1 |
|
- 从 MySQL 8.0.17 开始,InnoDB 支持多值索引,可用在 JSON 数组中。当我们使用 JSON_CONTAINS、MEMBER OF、JSON_OVERLAPS 进行数组相关的操作时,可使用多值索引来加快查询。
修改相关
JSON_ARRAY_APPEND(json_doc, path, val[, path, val] …)
- 向数组指定位置追加元素。如果指定 path 不存在,则不添加。
1 |
|
JSON_ARRAY_INSERT(json_doc, path, val[, path, val] …)
- 向数组指定位置插入元素。
1 |
|
JSON_MERGE_PATCH(json_doc, json_doc[, json_doc] …)
- MySQL 8.0.3 引入的,用来合并多个 JSON 文档。其合并规则如下:
- 如果两个文档不全是 JSON 对象,则合并后的结果是第二个文档。
- 如果两个文档都是 JSON 对象,且不存在着同名 KEY,则合并后的文档包括两个文档的所有元素,如果存在着同名 KEY,则第二个文档的值会覆盖第一个。
1 |
|
JSON_MERGE_PRESERVE(json_doc, json_doc[, json_doc] …)
- MySQL 8.0.3 引入的,用来代替 JSON_MERGE。也是用来合并文档,但合并规则与 JSON_MERGE_PATCH 有所不同。
- 两个文档中,只要有一个文档是数组,则另外一个文档会合并到该数组中。
- 两个文档都是 JSON 对象,若存在着同名 KEY ,第二个文档并不会覆盖第一个,而是会将值 append 到第一个文档中。
1 |
|
JSON_MERGE(json_doc, json_doc[, json_doc] …)
- 与 JSON_MERGE_PRESERVE 作用一样,从 MySQL 8.0.3 开始不建议使用,后续会移除。
其它辅助函数
JSON_QUOTE(string)
- 生成有效的 JSON 字符串,主要是对一些特殊字符(如双引号)进行转义。
1 |
|
- 除此之外,也可通过 CAST(value AS JSON)进行类型转换。
JSON_UNQUOTE(json_val)
- 将 JSON 转义成字符串输出。
1 |
|
- 直观地看,没加 JSON_UNQUOTE 字符串会用双引号引起来,加了 JSON_UNQUOTE 就没有。但本质上,前者是 JSON 中的 STRING 类型,后者是 MySQL 中的字符类型,这一点可通过 JSON_VALID 来判断。
JSON_OBJECTAGG(key, value)
- 取表中的两列作为参数,其中,第一列是 key,第二列是 value,返回 JSON 对象。如
1 |
|
JSON_ARRAYAGG(col_or_expr)
- 将列的值聚合成 JSON 数组,注意,JSON 数组中元素的顺序是随机的。
1 |
|
JSON_PRETTY(json_val)
- 将 JSON 格式化输出。
1 |
|
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。
1 |
|
JSON_LENGTH(json_doc[, path])
- 返回 JSON 文档的长度,其计算规则如下:
- 如果是标量值,其长度为 1。
- 如果是数组,其长度为数组元素的个数。
- 如果是对象,其长度为对象元素的个数。
- 不包括嵌套数据和嵌套对象的长度。
1 |
|
JSON_TYPE(json_val)
- 返回 JSON 值的类型。
1 |
|
JSON_VALID(val)
- 判断给定值是否是有效的 JSON 文档。
1 |
|
JSON_TABLE(expr, path COLUMNS (column_list) [AS] alias)
- 从 JSON 文档中提取数据并以表格的形式返回。
1 |
|
-
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 值扁平化为一行输出。
- name FOR ORDINALITY:序号。name 是列名。
1 |
|
JSON_SCHEMA_VALID(schema,document)
- 判断 document ( JSON 文档 )是否满足 schema ( JSON 对象)定义的规范要求。完整的规范要求可参考 Draft 4 of the JSON Schema specification 。如果不满足,可通过 JSON_SCHEMA_VALIDATION_REPORT()获取具体的原因。
1 |
|
- 它的要求如下:
- document 必须是 JSON 对象。
- JSON 对象必需的两个属性是 latitude 和 longitude。
- latitude 和 longitude 必须是数值类型,且两者的大小分别在 -90 ~ 90,-180 ~ 180 之间。
- 下面通过具体的 document 来测试一下。
1 |
|
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!