侧边栏壁纸
博主头像
Setyg的博客

当秋风染红落叶的时候,就是新生~

  • 累计撰写 86 篇文章
  • 累计创建 66 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

MySQL 对 JSON 数据的增删改查实践

Setyg
2026-09-20 / 0 评论 / 0 点赞 / 2 阅读 / 0 字

MySQL 从 5.7.8 开始提供原生 JSON 类型:字段存二进制格式而非纯文本,可以用 JSON_OBJECT() / JSON_ARRAY() 构造,用 JSON_SET() / JSON_INSERT() / JSON_REPLACE() / JSON_REMOVE() 局部修改,用 -> 和 ->> 取值。JSON 列最大的限制是不能直接建索引,需要靠虚拟列绕过去。本文把增删改查的常用写法和几个容易踩的坑整理一遍。

一、建表

CREATE TABLE `test_user`
(
    `id`   INT PRIMARY KEY AUTO_INCREMENT,
    `name` VARCHAR(50) NOT NULL,
    `info` JSON
);

几个前提要知道:

  • JSON 列可以为 NULL(除非显式声明 NOT NULL),且不能直接建索引,写 ADD INDEX (info) 会报错。
  • JSON 列早期不允许有默认值;MySQL 8.0.13+ 只接受表达式形式的默认值,如 DEFAULT (JSON_OBJECT()),写 DEFAULT '{}' 不合法。
  • 存储是规范化后的二进制格式:多余空格、键的书写顺序、重复键都会被处理掉,所以 SELECT 出来的内容和你插入的字符串不一定字面相同(重复键以最后一个为准)。

二、插入

直接写入 JSON 文本:

INSERT INTO test_user(`name`, `info`)
VALUES ('xiaoming', '{
  "sex": 1,
  "age": 18,
  "nick_name": "小萌"
}');

更稳妥的做法是用构造函数拼装,避免手写 JSON 字符串出差错:

-- JSON_OBJECT():构造对象
INSERT INTO test_user(`name`, `info`)
VALUES ('xiaohua', JSON_OBJECT("sex", 0, "age", 17));

-- JSON_ARRAY():构造数组,可以嵌套在对象里
INSERT INTO test_user(`name`, `info`)
VALUES ('xiaozhang', JSON_OBJECT("sex", 1, "age", 19, "tag", JSON_ARRAY(3, 5, 90)));

-- 也可以把现成的 JSON 文本显式转换
INSERT INTO test_user(`name`, `info`)
VALUES ('xiaolei', CAST('{"sex": 1, "age": 20}' AS JSON));

三、查询

1. -> 和 ->> 的区别

SELECT name,
       info->'$.nick_name'  AS nick_name_json,   -- 返回 JSON,字符串带双引号
       info->>'$.nick_name' AS nick_name,        -- 返回去引号后的文本
       info->>'$.sex'       AS sex,
       info->>'$.tag[2]'    AS tag
FROM test_user;
写法 等价函数 返回值
-> JSON_EXTRACT(info, '$.nick_name') JSON 值,"小萌" 会带双引号
->> JSON_UNQUOTE(JSON_EXTRACT(...)) 去掉引号的文本,适合直接展示和比较

所以取字符串务必用 ->>;等价的老写法是套一层 JSON_UNQUOTE():

SELECT name, JSON_UNQUOTE(info->'$.nick_name') AS nick_name
FROM test_user WHERE name = 'xiaoming';

2. 路径语法

路径 含义
$ 整个 JSON 文档
$.nick_name 对象的一级键
$.address.city 嵌套对象的键
$.tag[0] / $.tag[2] 数组下标,从 0 开始(负下标不支持)
$."nick name" 键里含空格等特殊字符时用双引号包起来
$**.nick_name 通配符递归匹配(8.0 起在 JSON_EXTRACT 等函数中支持)

路径不存在时返回 NULL(路径语法写错才是报错),这点和 SQL 查询的习惯一致。

3. 用 JSON 属性作为查询条件

-- 推荐:取出文本再比较
SELECT name, info->>'$.nick_name' AS nick_name
FROM test_user
WHERE info->>'$.nick_name' = '小萌';

WHERE info->'$.nick_name' = '小萌' 这种拿 JSON 值直接和字符串比较的写法在多数版本也能命中,但 JSON 与字符串的比较规则在 8.0.17 前后有过调整,跨版本迁移时容易出差异,统一写成 ->> 更保险。

需要判断「是否包含某个值 / 某个路径」时用专门函数:

-- 包含指定键值
SELECT * FROM test_user WHERE JSON_CONTAINS(info, '1', '$.sex');
SELECT * FROM test_user WHERE JSON_CONTAINS(info, '"小萌"', '$.nick_name');

-- 只判断路径是否存在
SELECT * FROM test_user WHERE JSON_CONTAINS_PATH(info, 'one', '$.tag');

-- 数组长度、所有键名、值的类型
SELECT JSON_LENGTH(info->'$.tag'), JSON_KEYS(info), JSON_TYPE(info->'$.sex') FROM test_user;

4. 用虚拟列给 JSON 属性建索引

JSON 列不能直接加索引,实际生产里靠「把属性抽成虚拟列,再给虚拟列建索引」来加速:

-- 注意用 ->> 而不是 ->,否则表达式类型是 JSON,赋给 varchar 列不成立
ALTER TABLE `test_user`
    ADD `nick_name` VARCHAR(50) GENERATED ALWAYS AS (info ->> '$.nick_name') VIRTUAL;

