生成列
生成列(generated column)是由数据库根据同一行其他列自动计算的列。调用方负责写入原始数据,数据库负责保证派生结果与原始数据保持一致。
例如,商品的含税价格可以由未税价格和税率计算:
CREATE TABLE products (
product_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
price NUMERIC(12, 2) NOT NULL,
tax_rate NUMERIC(5, 4) NOT NULL,
price_with_tax NUMERIC(12, 2)
GENERATED ALWAYS AS
(ROUND(price * (1 + tax_rate), 2)) STORED
);
生成列不是插入时填一次就结束的默认值。只要依赖列发生变化,生成结果也会按数据库定义重新计算或重新读取。
本文以 SQL 通用概念为主,语法示例采用 PostgreSQL。不同 PostgreSQL 版本和其他数据库对虚拟列、存储列及表达式限制的支持不同,使用前应核对目标版本。
生成列的两种形式
虚拟生成列
虚拟列不保存计算结果,读取时再计算。它节省表存储空间,但查询时需要计算,并且可使用的表达式往往受到更多限制。
部分数据库可写成:
total NUMERIC(12, 2)
GENERATED ALWAYS AS (quantity * unit_price) VIRTUAL
存储生成列
存储列在插入或更新时计算,并把结果保存在表中:
total NUMERIC(12, 2)
GENERATED ALWAYS AS (quantity * unit_price) STORED
读取通常更直接,但会占用空间,并增加写入时的计算与维护成本。
| 对比项 | 虚拟列 | 存储列 |
|---|---|---|
| 结果 | 读取时计算 | 写入时计算并保存 |
| 空间 | 通常不保存结果 | 占用表存储 |
| 成本 | 偏向读取 | 偏向写入 |
| 支持度 | 数据库与版本差异较大 | 较常见,但仍需核对版本 |
选择时应基于实际查询与写入负载,不要仅因为“预计算听起来更快”就使用存储列。
写入生成列
生成列的值由数据库控制,普通插入应省略它:
INSERT INTO products (price, tax_rate)
VALUES (100.00, 0.1300);
在允许列出生成列的数据库中,通常也只能写 DEFAULT:
INSERT INTO products (price, tax_rate, price_with_tax)
VALUES (100.00, 0.1300, DEFAULT);
不能把任意值强行写入生成列,否则数据库就无法保证派生结果正确。
更新依赖列时,结果随之变化:
UPDATE products
SET tax_rate = 0.0900
WHERE product_id = 1;
表达式限制
生成表达式必须能由当前行可靠地计算。常见限制包括:
- 只能引用当前行的普通列;
- 不能引用另一张表;
- 不能使用子查询;
- 不能直接引用另一个生成列;
- 通常不能使用会随时间或环境变化的易变函数;
- 不能依赖“当前用户”“当前时间”等会让同一输入产生不同结果的上下文。
PostgreSQL 要求表达式使用不可变(IMMUTABLE)函数。即使一个自定义函数被标记为不可变,开发者也必须真正保证它对相同输入始终返回相同结果;错误标记会让存储结果、索引和查询优化产生错误。
下面的需求不适合普通生成列:
订单总额 = 所有订单明细的金额之和
它需要读取其他行或其他表,应通过查询、视图、物化视图、事务内维护的汇总列或其他专门方案实现。
数据类型与精度
生成列仍然有自己的数据类型,表达式结果必须能转换为该类型:
CREATE TABLE order_items (
quantity INTEGER NOT NULL CHECK (quantity > 0),
unit_price NUMERIC(12, 2) NOT NULL CHECK (unit_price >= 0),
line_total NUMERIC(14, 2)
GENERATED ALWAYS AS
(ROUND(quantity * unit_price, 2)) STORED
);
设计金额表达式时要明确:
- 在哪一步舍入;
- 使用什么精度和小数位;
- 税率、折扣与汇率的精度;
- 行级金额之和与汇总后舍入是否可能不同。
生成列只能保证表达式一致,不能替你决定正确的财务规则。
为生成列建立索引
如果数据库允许,生成列可以像普通列一样用于查询和索引:
CREATE INDEX idx_products_price_with_tax
ON products (price_with_tax);
但有时直接创建表达式索引更简单:
CREATE INDEX idx_users_lower_email
ON users (LOWER(email));
两者的选择取决于是否需要:
- 在查询结果和数据库工具中暴露一个有名称的派生列;
- 对该值授予独立列权限;
- 在多处复用同一表达式;
- 仅为某个查询条件加速。
若只是为了索引一个表达式,不一定需要额外生成列。
权限与数据暴露
数据库可以对普通列和生成列分别授予读取权限。这有助于只公开处理后的结果,例如公开脱敏值而隐藏原始内容。
不过,权限隔离是否可靠取决于生成表达式中所用函数及数据库的执行规则。涉及敏感数据时,应审查函数是否会泄露输入、报错信息或侧信道,并核对目标 PostgreSQL 版本对虚拟/存储生成列权限的具体要求。
不要仅凭“用户看不到原始列”就认定脱敏设计安全。
修改生成列
数据库对直接修改生成表达式的支持不同。稳妥的迁移通常包括:
- 新增使用新表达式的列;
- 验证结果、索引、权限和依赖对象;
- 切换应用读取;
- 删除旧列并按需重命名。
对于大型表,新增或重算存储生成列可能扫描并重写大量数据,持有较强锁并产生大量日志。上线前应在接近生产规模的数据上验证耗时和锁影响。
与其他机制的区别
默认值
默认值只在插入时缺少输入时计算,之后不会因其他列变化而自动更新:
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
生成列始终从依赖列得出,调用方不能随意覆盖。
视图
视图适合组合多列、多表、聚合和过滤,不会为每张基础表增加实际列。若派生值只在少数查询中需要,视图通常更灵活。
物化视图
物化视图保存查询结果,但通常需要显式刷新,适合昂贵的跨行或跨表计算。生成列在每行写入或读取时自动保持一致。
触发器
触发器能执行更复杂的过程逻辑,也更难理解和维护。当前行内、确定性的简单表达式优先考虑生成列;需要跨表、副作用或条件流程时再考虑触发器。
适用场景
生成列适合:
- 单价与数量计算出的行金额;
- 从标准化文本得到的搜索键;
- 从时间戳提取的稳定日期部分;
- 根据同一行字段组合出的分类或显示值;
- 需要统一表达式并可能建立索引的派生值。
不适合:
- 当前时间、随机数或当前用户;
- 跨行合计;
- 需要访问其他表的值;
- 会频繁改变规则的复杂业务逻辑;
- 需要人工覆盖的结果。
常见误区
生成列可以引用任意查询
通常不可以。它主要处理当前行内的确定性表达式。
存储生成列没有维护成本
错误。它增加写入计算、存储占用,并可能让结构变更需要重算整表。
自定义函数标记为不可变就一定安全
标记是开发者对数据库的承诺,不是数据库自动验证出的事实。函数若读取配置、表或时间,就不应伪装成不可变。
生成列可以代替所有冗余数据维护
跨表和跨行派生仍需要查询、汇总表、物化视图或事务逻辑。
设计检查清单
- 派生值是否只依赖当前行;
- 相同输入是否必定得到相同结果;
- 应使用虚拟形式还是存储形式,目标版本是否支持;
- 数据类型、精度和舍入规则是否明确;
- 只需要表达式索引,还是确实需要一个可见列;
- 大表新增或修改生成列是否会重写数据;
- 权限设计是否真的能保护原始敏感数据;
- 规则频繁变化时,视图或查询是否更合适。
生成列的核心价值是让“同一行内可确定计算的结果”只有一个权威定义。表达式越简单、稳定、可验证,它带来的收益越大。