前言
在日常开发中,我们经常使用 MySQL JSON 字段存储扩展属性。但很多同学踩过坑:JSON 内部 key 查询很慢,即使写了
attr->>'$.name',explain一看依然是全表扫描。原生 JSON 字段内部属性本身不能直接建索引,MySQL 提供了生成列(Generated Column)来解决这个问题。本文从原理、实战代码、性能验证、线上坑点完整讲解。
一、什么是生成列?两种类型 VIRTUAL / STORED
生成列的值不是手动写入,而是通过表达式由其他列实时计算生成。JSON 场景就是用表达式提取 JSON 内部的值。
MySQL 提供两种生成列:
- VIRTUAL(默认) :不占用磁盘空间,读取的时候实时计算,不能创建索引。
- 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"),单纯生成列无法直接实现。
解决方案:
- 8.0 可以用 JSON_TABLE 将数组行转列配合查询;
- 业务上把数组元素冗余到单独字符串字段,再建索引;
- 高频数组检索场景,优先拆子表。
四、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修改表达式,只能:
- 删除旧生成列;
- 新建生成列;
大表执行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不要建。
六、线上最佳实践总结
- JSON内部字段需要WHERE过滤、范围查询:使用 STORED 生成列 + 索引;
- 仅展示读取,不需要检索:不要创建生成列,直接
attr->>'$.key'查询; - JSON数组检索,不要强行用生成列,优先考虑子表冗余;
- 大表新增生成列属于DDL操作,生产务必使用在线DDL工具,避免锁表;
- 索引不是越多越好,只给高频查询条件建立;
- 如果JSON字段更新极其频繁,尽量减少生成列数量,降低写入开销。
七、小结
MySQL JSON 字段本身无法直接索引内部属性,生成列就是用来打通这个能力。STORED生成列把JSON中需要检索的数据抽出来变成普通字段,支持索引加速查询。
它不是银弹,会带来写入成本,需要权衡读写场景。简单属性检索非常好用,但复杂嵌套、数组检索场景就会受限,要提前做好方案选型。