news 2026/10/22 14:29:15

MySQL JSON_EXTRACT详解:用法、路径规则与性能优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL JSON_EXTRACT详解:用法、路径规则与性能优化

从第一次在MySQL 5.7里见到JSON_EXTRACT这个函数,到后来在业务系统里频繁用它取数据,我一直觉得这个函数属于"看上去简单、用起来全是细节"的类型。很多同事问我的第一句话往往是"这个函数到底返回的是啥,带不带引号",第二句话是"为啥我查出来是null"。如果你也有类似的困惑,这篇文章把JSON_EXTRACT的用法、路径规则、坑点、以及和JSON_UNQUOTE、JSON_VALUE的区别一次性讲透,尤其适合正在用JSON字段存储业务数据、又经常被数据提取搞到头疼的开发同学。

1. JSON_EXTRACT是什么,为什么值得专门聊

1.1 从JSON字段说开去

MySQL从5.7版本开始正式支持JSON类型,这一下子让很多团队的建表思路发生了很大变化。以前我们习惯把不确定的字段拆成单独的表,或者用一个逗号分隔的字符串存"伪数组",又或者干脆加一堆冗余列。JSON类型出来之后,大家发现可以把一些结构化但又不太稳定的数据直接塞进一个JSON列里,查询的时候再用函数取出来。

JSON_EXTRACT就是这个"取出来"动作最基础的工具。它的作用说白了就一句话:从一个JSON文档里,按照你指定的路径,把对应的值取出来。打个比方,JSON字段就像是一个多层抽屉的柜子,JSON_EXTRACT就是那个告诉你"第几层第几个抽屉里放着什么"的帮手。

这个函数解决的实际问题非常具体:当你的数据不是固定列结构、或者某个字段里包含了一组相关的子信息时,你不需要把子信息拆成多个表,也不需要写一堆字符串截取的代码,直接通过JSON路径就能精准提取。

1.2 我实际项目中遇到的典型场景

我印象最深的一个场景是订单系统。订单表里除了订单号、金额、状态这些固定字段,还经常需要一个"商品快照"字段,记录下单那一刻商品名称、单价、数量、优惠明细。这些信息在下单之后不应该再被商品表的变化影响,所以快照必须存下来。这个字段如果用单独的关联表去存,查询订单列表时就要多关联一次,代码也复杂;用JSON字段存放,再配合JSON_EXTRACT取出商品名称或数量,一条SQL就能搞定。

类似的场景还包括:用户表的扩展信息(比如年龄、性别、偏好设置)、文章表的自定义属性、配置中心的动态配置项等等。只要字段结构不是100%固定,JSON加JSON_EXTRACT的组合就是很务实的方案。

2. JSON_EXTRACT语法和JSON路径表达式

2.1 基础语法先过一遍

JSON_EXTRACT的基本语法长这样:

JSON_EXTRACT(json_doc, path[, path] ...)
  • json_doc:你要提取数据的JSON文档,通常就是表里的某个JSON列。
  • path:JSON路径表达式,用来定位你要取的数据。路径可以写多个,如果写了多个路径,函数返回的结果是一个由多个值组成的JSON数组。

最简单的例子:

SELECT JSON_EXTRACT('{"name": "张三", "age": 30}', '$.name'); -- 结果: "张三"

注意看,返回结果是"张三",也就是带双引号的。这一点特别容易让新手懵掉,因为MySQL的JSON函数返回的JSON值,本身还带着JSON的格式。如果你想要纯文本张三,还得再处理一步。

再举个数组的例子:

SELECT JSON_EXTRACT('[10, 20, 30]', '$[1]'); -- 结果: 20

这里$[1]表示数组下标为1的元素,MySQL的JSON数组下标从0开始。

2.2 JSON路径表达式的规则

路径表达式是JSON_EXTRACT的核心,写错了就直接返回NULL。path由两部分组成:

  • $:代表整个JSON文档的根。
  • 后面跟一系列的定位符,用.表示对象成员,用[n]表示数组下标。

