MySQL 5.7升级到8.0之后,JSON查询的性能瓶颈真的解决了吗?

简介: MySQL 5.7引入原生JSON类型,8.0支持多值索引,至今已近十年。但大量开发人员仍在把JSON当“万能兜底字段”——不管什么数据都往里塞,等查询慢到怀疑人生才想起来排查。本文拆解JSON字段查询的5个高频踩坑场景,给出虚拟列索引、多值索引等正确的优化方案。

大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!

前两天刷到一个面试题:

“MySQL的JSON字段支持索引吗?”

下面一堆回答:“支持啊,MySQL 8.0开始支持JSON索引了。”

这个回答对了一半。

准确地说:MySQL 8.0引入了多值索引(Multi-Valued Index),但它是为JSON数组设计的。对于普通JSON字段的单个属性查询,你需要的是虚拟列索引,而不是直接在JSON字段上建索引。

如果搞混了这两者,就会遇到这样的情况:明明“建了索引”,但查询还是慢到怀疑人生。

今天不讲面试题,直接说5个生产环境里真实踩过的坑,以及对应怎么解决。

一、MySQL JSON的前世今生

MySQL 5.7.8开始引入原生JSON类型,和用VARCHAR/TEXT存JSON字符串有本质区别:

对比维度 VARCHAR/TEXT存JSON 原生JSON类型
存储格式 纯文本字符串 优化的二进制格式
数据验证 不验证,脏数据也能进 自动验证,非法JSON直接报错
修改效率 整体重写 局部修改,只重写变化的部分
索引支持 全文索引 多值索引 + 生成列索引

二、坑1:直接对JSON字段用函数查询

症状:用JSON_EXTRACT、->、->>在WHERE条件里查JSON内部字段,数据量一大就慢。

原理:MySQL的B+树索引不识别JSON路径表达式。直接对JSON字段用JSON_EXTRACT查询,优化器只能走全表扫描。

正确方法:用虚拟列(Generated Column)+ B-Tree索引。

MySQL 5.7:

-- 创建虚拟列
ALTER TABLE user_events 
ADD COLUMN event_type VARCHAR(32) 
GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(event_data, '$.event_type'))) VIRTUAL;

-- 在虚拟列上建索引
CREATE INDEX idx_event_type ON user_events(event_type);

MySQL 8.0+更简洁,支持直接创建函数索引:

ALTER TABLE user_events 
ADD INDEX idx_event_type((CAST(event_data->>'$.event_type' AS CHAR(32))));

实测数据:10万条数据,JSON_EXTRACT全表扫描约850ms,虚拟列+索引约3ms——提升280倍。

三、坑2:JSON数组查询索引用不上

症状:JSON里存了数组,想查“数组中包含某个值”的记录,怎么写都慢。

原理:虚拟列只能提取单个值(如$.tags[0]),无法处理数组的“包含”查询。

正确方法:MySQL 8.0.17开始支持多值索引(Multi-Valued Index) ,把JSON数组的每个元素作为独立的索引项存入B+树。

建索引:

ALTER TABLE t_config 
ADD INDEX idx_phone((CAST(extras->'$.phone' AS UNSIGNED ARRAY)));

查询:

-- 推荐写法
SELECT * FROM t_config WHERE '13800138000' MEMBER OF(extras->'$.phone');

-- 或
SELECT * FROM t_config 
WHERE JSON_CONTAINS(extras->'$.phone', '13800138000');

效果:多值索引让JSON数组查询从全表扫描变成索引范围扫描,性能提升上千倍。

注意:多值索引只能作为联合索引的最后一列。

四、坑3:字符集和排序规则不一致

症状:虚拟列建了、索引也建了,EXPLAIN显示用了索引,但查询还是慢。

原理:JSON_EXTRACT返回的字符串排序规则和虚拟列定义的排序规则不一致时,MySQL无法使用索引。

正确方法:

方案一:统一字符集和排序规则
建表时统一用utf8mb4和utf8mb4_0900_ai_ci(或utf8mb4_general_ci)。

方案二:查询时直接用虚拟列名,别用JSON_EXTRACT

-- ✅ 正确:直接用虚拟列
SELECT * FROM user_events WHERE event_type = 'page_view';

