MySQL VIRTUAL 与 STORED:生成列索引的真实维护成本
给生成列加上 VIRTUAL,并不代表每次写入都能省掉计算。如果这个虚拟列建了索引,数据库仍需要得到表达式结果并维护索引记录。把 VIRTUAL 简化成“只读时计算”、STORED 简化成“只写时计算”,会漏掉最影响成本的那条索引路径。
MySQL 8.4 的选择应围绕两个问题展开:结果是否需要保存在聚簇行中,读取能否从已有索引获得它。表达式复杂度、更新频率与返回列形状,比两个关键字哪个听起来更省空间更重要。
同一金额表达式,两种落地方式
下面使用独立库,金额和数量都非空,避免 NULL 语义干扰比较。
CREATE DATABASE generated_cost_lab;
USE generated_cost_lab;
CREATE TABLE line_virtual (
id BIGINT NOT NULL PRIMARY KEY,
quantity INT NOT NULL,
unit_price DECIMAL(10,2) NOT NULL,
amount DECIMAL(18,2) GENERATED ALWAYS AS
(quantity * unit_price) VIRTUAL,
KEY idx_amount (amount)
) ENGINE=InnoDB;
CREATE TABLE line_stored (
id BIGINT NOT NULL PRIMARY KEY,
quantity INT NOT NULL,
unit_price DECIMAL(10,2) NOT NULL,
amount DECIMAL(18,2) GENERATED ALWAYS AS
(quantity * unit_price) STORED,
KEY idx_amount (amount)
) ENGINE=InnoDB;
INSERT INTO line_virtual(id, quantity, unit_price)
VALUES (1, 2, 12.50), (2, 5, 20.00);
INSERT INTO line_stored(id, quantity, unit_price)
SELECT id, quantity, unit_price FROM line_virtual;
两张表的业务结果应一致:金额由数量乘单价产生,不需要应用再独立写一个 amount。结果列的 DECIMAL 类型是设计的一部分,超出精度、发生转换或表达式异常时,要按实际 SQL_MODE 和错误处理核对,不能把生成列当不受类型限制的数学表达式。
SELECT id, quantity, unit_price, amount FROM line_virtual ORDER BY id;
SELECT id, quantity, unit_price, amount FROM line_stored ORDER BY id;
UPDATE line_virtual SET quantity = 3 WHERE id = 1;
UPDATE line_stored SET quantity = 3 WHERE id = 1;
修改基础列后,两种生成列都必须与新基础值保持一致。索引中的旧金额入口也需要更新,这部分维护不因为关键字是 VIRTUAL 就消失。
行内存储与索引存储是两份成本
STORED 在表行中保留生成结果,读取普通行时可以直接取得该值;VIRTUAL 通常不把结果作为聚簇记录中的存储列保留。但给 VIRTUAL 建二级索引后,索引自己会占用空间,也会物化需要的键值。
因此,想减少写入负担,先看这个字段是否真的需要索引。对一个高频更新、几乎没人按金额查询的明细表,多一棵 amount 索引可能比选择 VIRTUAL 或 STORED 更值得讨论。
反过来,如果筛选和返回都能利用生成列索引,虚拟列的查询也未必每次都重新对所有候选行计算。要结合执行计划与取回列判断,而不是只根据列定义猜读时开销。
EXPLAIN
SELECT id, amount FROM line_virtual WHERE amount >= 50;
EXPLAIN
SELECT id, quantity, unit_price, amount
FROM line_virtual WHERE amount >= 50;
这两条查询返回的字段不同,回表需求也可能不同。样本很小时优化器可以选择扫描,实验重点是建立对照方式,而不是要求某个固定输出。
生成列不是历史金额快照
订单最终成交金额如果包含当时折扣、税费、人工调整,就不一定能由当前数量和单价重新计算。把它定义为生成列,可能把“应永久保存的业务结果”变成“随基础列变化的派生值”。
这属于数据含义问题,不能靠选择 STORED 修正。STORED 仍是按表达式维护的生成列,基础字段更新后结果会更新;它并不自动冻结过去发生的事实。需要保留成交快照,就应明确哪些值不可再变、哪些变化通过新版本或调整记录表达。
表达式也必须满足数据库的确定性与语法限制,不能依赖当前时间、外部表查询或应用变量,使同一行在不同执行时刻得到无法解释的索引键。
迁移时把重建与兼容窗口算进去
从普通列改成生成列,或在 VIRTUAL 与 STORED 之间切换,不能只看字段名相同。具体 ALTER 是否重建表、能否并发写入,要按 8.4 的操作支持范围逐项核对,给空间和维护窗口留出预算。
应用也可能受影响:旧代码仍在 INSERT 列表里给 amount 赋普通值,改造后会失败。上线前先找到这些写入入口,明确由数据库计算以后 ORM 如何排除该列,再执行结构变更。
性能验证应保留四组有意义的对照:基础列写入、更新基础列、按生成值筛选、读取完整行。使用同样数据和索引设置,记录实际资源与延迟,而不是只跑一次 SELECT 就宣称某种方式更快。
VIRTUAL 与 STORED 的区别首先是结果存在哪里。是否建索引、怎样更新、结果能否重算,才决定这次选择在业务里是否合适。