题干与适用场景
这道题不再比较所有生成列方案,只讨论把重复的行内表达式迁移为 PostgreSQL 18 的虚拟生成列。目标是统一计算语义并避免存储列带来的表重写,同时控制读时 CPU、函数限制和权限变化。
面试官考察点
- 是否知道 PostgreSQL 18 新增虚拟生成列,并将其设为默认类型。
- 是否先证明表达式只引用当前行、使用不可变的内置函数与类型。
- 是否用双读对账和真实查询计划验证迁移,而非只检查 DDL 成功。
- 是否理解虚拟列读取时计算、没有存储值,且不能用于分区键。
回答前需要澄清的问题
确认表达式是否只含内置函数和类型,哪些查询会读取或过滤该列,当前是否有表达式索引,以及应用能否在一段时间内同时读取旧表达式与新列。还要核对角色权限,因为生成列与基础列可以分别授权;若想用生成列隔离基础列,则要逐一验证表达式中的函数、操作符和类型转换是否满足 LEAKPROOF 条件。
30 秒回答框架
我会先静态审查表达式满足 PostgreSQL 18 虚拟列限制,再新增显式 VIRTUAL 列。迁移期应用影子读取旧表达式和新列,按空值、Unicode、异常输入和历史分区对账;同时比较代表性查询的 CPU、尾延迟和执行计划。结果与性能达标后切换读取,保留快速回退到旧表达式的开关,最后才清理重复 SQL。
分步骤深入解答
DDL 可以写成:
ALTER TABLE report_events
ADD COLUMN normalized_country text
GENERATED ALWAYS AS (lower(trim(country_code))) VIRTUAL;虚拟列在读取时计算且不占行存储。表达式只能引用当前行,不能包含子查询或其他生成列,所用函数必须不可变;虚拟列还不能依赖用户自定义函数或类型。这里显式写 VIRTUAL,避免团队把 PostgreSQL 18 的新默认行为误读成存储列。
先在只读副本或影子环境扫描历史数据,比较 normalized_country IS NOT DISTINCT FROM lower(trim(country_code))。样本要覆盖 NULL、空白、大小写和非 ASCII 输入。随后对高频查询运行 EXPLAIN (ANALYZE, BUFFERS),判断读时计算是否放大 CPU;若过滤路径需要索引,应单独验证当前版本是否支持目标索引表达式与写入代价,不能假定“无存储”等于“无成本”。
发布分三步:新增列;应用双读但仍以旧表达式为准并记录聚合差异;差异为零且性能门禁通过后切到新列。回滚只需恢复旧查询表达式。基础列和虚拟列的 SELECT 权限分别审计;若任一函数、操作符或转换不能证明为 LEAKPROOF,就不能把虚拟列权限视为对基础列的完整安全隔离。
高质量示范回答
我会把它当成查询契约迁移。先确认表达式只使用当前行的内置不可变函数,再新增显式虚拟列。虚拟列省去物理回填,却把计算留在每次读取,因此我会用生产查询形状验证 CPU 和尾延迟。
应用先影子比较旧表达式与新列,按输入类别统计不一致;结果一致后才切换读取。权限、视图和客户端列映射也要验收。若性能或语义回归,立即恢复旧表达式,因为基础字段始终保留。
常见错误
- 省略
VIRTUAL,让读者依赖版本默认值理解 DDL。 - 使用可变函数、子查询或用户自定义类型后才发现建列失败。
- 只抽样普通英文值,遗漏
NULL和 Unicode 行为。 - 把“没有行存储”误解为查询没有 CPU 成本。
- 切换同时删除旧表达式,失去快速回滚路径。
追问及应对
为什么这里不选 STORED?
迁移目标是统一一个计算便宜的读时表达式并避免物理回填。若它进入大量扫描、排序或高频过滤,压测可能证明 STORED 更合适;那属于另一项存储与复制决策。
虚拟列能做分区键吗?
不能。PostgreSQL 18 的生成列不能成为分区键;需要按该值分区时,应保留普通列并在写入路径维护。
如何验证权限没有扩大?
用实际应用角色分别查询基础列和生成列,核对列级授权以及表达式中函数的执行权限。若生成列承担隐藏基础列的安全边界,还要检查表达式中每个函数及操作符或类型转换背后的函数是否标记为 LEAKPROOF;PostgreSQL 不会替应用强制这项条件。只要有一条表达式路径无法证明,就不能把生成列授权当作完整隔离。
何时结束双读?
覆盖完整业务周期、历史分区和峰值负载后,差异计数持续为零,查询门禁与权限验收均通过,才结束双读并清理重复表达式。