-- ❌ 错误:还在用JSON_EXTRACT
SELECT * FROM user_events 
WHERE JSON_EXTRACT(event_data, '$.event_type') = 'page_view';

排查工具:

SHOW FULL COLUMNS FROM user_events LIKE 'event_type';
SELECT CHARSET(JSON_EXTRACT(event_data, '$.event_type')) FROM user_events LIMIT 1;

五、坑4:JSON过大导致外部存储

症状:JSON字段不算太大(几KB),但查询越来越慢。

原理:MySQL存储JSON时,小于1KB的JSON内联存储在行记录中,超过这个阈值则存储在外部页,每次查询需要额外的磁盘IO。

正确方法:

  • 保持JSON文档在1KB以下,能内联就内联

  • 单条JSON超过10KB不拆分,会导致临时表溢出磁盘,查询性能断崖式下降

  • 定期审查JSON字段大小,该拆就拆

  • 对频繁查询的JSON路径,一定建虚拟列索引

六、坑5:频繁更新JSON导致页分裂

症状:JSON字段频繁更新,表越来越大,性能持续下降。

原理:JSON_SET、JSON_REPLACE等更新操作虽然支持局部修改(只重写变化的部分),但如果修改导致JSON文档变长,可能触发InnoDB的页分裂——数据页一分为二,产生存储碎片,空间利用率下降。

正确方法:

  • 尽量避免频繁的小幅更新导致JSON不断变长

  • 如果更新频率高,考虑先读取、在应用层修改对象、再全量写回,减少页分裂次数

  • 定期执行OPTIMIZE TABLE回收碎片空间

  • 高更新频率的核心字段不要塞JSON,用普通列

七、JSON使用的决策框架

什么场景适合用JSON:

✅ 动态属性/扩展字段(不同记录字段差异大,无法预定义列)
✅ 配置信息(结构相对稳定,查询不频繁)
✅ 缓存数据(从其他系统同步过来的半结构化数据)

什么场景不适合用JSON:

❌ 需要关联查询的核心业务字段 → 拆成普通列+外键
❌ 需要NOT NULL、UNIQUE等约束的字段 → 走普通列
❌ 频繁作为WHERE、ORDER BY、JOIN条件的字段 → 走普通列+索引
❌ 频繁更新的字段 → 走普通列,避免页分裂

一句话决策规则:核心数据走普通列,扩展属性才用JSON。

八、小结

坑 症状 解决方案
坑1:函数查询 全表扫描 虚拟列+B-Tree索引 / 函数索引
坑2:数组查询 包含查询无法索引 多值索引 + MEMBER OF
坑3:字符集不一致 索引“假装”在工作 统一字符集,直接用虚拟列名
坑4:JSON过大 外部存储,额外IO 保持<1KB,超10KB拆分
坑5:频繁更新 页分裂,空间膨胀 应用层改完再写回,定期OPTIMIZE

JSON类型出来快10年了,不是不能用,是要用对地方、建对索引。

下次往表里加JSON字段之前,先问自己三个问题:

  1. 这个字段会频繁作为查询条件吗?→ 会就建虚拟列索引

  2. 这个字段会频繁更新吗?→ 会就考虑拆出去

  3. 这个字段需要约束吗?→ 需要就走普通列

小耶在手,SQL 不愁

还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽……我们下次见~

