不能对 json 字段直接建索引,必须通过 generated column(如 name_str varchar(64) generated always as (json_col->>"$.name") stored not null)提取值并对其建索引,否则查询无法走索引。

MySQL 8.0+ 怎么给 JSON 字段加索引
直接说结论:不能对 JSON 类型字段本身建索引,必须先用 GENERATED COLUMN(虚拟列)提取出具体值,再对这个虚拟列建索引。
这是 MySQL 的硬限制——JSON 是二进制格式存储的,内部结构不支持直接索引。你看到的 JSON_EXTRACT() 或 -> 操作,每次查询都要解析整个 JSON 文本,没索引时就是全表扫描。
常见错误现象:
• 执行 CREATE INDEX idx ON tbl (json_col->"$.name") 报错 ERROR 3152 (HY000): JSON column cannot be used in key specification
• 建了索引但 EXPLAIN 显示没走索引,因为建在了表达式上,而 MySQL 不支持函数索引(直到 8.0.13 才支持函数索引,且仅限确定性函数 + 虚拟列方式)
怎么定义虚拟列并建索引(含类型和 NOT NULL 注意点)
关键不是“能不能”,而是“怎么定义才有效”。虚拟列必须显式声明类型,并且要和 JSON 提取值的实际类型一致;否则索引无效或查询无法命中。
实操建议:
- 用
JSON_EXTRACT()或更简洁的->操作符提取路径,但结果是带双引号的 JSON 字符串(比如"alice"),想查字符串得用->>或JSON_UNQUOTE(JSON_EXTRACT(...)) - 虚拟列类型必须匹配提取值:字符串用
VARCHAR,数字用UNSIGNED INT或DECIMAL,布尔用TINYINT(1) - 虚拟列必须加
STORED(不推荐VIRTUAL,因为二级索引要求列物理存在)或至少确保它能被索引——MySQL 8.0+ 允许对VIRTUAL列建索引,但前提是该列是NOT NULL且有确定性表达式 - 强烈建议加
NOT NULL约束,否则索引可能跳过 NULL 行,导致查询结果不一致
示例(安全写法):
ALTER TABLE users ADD COLUMN name_str VARCHAR(64) GENERATED ALWAYS AS (json_col->>"$.name") STORED NOT NULL, ADD INDEX idx_name_str (name_str);
为什么用 ->> 而不是 ->?类型不匹配会导致索引失效
-> 返回的是 JSON 类型值(带引号、转义),->> 返回的是去引号的 MySQL 原生类型。如果虚拟列定义为 VARCHAR,但用 -> 提取,实际存的是 "\"alice\"" 这种格式,等于把 JSON 字符串当普通字符串存了——查 WHERE name_str = 'alice' 就永远不命中。
常见错误场景:
- 建虚拟列用
json_col->"$.name",类型设为VARCHAR(64),结果该列值是"alice"(含双引号),不是alice - 查询写
WHERE json_col->>"$.name" = 'alice',看似对,但优化器无法重写为走虚拟列索引,除非 WHERE 条件正好匹配虚拟列名 - 正确姿势:查询必须直接命中虚拟列,例如
WHERE name_str = 'alice'
验证是否生效:执行 EXPLAIN SELECT * FROM users WHERE name_str = 'alice';,看 key 列是否显示 idx_name_str。
性能和兼容性要注意的几个硬坑
虚拟列索引不是银弹。有些情况它反而让写入变慢、空间变大,甚至引发隐式转换。
- 每行 INSERT/UPDATE 都要计算虚拟列表达式,如果 JSON 路径深、嵌套多、数据量大,写入延迟明显上升
-
STORED虚拟列真实占用磁盘空间,一个 1KB 的 JSON 字段提取出 10 个字段,就多存 10KB/行(VIRTUAL不存,但部分旧版本不支持对其建索引) - MySQL 5.7 不支持对虚拟列建索引(只支持
STORED列,且需手动指定类型),必须升到 8.0.13+ - JSON 路径不存在时,
->>返回NULL,如果虚拟列没设NOT NULL,这些行就进不了索引 —— 查询WHERE name_str IS NOT NULL才能覆盖,但业务逻辑未必允许
最易被忽略的一点:虚拟列的字符集和排序规则必须和查询条件一致。比如虚拟列是 VARCHAR(64) COLLATE utf8mb4_0900_as_cs,但查询写 WHERE name_str = 'alice' COLLATE utf8mb4_general_ci,索引就失效。











