MySQL JSON字段实战指南:存储、函数与索引选型

发布时间:2026/10/20 5:58:30
MySQL JSON字段实战指南:存储、函数与索引选型
说起来有点不好意思我第一次在表结构评审里提出要不加个 JSON 字段吧是被当场驳回的。当时的理由很朴素MySQL 是关系型数据库你塞一个 JSON 进去等于把数据库当文本仓库用后面查询、索引、一致性全都会出问题。这个判断放在 MySQL 5.7 之前的时代不能说错但从 5.7 正式引入 JSON 数据类型之后就明显偏保守了。后来我在几个项目里把 JSON 字段从前到后完整用了一遍也在线上环境踩过不少坑才慢慢摸清它到底适合装什么数据、哪些函数应该怎么用、什么时候必须给它配索引、什么时候干脆别用。这篇文章想做的事很简单把 MySQL JSON 数据类型和相关函数从头到尾梳理一遍。包括它内部的存储机制、日常开发里最高频的那十几个函数、JSON Path 的写法与坑、生成列和多值索引的配合打法还有我在实际项目中遇到的误用案例和性能复盘。内容不追求面面俱到的文档式罗列更偏重我真实用过之后的判断。适合正在犹豫要不要上 JSON 字段的人也适合已经用了但总觉得不太踏实的同学。1. 为什么说JSON字段是关系模型装不下的那类数据1.1 什么场景下你该认真考虑JSON先别急着学语法先把业务形态想清楚。我做过的一些项目里适合用 JSON 的几乎都有下面这几个特征之一。第一种是第三方接口返回的动态结构。比如对接不同渠道的订单数据每个渠道回传的扩展字段都不一样今天是discount_amount明天可能冒出个voucher_info。如果用关系模型每次接口变化都要ALTER TABLE一张表改十几次之后连 DBA 都懒得理你了。JSON 字段就把变化封在了一个列里上游加字段不用改表结构。第二种是用户自定义属性或偏好设置。商品、用户的扩展属性特别多而且属性集合是稀疏的——一万个用户里可能只有三个人设置了favorite_color。为这种字段单独建列整列基本都是 NULL毫无意义。第三种是低频查询的附属信息。主流程查询走的是关系字段JSON 里的内容只是详情页展示用一年到头过滤不了几次。比如订单表里存一份下单时的快照信息拿去展示就行。第四种是配置类、埋点类数据结构天然松散写进 JSON 是最省事的方式。说白了JSON 在关系型数据库里的定位是弹性兜底层而不是主存储。主数据、核心业务字段老老实实用普通列弹性、松散、多变的那部分才交给 JSON。1.2 没有JSON之前大家是怎么凑合的在 MySQL 5.7 之前处理上面这类数据常见三种姿势各有各的尴尬。第一种是 EAV 表。一张实体表加一张属性表属性表存entity_id, attr_key, attr_value三列。好处是彻底规范化坏处是查询要疯狂自连——取一个实体的全部属性要 10 行记录还得在应用层拼装成对象。属性多了之后属性表的行数膨胀得很快索引体积也跟着膨胀。我见过一张 EAV 表跑到几千万行查一次详情要 50ms 起步全是索引 IO 和 JOIN 的锅。第二种是 LONGTEXT 存 JSON 字符串应用层自己解析。这个最普遍。问题在于数据库完全不理解里面的内容你要筛选 JSON 里的某个字段只能LIKE %key:value%一把梭全表扫还容易误匹配到值里包含相同子串的其他行。更别提维护两端对 JSON 结构的认知一致性应用层改了字段名数据库里的旧数据没人管。第三种是直接上文档数据库。多一套系统、多一份数据同步、多一种运维成本。业务量级不到那个程度的话纯属杀鸡用牛刀。JSON 类型出现之后第二种方案里数据库不理解内容这个最大痛点被解决了MySQL 会在写入时校验 JSON 合法性并提供一整套函数来做查询和修改。1.3 边界什么时候别碰JSON这个我必须放在前面说因为追悔莫及的人太多了。首先主流程要频繁过滤、排序、聚合的字段不要放 JSON。JSON 字段默认没法直接建索引WHERE JSON_EXTRACT(...)xxx这种写法看起来能用实际上等着你的就是全表扫描。后面第五章会讲索引方案但请记住那是补救手段不是设计理由。其次字段本身有强类型和强约束需求时别用 JSON。比如金额、状态码、必须唯一的标识规规矩矩用普通列。JSON 只保证是合法 JSON不保证里面有没有你业务上不能接受的值。你在 JSON 里存个 18 位订单号MYSQL 不拦你但查出来变成科学计数法的时候哭都来不及。第三单个文档超过几 MB 的大文本别用 JSON。JSON 类型的设计目标是结构化的小文档不是让你存附件和大报文。后面第六章我会细讲大文档的连锁反应。第四需要灵活 JOIN 的核心业务数据也别用 JSON。JSON_TABLE 可以把 JSON 摊平成行但那是做导出分析用的不是让你天天 JOIN 的。这些边界想清楚之后你再回头看 JSON 字段会发现它其实挺好使的——前提是它知道自己只是弹性兜底层。2. JSON字段存储的底层真相不是带格式的字符串2.1 写入瞬间字符串就被编译了很多人以为 JSON 字段就是能校验格式的 LONGTEXT这是个挺常见的误解。MySQL 5.7.8 引入 JSON 类型时专门设计了一种二进制格式来存储 JSON 文档。你往表里 INSERT 一段 JSON 字符串MySQL 会执行三个动作解析文本并校验 JSON 语法不合法直接报Invalid JSON text去掉键与值之间无关紧要的空格和换行把解析结果编码成内部的二进制节点结构对象键按排序后的顺序存储。这个过程很像源代码被编译成字节码。所以你写进表的{age: 28, name: Tom}实际落盘的不是你肉眼看到的那串文本而是一棵紧凑的、带类型标记的二进制树。这个设计直接决定了性能和可用性因为写入时已经完成了语法解析查询时JSON_EXTRACT这些函数读的是已经编译好的二进制树不需要再做一遍字符串解析。MySQL 的官方手册里也提过由于键是有序存储的按键取值可以用二分查找所以JSON 字段很慢这个印象在 5.7 之后其实站不太住脚——真正慢的往往是没走索引的函数过滤这个我们后面单独说。2.2 一个值多种类型二进制格式在省什么MySQL 的二进制 JSON 格式会对每个值打一个类型标记常见类型包括 object、array、string、number、boolean、null。这个类型标记让取值变得更精确应用层拿到JSON_EXTRACT的结果时它能区分取到的到底是一个对象、数组还是单个字符串这也是JSON_TYPE()函数能够工作的基础。因为键的存储是有序的两个语义相同的 JSON 对象哪怕键的写入顺序不一样存储后的二进制结果也是一样的。也就是说JSON 类型的比较是语义比较不是文本比较。这个特性很有用{a:1,b:2}和{b:2,a:1}作为 JSON 是相等的而你用 LONGTEXT 做同样的比较只会得到 false。2.3 和LONGTEXT对比什么时候JSON确实更快我做过一个很土但有效的对照实验同一张表一个列用 LONGTEXT 存 JSON 字符串另一个列用 JSON 类型存同样内容。在 WHERE 里过滤 JSON 内部字段时LONGTEXT 只能上 LIKE 全文本扫描而 JSON 列虽然同样走不了索引但至少能用JSON_EXTRACT精确提取并且因为二进制结构提取一次的开销远低于 LONGTEXT 把整段文本拉出来做正则匹配。如果是读取整段内容两者差别不大如果是频繁提取其中的某个字段JSON 类型的优势会随着文档变大而越来越明显。当然这个优势有个前提你读的是结构化字段不是打开整串文本看着用。还有一点容易被忽略JSON 列的内部存储容量同样受 LONGTEXT 的体系约束实际能写入的大小受max_allowed_packet参数限制默认值在 8.0 里通常是 64MB5.7 还要更小一些。别以为 JSON 类型能存无限大的东西。3. 核心函数逐个拆解读、写、改、删的完整工具箱3.1 读取JSON_EXTRACT、- 和 - 到底什么关系先建一张最简单的表后面所有例子都基于它CREATE TABLE user_detail ( id INT PRIMARY KEY, profile JSON, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); INSERT INTO user_detail (id, profile) VALUES (1, {name: Tom, age: 28, tags: [mysql, json], address: {city: Hangzhou, zip: 310000}}), (2, {name: Jerry, age: 32, tags: [nosql], address: {city: Shanghai, zip: 200000}});读取 JSON 内部字段有三种写法本质上是同一种SELECT JSON_EXTRACT(profile, $.name) AS a, profile-$.name AS b, profile-$.name AS c FROM user_detail WHERE id 1;JSON_EXTRACT(profile, $.name)是标准函数写法profile - $.name是它的等价操作符返回的仍然是 JSON 类型的值profile - $.name等价于JSON_UNQUOTE(JSON_EXTRACT(...))会去掉字符串外面的引号返回原生字符串。不用死记你只需要理解-和-只差一个大于号但语义差异极其容易踩坑。如果你用profile-$.name去和字符串Tom比较它实际是Tom带着引号的 JSON 字符串永远等不上。实战中只要想把 JSON 里的字符串取出来和普通列、变量、参数比较统一用-否则会出现那种肉眼明明看着一样就是等不了的诡异 bug。顺带说一句-取出数字时会得到字符串形态的数字比如28。如果后面要跟数值比较注意隐式转换的问题别偷懒省掉类型处理否则生成列的索引类型会对不上号。3.2 修改JSON_SET、JSON_INSERT、JSON_REPLACE三兄弟的区别这三个函数长得像行为上却差一个关键维度目标路径存不存在。我把它们的行为整理成一张表这张表值得你存下来函数路径已存在路径不存在典型场景JSON_SET(doc, path, val, ...)覆盖/更新新增整体同步一个 JSON 对象JSON_INSERT(doc, path, val, ...)保持不变新增补默认字段不覆盖已有值JSON_REPLACE(doc, path, val, ...)覆盖/更新忽略只更新已有字段禁止新增JSON_REMOVE(doc, path, ...)删除忽略清理废弃字段看几个例子就明白了UPDATE user_detail SET profile JSON_SET(profile, $.age, 29, $.city, Hangzhou) WHERE id 1; UPDATE user_detail SET profile JSON_INSERT(profile, $.level, VIP) WHERE id 1; UPDATE user_detail SET profile JSON_REPLACE(profile, $.name, Thomas) WHERE id 1; UPDATE user_detail SET profile JSON_REMOVE(profile, $.age) WHERE id 1;注意几个要点。第一这几个函数都是纯函数式的它们返回修改后的新 JSON不会原地修改原列。在 UPDATE 语句里你必须写SET profile JSON_SET(profile, ...)光写JSON_SET(profile, ...)是不生效的。这个和很多编程语言里修改对象属性的直觉不一样我第一次用的时候就漏了赋值。第二一个函数调用可以同时传多对 path-value比如JSON_SET(profile, $.a, 1, $.b, 2)。能合并的 UPDATE 尽量合并少写几条 SQL 就少几轮锁竞争。第三想往数组里追加元素用JSON_ARRAY_APPEND或JSON_ARRAY_INSERT。前者追加到末尾后者按下标插入别用 JSON_SET 去碰数组除非你想整体替换。MySQL 8.0 对UPDATE ... SET json_col JSON_SET(...)这类写法做了一个局部更新优化当 JSON 列没有被 WHERE 引用、且只是部分路径修改时引擎会尝试只重写受影响的部分而不是整个文档。这个优化对高频小改很友好后面的性能章节还会提到。3.3 判断与搜索JSON_CONTAINS、JSON_CONTAINS_PATH 和它的冷静兄弟们过滤 JSON 内容最常用的是JSON_CONTAINSSELECT id FROM user_detail WHERE JSON_CONTAINS(profile, JSON_ARRAY(mysql), $.tags);这句的意思是取profile.tags这个数组判断里面是否包含字符串mysql。这里有两个高频错误。第一个是候选值漏了引号。JSON_CONTAINS(profile, mysql, $.tags)会直接报错因为mysql不是合法的 JSON 值。JSON_CONTAINS的第二个参数要求是合法 JSON字符串必须写成mysql或者干脆用JSON_ARRAY(mysql)包一层。我见过同事排查了半天最后发现就是引号问题。第二个是路径写错层级。不传第三个参数时JSON_CONTAINS是拿整个文档和候选值比。很多人对嵌套结构想当然结果查询返回空还以为是数据问题。如果你只关心某个路径存不存在不管值是什么用JSON_CONTAINS_PATHSELECT id FROM user_detail WHERE JSON_CONTAINS_PATH(profile, one, $.name, $.age); SELECT id FROM user_detail WHERE JSON_CONTAINS_PATH(profile, all, $.name, $.age);第二个参数one表示任一路径存在即可all表示所有路径都必须存在。这个函数在判断JSON 里有没有某个可选字段时比JSON_EXTRACT(...) IS NOT NULL更可靠原因放在 4.3 里讲。其他几个冷静但好用的函数JSON_KEYS(doc [, path])返回对象的所有键返回的是一个 JSON 数组。JSON_LENGTH(doc [, path])返回数组的元素个数或者对象的键数量。注意它只数第一层不递归。JSON_TYPE(doc)返回最外层值的类型常见的有OBJECT、ARRAY、STRING、INTEGER、DOUBLE、BOOLEAN、NULL。JSON_DEPTH(doc)返回嵌套深度空 JSON{}的深度是 1。JSON_VALID(str)判断一段字符串是不是合法 JSON返回 1 或 0。这个函数在导入脏数据、清洗日志的场景里特别有用。3.4 从行到JSON从JSON到行聚合函数与JSON_TABLE构建 JSON 也有专门的函数。JSON_OBJECT(key, value, ...)手搓对象JSON_ARRAY(value, ...)手搓数组。这两个在接口层拼返回值时很常用但更常用的是两个聚合函数SELECT JSON_ARRAYAGG(name) FROM user_detail; -- 结果: [Tom, Jerry] SELECT JSON_OBJECTAGG(name, profile-$.age) FROM user_detail; -- 结果: {Jerry: 32, Tom: 29}JSON_ARRAYAGG把一列多行聚合成 JSON 数组JSON_OBJECTAGG把两列聚合成 JSON 对象。这两个函数配合GROUP BY特别适合做报表接口一次性把分组 明细打包返回应用层省掉一层手动组装。反过来JSON_TABLE是把 JSON 摊平成普通行的SELECT ud.id, jt.tag FROM user_detail ud, JSON_TABLE(ud.profile, $.tags[*] COLUMNS ( tag VARCHAR(20) PATH $ )) AS jt WHERE ud.id 1;这个查询把每一行的 tags 数组拆成多行每行一个 tag。JSON_TABLE是 MySQL 8.0 才有的功能5.7 没有。它适合做导出分析、跨 JSON 字段的统计关联但我强烈不建议在核心查询链路上频繁用它。它的本质是把半结构化数据还原成结构化数据做一次就是一遍展开计算数据量大时开销不小。合并 JSON 也有两个函数容易混淆JSON_MERGE_PRESERVE合并数组时是拼接合并对象时同键值会被拼成数组JSON_MERGE_PATCH更接近后者覆盖前者的补丁语义。8.0 里旧函数JSON_MERGE被重命名成了JSON_MERGE_PRESERVE旧名字虽然还在但已经废弃新代码里不要再用了。4. JSON Path表达式精准定位数据的地址系统4.1 语法速通一条路径就是从根出发的下标链JSON Path 就是你在 JSON 文档里定位数据的地址。规则不多记住下面这几个就能应付绝大多数场景$表示根节点所有路径都必须从$开头漏了它函数会直接报错.key表示取对象的某个键$.name就是取根对象的 name 键[N]表示取数组的第 N 个元素从 0 开始$.tags[0]取第一个 tag层层嵌套就一路往下写$.address.city取 address 对象的 city 键$.*表示取对象的所有值$[*]表示取数组所有元素$.tags[*]在 JSON_TABLE 里配合COLUMNS展开数组时几乎必用。拿前面的数据举个例子SELECT profile-$.tags[0] AS first_tag, profile-$.address.city AS city, profile-$.* AS all_values FROM user_detail WHERE id 1;MySQL 的 Path 语法是 ECMAScript 路径表达式的子集所以它不支持递归通配符那类花哨写法。写嵌套路径时脑子里把层级想清楚就行。4.2 键名带特殊字符怎么办如果 JSON 里的键名含有空格、点号、连字符或者干脆是数字开头路径里的普通写法会出问题。比如$.first-name会被解析成first 减号 name而不是一个完整键名。解决办法是用双引号把键名包起来-- 假设 JSON 里有 first-name: Tom SELECT profile-$.first-name FROM user_detail WHERE id 1;这个细节不遇到根本想不起来。但只要你对接过第三方系统键名带横线、带空格的情况特别常见属于老手也会偶尔翻车的点。4.3 三个高频陷阱路径不存在、JSON null、结果还是带引号第一个陷阱路径不存在时JSON_EXTRACT/-返回的是SQL 的 NULL不是 JSON 的 null。命令行里显示都是NULL肉眼根本分不出来。如果真要区分路径不存在和路径存在但值是 null要先JSON_CONTAINS_PATH判断或者把取到的值和CAST(null AS JSON)比较。我在业务代码里见过有人用JSON_EXTRACT(result, $.error) IS NOT NULL判断接口有没有报错结果 JSON 里恰好有这个键但值是 null判断直接失效。第二个陷阱-取出来的字符串是带引号的 JSON 值拿去和普通字符串比较永远不相等只有-才是干净字符串。这个前面说过但值得再强调一次因为它造成的线上事故我至少见过两回都是数据看起来一模一样但 WHERE 条件就是匹配不上。第三个陷阱嵌套数组的下标容易数错。$.tags本身是一个数组$.tags[0]才是第一个元素。很多人顺手写$.tags.0这在 MySQL 里直接报语法错误。记住数组一律用方括号下标。5. 索引才是性能关键生成列多值索引的组合打法5.1 别骗自己JSON函数过滤默认就是全表扫描先把丑话说在前面对 JSON 列直接调函数做过滤除非数据量小到可以忽略否则就是全表扫描。原因很简单普通 B 树索引建立在真实列名上而 JSON 内部字段对索引器来说不可见。WHERE profile-$.age 30这个表达式在优化器眼里就是对一个函数结果做等值判断函数结果上没有索引可用。所以实用路线只有两条生成列和多值索引。前者解决对象里的标量字段后者解决数组元素。两条都不会省但两条也都不是银弹。5.2 生成列把JSON字段翻译成可以索引的普通列生成列Generated Column是从其他列派生出来的列MySQL 负责自动维护它的值。我们可以把 JSON 里高频查询的字段提取出来做成生成列再给生成列建普通索引ALTER TABLE user_detail ADD COLUMN age INT GENERATED ALWAYS AS (profile-$.age) VIRTUAL; CREATE INDEX idx_age ON user_detail(age);之后查询直接走普通索引SELECT id, name FROM user_detail WHERE age 30;这里有两个关键点。第一生成列表达式必须用-而不是-否则提取出来的是带引号的 JSON 字符串赋值给 INT 列时会出问题。第二查询时直接用生成列名age不要写WHERE profile-$.age 30指望优化器自动替换成生成列。新版优化器有一定推导能力但依赖它不如自己写干净。生成列分VIRTUAL和STORED两种。VIRTUAL不占磁盘空间读取时动态计算但照样可以建索引STORED物化存储占用空间换查询速度。JSON 提取场景里默认用VIRTUAL就够了磁盘省一半索引照样走。只有生成列本身参与了频繁聚合、或者 VIRTUAL 计算成本已经影响性能时才考虑改成 STORED。5.3 多值索引给JSON数组元素建索引的办法如果 JSON 里的数组需要过滤比如找出 tags 包含 mysql 的用户生成列就只能提取整个数组出来没法对数组元素建普通列索引。MySQL 8.0.17 引入的多值索引Multi-Valued Index就是干这个的CREATE INDEX idx_tags ON user_detail ((CAST(profile-$.tags AS CHAR(20) ARRAY))); SELECT id FROM user_detail WHERE JSON_CONTAINS(profile, JSON_ARRAY(mysql), $.tags); SELECT id FROM user_detail WHERE mysql MEMBER OF (profile-$.tags);多值索引的原理是把 JSON 数组里的每个元素都作为一个索引项存进 B 树。这样某个值是否在数组里就能走索引快速判断。它支持的查询条件主要有三种写法JSON_CONTAINS(profile, JSON_ARRAY(mysql), $.tags)mysql MEMBER OF (profile-$.tags)JSON_OVERLAPS(profile, JSON_ARRAY(mysql, json))。使用多值索引有几条限制要记牢它对 JSON 数组中的 null 元素无效索引键不能被 GROUP BY、ORDER BY 直接使用CAST(... AS ... ARRAY)的数组元素类型要和实际元素类型匹配字符串用CHAR(n)数字用SIGNED或UNSIGNED。这个功能只有 8.0 有如果你的库还在 5.7这种数组过滤场景只能老老实实拆子表。5.4 索引不是银弹写放大和选择性问题最后泼一盆冷水JSON 上的任何索引都是写放大的来源。每一行 JSON 更新时相关生成列要重新计算相关多值索引要删旧插新。如果你的业务是高频 UPDATE JSON 字段建三个索引可能比不建还慢。所以我的实践原则是只为真正高频的查询路径建索引。JSON 里的冷字段不要为了万一以后用得上去建索引。冷字段让应用层在需要时做一次全表扫描通常比长期背着索引写入成本划算得多。我就遇到过那种索引建了五六个查询没跑几次写入倒是天天被拖慢的表后来删掉一半索引负载明显下降。6. 线上实战几类典型误用与性能复盘6.1 反面案例把JSON当万能口袋我经手过一个模拟项目最早的设计者把所有扩展字段塞进一个叫extra的 JSON 列连核心业务状态都往里放。上线三个月后业务方开始要求按状态统计按类型过滤查询变成一堆JSON_EXTRACT(extra, $.status) xxx全表扫。最终改造方案很痛苦把状态、类型这些高频字段拆成普通列重建索引迁移数据应用层接口全部重审。这个案例里JSON 本身没有错错的是拿它承载了主数据。它让我沉淀了一条红线如果未来大概率要在某字段上过滤、排序、JOIN那就别让它进 JSON。进 JSON 的字段应该同时满足低频访问、结构多变、格式宽松三个条件里的至少两个。6.2 大文档带来的连锁反应另一个容易低估的问题是文档体积。JSON 列没有独立的大小限制整个文档受max_allowed_packet约束。别看 8.0 的默认值有 64MB 就觉得空间充足实际业务里我强烈建议把单个 JSON 文档控制在几十 KB 以内最好不超过 100KB。原因不是存储装不下而是链路成本。每次读取MySQL 要把整个二进制文档解析、传输给客户端每次更新哪怕只改一个键都可能触发整文档重写主从复制时JSON 文档作为整体在 binlog 里传递文档越大主从延迟越高。我踩过把某个接口返回的原始报文整个塞进 JSON 列的坑单行 2MB 多主从延迟直接飙到几十秒后来是拆出去才救回来。判断 JSON 文档真实占多大空间用这个函数SELECT JSON_STORAGE_SIZE(profile) FROM user_detail WHERE id 1;返回的是字节数。建议上线前对存量数据跑一遍心里有个底别等出问题了才发现单行 JSON 已经数百 KB。6.3 版本差异5.7到8.0哪些行为变了如果你还在用 5.7这几条差异最好门清避免换版本后行为不一致的坑-操作符 5.7.13 之后才有更早版本只能用JSON_UNQUOTE(JSON_EXTRACT(...))JSON_TABLE是 8.0 才有的多值索引是 8.0.17 才有的JSON_MERGE在 8.0 中被拆分成JSON_MERGE_PRESERVE和JSON_MERGE_PATCH旧名已废弃8.0 的局部更新优化对高频小改 JSON 很友好8.0.13 开始JSON 列可以用DEFAULT (expression)形式设置默认值但要注意是带括号的表达式不能写裸字符串。功能上 8.0 几乎是 5.7 的超集如果业务允许我建议尽早升级。长期停在 5.7 意味着你享受不到局部更新、多值索引这些真正改善体验的能力。6.4 我给新项目的几条实操建议把零散的教训汇总成一份可以照抄的清单先定义字段白名单。应用层写 JSON 之前把允许出现的 key、类型、范围列清楚。数据库只校验是不是合法 JSON业务合法性要靠应用层兜底。高频查询字段立生成列。每次设计评审把 JSON 里的字段过一遍哪些未来三个月内大概率会被 WHERE 用到马上加生成列和索引省得以后业务上线了再迁移。写操作用局部更新函数。8.0 下能用JSON_SET/JSON_REPLACE/JSON_REMOVE完成的小改动就不要先查出来再整体覆盖写回尽量触发局部更新优化。监控 JSON 文档体积。用JSON_STORAGE_SIZE做定期巡检设置阈值告警别等主从延迟了才发现。JSON_TABLE 用在离线分析别用在热路径。核心接口里频繁摊开 JSON 再 JOIN迟早出性能事故。排序聚合前先想清楚语义。JSON 列直接 ORDER BY是按二进制表示和类型优先级排序的数字、字符串、数组、对象的内部顺序和直觉完全不一样。要对内部字段排序先-提取成普通标量再用生成列索引。我在实际项目里的体会是JSON 数据类型不是关系型数据库的叛徒它更像是在关系模型边缘划出的一块弹性区域。用好了它能替你省掉大量 ALTER TABLE 和胶水代码用不好它就是你全表扫描事故的源头。关键是设计阶段就把什么能进 JSON、什么必须进普通列这条界限划清楚。说实话我后来看过的多数 JSON 悲剧问题都不在 MySQL 身上而在最初的表设计里。