游乐游手机版
首页/AI教程/文章详情

MySQL JSON类型生产实战:部分更新、虚拟列索引、性能边界与Schema设计决策

时间:2026-06-07 16:11
MySQLJSON类型具有写入校验、二进制存储与部分更新优势,通过虚拟列加B-Tree索引可高效查询。适用于商品规格、配置中心、API缓存等动态属性场景,但不可直接WHERE比较,需用JSON_EXTRACT或虚拟列。

? 本文关键词:MySQL JSON 类型、JSON vs TEXT、JSON_EXTRACT、JSON_SET、JSON_TABLE、虚拟列索引、Generated Column、Schema-Free、部分更新、数据库设计、面试必备知识点

MySQL JSON 类型生产实战:部分更新、虚拟列索引、性能边界与 Schema 设计决策

从实际业务场景切入。之前有同学问:“每个商品的规格参数都不相同,到底是建立子表合适,还是用 JSON 字段存储?”这个问题非常有代表性。

前阵子接手了一个配置中心的需求,产品要求不同业务线可以自定义配置项。起初我打算建子表——config_key、config_value,再加一张关联表。方案写完后,前辈一句点拨:“你这个场景用 JSON 一个字段就搞定了,何必拆三张表?”说实话,一开始我不太服气,但实践之后发现他说得对。JSON 不是偷懒,在某些场景下它确实比拆表更合理。

当然,JSON 也有自己的局限性,用错了地方,后期维护起来比子表还麻烦。下面分享这段时间踩过的坑和积累的经验。

一、先搞清楚:JSON 类型和 TEXT 存 JSON 到底差在哪里

很多人(包括之前的我)会觉得:反正都是存一串 JSON 字符串,用 TEXT 不就行了?

其实差别很大。

TEXT 存 JSON:MySQL 把它当纯字符串处理

TEXT 字段对内容完全不关心。即使塞入一个 {name: 小学妹} 这样的非法 JSON,它也会照单全收。等到用 JSON_EXTRACT 去取值时,才发现取不出来,只能报语法错误。

而且 TEXT 存储的 JSON 每次更新都是整体重写。一个 5KB 的 JSON,只修改其中一个字段,也必须把整条 5KB 重新写一遍。

JSON 类型:MySQL 会认真对待它

MySQL 的 JSON 类型有三件 TEXT 做不到的事情。

第一,写入时自动校验。非法 JSON 直接报错,脏数据无法入库。

CREATE TABLE test_json (id INT PRIMARY KEY, info JSON);
-- 合法 JSON,写入成功
INSERT INTO test_json VALUES (1, '{"name": "小学妹", "skill": "MySQL"}');
-- 非法 JSON,直接报错
INSERT INTO test_json VALUES (2, '{name: 小学妹}');
-- ERROR 3140 (22032): Invalid JSON text

第二,内部采用二进制存储。JSON 类型不是保存原始字符串,而是转换成一种叫 Opaque 的二进制格式。好处是读取某个字段时不需要解析整个字符串,直接通过偏移量定位,速度更快。

第三,支持部分更新。这是最实用的功能。只修改 JSON 里的某个字段,MySQL 只会更新那一部分,无需重写整条记录。

-- 只改 name 字段,其他不动
UPDATE test_json SET info = JSON_SET(info, '$.name', '数据库小学妹') WHERE id = 1;

从 Binlog 角度来看也有区别。TEXT 整体重写后,ROW 模式下 Binlog 记录的是整条前后镜像;JSON 部分更新时,Binlog 记录的变更更小。数据量较大的时候,这个差异会直接影响主从同步的效率。

简单来说,TEXT 存 JSON 只是“假装 JSON”,MySQL 根本不把它当作 JSON 处理,也无法使用 JSON 的那些便利操作。如果想正经使用 JSON,就老老实实用 JSON 类型。

二、JSON 函数速查表

JSON 函数一开始看起来确实令人头疼,名字又长又像。整理了一张速查表,按功能分类之后就好记多了。

读取类

函数 简写 功能说明 示例
JSON_EXTRACT(json, '$.key') -> 取某个字段的值(带引号) SELECT info->'$.name' FROM test_json
JSON_UNQUOTE(JSON_EXTRACT(...)) ->> 取某个字段的值(去引号) SELECT info->>'$.name' FROM test_json
JSON_KEYS(json) 列出所有顶层 key SELECT JSON_KEYS(info) FROM test_json
JSON_LENGTH(json) 数组长度或 key 数量 SELECT JSON_LENGTH(info) FROM test_json
JSON_CONTAINS(json, val) 判断是否包含某个值 SELECT JSON_CONTAINS(info->'$.tags', '"MySQL"')

