直接where json_col->>'$.status'慢是因为->>是运行时函数调用,mysql无法索引,必须全表扫描并逐行解析json;应改用虚拟列+索引:add column status varchar(20) generated always as (json_col->>'$.status') stored, add index idx_status (status)。

为什么直接 WHERE json_col->>'$.status' 会慢成全表扫描
因为 ->> 是运行时函数调用,MySQL 无法为它建立索引。每次执行都要逐行解析整个 JSON 字符串、提取路径、去引号、比较值——哪怕只查一个字段,也得把几十万行的 JSON 全反序列化一遍。EXPLAIN 看到 type: ALL 就是这个原因。
常见错误现象:SELECT * FROM orders WHERE data->>'$.status' = 'done' 在 10 万行以上就明显卡顿;并发一上来 CPU 直接飙高。
- 别指望
JSON_EXTRACT()或JSON_CONTAINS()能走索引,它们全是全表扫描触发器 -
LIKE '%value%'查 JSON 字符串?更糟——既没索引,又容易误匹配、受转义干扰 - 低版本 MySQL(ERROR 3167
怎么创建能生效的虚拟列+索引
核心是两步:加列 + 建索引,且必须用对语法和类型。建完后,WHERE 条件里必须直接引用虚拟列名,否则索引白搭。
正确示例:
ALTER TABLE orders ADD COLUMN status VARCHAR(20) GENERATED ALWAYS AS (data->>'$.status') STORED; CREATE INDEX idx_status ON orders (status);
- 务必用
->>(不是->),否则存的是"done",而你写WHERE status = 'done'永远不命中 - 类型要显式声明:字符串用
VARCHAR(n),数字用INT或DECIMAL,别用TEXT或模糊的VARCHAR(255),否则隐式转换让索引失效 -
STORED比VIRTUAL更稳妥——某些 MySQL 5.7.x 小版本对VIRTUAL列的索引支持有 bug - 路径必须稳定:如果部分行没有
$.status,data->>'$.status'返回NULL,这时WHERE status IS NULL能走索引,但= ''不能
嵌套深、数组、多条件时怎么处理
虚拟列只适合扁平、稳定、高频查询的字段。一旦路径带数组下标或嵌套过深,就容易掉坑里。
-
$.items[0].product.id这类路径不能用于虚拟列——MySQL 不保证数组首项稳定,生成列值可能随机为NULL或错值 - 要查多个字段(比如
status和user_id),可以建两个虚拟列,再建复合索引:CREATE INDEX idx_status_uid ON orders (status, user_id) - 如果 JSON 里存的是时间戳(如
$.created_at),别用VARCHAR,改用DATETIME类型虚拟列,才能支持范围查询(BETWEEN、>=) - 结构频繁变(今天
$.status,下周改成$.state)?虚拟列维护成本高,不如提前在应用层拆成普通字段
最容易被忽略的生效前提
虚拟列建了、索引建了、SQL 也写了,但 EXPLAIN 还是 type: ALL——大概率是你没改老 SQL。
优化前的语句:WHERE data->>'$.status' = 'done'
优化后的语句:WHERE status = 'done'(注意这里用的是虚拟列名 status,不是原 JSON 字段)
- 任何包装函数都会让索引失效:
UPPER(status)、TRIM(status)、COALESCE(status, '') - 字段值末尾有空格?虚拟列不会自动
TRIM,WHERE status = 'done '就不匹配,得确认数据清洗逻辑 - 字符集不一致也会导致索引失效:如果原表是
utf8mb4_unicode_ci,但虚拟列没指定,可能默认用utf8mb4_0900_as_cs,排序规则不同就无法走索引











