结论:json字段高效查询需用stored虚拟列建索引,禁用where中json_extract();数组分析用json_table();应用层用typehandler统一序列化。

直接说结论:别在 WHERE 里用 JSON_EXTRACT() 做条件过滤,否则等于放弃索引;想查得快,必须把嵌套字段“固化”成虚拟列再建索引。
WHERE 条件里用 JSON_EXTRACT() 就是全表扫描
MySQL 不会对 JSON_EXTRACT(data, '$.items[0].price') 这类表达式自动建索引。每次执行都要读完整个 data 字段、解析 JSON、提取路径值、再比对——千万级表上单次查询轻松破 5 秒。
常见翻车点:
-
JSON_EXTRACT()返回的是 JSON 类型(带双引号),->>才返回去引号的字符串;混用会导致隐式类型转换,优化器更难走索引 - 数组下标从 0 开始:
'$.list[1]'是第二个元素,不是第一个 - 含空格或特殊字符的 key 必须写成
'$.user."full name"',写成'$.user.full name'会静默返回NULL -
JSON_CONTAINS()和JSON_OVERLAPS()同样不走索引,只是语法更短,底层开销没区别
真正可检索的方案:虚拟列 + STORED + 索引
想让 $.items[0].price 可高效查询,必须把它“落地”为普通列:
- 添加虚拟列时必须用
STORED(不能用VIRTUAL),否则无法建索引:ALTER TABLE orders ADD COLUMN price DECIMAL(10,2) AS (JSON_EXTRACT(data, '$.items[0].price')) STORED; - 立刻建 B+ 树索引:
CREATE INDEX idx_price ON orders(price); - 之后就能走索引查询:
SELECT * FROM orders WHERE price > 99.99; - 注意:动态路径如
'$.tags[*].id'不支持,因为虚拟列不处理数组通配符
JSON 数组聚合分析用 JSON_TABLE(),别硬写多层 JSON_EXTRACT()
当你要统计订单中所有商品的总价、平均单价或数量分布时,JSON_EXTRACT() 配合子查询或自连接非常脆弱且低效。
正确做法是用 JSON_TABLE() 把 JSON 数组展开成临时关系表:
SELECT jt.sku, jt.price, jt.qty FROM orders o, JSON_TABLE(o.data, '$.items[*]' COLUMNS ( sku VARCHAR(50) PATH '$.sku', price DECIMAL(10,2) PATH '$.price', qty INT PATH '$.qty' )) AS jt WHERE o.id = 12345;
这样就能直接套用 SUM()、GROUP BY、JOIN 等标准 SQL 能力,且 MySQL 8.0 会为该展开过程做一定优化。
应用层存取 JSON 别手动序列化,用 MyBatis TypeHandler
Spring Boot 项目里,如果每个 Service 都手写 JSON.parse() 或 JSON.toJSONString(),不仅重复、易错,还绕过了 MySQL 原生 JSON 的校验和二进制优势。
推荐做法:
- 定义泛型
TypeHandler<t></t>,封装 Jackson 序列化逻辑 - 在 MyBatis 映射文件中为 JSON 字段指定 handler:
<result column="attributes" property="attributes" javatype="com.example.ProductSpec" typehandler="com.example.JsonTypeHandler"></result> - 确保数据库字段声明为
JSON类型(不是VARCHAR),否则 MySQL 不会做格式校验 - 配合
CHECK (JSON_VALID(attributes))约束,从 DB 层拦截非法 JSON
虚拟列要 STORED、数组分析靠 JSON_TABLE()、应用层交由 TypeHandler 统一收口——这三个动作漏掉任何一个,都可能让 JSON 字段从“灵活利器”退化成“性能黑洞”。