具体规则可以拆成三类:

对象字段定位

-- 取嵌套对象里的字段 SELECT JSON_EXTRACT('{"user": {"name": "李四", "age": 25}}', '$.user.name'); -- 结果: "李四"

数组下标定位

-- 取数组第一个元素 SELECT JSON_EXTRACT('{"tags": ["MySQL", "JSON", "开发"]}', '$.tags[0]'); -- 结果: "MySQL"

数组下标支持负数吗?不支持。但是MySQL 8.0开始支持[last]这种写法,[last]代表最后一个元素,[last-1]代表倒数第二个元素。这一点在5.7里是没有的,如果你还在用5.7,老老实实数下标。

通配符

-- $.* 表示所有成员的值 SELECT JSON_EXTRACT('{"a": 1, "b": 2}', '$.*'); -- 结果: [1, 2] -- $.user.* 表示user对象下的所有值 SELECT JSON_EXTRACT('{"user": {"name": "王五", "age": 28}}', '$.user.*'); -- 结果: ["王五", 28]

通配符在提取一段范围内的数据时很好用,但它返回的是一个数组,不是单个值,这一点要用对地方。

深层路径的写法

-- 直接用点号一层层往下写 SELECT JSON_EXTRACT('{"a": {"b": {"c": 100}}}', '$.a.b.c'); -- 结果: 100

2.3 容易被忽略的路径使用细节

我自己在实际使用中总结出几个容易踩的细节:

路径里的字段名如果包含特殊字符,需要加双引号。比如字段名是"user-name"这种带横杠的,直接写$.user-name会被解析成减法运算,必须写成$."user-name"。

SELECT JSON_EXTRACT('{"user-name": "赵六"}', '$."user-name"'); -- 结果: "赵六"

字段名大小写敏感。JSON对象里的字段名是严格区分大小写的,'$.Name'和'$.name'是两个完全不同的路径。写SQL的时候如果发现返回NULL,先检查是不是大小写写错了。

数组下标越界不会报错,而是返回NULL。比如数组只有3个元素,你去取$[5],结果就是NULL,MySQL不会给你任何提示。所以在代码里做空值判断很有必要。

3. JSON_EXTRACT的五个常见实际应用方案

3.1 方案一:订单系统的商品快照数据提取

订单系统的商品快照场景前面提过,这里把SQL写细一点。

假设表结构长这样:

CREATE TABLE orders ( id BIGINT PRIMARY KEY, order_no VARCHAR(64), goods_snapshot JSON, created_at DATETIME );

goods_snapshot里存的数据类似:

{ "goods_name": "无线鼠标", "price": 89.90, "quantity": 2, "spec": { "color": "黑色", "interface": "USB" } }

现在我要查订单里商品名称和单价,SQL就这么写:

SELECT order_no, JSON_EXTRACT(goods_snapshot, '$.goods_name') AS goods_name, JSON_EXTRACT(goods_snapshot, '$.price') AS price FROM orders WHERE id = 1001;

查询结果里goods_name会带着双引号,如果程序里直接拿这个值去展示,页面上会出现一对引号,很难看。所以这个场景下,我一般会把JSON_EXTRACT和JSON_UNQUOTE配合使用:

SELECT order_no, JSON_UNQUOTE(JSON_EXTRACT(goods_snapshot, '$.goods_name')) AS goods_name, JSON_EXTRACT(goods_snapshot, '$.price') AS price FROM orders WHERE id = 1001;

JSON_UNQUOTE的作用就是去掉JSON值外面的双引号,把"无线鼠标"变成无线鼠标。价格这类数值类型虽然不会有引号,但在某些情况下也需要处理精度问题,后面章节会详细说。

3.2 方案二:用户扩展信息提取

很多系统的用户表都有类似extra_info这样的JSON字段,用来存用户自己填写的爱好、职业、所在城市等不固定信息。

