MySQL JSON 生成列索引原理、实战与坑点

前言

在日常开发中,我们经常使用 MySQL JSON 字段存储扩展属性。但很多同学踩过坑:JSON 内部 key 查询很慢,即使写了 attr->>'$.name',explain 一看依然是全表扫描。

原生 JSON 字段内部属性本身不能直接建索引,MySQL 提供了生成列(Generated Column)来解决这个问题。本文从原理、实战代码、性能验证、线上坑点完整讲解。

一、什么是生成列?两种类型 VIRTUAL / STORED

生成列的值不是手动写入,而是通过表达式由其他列实时计算生成。JSON 场景就是用表达式提取 JSON 内部的值。

MySQL 提供两种生成列:

  1. VIRTUAL(默认) :不占用磁盘空间,读取的时候实时计算,不能创建索引。
  2. STORED :写入/更新数据时,计算结果并持久化存储在磁盘,支持建立索引。

核心原理:

当我们创建 STORED 生成列提取 JSON 中的某个key,这一列就相当于一个普通字段。更新原 JSON 字段时,MySQL 会自动同步更新 STORED 生成列的值,索引也同步维护。查询时优化器可以直接走这个索引,不再解析 JSON 文档。
⚠️ 一句话总结:索引并不是建在 JSON 字段上,而是建在提取JSON值的STORED生成列上。

二、实战演示

1. 准备测试表

sql 复制代码
CREATE TABLE product (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    attr JSON COMMENT '商品扩展属性'
);

-- 写入测试数据
INSERT INTO product(name, attr) VALUES
('手机', '{"color":"黑色","price":3999,"tags":["旗舰","5G"]}'),
('耳机', '{"color":"白色","price":299,"tags":["蓝牙","降噪"]}'),
('平板', '{"color":"灰色","price":2499,"tags":["大屏"]}');

2. 创建 STORED 生成列 + 索引

提取JSON中的color字段:

sql 复制代码
-- 新增生成列,从attr中提取color
ALTER TABLE product
ADD COLUMN product_color VARCHAR(50)
GENERATED ALWAYS AS (attr->>'$.color') STORED;

-- 给生成列建立索引
CREATE INDEX idx_product_color ON product(product_color);

3. 查询,验证索引生效

sql 复制代码
EXPLAIN SELECT * FROM product WHERE product_color = '黑色';

执行计划 type 会变成 ref,key 显示 idx_product_color,成功命中索引。

我们也可以直接在where里写表达式,优化器也能识别:

sql 复制代码
EXPLAIN SELECT * FROM product WHERE attr->>'$.color' = '黑色';

MySQL优化器会识别 attr->>'$.color' 这个表达式,匹配到对应的生成列,自动使用索引。

4. 多字段:给JSON内数字类型创建生成列

price是数字,注意类型定义,不要存字符串:

sql 复制代码
ALTER TABLE product
ADD COLUMN product_price INT
GENERATED ALWAYS AS (attr->>'$.price') STORED;

CREATE INDEX idx_product_price ON product(product_price);

范围查询直接走索引:

sql 复制代码
SELECT * FROM product WHERE product_price > 1000;

5. 嵌套JSON对象生成列

原始JSON:{"detail":{"weight":"200g"}}

sql 复制代码
ALTER TABLE product
ADD COLUMN weight VARCHAR(20)
GENERATED ALWAYS AS (attr->>'$.detail.weight') STORED;
CREATE INDEX idx_weight ON product(weight);

三、数组场景:JSON数组怎么用生成列?

重要限制:生成列只能提取单个值,不能直接提取整个数组来建索引 。

如果想实现数组元素检索(例如tags数组包含"5G"),单纯生成列无法直接实现。

解决方案:

  1. 8.0 可以用 JSON_TABLE 将数组行转列配合查询;
  2. 业务上把数组元素冗余到单独字符串字段,再建索引;
  3. 高频数组检索场景,优先拆子表。

四、VIRTUAL 和 STORED 选型对比

类型 存储 索引支持 写入开销 读取开销 适用场景
VIRTUAL 不存储 ❌ 不能建索引 无 查询实时计算 仅展示,不需要查询过滤
STORED 持久化 ✅ 支持索引 INSERT/UPDATE需要计算并落盘 无额外开销 需要WHERE条件检索,需要索引加速

