MySQL Json 怎么处理,8.021后有什么变化,哪些不应该用JSON
❝开头还是介绍一下群,如果感兴趣PolarDB ,MongoDB ,MySQL ,PostgreSQL ,Redis, OceanBase, Sql Server等有问题,有需求都可以加群加群请联系 liuaustin3 ,(共3400人左右 1 + 2 + 3 + 4 +5 + 6 + 7 + 8 )(1 2 3 4 5 6 7 8群已经爆满 9群 为纯聊天群,默认不加入不得发广告,自己公众号文章链接等,发一次直接踢,默认加入8群,开10群PolarDB专业学习群115+)
本文重点(读完就能上手)
① JSON 类型给关系型表加了"弹性扩展区"——固定字段用 VARCHAR/INT,变化多端的塞 JSON;
② Generated Column(虚拟生成列)+ 索引 是 JSON 性能的核心,8.0 优化器自动识别,你不用改查询;
一、为什么要用 JSON 类型(而不是 TEXT)
MySQL 从 5.7 开始支持原生 JSON 类型,8.0 又加入了 JSON_TABLE、Multi-Valued Index 等杀手功能。它不是 MongoDB 的替代品,而是给关系型表开了一扇弹性扩展的门。
用 TEXT 存 JSON 字符串有三个问题:写入不校验、每次读取要重新解析、无法建索引。原生 JSON 类型在写入时自动校验合法性、内部用二进制格式存储(键排序、快速定位)、支持全套 JSON 函数和 Generated Column 索引。
一句话:99% 场景用 JSON 类型,只有纯日志写入永远不按内部字段查询的场景才考虑 TEXT。
二、路径语法:"->" 和 "->>" 的生死区别 MySQL JSON 路径从 $ 开始,点 . 访问对象键,[n] 访问数组下标。
但有一个让无数人踩坑的细节:col->path 和 col->>path 返回值类型不同。
原因很简单:MySQL 做字符串比较时,"Apple"(带引号)和 Apple(纯文本)是两个不同值。你写 WHERE attributes->'$.brand' = 'Apple',MySQL 实际在比较 "Apple" = "Apple"——JSON 值带引号,而右侧是纯文本,大概率匹配失败。
三、Generated Column:JSON 性能的核心 JSON 列不能直接建索引——MySQL 不允许 CREATE INDEX ON t(json_col),因为 JSON 没有固定的比较规则。但可以通过 Generated Column(生成列) 间接地给 JSON 里的字段建索引。
步骤分两步:先给 JSON 里的高频字段加一个 VIRTUAL 生成列(查询时实时计算,平时不占存储空间),再给这个生成列建普通索引。
-- Step 1: 给 brand(品牌)和 weight(重量)加生成列
ALTER TABLE products
ADD COLUMN brand VARCHAR(50)
GENERATED ALWAYS AS (attributes->>'$.brand') VIRTUAL,
ADD COLUMN weight_g INT
GENERATED ALWAYS AS (CAST(attributes->>'$.weight' AS UNSIGNED)) VIRTUAL;
-- Step 2: 给生成列建索引(MySQL 8.0 优化器自动识别)
CREATE INDEX idx_brand ON products (brand);
CREATE INDEX idx_weight ON products (weight_g);
-- Step 3: 直接用生成列名查询,或者继续用 JSON 写法——都能走索引!
EXPLAIN SELECT name FROM products
WHERE brand = 'Nike'; -- ✅ 走 idx_brand
EXPLAIN SELECT name FROM products
WHERE attributes->>'$.brand' = 'Nike'; -- ✅ 也自动走 idx_brand(8.0 优化器)
VIRTUAL 生成列不占实际存储空间(只存计算表达式),有二级索引时查询走索引、不计算,无开销。这是零成本高性能方案。
四、JSON_TABLE:把数组展开成行(8.0 杀手级功能) 如果说 Generated Column 解决了"给 JSON 单值建索引",那 JSON_TABLE 解决了"把 JSON 数组展开成多行"。它相当于 PostgreSQL 的 jsonb_each——把一个 JSON 数组字段变成一张临时表,然后跟普通表一样做 JOIN、GROUP BY、聚合。
电商订单表的 items 字段是 JSON 数组(每个订单可能有多个商品),要统计 SKU 销量,用 JSON_TABLE 一行搞定:
-- 订单表 items 格式: [{"sku":"SHOE-001","qty":2},{"sku":"SHIRT-003","qty":3}]
-- 用 JSON_TABLE 展开每个订单项,再聚合
SELECT jt.sku, SUM(jt.qty) AS total_sold
FROM orders o
CROSS JOIN JSON_TABLE(
o.items, -- 要展开的 JSON 列
'$[*]' -- 遍历数组每个元素
COLUMNS (
sku VARCHAR(64) PATH '$.sku',
qty INT PATH '$.qty'
)
) AS jt
GROUP BY jt.sku
ORDER BY total_sold DESC;
这段 SQL 的效果:CROSS JOIN 把每个订单的 items 数组拆成多行(每行是一个订单项),然后聚合统计。MySQL 8.0.14+ 对 JSON_TABLE 做了引擎级优化,不是应用层循环,配合 Generated Column 索引性能很好。
五、JSON 修改函数:部分更新不重写整个 JSON 高频场景是:用户下单后要更新商品 attributes 的 price 字段。传统做法是应用层取出来、改完、整个 JSON UPDATE 回去。但 MySQL 8.0 提供 JSON_SET / JSON_REPLACE / JSON_REMOVE 等函数,做部分更新——只改叶子节点,不重写整个 JSON。实测快 3~5 倍。
-- JSON_SET: 存在则更新,不存在则新增
UPDATE products
SET attributes = JSON_SET(attributes, '$.price', 699.00, '$.discount', true)
WHERE sku = 'SHOE-001';
-- JSON_REMOVE: 删除字段
UPDATE products
SET attributes = JSON_REMOVE(attributes, '$.discount')
WHERE sku = 'SHOE-001';
-- JSON_ARRAY_APPEND: 向数组追加元素
UPDATE products
SET attributes = JSON_ARRAY_APPEND(attributes, '$.color', 'navy')
WHERE sku = 'SHOE-001';
六、JSON 性能调优和调试技巧 第一个技巧:用 JSON_PRETTY() 格式化输出。存进去的 JSON 在存储前会被压缩(去掉所有空格换行),SELECT 出来是一长串很难看。调试时用 SELECT JSON_PRETTY(attributes) FROM products 就能得到缩进后的格式,方便肉眼检查。JSON_PRETTY 不影响存储,只改变返回格式。
第二个技巧:JSON_STORAGE_SIZE() 看实际占用。SELECT JSON_STORAGE_SIZE(attributes) FROM products 返回 JSON 二进制存储的字节数,跟 TEXT 存原始字符串对比一下——你会发现原生 JSON 因为键排序和去冗余,实际占用比 TEXT 小不少。这也是 JSON 类型的隐性优势之一。
第三个技巧:用 EXPLAIN 确认 Generated Column 索引生效。写完查询后加 EXPLAIN 看执行计划,type 应该是 ref 或 range,key 列应该显示你的索引名。如果还是 ALL(全表扫描),检查是不是路径写错了、或者 MySQL 版本低于 8.0 不能自动识别。
第四个技巧:JSON 键名大小写敏感。attributes->>'.brand' 是两个不同的路径。设计 JSON schema 时统一用 snake_case 或 camelCase,别混用。我推荐 snake_case——跟数据库列名风格一致,团队协作时不会混乱。
第五个技巧:JSON 列上限 1GB,但实际别超过几 MB。MySQL JSON 列理论上能存 1GB,但超过几 MB 后 JSON 函数的性能会显著下降,而且 InnoDB 溢出到 BLOB 页后还会影响表级性能。一个经验规则:单个 JSON 控制在 100KB 以内,超过就该拆表了。
三个必须知道的技术点 知识 1:-> 返回 JSON 值(带引号),->> 返回 SQL 标量(无引号)。WHERE 和 ORDER BY 里只能用 ->>。
知识 2:VIRTUAL 生成列不占存储空间,有二级索引时查询走索引不计算。STORED 才占空间(INSERT/UPDATE 时计算存储)。默认用 VIRTUAL。
知识 3:MySQL 8.0 优化器足够聪明——你写 attributes->>'$.brand',它自动匹配到 brand 生成列上的索引。但为了可读性建议直接写生成列名。
JSON_VALUE() 表达式索引(8.0.21+ 新写法)
8.0.21 引入了更简洁的写法——直接在 JSON_VALUE() 表达式上建索引,不需要先加 Generated Column:
-- 直接建表达式索引,不用先 ALTER TABLE
CREATE INDEX idx_attr_brand ON products
((JSON_VALUE(attributes, '$.brand')));
-- MySQL 自动识别这个表达式,查询走索引
SELECT * FROM products
WHERE JSON_VALUE(attributes, '$.brand') = 'Nike';
-- 原来的 ->> 写法也自动匹配这个索引
SELECT * FROM products
WHERE attributes->>'$.brand' = 'Nike';
JSON_VALUE() 的优势是可以指定返回类型——JSON_VALUE(attributes, '$.price' RETURNING DECIMAL(10,2)),比 CAST 更清晰。如果你的 MySQL 版本 ≥ 8.0.21,推荐用这种写法,比 Generated Column 更干净。
实际业务反例:JSON 什么时候不该用
说了这么多 JSON 好的地方,但它不是银弹。有几种场景绝对不该用 JSON:
第一,高频查询 + 频繁过滤的字段。如果一个字段在 50% 的查询里都出现在 WHERE 里,直接建固定列。JSON + Generated Column 虽然能补索引,但每次 INSERT/UPDATE 都要额外计算生成列值,写入开销比纯固定列大。
第二,需要 JOIN 的字段。如果你要"按 attributes 里的 category 字段 JOIN 到类别表",别用 JSON。Generated Column + JOIN 能跑,但查询计划会很丑,而且 MySQL 优化器对这种场景的支持不如原生列。老实建 category_id 外键。
第三,需要唯一约束的字段。JSON 列不能直接建 UNIQUE 约束(MySQL 不支持),Generated Column 可以,但语义上很奇怪。如果一个字段需要唯一,它本质上就是重要的业务字段,应该是固定列。
总结一下 JSON 的正确姿势:JSON 存"不查或很少查"的弹性字段,固定列存"高频查"的业务字段。70/30 原则——70% 字段固定列,30% 变化字段塞 JSON。别反过来。
微软通过AI 变身企业数据分析软件公司,是做数据库和操作系统不赚钱了???
普通人的AI 终极武器,怎么3分钟把一周的活干完--workbuddy
PostgreSQL 版本升级方法总结,具体pg_upgrade怎么操作
PostgerSQL 14-17备份的变化 PG17更贴近商业数据库 与 实际命令
PostgreSQL 怎么用好高版本的PG调优--PG14-PG18
同学问 PG17 的备份比老的版本 好哪了? 你给总结总结 !!
算法领主与数据农奴:AI时代的不能说的问题-- 此文为AI临时工所做与公众号作者无关
从亚马逊 AGI 部门裁员看 AI 商业逻辑的必然转向 -- 资本不会给AGI 半点脸
比起简单的Skill技能,我更想建立Agent Skill的系统思维能力--- 感谢本书作者答疑解惑
MongoDB 全文索引 与 展示查询数据的一部分,提高性能
体现价值-我们靠PostgreSQL迁移PolarDB,给公司省下了100万 “巨款”
《告别迁移焦虑:OceanBase MySQL 模式能否兼容 DBA 的“祖传”运维 SQL?》
干数据库不是买白菜:光盯着License几毛钱,看不见300台机器的电费?
一个秘密,不是你 SQL 写对了,是优化器帮“擦了屁股” 客户问迁移后为什么快了--迁移到PolarDB后的故事
AI 引入后,MySQL 列权限控制,插入,更新,读取,删除 --有了AI 真是越帮越忙
PostgreSQL 大表改字段卡死的问题解决了吗? 解决了方案在此
AI 引入DBA 工作,造成工作量增加,忙不过来,根本忙不过来!!!
三无项目导致MongoDB 持续1406% CPU 问题解决