不能直接在JSON字段上建索引,必须用GENERATED COLUMN提取值再建普通索引,否则WHERE json_col->>'$.field'必全表扫描;因其为运行时函数调用,优化器无法关联B+树索引,且CREATE INDEX idx ON t (json_col->>'$.status')会报ERROR 3152。

不能直接在 JSON 字段上建索引,必须用 GENERATED COLUMN 提取值,再对这个列建普通索引 —— 否则所有 WHERE json_col->>'$.field' 查询都走全表扫描。
为什么 WHERE json_col->>'$.status' 一定不走索引
MySQL 的 B+ 树索引只认具体列名,不认表达式。json_col->>'$.status' 是运行时函数调用,每次执行都要解析整个 JSON 文本、提取路径、去引号、比较 —— 优化器根本没法把它和任何物理索引结构关联起来。EXPLAIN 看到 type: ALL 就是铁证。
- 哪怕数据只有 1 万行,CPU 也会在 JSON 解析上空转
-
JSON_CONTAINS()、JSON_EXTRACT()、JSON_OVERLAPS()全部同理,无法命中索引 - 写
CREATE INDEX idx ON t (json_col->>'$.status')会直接报错ERROR 3152
怎么加虚拟列并确保索引真正生效
核心是两步:定义列 + 建索引,但每一步都有硬性约束。
- 必须用
->>(不是->),否则存的是带双引号的字符串,比如"shipped",而你查WHERE status = 'shipped'永远不匹配 - 类型必须显式声明且匹配实际值:
INT UNSIGNED对数字,VARCHAR(32)对短字符串,别用TEXT或模糊的VARCHAR(255) - 推荐用
STORED(不是VIRTUAL):MySQL 8.0.13+ 对VIRTUAL列索引有优化器路径失效风险;STORED值物理落地,行为可预测 - 如果字段可能为空,列定义要允许 NULL,比如
INT UNSIGNED NULL,否则插入缺失该路径的 JSON 会失败
正确示例:
ALTER TABLE orders ADD COLUMN status VARCHAR(20) GENERATED ALWAYS AS (data->>'$.status') STORED, ADD INDEX idx_status (status);
查询时必须直接引用虚拟列名
建完索引不代表自动生效。WHERE 条件里如果还写 JSON 表达式,索引就完全被忽略。
- ✅ 走索引:
SELECT * FROM orders WHERE status = 'shipped'; - ❌ 不走索引(哪怕逻辑等价):
SELECT * FROM orders WHERE data->>'$.status' = 'shipped'; - ⚠️ 隐式转换也废索引:
WHERE status = 123(列是VARCHAR)或WHERE status = 'shipped '(末尾空格) - 函数包装必废索引:
WHERE UPPER(status) = 'SHIPPED'—— 虚拟列索引彻底失效
哪些场景不适合走虚拟列这条路
不是所有 JSON 字段都适合提取建索引。一旦踩中下面任意一条,性能可能更差,或维护成本失控:
- 路径含数组下标,比如
$.items[0].id—— MySQL 不保证首项稳定,生成列值可能随机为 NULL - JSON 结构频繁变更(今天叫
$.status,下周改成$.state)—— 每次都要 DDL 修改虚拟列 - 单条 JSON 超过 500KB —— 即使只取一个字段,MySQL 解析开销仍不可忽略
- 需要范围查询但用了字符串类型(如把时间戳存成
VARCHAR)—— 改用DATETIME类型虚拟列才能支持BETWEEN
最常被忽略的一点:虚拟列本身不解决写入瓶颈。每插入或更新一行,MySQL 都要实时计算生成列值 —— 如果写多读少,STORED 列的 CPU 开销反而比全表扫描更重。