很多新手踩坑:创建生成列忘记写 STORED,默认 VIRTUAL,后面建索引直接报错。

五、高频坑点(线上最容易出问题)

坑1:表达式写错,-> 和 ->> 混用

-> 返回带双引号的JSON值,->> 是不带引号的字符串。

生成列推荐统一使用 ->>。

sql 复制代码
-- ❌ 错误示例,取出带引号字符串 "黑色"
GENERATED ALWAYS AS (attr->'$.color') STORED
-- ✅ 正确示例
GENERATED ALWAYS AS (attr->>'$.color') STORED

坑2:生成列类型长度设置过小导致截断

sql 复制代码
-- 如果color值超过20字符,会被截断,查询匹配异常
ALTER TABLE product ADD COLUMN product_color VARCHAR(20) GENERATED ALWAYS AS (attr->>'$.color') STORED;

建议:预估业务最大长度,预留冗余。

坑3:更新JSON字段,会自动更新生成列,带来写入开销

执行 UPDATE product SET attr=JSON_SET(attr,'$.color','红色') WHERE id=1

MySQL会自动计算并更新 product_color 生成列,同时维护索引。

业务高频大量更新JSON时,会增加写入压力,索引越多开销越大。

坑4:生成列表达式一旦创建,不能直接修改

如果后续JSON路径调整,不能alter修改表达式,只能:

  1. 删除旧生成列;
  2. 新建生成列;
    大表执行DDL会锁表,生产环境注意使用在线DDL(pt-online-schema-change / gh-ost)。

坑5:NULL 值处理

当attr字段本身为NULL,attr->>'$.color' 返回NULL,生成列的值为NULL。

NULL也会进入索引,等值查询 product_color = NULL 不能用,要用 IS NULL。

坑6:联合索引场景

可以把生成列和普通字段一起创建联合索引,用法和普通字段完全一致。

sql 复制代码
CREATE INDEX idx_color_name ON product(product_color,name);

坑7:不要创建过多生成列

每一个 STORED 生成列都占用磁盘,并且每次更新JSON都要同步计算。

最佳实践:只对业务高频检索的JSON key创建生成列索引,不常查询的key不要建。

六、线上最佳实践总结

  1. JSON内部字段需要WHERE过滤、范围查询:使用 STORED 生成列 + 索引;
  2. 仅展示读取,不需要检索:不要创建生成列,直接 attr->>'$.key' 查询;
  3. JSON数组检索,不要强行用生成列,优先考虑子表冗余;
  4. 大表新增生成列属于DDL操作,生产务必使用在线DDL工具,避免锁表;
  5. 索引不是越多越好,只给高频查询条件建立;
  6. 如果JSON字段更新极其频繁,尽量减少生成列数量,降低写入开销。

七、小结

MySQL JSON 字段本身无法直接索引内部属性,生成列就是用来打通这个能力。STORED生成列把JSON中需要检索的数据抽出来变成普通字段,支持索引加速查询。

它不是银弹,会带来写入成本,需要权衡读写场景。简单属性检索非常好用,但复杂嵌套、数组检索场景就会受限,要提前做好方案选型。

相关推荐
wno7041 小时前
MYSQL 常用合并函数
android·数据库·mysql
远方 hi1 小时前
MySQL 管理
adb
for_ever_love__3 小时前
LangGraph State 设计:多节点之间怎么安全地共享数据
redis·mysql·状态模式·checkpoint·state·状态图·langgraph
l1t4 小时前
DeepSeek总结的DuckDB 从 JSON 字符串到类型化列
数据库·sql·json
codeejun4 小时前
每日一Go·MySQL-7、MySQL 核心原理全景图谱
开发语言·mysql·golang
minebmw75 小时前
第 04 讲 深入浅出索引(上):B+树索引的数据结构与查询过程
mysql
bksczm5 小时前
MySQL进阶篇之备份(下半篇)
数据库·mysql
骑着蜗牛撵大象3275 小时前
SQL 编辑器不是高级记事本:KStudio 与 KSQL Developer 里的建表与增删改查手法
数据库·sql·mysql·编辑器·crud·执行计划
CaptainT6 小时前
WHERE 到底筛掉了哪些行
sql·mysql