必须用stored虚拟列提取json字段并显式声明类型后建索引,否则data->>'$.status'等查询无法走索引;因b+树索引不支持运行时函数,优化器无法识别表达式结构,导致全表扫描。

不能直接对 JSON 字段建索引,必须用虚拟列(STORED 更稳妥)提取路径值并显式声明类型,再在该列上建普通索引;否则所有 WHERE data->>'$.xxx' 查询都走全表扫描。
为什么 data->>'$.status' 查询不走索引
MySQL 的 B+ 树索引无法作用于运行时函数调用。每次执行 data->>'$.status' 都要解析整条 JSON、提取字段、去引号、比较——优化器看不到“结构”,只能全表扫。EXPLAIN 显示 type: ALL 就是这个原因。
-
JSON_EXTRACT()、JSON_CONTAINS()、JSON_OVERLAPS()全部同理,不落地就无索引可言 - 即使字段只有 1KB,10 万行也意味着 100GB+ 的 JSON 解析开销(含 CPU 和内存)
- 错误示例:
CREATE INDEX idx ON t (data->>'$.status');会报错ERROR 3105
怎么安全添加虚拟列并建索引
关键不是加列,而是表达式、类型、存储方式三者匹配。错一个,索引就白建。
- 必须用
->>(不是->):前者返回标量值(如active),后者返回带引号的 JSON 字符串(如"active") - 类型必须显式且合理:字符串用
VARCHAR(64),数字用INT UNSIGNED,时间用DATETIME;别用TEXT或过大的VARCHAR(255) - 优先选
STORED:物理存储值,索引行为稳定;VIRTUAL在某些优化器路径下可能失效(尤其 MySQL 8.0.13 前) - 字段名不能和已有列或保留字冲突,比如别叫
order、group
正确示例:
ALTER TABLE orders ADD COLUMN status VARCHAR(20) GENERATED ALWAYS AS (data->>'$.status') STORED, ADD INDEX idx_status (status);
查询时必须改写 WHERE 条件
建完虚拟列后,老 SQL 不改,索引等于没建。优化器不会自动把 data->>'$.status' 映射到新列。
- ✅ 走索引:
SELECT * FROM orders WHERE status = 'shipped'; - ❌ 不走索引(哪怕逻辑等价):
SELECT * FROM orders WHERE data->>'$.status' = 'shipped'; - ⚠️ 类型隐式转换风险:虚拟列是
INT,但传入字符串'123',会导致索引失效;应统一用123 - ⚠️ 路径不存在时
->>返回NULL,所以WHERE status IS NULL可走索引,但= ''不行
嵌套深、数组、多条件时怎么处理
虚拟列只适合扁平、稳定、高频查询的字段。一碰动态结构,就容易掉坑里。
-
$.items[0].id这类带数组下标的路径不可靠:首项不稳定,生成列值可能随机为NULL - 查多个字段(如
status和user_id):建两个虚拟列,再建复合索引CREATE INDEX idx_st_uid ON orders (status, user_id) - JSON 里存时间戳(如
$.created_at):虚拟列必须用DATETIME类型,才能支持BETWEEN或>= - 结构频繁变更(今天
$.status,下周改成$.state):虚拟列维护成本高,不如应用层拆成普通字段
最常被忽略的一点:虚拟列本身不解决写入性能问题。每次 INSERT 或 UPDATE 都要重新计算表达式,路径越深、JSON 越大,写延迟越明显——读写比低于 5:1 时,得重新评估方案。











