当谈到 Postgres 18 时,异步 I/O、UUID v7 和升级后统计数据(post-upgrade statistics)可能会成为焦点。不过,我个人最喜欢的即将发布的功能是虚拟生成列 。
"Post-upgrade statistics" 在 PostgreSQL 18(Postgres 18)中指的就是 升级之后,优化器(planner)所使用的数据分布统计信息(optimizer statistics) 。这个特性可以让在使用
pg_upgrade
或pg_dump/pg_restore
升级数据库时,保留这些统计信息 ,从而避免升级后必须重新运行ANALYZE
来生成统计数据所带来的延迟和性能损失。(CYBERTEC PostgreSQL | Services & Support)
背景说明
Optimizer statistics(优化器统计信息) :这些信息记录表的列的数值分布、最常见值、直方图边界、Null 比例、平均宽度等等,存储在系统 catalog 表(如
pg_statistic
)中。它们用于帮助查询规划器估算查询成本和返回行数,从而生成高效的执行计划。(CYBERTEC PostgreSQL | Services & Support)在 PostgreSQL 18 之前,使用
pg_upgrade
升级数据库后 ,不会自动保留这些统计信息。结果通常需在升级后执行ANALYZE
,这可能对于大规模数据库花费大量时间,导致升级完成后的查询性能一段时间不理想。(CYBERTEC PostgreSQL | Services & Support)
PostgreSQL 18 中的新行为
PostgreSQL 18 引入了一个关键改进:
pg_upgrade
将保留大部分优化器统计信息 ,包括 per-table 和 per-column 统计数据。这样升级后,数据库能迅速恢复之前的查询性能,无需等待大规模统计重建。(CYBERTEC PostgreSQL | Services & Support)注意 :扩展统计(extended statistics,如通过
CREATE STATISTICS
创建的,用于列组合或表达式的统计)目前仍不被保留 ,升级后仍需手动ANALYZE
相关表。可以使用如下命令快速补全缺失统计:
bashvacuumdb --all --analyze-in-stages --missing-stats-only
或者:
bashvacuumdb --all --analyze-only --missing-stats-only
来仅分析那些未传递过来的统计信息。(CYBERTEC PostgreSQL | Services & Support)
为什么这是个重点功能?
节省时间:升级完成后数据库立即可用于生产,减少停机时间。
提升性能稳定性 :查询优化器无需等
ANALYZE
,即可拿到正确的统计数据生成高效执行计划。简化运维流程 :避免了升级后手动长时间运行
ANALYZE
,尤其在数据量极大的环境中效果显著。
总结
"post-upgrade statistics" 就是 PostgreSQL 18 引入的功能,用于 在 major upgrade(如通过
pg_upgrade
升级版本)后保留 optimizer statistics 的机制。它大幅提升升级效率和性能稳定性,减少升级完成后因缺乏统计信息带来的性能问题。需要注意扩展统计并不被保留,仍需使用vacuumdb --missing-stats-only
等命令进行补充分析。(CYBERTEC PostgreSQL | Services & Support)
生成列是那些可以免去编写大量样板代码、简化工作的功能之一。您无需在视图、查询或触发器之间重复相同的表达式,而是可以让数据库为您管理它们。它们将逻辑封装在数据附近,减少重复,并使模式更易于推理。
生成的列有两种形式: 存储列 (stored)和虚拟列(virtual) 。
存储(stored)生成列在插入/更新时仅计算一次,并持久化到磁盘(占用存储空间)。之后,它们可以像"真实"列一样被索引。对于不想每次都重新计算的昂贵表达式,它们非常适用。
虚拟(virtual)生成列(PG 18 中的新功能!)不存储在磁盘上。它们在查询时动态计算。它们非常适合轻量级表达式或不希望表膨胀的情况。
考虑以下示例:
sql
create table users (
id serial primary key,
name text not null,
-- stored generated column: persists value on disk, indexable
name_upper text
generated always as (upper(name)) stored,
-- virtual generated column: computed only when queried
name_lower text
generated always as (lower(name)) virtual
);
insert into users (name) values ('Florents'), ('Αθηνά'), ('postgres');
select id, name, name_upper, name_lower from users;
id | name | name_upper | name_lower
----+----------+------------+------------
1 | Florents | FLORENTS | florents
2 | Αθηνά | ΑΘΗΝΆ | αθηνά
3 | postgres | POSTGRES | postgres
这里 name_upper
被存储并写入磁盘,但 name_lower
是虚拟的并且是动态计算的。
生成的列很有趣,因为它们模仿了反应式编程(reactive programming)的风格,这种风格使得 Microsoft Excel 成为有史以来最受欢迎、最高效、最经久不衰的软件之一。你定义一个源列(自变量),一些生成的列(因变量)就是该源列的函数。
总的来说,我发现它们是一种非常有用的工具,因为它们将一些精力从数据库维护中解放出来,而这些精力可以投入到适当的数据库设计和规范化中。
一个实际的例子是全文搜索。假设你想要支持跨多种语言或文本配置的搜索。你可以存储三个不同的生成列,每个列使用不同的文本搜索配置:
sql
create table docs (
id serial primary key,
body text not null,
body_fts_simple tsvector
generated always as (to_tsvector('simple', body)) stored,
body_fts_en tsvector
generated always as (to_tsvector('english', body)) stored,
body_fts_el tsvector
generated always as (to_tsvector('greek', body)) stored
);
现在您可以在每个 tsvector 列上创建索引并根据用户的语言进行查询:
sql
create index on docs using gin (body_fts_simple);
create index on docs using gin (body_fts_en);
create index on docs using gin (body_fts_el);
这样,您可以避免触发器,保持模式声明性,并确保您的 FTS 列始终与原列一致。
当然,人们可以绕过存储数据并在表达式上创建索引,但我发现查看单个token和分数对于调试搜索结果和预处理步骤来说太有必要了。
对源列的后续更新会导致对生成的列进行重新计算,然后根据函数表达式进行更新。
虽然也能用触发器实现,但在系统规模变大时,要维持数据库一致性和可维护性会让人抓狂。
生成的列还允许您像乐高积木一样逐列构建表。在实践中,我见过生成列在 Postgres 中被用作"展平" JSON 文档的一种非常流行的方法。请注意,存储的生成列自 PG 12 版本开始可用。从那时起,Postgres 的 JSON 基础架构中添加了许多新功能:从 jsonpath 表达式到更复杂 的JSON_TABLE
函数。
当此类 JSON 功能不可用时,或者人们不愿意学习高级 jsonpath 而宁愿坚持使用充满 jsonb -> 'k1' -> 'k2'
的简单操作时,生成的列是一种非常方便的替代方案。
使用存储的生成列虽然很方便,但需要额外的存储空间,因为 JSON 文档可能非常冗长。对于只在 SELECT
子句中投影的元素,这种冗余的存储方式并不会带来太大的帮助。另一方面,虚拟生成列允许您将这些 JSON 字段公开为常规列,而无需在磁盘上重复数据。您仍然可以获得更清晰的查询界面,但存储开销为零。
您可以阅读 讨论原始补丁的帖子 请向 Peter Eisentraut 了解有关内部运作的更多详细信息。
一个重要的权衡是性能。存储列(Stored columns)会增加一些写入开销,因为每次插入或更新都必须重新计算并持久化值。这样做的好处是读取速度更快 ,并且可以直接利用索引。虚拟列(Virtual columns)则相反:它们写入成本低,但让计算工作在查询时完成。可以将存储列视为物化列 ,将虚拟列视为 按需计算 。
另一个实际考虑是模式演进。向大型表添加虚拟列 是即时的,因为不会将任何内容写入磁盘。然而,添加存储列需要回填值,并且可能触发全表重写。这使得虚拟列在您想要快速进行实验而不锁定生产表时尤其具有吸引力。
生成的列有几个相当明显的限制,对生成列(generated columns)的限制 之所以存在,是为了防止你的数据库因为各种事件触发、依赖链、级联更新等而变得混乱不堪 (如果生成列没有这些限制,你可能会写出互相依赖、无限循环触发的列定义或更新逻辑,让数据库像意大利面一样缠绕不清。)。它们都列在文档中。
动态求值还会带来一些微妙的安全隐患, 本主题对此进行了讨论。如果您的数据库严重依赖自定义函数或用户定义类型,则值得仔细阅读。虚拟列可能会引发一些令人意外的行为,如果没有适当的规划,尝试引入它们可能会导致一些相当严重的问题。