SELECT id, nickname, JSON_UNQUOTE(JSON_EXTRACT(extra_info, '$.city')) AS city, JSON_UNQUOTE(JSON_EXTRACT(extra_info, '$.job')) AS job FROM users WHERE id = 888;

这种查询在用户管理后台、画像系统、推荐系统里很常见。你不需要为每个新加的属性去改表结构,只需要约定好JSON里的字段名,查询的时候用路径取出来就行。不过这里有个前提:团队内部对JSON字段的key命名要有规范,不然每个人都按自己的习惯写,过几个月这个字段就变成没人敢动的"混沌之地"了。

3.3 方案三:配置中心动态配置读取

配置表存配置项的时候,value字段往往用JSON存放,因为配置项有时候是一组参数,不是一个简单的字符串。

CREATE TABLE sys_config ( config_key VARCHAR(64) PRIMARY KEY, config_value JSON );

假设某条配置是:

{ "retry_count": 3, "timeout_seconds": 30, "enable_cache": true }

查询的时候:

SELECT JSON_EXTRACT(config_value, '$.retry_count') AS retry_count, JSON_EXTRACT(config_value, '$.timeout_seconds') AS timeout_seconds, JSON_EXTRACT(config_value, '$.enable_cache') AS enable_cache FROM sys_config WHERE config_key = 'api_gateway';

配置中心这种场景下,JSON_EXTRACT的价值在于:你可以在一个value字段里同时读取多个配置项,而不需要为每个配置项单独建列。

3.4 方案四:结合WHERE条件筛选数据

JSON_EXTRACT不光能出现在SELECT后面,放在WHERE条件里也完全没问题。比如订单系统里,我想查所有购买数量大于1的订单:

SELECT order_no FROM orders WHERE JSON_EXTRACT(goods_snapshot, '$.quantity') > 1;

再比如查所有颜色为黑色的订单:

SELECT order_no FROM orders WHERE JSON_EXTRACT(goods_snapshot, '$.spec.color') = '黑色';

这里注意一点:JSON_EXTRACT(goods_snapshot, '$.spec.color')返回的JSON字符串是带引号的"黑色",跟普通字符串'黑色'比较时,MySQL内部会做类型转换,一般情况下能匹配上。但如果遇到比较怪异的数据类型,建议先用JSON_UNQUOTE包一层再比较,避免隐式转换带来的意外结果:

SELECT order_no FROM orders WHERE JSON_UNQUOTE(JSON_EXTRACT(goods_snapshot, '$.spec.color')) = '黑色';

3.5 方案五:多行JSON数组展开查询

这个场景稍微进阶一点。如果JSON字段里存的不是单个对象,而是一个对象数组,比如:

[ {"name": "商品A", "qty": 2}, {"name": "商品B", "qty": 1} ]

我的目标是把数组里的每个元素拆成一行记录。MySQL 5.7里没有JSON_TABLE函数,处理这种需求比较麻烦,一般要借助一个序数表来模拟。到了MySQL 8.0,直接可以用JSON_TABLE实现,这个不是JSON_EXTRACT的范畴了。但如果你的场景只需要取数组里的某一个特定元素,JSON_EXTRACT依然方便:

SELECT JSON_UNQUOTE(JSON_EXTRACT(goods_list, '$[0].name')) AS first_goods_name FROM orders;

4. JSON_EXTRACT和JSON_UNQUOTE、JSON_VALUE的差异

4.1 让很多新人困惑的JSON_UNQUOTE

JSON_UNQUOTE不是一个独立函数,它经常和JSON_EXTRACT搭配使用。它的作用是把JSON值中的双引号去掉,把JSON字符串转换成普通字符串。

SELECT JSON_EXTRACT('{"name": "张三"}', '$.name'); -- 结果: "张三" (JSON字符串) SELECT JSON_UNQUOTE(JSON_EXTRACT('{"name": "张三"}', '$.name')); -- 结果: 张三 (普通字符串)

