mysql 5.7 中虚拟列必须声明为 stored 才能建索引,virtual 类型不支持索引;需用 alter table modify/change column 显式转为 stored,并确保类型宽度足够且表达式确定。

MySQL 5.7 虚拟列必须显式声明 STORED 才能建索引
直接对 GENERATED ALWAYS AS 的虚拟列(即默认的 VIRTUAL 类型)执行 CREATE INDEX 会报错:ERROR 3107 (HY000): Generated column can only be indexed if it is STORED。MySQL 5.7 不允许为 VIRTUAL 列建索引,这是硬性限制,不是配置问题。
解决办法只有一个:把列定义从默认的 VIRTUAL 改成 STORED。这意味着该列值会在写入时计算并物理存储,占用磁盘空间,但换来的是可索引性和确定性执行(避免每次查询都实时计算)。
-
GENERATED ALWAYS AS (expr) STORED是唯一支持索引的语法形式 - 如果已有表,需用
ALTER TABLE ... MODIFY COLUMN或CHANGE COLUMN显式转为STORED -
VIRTUAL列无法通过任何方式绕过限制加索引——优化器不支持,DDL 直接拒绝
给函数表达式创建 STORED 虚拟列并建索引的完整步骤
假设你想加速对 UPPER(name) 的查询,不能直接在 WHERE UPPER(name) = 'ABC' 上依赖函数索引(MySQL 5.7 不支持函数索引),只能靠虚拟列 + 索引模拟。
正确做法是:
- 添加列:
ALTER TABLE users ADD COLUMN name_upper VARCHAR(255) GENERATED ALWAYS AS (UPPER(name)) STORED; - 立即建索引:
CREATE INDEX idx_name_upper ON users(name_upper); - 改写查询:把
WHERE UPPER(name) = 'ABC'换成WHERE name_upper = 'ABC'
注意:UPPER() 表达式必须满足确定性(deterministic),否则建表/修改会失败;常见字符串函数如 UPPER、LOWER、SUBSTRING、CONCAT 都是安全的,但 NOW()、RAND()、UUID() 不行。
STORED 虚拟列的性能和存储代价必须手动评估
STORED 列不是免费的。它会在每行 INSERT/UPDATE 时触发计算,并写入磁盘,带来额外开销。
- INSERT 性能下降:表达式计算 + 额外字段写入,尤其当表达式复杂或列宽大时更明显
- 磁盘空间增加:按行数 × 字段平均长度增长,比如千万级用户表加一个
VARCHAR(255),可能多占几百 MB - UPDATE 影响范围扩大:哪怕只更新原列
name,也会连带重算并写入name_upper - 索引维护成本同步上升:B+ 树插入/分裂频率随数据量和更新频次升高
别只看“能加索引”就上,先用 EXPLAIN 确认原查询是否真因函数导致全表扫描,再权衡是否值得引入 STORED 列。
常见错误:ALTER COLUMN 时漏写 STORED 或类型不匹配
最容易踩的坑是想“升级”现有虚拟列为可索引,却写错语法:
- 错:
ALTER TABLE users MODIFY COLUMN name_upper VARCHAR(255) GENERATED ALWAYS AS (UPPER(name));→ 默认仍是VIRTUAL,索引仍建不了 - 错:
ALTER TABLE users CHANGE COLUMN name_upper name_upper VARCHAR(50) GENERATED ALWAYS AS (UPPER(name)) STORED;→ 新类型VARCHAR(50)可能截断原name值,导致索引结果错误 - 对:务必复用原表达式 + 显式声明
STORED+ 类型宽度 ≥ 表达式最大可能输出长度
建议用 SELECT MAX(LENGTH(UPPER(name))) FROM users; 测宽度,留点余量再定长,比如结果是 42,就设 VARCHAR(64)。











