由 John Doe 三月 21, 2025
在其他的关系数据库中,经常会用到虚拟计算列,其值是自动从其他列计算出来的,且不占用磁盘空间。现在,PostgreSQL 也可以了。
特性提交日志
虚拟生成列
这增加了一种新的生成列形态,它在读取时进行计算(类似于常规的视图,它与现有的存储生成列不同,存储生成列在写入时进行计算,类似于物化视图)。
列定义的语法是:
... GENERATED ALWAYS AS (...) VIRTUAL
VIRTUAL
也是可选的。为了与其他各种关系数据库保持一致,默认使用VIRTUAL
而不是STORED
。SQL 标准并没有规定这种行为,而且它也没有涉及VIRTUAL
或STORED
。另外,我们默认使用的视图也是虚拟的,而不是物化的。
虚拟生成列在元组中存储为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