MySQL从5.7.8开始支持JSON数据类型,在此之前,针对JSON类型的数据,只能通过字符类型来进行存储和处理。
虽然MySQL对JSON类型已经支持很长时间了,而且具有不少优势,但不少开发人员还对JSON类型的存储抱有一定的偏见。
这篇文章就带大家详细学习一遍JSON数据类型的使用和特点,帮助大家更好地认识和实践JSON类型。
MySQL的JSON数据类型简介
MySQL从5.7版本开始正式支持JSON数据类型,MySQL 8.0又丰富了JSON函数集,加入了更强大的功能例如 JSON_TABLE()
(将JSON数据动态映射为关系型表)。
JSON类型的数据以二进制格式 存储,称为binary JSON (BIN_JSON) ,相比明文字符串格式更高效,支持快速访问和解析。这种存储方式允许直接访问嵌套的属性,不需要将整个JSON字符串加载到内存中进行解析。
MySQL提供一系列原生的JSON函数,允许对JSON内容进行查询、修改和操作。例如:
JSON_EXTRACT()
:提取JSON数据中的特定字段。JSON_SET()
:更新JSON数据中的某些字段。JSON_ARRAY()
:创建JSON数组。JSON_OBJECT()
:创建JSON对象。JSON_CONTAINS()
:检查JSON数据中是否包含某个键或值。JSON_ARRAYAGG()
和JSON_OBJECTAGG()
:可以在GROUP BY查询中生成JSON结果。
对于插入JSON类型字段的数据,MySQL会自动验证插入到JSON列的数据是否是合法的JSON格式。如果格式无效,会报错。
另外,JSON类型还支持创建虚拟生成列(Generated Column) 来基于JSON字段的某些值建立索引。此部分实现可参考《如何为MySQL中的JSON字段设置索引》一文。
使用JSON类型前后对比
在进行JSON类型介绍之前,我们先来看一下不使用JSON数据类型和使用JSON类型的差别。
未使用JSON类型
在 MySQL 5.7 版本之前(或者不使用 JSON 类型的情况下),我们通常会将 JSON 格式的数据以字符串的形式存储在传统的 CHAR
、VARCHAR
或 TEXT
类型的列中。此时,数据库并不具备对 JSON 数据的原生操作能力。
此时,如果需要读取 JSON 的某个字段或元素,应用程序通常会从数据库中查询 JSON 字符串,然后在应用层通过 JSON 解析库(如 Python 的 json
模块)解析成 JSON 对象,最终提取所需的字段。
其中,解析和获取指定元素的过程,用Python实现的示例代码如下:
makefile
import json
# 从数据库中读取到的 JSON 字符串(模拟)
jsonStr = '{"name":"John","age":30,"city":"New York"}'
# 将 JSON 字符串解析为 Python 的字典对象
y = json.loads(jsonStr)
# 读取 JSON 对象中指定的元素值
print(y['age']) # 输出:30
这里的 json.loads()
方法可将 JSON 字符串解析为 Python 的原生字典类型,之后可以通过键值访问方式提取某个字段。
这种方式虽然能够达到存储和读取JSON数据类型的目的,但却存在着一些缺点:
- 第一,消耗磁盘IO:从数据库中查询存储的大型 JSON 格式字符串会触发磁盘 IO 操作,而且存储类型(如
TEXT
或VARCHAR
)可能会影响 IO 性能。如果 JSON 字符串较大或查询结果返回大量数据,磁盘 IO 会成为瓶颈。 - 第二,如果JSON数据比较大,对网络带宽造成压力。尤其是在大型分布式系统中,在高并发的场景下,会对计算节点的网络链路和吞吐量产生影响。
使用JSON类型
MySQL 从 5.7 版本开始支持 JSON 类型,提供了原生处理 JSON 字段的能力,可以避免需通过应用层解析 JSON 字符串的方式。也就是说,可以直接通过SQL语句即可完成指定JSON元素的查询。
创建表及插入数据,SQL语句示例如下:
sql
-- 使用 JSON 类型创建表
CREATE TABLE example (
id INT AUTO_INCREMENT PRIMARY KEY,
data JSON
);
-- 插入 JSON 数据
INSERT INTO example (data) VALUES ('{"name": "John", "age": 30, "city": "New York"}');
-- 查询 JSON 数据中的某个字段
SELECT JSON_EXTRACT(data, '$.age') AS age FROM example;
通过->
的方式查询操作:
sql
mysql> select data->"$.age" from example;
+---------------+
| data->"$.age" |
+---------------+
| 30 |
+---------------+
1 row in set (0.01 sec)
通过基于JSON_EXTRACT()
函数进行查询:
sql
mysql> SELECT JSON_EXTRACT(data, '$.age') AS age FROM example;
+------+
| age |
+------+
| 30 |
+------+
1 row in set (0.00 sec)
可以看出,无论通过上述的哪种方式来进行查询,都只需要从 JSON 中提取查询所需字段,而不是加载整个字符串到内存解析。这不仅节省了网络带宽,还能降低磁盘IO的消耗。
下面,我们就来详细的了解一下MySQL对JSON数据类型的功能支持。
JSON类型的增删改查
下面我们看看 JSON 字段常见的增删改查操作,具体示例依旧采用上面的example
表。
插入操作
MySQL 的 JSON
类型遵循 JSON 标准 (RFC 7159),这一标准明确规定了 JSON 数据的有效结构包括以下两种形式:
- JSON对象 (
{"key": value}
格式的键值对结构)。 - JSON数组 (
[value1, value2, value3]
格式的值列表结构)。
因此,MySQL支持上述两种结构的数据插入到JSON字段中。
插入JSON对象的SQL语句:
erlang
-- 插入 JSON 数据
INSERT INTO example (data) VALUES ('{"name": "John", "age": 30, "city": "New York"}');
插入JSON数组的SQL语句:
sql
INSERT INTO example (data) VALUES ('["John", 30, "New York"]');
下面,我们再看一下,如果插入的是一个非JSON的文本,看看效果:
sql
mysql> INSERT INTO example (data) VALUES ('师兄奇谈');
ERROR 3140 (22032): Invalid JSON text: "Invalid value." at position 0 in value for column 'example.data'.
这里可以看到,MySQL数据对插入JSON字段类型的数据进行了校验,当插入非JSON类型时,会直接保持。
另外需要注意的是,JSON数据中的KEY不能重复。如果插入的值中存在重复KEY,在 MySQL 8.0.3 之前,遵循 first duplicate key wins 原则,会保留第一个 KEY,后面的将被丢弃掉。从 MySQL 8.0.3 开始,遵循的是 last duplicate key wins 原则,只会保留最后一个 KEY。
构造JSON数组
通常,可通过JSON_ARRAY()
来构造JSON数组,示例如下:
sql
mysql> select json_array(1, "John", 30, "New York");
+---------------------------------------+
| json_array(1, "John", 30, "New York") |
+---------------------------------------+
| [1, "John", 30, "New York"] |
+---------------------------------------+
构造JSON对象
通常,可通过JSON_OBJECT()
来构造JSON对象,示例如下:
sql
mysql> select json_object('name', 'John','age','30','city','New York');
+----------------------------------------------------------+
| json_object('name', 'John','age','30','city','New York') |
+----------------------------------------------------------+
| {"age": "30", "city": "New York", "name": "John"} |
+----------------------------------------------------------+
查询操作
MySQL 对JSON类型字段提供许多内置函数,可以灵活的处理JSON数据。常见操作包括:提取 JSON 字段值(通过键或路径查询)、检索嵌套字段值、过滤和排序 JSON 数据等。
提取 JSON 字段值
JSON_EXTRACT()
是提取字段值的核心函数,用来从 JSON 数据字段中获取指定路径的值。JSON_EXTRACT()
函数的语法格式如下:
lua
JSON_EXTRACT(json_doc, path[, path] ...)
其中,json_doc是 JSON 文档,path 是路径。该函数会从 JSON 文档提取指定路径(path)的元素。如果指定 path 不存在,会返回 NULL。可指定多个 path,匹配到的多个值会以数组形式返回。
获取JSON对象的SQL示例:
sql
-- 提取 JSON 字段中的 "name"
SELECT JSON_EXTRACT(data,'$.name') AS name FROM example;
-- 返回结果: "John"
-- 提取JSON字段的"name"和"age"字段,结果以数组形式返回
mysql> SELECT JSON_EXTRACT(data,'$.name','$.age') FROM example;
+-------------------------------------+
| JSON_EXTRACT(data,'$.name','$.age') |
+-------------------------------------+
| ["John", 30] |
数组的路径是通过下标来表示的。第一个元素的下标是 0。
获取JSON数组的SQL示例:
sql
SELECT JSON_EXTRACT(data,'$[0]') AS name FROM example;
-- 返回结果: "John"
除此之外,数组类型的JSON,还可以通过[M to N]
获取数组的子集,通过[*]
获取数组中的所有元素。
sql
mysql> SELECT JSON_EXTRACT(data,'$[0 to 1]') AS name FROM example;
+---------------------------------------------------+
| name |
+---------------------------------------------------+
| ["John", 30] |
+---------------------------------------------------+
mysql> SELECT JSON_EXTRACT(data,'$[*]') AS name FROM example;
+--------------------------+
| name |
+--------------------------+
| ["John", 30, "New York"] |
+--------------------------+
需要注意的是,虽然JSON类型同时支持JSON对象和JSON数组两种类型,但在使用的时候最好不要两者混用,否者在上述两种查询种会出现一些不符合预期的结果。
比如:
sql
mysql> SELECT JSON_EXTRACT(data,'$[0]') AS name FROM example;
+-------------------------------------------------+
| name |
+-------------------------------------------------+
| {"age": 30, "city": "New York", "name": "John"} |
| "John" |
+-------------------------------------------------+
我们预期的是输出"John",但也将JSON对象类型的数据查询了出来。
去除引号以获得原始值
JSON_UNQUOTE()
用于移除 JSON_EXTRACT()
返回值中的双引号,提取字段的原始值。
sql
-- 提取字段值并去除引号
SELECT JSON_UNQUOTE(JSON_EXTRACT(data, '$.name')) AS name FROM example;
-- 返回结果: John
检索嵌套字段值
如果 JSON 数据包含嵌套结构,可以通过路径表达式逐层访问。假设以下 JSON 数据:
json
{
"name": "John",
"details": {
"age": 30,
"location": {
"city": "New York",
"country": "USA"
}
}
}
可以查询嵌套字段值:
sql
-- 查询嵌套字段 "details.age"
SELECT JSON_EXTRACT(data, '$.details.age') AS age FROM example;
-- 查询嵌套字段 "details.location.city"
SELECT JSON_EXTRACT(data, '$.details.location.city') AS city FROM example;
过滤和排序 JSON 数据
通过 WHERE
子句和路径表达式筛选满足条件的数据。
javascript
-- 查询 JSON 数据中 "age" 大于 25 的记录
SELECT * FROM example WHERE JSON_EXTRACT(data, '$.age') > 25;
-- 查询 JSON 数据中 "name" 为 "John" 的记录
SELECT * FROM example WHERE JSON_UNQUOTE(JSON_EXTRACT(data, '$.name')) = 'John';
查询函数的语法糖
在 MySQL 中,提供了基于->
和 ->>
操作符的语法糖操作,它们会在底层转换为调用 JSON_EXTRACT()
函数(对于 ->
)或 JSON_UNQUOTE(JSON_EXTRACT(...))
函数(对于 ->>
)。
其中,column->path
用于提取 JSON 数据字段,等价于 JSON_EXTRACT(column, path)
,返回结果是 JSON 格式(即带有引号的字符串或其他合法的 JSON 数据类型)。
示例如下:
sql
SELECT data->'$.name' AS name FROM example; -- 返回结果: "John"
column->>path
用于提取 JSON 字段并返回原始值(移除了 JSON 格式中的双引号)。等价于 JSON_UNQUOTE(JSON_EXTRACT(column, path))
。
sql
-- 使用 `->>` 提取原始值
SELECT data->>'$.name' AS name_raw FROM example;
-- 返回结果: John (原始值,去掉了引号)
MySQL 的 JSON 路径遵循以下规则:
- 使用
$
表示 JSON 数据的根节点。 - 使用
.
表示访问子节点,例如$.name
。 - 使用
[index]
表示访问数组的元素,例如$[0]
。 - 无法通过
->
或->>
操作符同时提取多个路径。
如果需要处理复杂路径(例如多层嵌套或多个字段的提取),通常需要结合多个查询或者写入虚拟生成列。
修改操作
修改操作包含JSON_SET()和JSON_REPLACE()两个函数。
JSON_SET()函数
JSON_SET()的语法格式为JSON_SET(json_doc, path, val[, path, val] ...),当执行JSON_SET()函数时:
- 如果指定路径已经存在,则会更新此路径上的值;
- 如果指定路径不存在,则会插入新的路径和值。
示例SQL:
sql
mysql> SELECT JSON_SET(data, '$.age', 35) AS new_data FROM example;
+-------------------------------------------------+
| new_data |
+-------------------------------------------------+
| {"age": 35, "city": "New York", "name": "John"} |
-- 不存在则新增字段
mysql> SELECT JSON_SET(data, '$.gender', 'male') AS new_data FROM example;
+-------------------------------------------------------------------+
| new_data |
+-------------------------------------------------------------------+
| {"age": 35, "city": "New York", "name": "John", "gender": "male"} |
JSON_REPLACE()函数
JSON_REPLACE()函数的语法格式为JSON_REPLACE(json_doc, path, val[, path, val] ...),主要用于在 JSON 数据中替换指定路径上的值。如果目标路径中已经存在值,那么会将其替换为新的值;如果目标路径不存在,则不会修改 JSON 数据,也不会新增路径。
示例SQL:
sql
mysql> SELECT JSON_REPLACE(data, '$.age', 35) AS new_data FROM example;
+-------------------------------------------------+
| new_data |
+-------------------------------------------------+
| {"age": 35, "city": "New York", "name": "John"} |
+-------------------------------------------------+
删除操作
JSON_REMOVE()
是 MySQL 中用于从 JSON 数据中删除指定路径上的值的函数。它适合用于删除 JSON 数据中不需要的字段,同时删除嵌套的字段或数组元素。
- 如果路径存在,则删除目标路径及其内容。
- 如果路径不存在,则返回原 JSON 数据,不报错。
语法:
lua
JSON_REMOVE(json_doc, path [, path] ...)
参数说明:
json_doc
: JSON 数据的字段值,可以是 JSON 类型字段或合法的 JSON 字符串。path
: 要删除值的路径,遵循 JSON 路径表达式(例如$
表示根节点,$.key
表示子节点,$[index]
表示数组索引位置)。
SQL示例:
sql
mysql> SELECT JSON_REMOVE(data, '$.gender') AS new_data FROM example;
+-------------------------------------------------+
| new_data |
+-------------------------------------------------+
| {"age": 35, "city": "New York", "name": "John"} |
上述SQL语句删除了JSON 数据中的 gender
字段。
JSON_REMOVE()
同时支持删除多个路径上的字段值。例如,删除 age
和 city
:
sql
SELECT JSON_REMOVE(data, '$.age', '$.city') AS new_data FROM example;
JSON_REMOVE()
同样支持删除数组中的指定元素,使用索引来定位目标值(索引从 0 开始)。
小结
本文带大家了解了MySQL支持的JSON数据类型以及常见的增删改查操作,相较于之前通过字符串类型来的处理方式,无论从性能和便捷性等方面来说,都有了极大的提升。文中只列举了场景的一些函数和功能,更多关于JSON数据类型的操作,等待大家的继续探索。