最常用的是 ->>-> 取出来的值带双引号("小学妹"),->> 取出来是纯文本(小学妹)。一般查询用 ->> 就对了。

修改类

函数 功能说明 关键区别
JSON_SET(json, path, val) 设置值(存在就更新,不存在就新增) 最常用,90%的场景用它
JSON_INSERT(json, path, val) 只在 key 不存在时插入 已有 key 不修改
JSON_REPLACE(json, path, val) 只在 key 存在时替换 没有 key 不操作
JSON_REMOVE(json, path) 删除某个 key 删除不存在的 key 不报错

记忆技巧:JSON_SET = INSERT + REPLACE 的合体,最省心,不知道该用哪个的时候就选它。

聚合类

函数 功能说明 示例
JSON_ARRAYAGG(col) 把多行合并成一个 JSON 数组 SELECT JSON_ARRAYAGG(username) FROM users
JSON_OBJECTAGG(key, val) 把多行合并成一个 JSON 对象 SELECT JSON_OBJECTAGG(id, username) FROM users

这两个函数在做报表聚合时特别实用,直接在 SQL 层把结果拼成 JSON 返回,省得应用层再组装。

表函数

JSON_TABLE 是 MySQL 8.0 的大招,作用是把 JSON 数组“展开”成一个虚拟表,然后就可以对它做 JOIN、WHERE、GROUP BY。

SELECT jt.* FROM products, JSON_TABLE(products.attrs, '$.specs[*]' COLUMNS (spec_name VARCHAR(50) PATH '$.name', spec_value VARCHAR(100) PATH '$.value')) AS jt WHERE spec_name = '屏幕尺寸';

这条 SQL 把 products 表里 attrs 字段中的 specs 数组展开成行,每行有 spec_name 和 spec_value 两列,然后正常做 WHERE 过滤。不再需要在应用层手动解析 JSON 数组再循环查询。

三、JSON 索引方案:虚拟列 + B-Tree 索引

这是很多人不知道的一点:JSON 字段本身不能直接建索引。

ALTER TABLE products ADD INDEX idx_brand (attrs->'$.brand');
-- ERROR 3152: JSON column '...' supports indexing only via virtual generated columns

MySQL 直接告诉你:要索引就得走虚拟列。

方案:Generated Column(生成列)+ 索引

思路很简单:从 JSON 字段里提取需要查询的属性,放到一个“虚拟列”里,然后对这个虚拟列建索引。

-- 第一步:加虚拟列,从 JSON 中提取 brand 字段
ALTER TABLE products ADD COLUMN brand VARCHAR(100) GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(attrs, '$.brand'))) VIRTUAL;
-- 第二步:在虚拟列上建索引
ALTER TABLE products ADD INDEX idx_brand (brand);

加完之后,正常查询就能走索引了:

-- 走索引!
SELECT * FROM products WHERE brand = 'Apple';

VIRTUAL vs STORED

生成列有两种模式,区别如下:

对比项 VIRTUAL(默认) STORED
存储 不占磁盘空间,查询时实时计算 占磁盘空间,写入时计算并存储
写入性能 不影响 有额外开销
查询性能 计算一次(有索引时走索引) 直接读值
能建索引 可以(MySQL 5.7+) 可以

大部分场景用 VIRTUAL 就够了。不额外占用空间,索引也照常走。只有发现某个 JSON 属性查询特别频繁、计算开销明显时,再考虑换成 STORED。

完整实战示例:商品灵活属性表

不同商品有不同的规格参数,手机有“屏幕尺寸”和“处理器”,衣服有“尺码”和“面料”。如果每种商品都建一张规格子表,种类一多表就会爆炸。

用 JSON 一个字段搞定:

CREATE TABLE products (id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(200) NOT NULL, price DECIMAL(10,2) NOT NULL, attrs JSON COMMENT '商品属性,不同品类结构不同', created_at DATETIME DEFAULT CURRENT_TIMESTAMP);
-- 手机
INSERT INTO products (name, price, attrs) VALUES ('iPhone 16', 6999.00, '{"brand": "Apple", "screen": "6.1寸", "cpu": "A18", "color": ["黑色", "白色", "蓝色"]}');
-- 衣服
INSERT INTO products (name, price, attrs) VALUES ('纯棉T恤', 89.00, '{"brand": "优衣库", "size": ["S", "M", "L", "XL"], "material": "100%棉"}');

给常用的查询属性加虚拟列索引:

ALTER TABLE products ADD COLUMN brand VARCHAR(100) GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(attrs, '$.brand'))) VIRTUAL, ADD INDEX idx_brand (brand);

查询的时候就像查普通字段一样:

-- 按品牌查,走索引
SELECT name, price, attrs->>'$.screen' AS screen FROM products WHERE brand = 'Apple';
-- 查某个数组里是否包含特定值
SELECT name, price FROM products WHERE JSON_CONTAINS(attrs->'$.color', '"蓝色"');

四、三大实战场景

场景 1:用户动态属性(电商商品规格)

就是上面那个例子。不同品类的商品属性完全不同,硬建子表就得一个品类一张表,或者搞一张超宽的“万能属性表”(100 个列,90% 是 NULL)。

用 JSON 就一个字段搞定,新增属性不用 ALTER TABLE。

场景 2:配置中心表设计

CREATE TABLE app_config (app_name VARCHAR(50) PRIMARY KEY, config JSON NOT NULL, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP);
INSERT INTO app_config (app_name, config) VALUES ('order-service', '{"timeout": 30, "retry": 3, "max_connections": 100, "features": {"dark_mode": true, "new_checkout": false}}');

一个字段存放一整套配置。读取时直接得到 JSON,应用层无需拼装。修改某一项配置用 JSON_SET 部分更新,不用整条覆盖。

相比 config_key / config_value 这种 EA V 模式(Entity-Attribute-Value),JSON 方案查询更快、维护更简单,还省去了大量 JOIN。

场景 3:第三方 API 响应缓存

对接外部接口的时候,返回的 JSON 往往结构不固定,或者经常变化。与其每次收到响应都拆字段入库(字段变了还得改表),不如直接把原始 JSON 存入 JSON 列,常用的查询字段用虚拟列索引提取。

CREATE TABLE api_cache (id INT PRIMARY KEY AUTO_INCREMENT, source VARCHAR(50) NOT NULL, response JSON NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP);
-- 第三方接口返回啥就存啥
INSERT INTO api_cache (source, response) VALUES ('weather_api', '{"city": "北京", "temp": 28, "humidity": 65, "forecast": [...]}');

后面需要查询某个城市的温度,虚拟列 + 索引就解决了。

五、JSON 的 5 个坑(踩过的都在这了)

坑 1:不能直接 WHERE json_col = xxx

-- 这样写不行!
SELECT * FROM products WHERE attrs = '{"brand": "Apple"}';

JSON 类型的比较是基于二进制值的,而不是按内容。key 的顺序不同、空格不同,都会导致不相等。

正确写法是使用 JSON_EXTRACT 或虚拟列:

SELECT * FROM products WHERE attrs->>'$.brand' = 'Apple';

坑 2:JSON 内部字段无法加 NOT NULL 约束

可以保证 MySQL 表的字段不为空,但无法保证 JSON 内部某个 key 一定存在。attrs->'$.brand' 可能返回 NULL,MySQL 不会帮忙拦截。

解法:如果某个 JSON 属性是必须的,要么用触发器做校验,要么在应用层保证。或者,该用传统列就用传统列,别硬套 JSON。

坑 3:Schema-Free 反模式

这个坑差点踩了。当时想着“反正 JSON 什么都能存,那把所有字段都塞 JSON 里算了”。

前辈及时拉住了:把所有字段都塞 JSON 里,等于主动放弃类型检查、约束、索引。那用关系型数据库干什么?

JSON 适合存储“不确定结构的扩展属性”,不适合存储“确定结构的核心数据”。商品名称、价格、上架时间这些,该用普通列就用普通列。

坑 4:JSON 列排序按二进制值

对 JSON 列做 ORDER BY,排序的是二进制存储值,而不是肉眼看到的内容语义。想按 JSON 里的某个字段排序,需要提取出来再排:

-- 按品牌名排序
SELECT * FROM products ORDER BY attrs->>'$.brand';

坑 5:大 JSON 文档的性能边界

JSON 字段太大,读写性能都会下降。根据测试,单条 JSON 超过 10KB 就应该考虑拆分。

太大的 JSON 还有一个隐形问题:临时表排序时如果包含 JSON 列,tmp_table_size 很容易超限,直接溢出到磁盘临时表,查询速度断崖式下降。

六、什么时候不该用 JSON(该建子表还是得建)

JSON 解决不了所有问题。以下几种情况,老老实实建子表才是正确选择:

