MySQL 5.7 中 JSON 字段不能直接建索引,必须通过 STORED 生成列(如 email VARCHAR(255) GENERATED ALWAYS AS (properties->>"$.request.email") STORED)并为其创建普通索引才能实现高效查询;否则 WHERE properties->>"$.email"=... 将全表扫描。

MySQL 5.7 中 JSON 字段不能直接建索引,必须通过生成列(Generated Column)+ 普通索引组合实现高效查询;跳过这步,WHERE properties->>"$.email" 类查询永远走不了索引。
为什么 JSON 列本身无法加索引
MySQL 5.7 的 JSON 类型是二进制存储的,但优化器不支持对整个 JSON 值做 B+ 树索引。你执行 CREATE INDEX idx_email ON activity_log (properties) 会直接报错:JSON column 'properties' cannot be used in key specification。这不是语法写错,而是内核限制——它连前缀索引都不允许,更别说函数索引(那是 8.0.13 才有的)。
常见错误现象包括:明明写了 WHERE properties->>"$.request.email" = 'x@y.z',EXPLAIN 却显示 type: ALL,全表扫描。
- JSON 列只能参与计算,不能作为索引键的原始字段
- 所有路径提取操作(如
->、->>、JSON_EXTRACT)都属于运行时计算,无法被索引覆盖 - 即使字段内容高度重复(比如大量
{"status":"active"}),也无法靠INDEX(properties)加速WHERE properties->>'$.status' = 'active'
创建虚拟生成列并建索引的完整流程
核心思路:把 JSON 内部某个路径的值“抽出来”,存成一个普通列(虚拟列),再给这个列建索引。MySQL 5.7 支持 VIRTUAL 生成列,不占磁盘空间,只在查询时动态计算。
以 activity_log 表中提取 properties->>"$.request.email" 为例:
ALTER TABLE activity_log
ADD COLUMN email VARCHAR(255)
GENERATED ALWAYS AS (properties->>"$.request.email") STORED,
ADD INDEX idx_email (email);注意这里用了 STORED 而非 VIRTUAL ——虽然 5.7 默认是 VIRTUAL,但 VIRTUAL 列**不能建索引**(官方文档明确限制),必须用 STORED。代价是多存一份数据,但换来的是真实索引能力。
-
properties->>"$.request.email"是带去引号提取,返回字符串;用->会包一层双引号,导致索引失效 - 类型要匹配:如果 JSON 里 email 可能超长,
VARCHAR(255)不够就得调大,否则截断后查不到 - 路径表达式必须确定性(deterministic):不能含
NOW()、RAND()等函数,否则建表失败
WHERE 条件和 GROUP BY 必须严格对齐生成列
生成列建好后,查询时必须直接引用该列名,不能“绕回去”用原始 JSON 表达式,否则索引失效。
使用 JSON Schema 验证 JSON 数据,从示例 JSON 生成 schema,并将其转换为 TypeScript 接口、Python 数据类或 Markdown 文档。
✅ 正确(走索引):
SELECT * FROM activity_log WHERE email = 'a@b.c';
❌ 错误(全表扫描):
SELECT * FROM activity_log WHERE properties->>"$.request.email" = 'a@b.c';
分组场景同理。如果你要按 email 分组统计,必须写 GROUP BY email,而不是 GROUP BY properties->>"$.request.email"。
- 哪怕两个表达式逻辑完全等价,优化器也不会自动重写为生成列引用
- 应用层拼 SQL 时容易忽略这点,尤其 ORM 自动生成条件时,默认还是用
JSON_EXTRACT风格 - 可以用
SHOW CREATE TABLE activity_log确认生成列是否标记为STORED且有索引
性能与兼容性关键细节
生成列索引不是银弹。它解决了“单路径精确匹配”,但对模糊查询、范围扫描、多路径 OR 条件依然乏力。
例如,你想查 email 以 @gmail.com 结尾,email LIKE '%@gmail.com' 无法利用 B+ 树索引做高效前缀匹配,除非加全文索引(但 email 是生成列,全文索引不支持)。
-
STORED生成列写入时会触发计算并落盘,轻微增加 INSERT/UPDATE 开销 - JSON 路径若不存在(如某条记录没
request对象),properties->>"$.request.email"返回NULL,该行会进入索引的 NULL 分区,WHERE email IS NOT NULL可过滤 - 升级到 MySQL 8.0 后,可改用函数索引(
CREATE INDEX idx_email ON activity_log ((properties->>"$.request.email"))),无需额外列,但 5.7 只能靠生成列硬扛
最易被忽略的一点:生成列的表达式一旦写死,就和业务 JSON 结构强耦合。如果前端突然把 request.email 改成 user.contact.email,所有依赖旧路径的生成列和索引立刻失效,且不会报错——只是变慢。上线前务必确认 JSON Schema 已收敛。

















