PostgreSQL 18 升级:半夜才会发现的三个变化
大版本升级的坑通常不在新特性,而在那些“没人说要改,但行为变了”的地方。PostgreSQL 18 里有三处变化,白天跑测试不容易暴露,凌晨跑维护窗口时却会撞上。
一、VACUUM / ANALYZE 默认递归到继承子表
以前对父表执行 VACUUM,只处理父表本身;对分区表来说,谁也不会想到它会一路扫到所有子分区。18 起,VACUUM 和 ANALYZE 会处理父表的继承子表。原本夜里跑 VACUUM parent_table、几秒收工的脚本,现在可能把整个分区树扫一遍,时间和 I/O 都会差一个量级。
要恢复旧行为,使用新加的 ONLY 选项:
VACUUM (ONLY) parent_table;
ANALYZE (ONLY) parent_table;
ONLY 也可以和其他选项并列,例如 VACUUM (VERBOSE, ONLY) parent_table;。注意 ONLY 只对继承/分区结构有意义,普通表的写法不受影响。
是否加 ONLY 取决于脚本原本的意图。如果维护脚本只想回收父表上的空间、子分区由各自任务处理,就该加 ONLY;如果原本就是希望顺带清理所有子分区,只是从前靠手工循环,那么不加反而简化了脚本——代价是要重新估算窗口时长和锁范围,尤其是分区多、单分区大的表。
二、initdb 默认开启 data checksums,pg_upgrade 要求两边一致
18 的 initdb 默认开启数据校验和,此前默认关闭。pg_upgrade 要求新旧集群的 checksum 设置匹配,所以从没有校验和的集群升级时,用默认参数建出来的新集群会因设置不一致而被 pg_upgrade 直接拒绝,升级第一步就失败。
建新集群时要显式关掉:
initdb -D /new/data --no-data-checksums
这个开关在新建集群这一步,pg_upgrade 本身只做一致性检查。升级前用 SHOW data_checksums; 在两个集群上各确认一次,比看日志快。
反过来,如果旧集群本来就开了校验和,就直接用 initdb 默认值,不要再加这个参数,否则同样会不匹配。
校验和默认开启是长期方向,能更早发现存储层静默损坏。但从关闭切到开启,必须走“重新初始化 + 逻辑/物理迁移”的路径,不能靠 pg_upgrade 顺手带上;在大库上,这是另一件事,不适合塞进同一个维护窗口。
三、AFTER 触发器按事件入队时的角色执行
AFTER 触发器以前在执行时(通常是 COMMIT 时)取当前活跃角色,18 改为按事件入队那一刻的角色执行。会话中途切过角色的话,结果会不同。
典型场景是:一个操作里先 SET ROLE app_writer 写数据,再切回更高权限的角色去提交;旧版本下触发器函数里的 current_user 是提交时的角色,新版本下是入队时的 app_writer。审计、行级权限校验、SECURITY DEFINER 之外依赖当前身份判断的触发器逻辑,都要按新语义复核一遍。
如果应用连接池从不切角色,这条基本无感;有 SET ROLE / SET SESSION AUTHORIZATION 混用的系统,建议单独写一个事务来验证:入队前切一次角色,提交前再切回,确认触发器里看到的是哪一个。
另外三处容易漏掉的
禁止 unlogged 分区表。 以前 ALTER TABLE ... SET [UN]LOGGED 对分区表不起作用,建 unlogged 分区表也不会把子表变成 unlogged。18 起这类结构被明确禁止,升级前先查一遍:
SELECT n.nspname, c.relname, c.relpersistence
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relpersistence = 'u' AND c.relkind IN ('r', 'p', 'I');
对遗留的 unlogged 表,按用途决定 ALTER TABLE ... SET LOGGED 还是拆掉分区结构,不能指望它继续以“名字叫 unlogged、实际不是”的状态存在。
COPY FROM 读 CSV 不再把单独一行的 \. 当文件结束符。 psql 从 STDIN 读 CSV 时仍视为结束符。问题出在旧版 psql 连 18 服务器:客户端按老规则把 \. 当作结束发送,服务端不再认,\copy 可能提前截断或报错。升级后统一用 18 的 psql 客户端做批量导入,别让旧客户端留在运维机上。
时区缩写优先取会话时区。 现在先看当前会话的时区缩写,再查 timezone_abbreviations,此前相反。同名缩写在不同时区里含义不同的场景,解析结果会变。用 pg_timezone_abbrevs 核对一下业务里实际用到的缩写,比等报表时间偏一小时再回头查要省事。
这三条影响的是“脚本能不能跑通”和“导入导出是否完整”,不涉及数据格式,但都会在无人值守的批处理里安静地出错。
升级窗口内的执行顺序
先确认旧集群 SHOW data_checksums;,据此决定新集群的 initdb 参数;建好新集群后跑 pg_upgrade --check,把 checksum 不一致这类问题在停机前暴露出来。然后在旧集群上扫一遍 unlogged 表和 VACUUM 脚本里的父表调用,把要加 ONLY 的位置改掉。触发器与角色相关的逻辑无法静态检查,用一个显式切角色的事务在测试库上跑一遍最直接。最后把客户端 psql 一并升到 18,避免 \copy 在导入环节出问题。