场景 为什么不该用 JSON 该怎么做
需要 JOIN 关联查询 JSON 内部数据没法直接 JOIN 建子表,使用外键关联
需要复杂聚合/排序 JSON 提取后再聚合,性能差且写法复杂 建子表,使用原生聚合函数
字段需要加约束 JSON 内部不支持外键、唯一、CHECK 建子表,该加约束就加约束
需要按多个维度频繁查询 每个查询的属性都要加虚拟列索引,多了维护成本高 建子表更清晰
数据量大且结构完全固定 结构已确定,用 JSON 纯属自找麻烦 建子表

判断标准其实不复杂:如果只需要存和取,不需要对内部字段做复杂查询、关联、约束,那 JSON 就够用了。其他情况,建子表。

面试怎么答

面试官可能会问:MySQL 的 JSON 类型和直接用 TEXT 存 JSON 有什么区别?

另一个常见问题:什么场景适合用 JSON,什么场景不适合?

避坑清单

# 后果 正确做法
1 用 TEXT 存 JSON 当 JSON 类型用 没有校验、没有部分更新、没有性能优化 MySQL 8.0 直接用 JSON 类型
2 所有字段都塞 JSON 里(Schema-Free) 丢失类型检查、约束、索引优势 核心字段用普通列,扩展属性才用 JSON
3 忘记 JSON 字段不能直接建索引 查询全表扫描,慢到怀疑人生 用虚拟列 + B-Tree 索引方案
4 单条 JSON 超过 10KB 不拆分 临时表溢出磁盘,查询性能断崖下降 定期审查 JSON 字段大小,该拆就拆
5 直接 WHERE json_col = xxx 比较 二进制比较,内容相同但格式不同就查不到 ->> 提取字段后比较
6 靠 JSON 内部字段做业务约束 JSON 不支持 NOT NULL、唯一约束 必须约束的字段走普通列
7 JSON_SET 路径写错不报错 新增了一个错误的 key,数据悄悄出错 写完 UPDATE 后立刻 SELECT 验证

JSON 类型已经推出好几年了,但身边好些同事还是不太敢用,怕踩坑,或者用了之后发现坑比想象的多。

总结一下:动态属性、配置、缓存这类场景,JSON 比拆子表省事太多,该用就用。但核心数据需要关联查询、需要约束的,子表才是正路,别硬塞 JSON。另外有一点容易被忽略:使用前先想清楚索引方案,虚拟列 + 索引在建表时就配好,别等查询慢了才想起来加。

有踩过 JSON 坑的同学,欢迎在评论区聊聊?

来源:https://developer.aliyun.com/article/1739682
上一篇2026前端效能革命:常用代码编写辅助工具有哪些 下一篇小红书矩阵系统基于阿里云高并发架构资源优化方案
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
Coze本地模型运行下载与路径配置性能优化指南
AI教程 · 2026-07-12

Coze本地模型运行下载与路径配置性能优化指南

Coze接入本地模型的核心在于先准备推理服务,再完成模型下载、存放路径配置、接口适配与性能调优,适合开发测试、数据内控和低成本原型验证场景。

Coze插件安装教程:浏览器、编辑器与扩展市场配置
AI教程 · 2026-07-12

Coze插件安装教程:浏览器、编辑器与扩展市场配置

Coze插件安装可分为平台内插件、浏览器扩展与编辑器扩展三类。配置前需确认账号权限、来源可信、授权范围和测试环境,新手按创建项目、安装插件、填写参数、调试发布的顺序操作更稳妥。

Coze API Key配置教程 账号注册 密钥获取 国内网络设置
AI教程 · 2026-07-12

Coze API Key配置教程 账号注册 密钥获取 国内网络设置

围绕Coze与扣子平台的APIKey配置流程,梳理账号注册、密钥创建、环境变量设置、国内网络连通性检查、调用验证、常见报错处理及安全使用边界。

Coze Docker一键部署教程:镜像拉取端口映射数据目录配置
AI教程 · 2026-07-12

Coze Docker一键部署教程:镜像拉取端口映射数据目录配置

面向想在本地或服务器部署Coze的用户,梳理Docker环境准备、镜像拉取、端口映射、数据目录挂载、启动检查、升级回滚与安全注意事项,帮助快速搭建稳定的AI智能体平台运行环境。

Coze Linux服务器部署:从环境准备到后台运行完整教程
AI教程 · 2026-07-12

Coze Linux服务器部署:从环境准备到后台运行完整教程

Coze适合团队搭建智能体应用与工作流。Linux部署建议优先采用容器方式,提前规划配置、端口、数据目录与安全策略,并通过服务化方式实现稳定后台运行。