-- MySQL 5.7 / 8.0 的 InnoDB 都支持给虚拟列建二级索引
ALTER TABLE `test_user` ADD INDEX idx_nick_name (nick_name);

-- 建好之后按普通列查,能走索引
SELECT name, nick_name FROM test_user WHERE nick_name = '小萌';

虚拟列不占数据文件空间(值在读取时计算),STORED 则会把计算结果落盘,可按需选择。JSON 数组类字段(如 tag)如果要在 JSON_CONTAINS 上走索引,MySQL 8.0.17+ 还支持多值索引:

ALTER TABLE test_user
    ADD INDEX idx_tag ((CAST(info->'$.tag' AS UNSIGNED ARRAY)));

四、更新

四个函数的语义完全不同,选错会「以为改了其实没改」:

函数 路径已存在 路径不存在
JSON_INSERT() 保持原值不动 插入
JSON_SET() 覆盖 插入
JSON_REPLACE() 替换 忽略(不插入)
JSON_REMOVE() 删除 忽略
-- 只插入新值,已存在的字段不受影响
UPDATE test_user
SET info = JSON_INSERT(info, '$.sex', 1, '$.nick_name', '小花')
WHERE id = 2;

-- 插入新值并覆盖已存在的字段(最常用)
UPDATE test_user
SET info = JSON_SET(info, '$.sex', 2, '$.nick_name', '小张')
WHERE id = 3;

-- 只替换已存在的字段,路径不存在则跳过
UPDATE test_user
SET info = JSON_REPLACE(info, '$.sex', 1, '$.tag', JSON_ARRAY(1, 2, 3))
WHERE id = 2;

这里有个高频坑:JSON 函数的参数是 SQL 值,字符串会原样变成 JSON 字符串,不会被当成 JSON 文本解析。所以 JSON_REPLACE(info, '$.tag', '[1,2,3]') 存进去的是字符串 "[1,2,3]",而不是数组 [1,2,3],后续 JSON_LENGTH、JSON_CONTAINS 都会出意外结果:

-- 想要数组,用 JSON_ARRAY() 构造
SET info = JSON_SET(info, '$.tag', JSON_ARRAY(1, 2, 3))

-- 或者显式转换已有的 JSON 文本
SET info = JSON_SET(info, '$.tag', CAST('[1,2,3]' AS JSON))

另外注意上面第二条 UPDATE 的对象:id = 2 这行是 JSON_OBJECT("sex", 0, "age", 17) 插入的,没有 tag 字段,而 JSON_REPLACE 不会插入不存在的路径,所以这句里 $.tag 部分实际什么也没做——「改了没生效」通常就是这类静默忽略。

五、删除

-- 一次删除多个路径,路径不存在时忽略
UPDATE test_user
SET info = JSON_REMOVE(info, '$.sex', '$.tag')
WHERE id = 1;

-- 删除整个字段(含值)直接置 NULL 即可
UPDATE test_user SET info = NULL WHERE id = 1;

-- 只想删数组里的某个元素,也用 JSON_REMOVE 按下标操作
UPDATE test_user SET info = JSON_REMOVE(info, '$.tag[0]') WHERE id = 3;

六、常见坑汇总

  • JSON 列不能直接建索引,要建索引必须走虚拟列 / 多值索引。
  • 取字符串用 ->>,-> 返回的 JSON 值带双引号,直接展示或比较会出问题。
  • JSON 函数里的字符串参数不是 JSON 文本,数组 / 对象要用 JSON_ARRAY()、JSON_OBJECT() 或 CAST(... AS JSON)。
  • JSON_INSERT 不覆盖、JSON_REPLACE 不插入,路径不存在时静默忽略,不报错。
  • 存储经过规范化:重复键去重、键顺序与空白会被重排,不能拿字符串比较来判断两个 JSON 是否「一样」——用 JSON_CONTAINS 或先取字段值再比。
  • 大 JSON 慎用:JSON 列不参与统计信息收集,属性又多又深时查询会退化成全表扫描 + 逐行解析,能拆成普通列的字段尽量拆出来。

七、速查卡

-- 取值:-> 返回 JSON(带引号),->> 返回文本
SELECT info->'$.a', info->>'$.a', info->>'$.arr[0]' FROM t;

-- 构造
JSON_OBJECT('k', 1), JSON_ARRAY(1, 2), CAST('{"a":1}' AS JSON)

-- 判断
JSON_CONTAINS(info, '1', '$.sex')            -- 是否包含键值
JSON_CONTAINS_PATH(info, 'one', '$.tag')     -- 路径是否存在
JSON_LENGTH(info->'$.tag'), JSON_KEYS(info), JSON_TYPE(info->'$.a')

-- 修改
JSON_INSERT(info, '$.a', 1)      -- 只插入,不覆盖
JSON_SET(info, '$.a', 1)         -- 插入或覆盖
JSON_REPLACE(info, '$.a', 1)     -- 只替换,不插入
JSON_REMOVE(info, '$.a', '$.b')  -- 删除路径

-- 索引:虚拟列 + 虚拟列索引
ALTER TABLE t ADD c VARCHAR(50) GENERATED ALWAYS AS (info->>'$.c') VIRTUAL;
ALTER TABLE t ADD INDEX idx_c (c);
0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区