PostgreSQL 18 预览: 虚拟生成列

John Doe 三月 21, 2025

在其他的关系数据库中,经常会用到虚拟计算列,其值是自动从其他列计算出来的,且不占用磁盘空间。现在,PostgreSQL 也可以了。

草原上奔驰的大象

特性提交日志

虚拟生成列

这增加了一种新的生成列形态,它在读取时进行计算(类似于常规的视图,它与现有的存储生成列不同,存储生成列在写入时进行计算,类似于物化视图)。

列定义的语法是:

... GENERATED ALWAYS AS (...) VIRTUAL

VIRTUAL也是可选的。为了与其他各种关系数据库保持一致,默认使用VIRTUAL而不是STORED。SQL 标准并没有规定这种行为,而且它也没有涉及VIRTUALSTORED。另外,我们默认使用的视图也是虚拟的,而不是物化的。

虚拟生成列在元组中存储为null值。这个特性的早期版本曾试图完全不存储它们。但是,如果元组中间的某个列完全缺失,会导致很多问题或混淆。这是一种折中的办法,相比于强制使用存储生成列,它仍然节省了空间。如果后面找到改进的方法,通过利用pg_upgrade的一些巧妙设计,也可以升级到更新的方案。

虚拟生成列的功能和限制与存储生成列大致相同。在某些情况下,本次合入对虚拟生成列的限制比技术上实际需要的更严格,以保持两类生成列的一致性。其中一些限制在经过单独仔细考虑后,以后可能会放宽。

目前不支持但可能会作为增量特性逐步添加的,有下面这些功能,它们的实现难度各不相同:

  • 对虚拟列建立索引,或使用虚拟列进行索引
  • 因为上面一条,虚拟列也不支持唯一约束
  • 虚拟列的扩展统计信息
  • 虚拟列的外键约束
  • 虚拟列的非空约束(检查约束是支持的)
  • ALTER TABLE / DROP EXPRESSION
  • 虚拟列不能具有域类型
  • 逻辑复制不支持虚拟列

generated_virtual.sql中的测试是从generated_stored.sql复制过来的,并替换了关键字。通过这种方式,我们可以确保行为基本一致,并且差异也能显现出来。目前,一些针对当前不支持功能的测试被注释掉了。

讨论:https://www.postgresql.org/message-id/flat/a368248e-69e4-40be-9c07-6c3b5880b0a6@eisentraut.org

示例

很久以前,我们有了第一种形式的生成列:标识列。两个版本之后,又有了存储生成列。现在,我们有了生成但不存储的列:虚拟列。

虚拟列的想法非常简单:如果你有可以计算的内容,你可以给它命名,并在表中使用,而无需输入完整的表达式来生成值,并且无需存储数据。

让我们看看它的实际应用。首先,创建一个没有生成列的测试表,看看添加生成列后会发生什么:

-- 创建测试表
create table test (
    id int8 generated always as identity primary key,
    n int4
);
-- 插入数据
insert into test (n) select random() * 200 + 100 from generate_series(1,10000) i;

这样我们就得到了一个有 10000 行数据的表,其中n是一些随机值:

-- 查看部分数据
select * from test limit 5;
 id |  n  
----+-----
  1 | 187
  2 | 150
  3 | 256
  4 | 210
  5 | 237
(5 rows)

该表在磁盘上的文件总大小为 712kB:

-- 查看表大小
select pg_total_relation_size('test'::regclass);
 pg_total_relation_size 
------------------------
                 729088
(1 row)

现在,假设我们想添加一个文本列,该列将重复n次 “PgSQL” 这个词。如果我将其添加为虚拟列:

-- 添加虚拟列
alter table test add column v_text text generated always as ( repeat('PgSQL', n) ) virtual;

表的大小不会改变:

-- 再次查看表大小
select pg_total_relation_size('test'::regclass);
 pg_total_relation_size 
------------------------
                 729088
(1 row)

但数据会存在(此处对输出进行了截断,以免内容太宽):

-- 查看数据
select id, n, length(v_text), substr(v_text, 1, 27) || '…'
    from test
    limit 5;
 id |  n  | length |           ?column?           
----+-----+--------+------------------------------
  1 | 187 |    935 | PgSQLPgSQLPgSQLPgSQLPgSQLPg
  2 | 150 |    750 | PgSQLPgSQLPgSQLPgSQLPgSQLPg
  3 | 256 |   1280 | PgSQLPgSQLPgSQLPgSQLPgSQLPg
  4 | 210 |   1050 | PgSQLPgSQLPgSQLPgSQLPgSQLPg
  5 | 237 |   1185 | PgSQLPgSQLPgSQLPgSQLPgSQLPg
(5 rows)

将其与之前的存储生成列的方式进行比较:

-- 添加存储生成列
alter table test add column s_text text generated always as ( repeat('PgSQL', n) ) stored;

此时表的大小发生了很大变化:

-- 查看表大小
select pg_total_relation_size('test'::regclass);
 pg_total_relation_size 
------------------------
               11788288
(1 row)

评价

在某些情况下,这非常有用(至少可以使查询更简单一些)。此外,添加虚拟列非常快:只需修改元信息,表数据保持不变,因此无需重写表。而对于存储生成列,如果要向现有表中添加列,则必须重写表,以便实际存储新列的值。

不幸的是,它并非没有限制。提交信息中列出了这些限制,这里只提一点:无法在虚拟列上建立索引。如果可以的话,这对于全文搜索(tsearch)的索引来说将是一个非常棒的解决方案。通常人们会在某个列中构建tsvector,然后对其建立索引。如果我们能在虚拟列上建立索引,就可以使虚拟列的定义任意复杂,并在其上添加gist/gin索引,从而得到一个很棒的索引,而无需在每次搜索时重新输入复杂的表达式,也无需在表中 “浪费” 磁盘空间来存储tsvector数据。

希望这些缺失的功能能够尽快添加,因为这将极大地扩展虚拟列的可用性。但即便没有这些功能,这个新功能也很不错,希望您能尽快在自己的项目中使用到它。

非常感谢社区所有的相关人员。

参考

提交日志:https://git.postgresql.org/pg/commitdiff/83ea6c54025bea67bcd4949a6d58d3fc11c3e21b