SQL与数据库开发(三):MySQL 原生 JSON 操作实战

在敏捷开发中,业务需求变化极快。今天产品经理说要给用户加个"星座"属性,明天又要加个"偏好标签"。如果每次加字段都去执行 ALTER TABLE,不仅要提工单走审批,大表 DDL 还极易引发线上锁表事故。

为了偷懒,很多后端的常见做法是:在表里建一个 VARCHAR 或 TEXT 类型的 extra_info 字段,把所有不固定的属性序列化成一个 JSON 字符串塞进去。 存的时候是很爽,但出来混迟早要还。当业务要求"查出所有偏好标签里包含'二次元'的用户"时,开发人员只能写出类似 WHERE extra_info LIKE '%"tag":"二次元"%' 这种令人绝望的代码。这不仅会让索引彻底瘫痪,还会因为 JSON 格式的不严谨引发各种解析 Bug。

实际上,从 5.7 版本开始,MySQL 就已经吹响了向 MongoDB 等 NoSQL 数据库反击的号角,引入了原生的 JSON 数据类型与强大的处理函数。本文将带你实战这套关系型数据库中的"非结构化"玩法。

一、 告别 LIKE:精准提取 JSON 节点

需求说明: 查出所有注册渠道为 "AppStore" 的用户。

表结构参考: users (id, username, attributes JSON) 注:attributes 内存放类似 {"age": 25, "channel": "AppStore", "tags": "游戏", "动漫"} 的数据。

错误解法: 使用字符串匹配 LIKE '%"channel":"AppStore"%'。

使用原生 JSON 操作符: MySQL 提供了 JSON_EXTRACT() 函数,并且在 5.7.9 之后提供了极为优雅的简写符号:-> 和 ->>。

sql 复制代码
-- 提取 JSON 中的 channel 属性进行比对
SELECT 
    username, 
    attributes->>'$.age' AS user_age 
FROM users 
WHERE attributes->>'$.channel' = 'AppStore';

原理解析(极其重要的避坑点):

  • 代表整个 J S O N 根文档。 代表整个 JSON 根文档。 代表整个JSON根文档。.channel 就是沿着路径往下找。

  • -> 和 ->> 的致命区别:

  • attributes->'$.channel' 提取出来的值是带双引号的 JSON 字符串(结果为 "AppStore")。如果直接拿去和 'AppStore' 比较,结果永远是 false。

  • attributes->>'$.channel'(多了一个 >)的作用是去掉引号(Unquote) ,提取出纯文本(结果为 AppStore)。在做条件比对时,务必使用 ->>

二、 性能破局:JSON 无法建索引怎么办?

很多开发学到了上面的 ->> 语法,立刻就用到生产环境去,结果发现查询依然慢得令人发指。 原因很简单:JSON 列本身是一个大文本,MySQL 无法直接为 JSON 内部的某个动态 Key 建立 B+ 树索引。 WHERE attributes->>'$.channel' = 'AppStore' 依然是一次极其耗费 CPU 的全表扫描。

终极解法:虚拟生成列(Generated Column)+ B+树索引

这是 MySQL 为 JSON 性能插上翅膀的核心黑科技。我们可以把 JSON 里高频查询的字段,"虚构"成表里的一个普通物理列,并在这个虚数列上建索引。

sql 复制代码
-- 步骤 1:增加一个虚拟列(VIRTUAL),这个列的数据不需要物理存储,而是由 JSON 实时计算得出
ALTER TABLE users 
ADD COLUMN v_channel VARCHAR(32) GENERATED ALWAYS AS (attributes->>'$.channel') VIRTUAL;

-- 步骤 2:像给普通列加索引一样,给这个虚拟列建 B+ 树索引
CREATE INDEX idx_user_channel ON users(v_channel);

-- 步骤 3:修改查询语句,直接查虚拟列
SELECT username FROM users WHERE v_channel = 'AppStore';

工程价值: 数据写入时,你依然只需要往 attributes 塞 JSON 字符串,MySQL 会在内部自动维护计算 v_channel 的值并更新索引树。你完美兼顾了 NoSQL 的动态扩展性关系型数据库的百万级索引性能

三、 告别"并发覆盖":局部更新 JSON 属性

需求说明: 将 ID 为 1 的用户的年龄(age)修改为 26 岁。

传统灾难解法:

  1. 查出整行数据:SELECT attributes FROM users WHERE id = 1。
  2. 在 Java 代码里反序列化成 Map,修改 age = 26,再序列化成 JSON 字符串。
  3. 把整个字符串 UPDATE 回去。 在高并发下,如果有另一个线程同时修改了 channel 字段,这种"全量拉取-修改-全量覆盖"的做法会导致严重的更新丢失(脏写)

使用 JSON 函数就地更新: 让数据库自己去局部修改 JSON 节点,避免业务层面的并发冲突。

ini 复制代码
-- 使用 JSON_SET 原地修改属性
UPDATE users 
SET attributes = JSON_SET(attributes, '$.age', 26)
WHERE id = 1;

配套函数扩展:

  • JSON_INSERT():只插入新属性,如果原 key 存在则不修改。
  • JSON_REPLACE():只替换旧属性,如果原 key 不存在则不插入。
  • JSON_REMOVE():直接删除某个属性节点。

四、 总结

不要再把 JSON 当作"字符串黑洞"来使用了。

  1. 取值: 使用 ->> 摆脱丑陋的 LIKE 模糊匹配,获取干净的文本值。
  2. 性能: 遇到高频查询的 JSON 属性,务必使用 "虚拟列 + 索引" 的组合拳。
  3. 更新: 坚决执行 JSON_SET/REPLACE 局部修改,禁止将整个 JSON 拖回应用层覆盖。

架构边界提示: 虽然 MySQL 提供了 JSON 支持,但它依然是个关系型数据库。核心的、参与多表 JOIN 关联的、强一致性要求的字段(如金额、状态、主键),必须老老实实建立普通表字段。 JSON 仅仅只适用于那些"食之无味弃之可惜"的边缘动态扩展属性。

相关推荐
GoGeekBaird13 小时前
我开源了 BeeWeave,给 AI Agent 搭一个越用越懂你的知识创作台
后端·github
犀利豆16 小时前
AI in Harness(四)
人工智能·后端
Hui Baby16 小时前
Spring Security
java·后端·spring
逝水无殇17 小时前
C# 运算符重载详解
开发语言·后端·c#
苏三说技术17 小时前
2026编程圈很火的10个Skills
后端
用户83562907805117 小时前
使用 Python 自动化 Excel 公式和函数:完整指南
后端·python
编程(变成)小辣鸡18 小时前
Spring事务失效场景
java·后端·spring
PinkSun18 小时前
MySQL 建表报 1030,能查不能建,排查 1 小时发现是我自己改了一行权限
后端
无相求码19 小时前
为什么 printf 可以接受任意数量参数?变长参数的底层真相
后端
郡杰19 小时前
Boot:MP|测验|结果封装|异常处理|前后联调|拦截器
后端