JSON 数据类型是 MySQL 5.7.8 开始支持的。在此之前,只能通过字符类型(CHAR,VARCHAR 或 TEXT )来保存 JSON 文档。
相对字符类型,原生的 JSON 类型具有以下优势:
在 JSON 类型引入之前,如果我们想要获取 JSON 文档中的某个元素,必须首先读取整个 JSON 文档,然后在客户端将其转换为 JSON 对象,最后再通过对象获取指定元素的值。
下面是 Python 中的获取方式。
import json
# JSON 字符串:
x = '{ "name":"John", "age":30, "city":"New York"}'
# 将 JSON 字符串转换为 JSON 对象:
y = json.loads(x)
# 读取 JSON 对象中指定元素的值:
print(y["age"])
这种方式有两个弊端:一、消耗磁盘 IO,二、消耗网络带宽,如果 JSON 文档比较大,在高并发场景,有可能会打爆网卡。
如果使用的是 JSON 类型,相同的需求,直接使用 SQL 命令就可搞定。不仅能节省网络带宽,结合后面提到的函数索引,还能降低磁盘 IO 消耗。
mysql> create table t(c1 json);
Query OK, 0 rows affected (0.09 sec)
mysql> insert into t values('{ "name":"John", "age":30, "city":"New York"}');
Query OK, 1 row affected (0.01 sec)
mysql> select c1->"$.age" from t;
+-------------+
| c1->"$.age" |
+-------------+
| 30 |
+-------------+
1 row in set (0.00 sec)
本文将从以下几个方面展开:
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 字段常见的增删改查操作:
可直接插入 JSON 格式的字符串。
mysql> create table t(c1 json);
Query OK, 0 rows affected (0.03 sec)
mysql> insert into t values('[1, "abc", null, true, "08:45:06.000000"]');
Query OK, 1 row affected (0.01 sec)
mysql> insert into t values('{"id": 87, "name": "carrot"}');
Query OK, 1 row affected (0.01 sec)
也可使用函数,常用的有 JSON_ARRAY() 和 JSON_OBJECT(),前者用于构造 JSON 数组,后者用于构造 JSON 对象。如,
mysql> select json_array(1, "abc", null, true,curtime());
+--------------------------------------------+
| json_array(1, "abc", null, true,curtime()) |
+--------------------------------------------+
| [1, "abc", null, true, "10:12:25.000000"] |
+--------------------------------------------+
1 row in set (0.01 sec)
mysql> select json_object('id', 87, 'name', 'carrot');
+-----------------------------------------+
| json_object('id', 87, 'name', 'carrot') |
+-----------------------------------------+
| {"id": 87, "name": "carrot"} |
+-----------------------------------------+
1 row in set (0.00 sec)
对于 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> select json_object('key1',10,'key2',20,'key1',30);
+--------------------------------------------+
| json_object('key1',10,'key2',20,'key1',30) |
+--------------------------------------------+
| {"key1": 10, "key2": 20} |
+--------------------------------------------+
1 row in set (0.02 sec)
MySQL 8.0.27
mysql> select json_object('key1',10,'key2',20,'key1',30);
+--------------------------------------------+
| json_object('key1',10,'key2',20,'key1',30) |
+--------------------------------------------+
| {"key1": 30, "key2": 20} |
+--------------------------------------------+
1 row in set (0.00 sec)
JSON_EXTRACT(json_doc, path[, path] ...)
其中,json_doc 是 JSON 文档,path 是路径。该函数会从 JSON 文档提取指定路径(path)的元素。如果指定 path 不存在,会返回 NULL。可指定多个 path,匹配到的多个值会以数组形式返回。
下面我们结合一些具体的示例来看看 path 及 JSON_EXTRACT 的用法。
首先我们看看数组。
数组的路径是通过下标来表示的。第一个元素的下标是 0。
mysql> select json_extract('[10, 20, [30, 40]]', '$[0]');
+--------------------------------------------+
| json_extract('[10, 20, [30, 40]]', '$[0]') |
+--------------------------------------------+
| 10 |
+--------------------------------------------+
1 row in set (0.00 sec)
mysql> select json_extract('[10, 20, [30, 40]]', '$[0]', '$[1]','$[2][0]');
+--------------------------------------------------------------+
| json_extract('[10, 20, [30, 40]]', '$[0]', '$[1]','$[2][0]') |
+--------------------------------------------------------------+
| [10, 20, 30] |
+--------------------------------------------------------------+
1 row in set (0.00 sec)
除此之外,还可通过 [M to N]
获取数组的子集。
mysql> select json_extract('[10, 20, [30, 40]]', '$[0 to 1]');
+-------------------------------------------------+
| json_extract('[10, 20, [30, 40]]', '$[0 to 1]') |
+-------------------------------------------------+
| [10, 20] |
+-------------------------------------------------+
1 row in set (0.00 sec)
# 这里的 last 代表最后一个元素的下标
mysql> select json_extract('[10, 20, [30, 40]]', '$[last-1 to last]');
+---------------------------------------------------------+
| json_extract('[10, 20, [30, 40]]', '$[last-1 to last]') |
+---------------------------------------------------------+
| [20, [30, 40]] |
+---------------------------------------------------------+
1 row in set (0.00 sec)
也可通过 [*]
获取数组中的所有元素。
mysql> select json_extract('[10, 20, [30, 40]]', '$[*]');
+--------------------------------------------+
| json_extract('[10, 20, [30, 40]]', '$[*]') |
+--------------------------------------------+
| [10, 20, [30, 40]] |
+--------------------------------------------+
1 row in set (0.00 sec)
接下来,我们看看对象。
对象的路径是通过 KEY 来表示的。
mysql> set @j='{"a": 1, "b": [2, 3], "a c": 4}';
Query OK, 0 rows affected (0.00 sec)
# 如果 KEY 在路径表达式中不合法(譬如存在空格),则在引用这个 KEY 时,需用双引号括起来。
mysql> select json_extract(@j, '$.a'), json_extract(@j, '$."a c"'), json_extract(@j, '$.b[1]');
+-------------------------+-----------------------------+----------------------------+
| json_extract(@j, '$.a') | json_extract(@j, '$."a c"') | json_extract(@j, '$.b[1]') |
+-------------------------+-----------------------------+----------------------------+
| 1 | 4 | 3 |
+-------------------------+-----------------------------+----------------------------+
1 row in set (0.00 sec)
除此之外,还可通过 .*
获取对象中的所有元素。
mysql> select json_extract('{"a": 1, "b": [2, 3], "a c": 4}', '$.*');
+--------------------------------------------------------+
| json_extract('{"a": 1, "b": [2, 3], "a c": 4}', '$.*') |
+--------------------------------------------------------+
| [1, [2, 3], 4] |
+--------------------------------------------------------+
1 row in set (0.00 sec)
# 这里的 $**.b 匹配 $.a.b 和 $.c.b
mysql> select json_extract('{"a": {"b": 1}, "c": {"b": 2}}', '$**.b');
+---------------------------------------------------------+
| json_extract('{"a": {"b": 1}, "c": {"b": 2}}', '$**.b') |
+---------------------------------------------------------+
| [1, 2] |
+---------------------------------------------------------+
1 row in set (0.00 sec)
column->path
column->path,包括后面讲到的 column->>path,都是语法糖,在实际使用的时候都会转化为 JSON_EXTRACT。
column->path 等同于 JSON_EXTRACT(column, path) ,只能指定一个path。
create table t(c2 json);
insert into t values('{"empno": 1001, "ename": "jack"}'), ('{"empno": 1002, "ename": "mark"}');
mysql> select c2, c2->"$.ename" from t;
+----------------------------------+---------------+
| c2 | c2->"$.ename" |
+----------------------------------+---------------+
| {"empno": 1001, "ename": "jack"} | "jack" |
| {"empno": 1002, "ename": "mark"} | "mark" |
+----------------------------------+---------------+
2 rows in set (0.00 sec)
mysql> select * from t where c2->"$.empno" = 1001;
+------+----------------------------------+
| c1 | c2 |
+------+----------------------------------+
| 1 | {"empno": 1001, "ename": "jack"} |
+------+----------------------------------+
1 row in set (0.00 sec)
column->>path
同 column->path 类似,只不过其返回的是字符串。以下三者是等价的。
mysql> select c2->'$.ename',json_extract(c2, "$.ename"),json_unquote(c2->'$.ename'),c2->>'$.ename' from t;
+---------------+-----------------------------+-----------------------------+----------------+
| c2->'$.ename' | json_extract(c2, "$.ename") | json_unquote(c2->'$.ename') | c2->>'$.ename' |
+---------------+-----------------------------+-----------------------------+----------------+
| "jack" | "jack" | jack | jack |
| "mark" | "mark" | mark | mark |
+---------------+-----------------------------+-----------------------------+----------------+
2 rows in set (0.00 sec)
JSON_INSERT(json_doc, path, val[, path, val] ...)
插入新值。
仅当指定位置或指定 KEY 的值不存在时,才执行插入操作。另外,如果指定的 path 是数组下标,且 json_doc 不是数组,该函数首先会将 json_doc 转化为数组,然后再插入新值。
下面我们看几个示例。
mysql> select json_insert('1','$[0]',"10");
+------------------------------+
| json_insert('1','$[0]',"10") |
+------------------------------+
| 1 |
+------------------------------+
1 row in set (0.00 sec)
mysql> select json_insert('1','$[1]',"10");
+------------------------------+
| json_insert('1','$[1]',"10") |
+------------------------------+
| [1, "10"] |
+------------------------------+
1 row in set (0.01 sec)
mysql> select json_insert('["1","2"]','$[2]',"10");
+--------------------------------------+
| json_insert('["1","2"]','$[2]',"10") |
+--------------------------------------+
| ["1", "2", "10"] |
+--------------------------------------+
1 row in set (0.00 sec)
mysql> set @j = '{ "a": 1, "b": [2, 3]}';
Query OK, 0 rows affected (0.00 sec)
mysql> select json_insert(@j, '$.a', 10, '$.c', '[true, false]');
+----------------------------------------------------+
| json_insert(@j, '$.a', 10, '$.c', '[true, false]') |
+----------------------------------------------------+
| {"a": 1, "b": [2, 3], "c": "[true, false]"} |
+----------------------------------------------------+
1 row in set (0.00 sec)
JSON_SET(json_doc, path, val[, path, val] ...)
插入新值,并替换已经存在的值。
换言之,如果指定位置或指定 KEY 的值不存在,会执行插入操作,如果存在,则执行更新操作。
mysql> set @j = '{ "a": 1, "b": [2, 3]}';
Query OK, 0 rows affected (0.00 sec)
mysql> select json_set(@j, '$.a', 10, '$.c', '[true, false]');
+-------------------------------------------------+
| json_set(@j, '$.a', 10, '$.c', '[true, false]') |
+-------------------------------------------------+
| {"a": 10, "b": [2, 3], "c": "[true, false]"} |
+-------------------------------------------------+
1 row in set (0.00 sec)
JSON_REPLACE(json_doc, path, val[, path, val] ...)
替换已经存在的值。
mysql> set @j = '{ "a": 1, "b": [2, 3]}';
Query OK, 0 rows affected (0.00 sec)
mysql> select json_replace(@j, '$.a', 10, '$.c', '[true, false]');
+-----------------------------------------------------+
| json_replace(@j, '$.a', 10, '$.c', '[true, false]') |
+-----------------------------------------------------+
| {"a": 10, "b": [2, 3]} |
+-----------------------------------------------------+
1 row in set (0.00 sec)
JSON_REMOVE(json_doc, path[, path] ...)
删除 JSON 文档指定位置的元素。
mysql> set @j = '{ "a": 1, "b": [2, 3]}';
Query OK, 0 rows affected (0.00 sec)
mysql> select json_remove(@j, '$.a');
+------------------------+
| JSON_REMOVE(@j, '$.a') |
+------------------------+
| {"b": [2, 3]} |
+------------------------+
1 row in set (0.00 sec)
mysql> set @j = '["a", ["b", "c"], "d", "e"]';
Query OK, 0 rows affected (0.00 sec)
mysql> select json_remove(@j, '$[1]');
+-------------------------+
| JSON_REMOVE(@j, '$[1]') |
+-------------------------+
| ["a", "d", "e"] |
+-------------------------+
1 row in set (0.00 sec)
mysql> select json_remove(@j, '$[1]','$[2]');
+--------------------------------+
| JSON_REMOVE(@j, '$[1]','$[2]') |
+--------------------------------+
| ["a", "d"] |
+--------------------------------+
1 row in set (0.00 sec)
mysql> select json_remove(@j, '$[1]','$[1]');
+--------------------------------+
| JSON_REMOVE(@j, '$[1]','$[1]') |
+--------------------------------+
| ["a", "e"] |
+--------------------------------+
1 row in set (0.00 sec)
最后一个查询,虽然两个 path 都是 '$[1]' ,但作用对象不一样,第一个 path 的作用对象是 '["a", ["b", "c"], "d", "e"]' ,第二个 path 的作用对象是删除了 '$[1]' 后的数组,即 '["a", "d", "e"]' 。
同 TEXT,BLOB 字段一样,JSON 字段不允许直接创建索引。
mysql> create table t(c1 json, index (c1));
ERROR 3152 (42000): JSON column 'c1' supports indexing only via generated columns on a specified JSON path.
即使支持,实际意义也不大,因为我们一般是基于文档中的元素进行查询,很少会基于整个 JSON 文档。
对文档中的元素进行查询,就需要用到 MySQL 5.7 引入的虚拟列及函数索引。
下面我们来看一个具体的示例。
# C2 即虚拟列
# index (c2) 对虚拟列添加索引。
create table t ( c1 json, c2 varchar(10) as (JSON_UNQUOTE(c1 -> "$.name")), index (c2) );
insert into t (c1) values ('{"id": 1, "name": "a"}'), ('{"id": 2, "name": "b"}'), ('{"id": 3, "name": "c"}'), ('{"id": 4, "name": "d"}');
mysql> explain select * from t where c2 = '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 |
+----+-------------+-------+------------+------+---------------+------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
mysql> explain select * from t where c1->'$.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 |
+----+-------------+-------+------------+------+---------------+------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
可以看到,无论是使用虚拟列,还是文档中的元素来查询,都可以利用上索引。
注意,在创建虚拟列时需指定 JSON_UNQUOTE,将 c1 -> "$.name" 的返回值转换为字符串。
在 MySQL 支持 JSON 类型之前,对于 JSON 文档,一般是以字符串的形式存储在字符类型(VARCHAR 或 TEXT)中。
在 JSON 类型出来之后,如何将这些字符字段升级为 JSON 字段呢?
为方便演示,这里首先构建测试数据。
create table t (id int auto_increment primary key, c1 text);
insert into t (c1) values ('{"id": "1", "name": "a"}'), ('{"id": "2", "name": "b"}'), ('{"id": "3", "name": "c"}'), ('{"id", "name": "d"}');
注意,最后一个文档有问题,不是合格的 JSON 文档。
如果使用 DDL 直接修改字段的数据类型,会报错。
mysql> alter table t modify c1 json;
ERROR 3140 (22032): Invalid JSON text: "Missing a colon after a name of object member." at position 5 in value for column '#sql-7e1c_1f6.c1'.
下面,我们看看具体的升级步骤。
(1)使用 json_valid 函数找出不满足 JSON 格式要求的文档。
mysql> select * from t where json_valid(c1) = 0;
+----+---------------------+
| id | c1 |
+----+---------------------+
| 4 | {"id", "name": "d"} |
+----+---------------------+
1 row in set (0.00 sec)
(2)处理不满足 JSON 格式要求的文档。
mysql> update t set c1='{"id": "4", "name": "d"}' where id=4;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
(3)将 TEXT 字段修改为 JSON 字段。
mysql> select * from t where json_valid(c1) = 0;
Empty set (0.00 sec)
mysql> alter table t modify c1 json;
Query OK, 4 rows affected (0.13 sec)
Records: 4 Duplicates: 0 Warnings: 0
SQL 复制 全屏
对于 JSON 类型,有以下几点需要注意:
在 MySQL 8.0.13 之前,不允许对 BLOB,TEXT,GEOMETRY,JSON 字段设置默认值。从 MySQL 8.0.13 开始,取消了这个限制。
设置时,注意默认值需通过小括号()
括起来,否则的话,还是会提示 JSON 字段不允许设置默认值。
mysql> 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。
转发自:一文说透 MySQL JSON 数据类型(收藏) - iVictor - 博客园