关键是理解为什么JSON_EXTRACT返回的值带引号。因为JSON本身是一种有格式的文本,字符串在JSON里就是用双引号包裹的。JSON_EXTRACT返回的是一个JSON片段,所以这个片段如果是字符串类型,它自然带着JSON格式的双引号。

然而JSON_UNQUOTE只处理字符串类型。如果你对一个数值类型的JSON值执行JSON_UNQUOTE,比如JSON_UNQUOTE(JSON_EXTRACT('{"age": 30}', '$.age')),MySQL会先把数值转成字符串再去掉引号,所以返回的是一个字符串'30',而不是数值30。做数值运算的时候需要留意这一点。

4.2 更符合直觉的JSON_VALUE

MySQL 8.0引入了JSON_VALUE函数,它相当于JSON_EXTRACT+JSON_UNQUOTE的合体,直接返回一个字符串值:

SELECT JSON_VALUE('{"name": "张三"}', '$.name'); -- 结果: 张三 (普通字符串)

JSON_VALUE还有一个额外的好处:可以指定返回类型:

SELECT JSON_VALUE('{"age": 30}', '$.age' RETURNING UNSIGNED); -- 结果: 30 (整数类型)

这个RETURNING子句非常实用,可以避免拿到字符串之后再手动转类型的麻烦。

但要注意,JSON_VALUE有一个行为特点:如果路径匹配不到数据,它会返回NULL(在某些模式下会报错,取决于json_value_on_error的设置)。而JSON_EXTRACT匹配不到数据同样返回NULL,这一点两者一致。

4.3 我应该怎么选

如果你在MySQL 5.7环境下工作,只能用JSON_EXTRACT配合JSON_UNQUOTE。如果你已经用上了MySQL 8.0,优先考虑JSON_VALUE,因为代码更简洁、类型更可控。

但我个人在实际开发中发现,很多团队的大多数SQL还是在用JSON_EXTRACT,原因有两个:一是习惯问题,代码里已经写了很多,不想大规模替换;二是JSON_EXTRACT返回JSON片段,有时候你想在结果里继续处理JSON格式的数据,用JSON_EXTRACT反而更顺手。比如你取出来的数据还要再次和另一个JSON函数嵌套使用,JSON_EXTRACT更自然。

这里给出我自己的选择原则:

需求场景推荐写法
需要提取字符串且去掉引号JSON_VALUE 或 JSON_UNQUOTE(JSON_EXTRACT(...))
需要提取数值并进行数值比较JSON_VALUE(..., RETURNING ...) 或 JSON_EXTRACT后转类型
需要提取JSON片段供其他JSON函数处理JSON_EXTRACT
需要提取多个路径的值JSON_EXTRACT(json_doc, '$.a', '$.b')
MySQL 5.7环境JSON_EXTRACT + JSON_UNQUOTE

5. 实际工作中踩过的坑和解决思路

5.1 坑一:返回的字符串带着引号

这是最常见的坑,没有之一。新手写JSON_EXTRACT取用户姓名,前端直接显示"张三",带着两个双引号,乍一看以为是数据错了。其实不是,这就是JSON返回值的正常格式。

解决办法有两个方向:在SQL里用JSON_UNQUOTE或JSON_VALUE去掉引号;或者在程序代码里对返回值做字符串处理。我的建议是前者,因为数据在进入业务代码之前应该是最干净的状态。

5.2 坑二:返回的数值类型不对

JSON_EXTRACT返回的数值,在MySQL里会按照JSON数值类型处理,查询结果看起来是数字,但如果你在程序端拿到的是字符串,就很麻烦了。比如你用Java的JDBC去取结果,可能拿到的是一个BigDecimal或者String,具体取决于驱动和SQL写法。

我之前遇到过一个案例:代码里判断quantity > 1,但是因为JSON_EXTRACT返回的quantity是字符串'2',程序里直接做了字符串比较,导致排序结果完全不对。排查了半天才发现是类型问题。后来统一在处理JSON数值时用JSON_VALUE加RETURNING UNSIGNED或DECIMAL,才从根上解决。

5.3 坑三:路径写错返回NULL,没有报错