相关文章
|
29天前
|
SQL 存储 关系型数据库
分区裁剪失效、DDL卡死、元数据爆炸:分区表的3个真实代价
很多人觉得分区表是“轻量级分库分表”——数据分开放、查询只扫一个区、过期数据直接DROP分区,听起来很完美。但分区表有严格的适用边界和隐藏代价:分区键选错导致所有查询都扫全部分区、分区数量过多导致DDL巨慢、跨分区查询比普通表还慢……本文从分区表的核心原理出发,拆解4种分区类型、3个真实踩坑场景,以及分区表与分库分表的本质区别,帮你一次性搞清楚到底该不该用。
|
29天前
|
存储 SQL 容灾
共享存储集群 vs 分布式多副本:同城双活两条技术路线怎么选?
同城双活正在成为金融、政务等核心系统的容灾标配——RPO=0、RTO<30秒。但真正的落地远不止“两个机房各放一套数据库”那么简单。网络延迟的容忍度、脑裂预防机制、同步复制的性能代价、以及故障切换后的数据回滚,每一个环节都可能成为“最后一公里”的绊脚石。本文从容灾架构演进入手,拆解同城双活的核心技术原理、关键挑战与应对方案,并结合同城双中心方案及实测数据进行深度解析。
|
1月前
|
SQL 运维 关系型数据库
2026年分布式数据库有哪些主流选择?先评估这5个维度再决定
分布式数据库选型,99%的文章在列表格比参数——但真正的决策关键不在厂商PPT里,在上线后的真实运维里。本文从网络延迟容忍度、SQL兼容性验证、在线扩容能力、全局索引代价、运维工具链成熟度5个维度出发,给出可落地的评估方法和决策建议,帮助你在选型阶段避开那些“只有上线后才会发现”的坑。
|
29天前
|
SQL 关系型数据库 MySQL
死锁报错看了三遍没看懂?我拆给你看(附定位SQL)
从一次真实死锁现场切入,讲清行锁、间隙锁、插入意向锁的加锁机制与死锁形成原理,手把手教你怎么用show engine innodb status和information_schema定位死锁,并给出加锁顺序设计等避坑清单。
|
27天前
|
SQL 人工智能 运维
3个月AI Agent运维实测:慢SQL它管,根因还得我上
以三个月实测的视角,划清AI Agent自治运维的真实能力边界:巡检、慢SQL发现等重复活已可替代,复杂根因、变更审批、数据兜底仍需人把关,探讨DBA角色从救火队员向定规则、把关人的转型。
|
28天前
|
存储 关系型数据库 MySQL
面试总问的B+树,我把磁盘IO到底怎么算的讲清楚了
从磁盘IO的底层约束讲起,逐层对比哈希、二叉、红黑树、B树与B+树,讲清MySQL为什么选B+树(矮胖树、顺序IO、范围查询、查询稳定),并用这套底层理解反过来指导覆盖索引、前缀索引、最左前缀等日常索引设计。
|
23天前
|
SQL 监控 关系型数据库
MySQL索引合并优化器陷阱:为什么复合索引比索引合并快一个数量级?
MySQL优化器有一个“自作聪明”的行为——当单列索引无法完全覆盖查询时,它可能选择索引合并(Index Merge) ,同时使用多个单列索引,把结果集合并起来。听起来很合理对吧?但索引合并有严格的适用条件,用错了比全表扫描还慢——尤其是UNION类型的索引合并,需要对多个结果集去重和排序,代价极高。本文拆解索引合并的3种类型、3个踩坑场景,以及什么时候该用复合索引替代。
|
24天前
|
SQL 关系型数据库 MySQL
别再盯着EXPLAIN的rows列了,8.0.18之后有更好的选择
EXPLAIN是DBA最常用的工具之一,但大多数人还在看type、rows、Extra这些传统字段——然后靠经验猜。MySQL 8.0.18开始引入的EXPLAIN ANALYZE,直接把实际执行时间和行数输出给你看,不用猜了。本文对比传统EXPLAIN和EXPLAIN ANALYZE的差异,展示如何用新工具把执行计划分析这件事从“猜”变成“看”。
|
20天前
|
SQL Java 数据库连接
1万行插入13秒到0.9秒:ORM批量插入只差一个参数
从一次列表接口慢的排障讲起,发现2000多条一模一样的N+1查询。文章拆开ORM生成慢SQL的三类典型病:N+1懒加载、逐条批量插入(只差一个rewriteBatchedStatements参数)、隐式转换让索引白建。给出JOIN/批量IN/@BatchSize的取舍、MyBatis与JPA各自的修法,以及用performance_schema按SQL指纹抓N+1、测试环境打印真实SQL的协作办法。
|
21天前
|
SQL 运维 算法
订单表上亿行,我按用户ID拆成128片之后怎么样了
从单表几千万行慢查询的痛点出发,讲清垂直拆分与水平拆分的区别、分片键怎么选、分片算法(hash取模/range/一致性hash)怎么权衡,以及分库分表带来的分布式ID、跨片查询、分布式事务等问题,给出避免过度拆分的避坑清单。