JSON_EXTRACT的路径写错,MySQL不会给你抛异常,也不会给提示,直接安静地返回一个NULL。这导致很多bug隐藏得很深。我排查过很多次"为什么这条数据查不出来"的问题,最后发现是JSON路径里字段名大小写写错了,或者是路径层级多写了一层少写了一层。

一个排查技巧:先用最简单的SQL把原始JSON字段查出来,肉眼确认路径,再逐步加JSON_EXTRACT。千万不要在复杂SQL里直接盲写路径。

5.4 坑四:JSON_EXTRACT在WHERE条件里用导致性能问题

把JSON_EXTRACT放在WHERE子句里,尤其是在大表上,是一件需要慎重的事。因为MySQL的普通索引对JSON字段内部的某个值并不生效。你对JSON字段建立索引,和直接全表扫描的区别不大,除非你使用5.7的生成列加索引,或者8.0的函数索引。

-- 如果这张表有几十万行,这个查询会扫描全部数据 SELECT * FROM orders WHERE JSON_EXTRACT(goods_snapshot, '$.quantity') > 2;

优化方案之一是使用生成列(Generated Column):

ALTER TABLE orders ADD COLUMN quantity INT GENERATED ALWAYS AS (JSON_EXTRACT(goods_snapshot, '$.quantity')) STORED; CREATE INDEX idx_quantity ON orders(quantity);

之后再按quantity查询就直接用新列,性能会有质的提升。这个思路在MySQL 5.7和8.0都支持,值得一试。

6. JSON_EXTRACT常见问题速查与建议

6.1 问题排查速查表

问题现象可能原因排查方法
查询结果返回NULLJSON路径写错、字段名大小写不对、路径层级不对先把原始JSON查出来对照路径
返回的字符串带双引号JSON_EXTRACT返回的是JSON片段用JSON_UNQUOTE或JSON_VALUE处理
数值比较结果不对JSON数值被当成字符串处理用JSON_VALUE...RETURNING或者CAST显式转换
包含特殊字符字段无法查询路径里没给字段名加双引号字段名改成$."字段名"格式
查询很慢WHERE里直接用JSON_EXTRACT,索引没用上使用生成列+索引优化
数组取值越界数组长度不足加IFNULL判断或检查数据完整性

6.2 使用JSON_EXTRACT的几点建议

从实践经验看,用好JSON_EXTRACT不只是记住函数语法的问题。这里分享几条建议:

一是在设计JSON字段的时候就要考虑查询路径。字段名用统一风格命名,不要一会儿驼峰一会儿下划线,不然每个人的SQL都要猜字段名。

二是JSON字段适合存"查询频率低、结构多变"的数据。如果某个字段每天要被WHERE条件大量过滤,说明它其实应该被提取成真正的列。

三是尽量在SQL里做好值处理。能在数据库层面去掉引号、转好类型,就不要让程序代码来做。这样代码更清晰,也不会因为驱动差异导致行为不一致。

四是对性能敏感的场景,提前用生成列或者函数索引优化,不要等数据量大了才想起来。

6.3 关于JSON使用边界的一点思考

JSON字段确实方便,但也不是万能的。早期的数据库设计理论强调规范化,JSON字段本质上是在反规范化。它带来了灵活性和开发效率的提升,但也放弃了关系模型的一部分优势,比如外键约束、强类型约束、索引优化等。

我自己在使用中的平衡点是:核心业务数据尽量用规范的列或关联表存储,保证约束和查询性能;扩展性需求、低频查询的配置信息、外部系统返回的数据快照,用JSON字段存储。JSON_EXTRACT在这个平衡中扮演的角色,就是让JSON数据不仅能"存得进去",还能"取得出来"。

7. 一个完整的综合示例

为了让你更直观地看到JSON_EXTRACT在真实业务中的样子,我拿一个简化版的活动报名系统当例子。

假设有个活动表,每个活动可以配置多条报名条件,报名条件以JSON数组的形式存在conditions字段中:

[ {"type": "age", "operator": "gte", "value": 18}, {"type": "vip_level", "operator": "eq", "value": 2} ]

我需要查询所有"年龄大于等于18"且"VIP等级等于2"的活动:

SELECT id, activity_name, JSON_EXTRACT(conditions, '$[0].value') AS age_value, JSON_EXTRACT(conditions, '$[1].value') AS vip_value FROM activities WHERE JSON_EXTRACT(conditions, '$[0].value') >= 18 AND JSON_EXTRACT(conditions, '$[1].value') = 2;

这个查询能跑,但它对JSON数组下标的依赖太强,一旦配置顺序变了,结果就错了。更稳健的做法是配合JSON_SEARCH或者把JSON预处理成更稳定的结构。这说明一个问题:JSON函数好用,但JSON数据模型的设计同样重要,顺序敏感的数组结构在查询时要格外小心。

8. 最后分享一点个人经验

用JSON_EXTRACT这几年,最大的感受是:这个函数本身不复杂,复杂的是使用场景里那些隐藏的类型问题、路径问题、性能问题。很多人一开始觉得"不就取个JSON里的值嘛",结果踩了坑之后才发现细节远比想象的多。

我的建议是先明确你的MySQL版本,5.7和8.0在JSON函数上的能力差异不小。其次,写SQL之前花一分钟想清楚路径怎么写,避免在复杂嵌套中反复试错。最后,如果JSON字段里的数据要被当作查询条件频繁使用,一定要认真考虑索引方案,不要让数据库做无谓的全表扫描。

JSON_EXTRACT是JSON系列函数的入门钥匙,掌握了它,再去学JSON_CONTAINS、JSON_SEARCH、JSON_TABLE会顺畅很多。工具是用来解决问题的,搞清楚边界、避开常见的坑,它就能真正成为你日常开发中的得力帮手。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 5:08:43

AI无法处理请求?解析常见原因与应对方案

简介:E4A魔改版v1.5.2压缩包提供了一整套基于E4A(Easy4Android)的Android快速开发资源,面向想借助中文式低代码环境编写应用、同时需要更强事件监听与类库扩展能力的开发者。包体约5.14MB,内含.e4a源工程、.elb类库、.…

作者头像 李华
网站建设 2026/10/19 15:25:27

从口令实验到KV cache:共享状态与隔离问题的通用解法

1. 从一次口令实验说起:为什么共享状态会变成问题源头第一次让我真正意识到“共享状态”和“隔离问题”之间张力的,不是什么复杂的分布式系统,而是一个看起来人畜无害的口令生成实验。当时的需求很简单:给定一组字符集和长度约束&…

作者头像 李华
网站建设 2026/10/17 22:52:49

学术合规性如何?8款AI写作辅助平台排名,毕业论文轻松搞定!

论文选题总在反复纠结,文献综述怎么也写不出逻辑性?查重率居高不下,格式调整又让人抓狂? 别担心!AI论文写作工具正成为高校学生的高效帮手。本文将从学术合规性、内容逻辑性、查重适配度和使用便捷性四个维度&#xff…

作者头像 李华
网站建设 2026/10/21 9:33:42

LabVIEW用S7.net读取西门子PLC变量并实时绘制曲线的方法

简介:面向LabVIEW与西门子PLC通信开发者的课程资料包,聚焦通过S7.net.dll读取PLC变量并利用XY图实时绘图,适用于自动化工程师、电气调试人员及工业控制方向学习者。内容重点包括S7.net.dll的调用方法、LabVIEW中字节数组转换为单精度浮点数的…

作者头像 李华
网站建设 2026/10/21 5:39:43

MFC种子填充算法实现与GDI图形编程实战

1. 项目概述:为什么在MFC里实现种子填充,远不止是“画个实心圆”那么简单 “计算机图形学 实验5 区域填充——种子填充算法(MFC中)”,这个标题乍看像一份普通教学实验报告,但如果你真在Windows桌面开发一线…

作者头像 李华