新服务器上的数据库是 1panel 面板装的,不是我手动起的。
装完查了一下版本,18.6-alpine。
项目 init.sql 的第一行写着:
sql
-- 引擎: PostgreSQL 15+
本地开发环境一直是 postgres:15-alpine。从 15 到 18.6,中间跨了 3 个大版本。
跨版本迁移最怕的事只有一件,老 SQL 在新库上悄悄跑不通。
那时候面前有两条路。一条是把用到的语法逐项对着新版本核一遍,核完没问题就导入。另一条是让导入过程本身当判决书。
我当时挑了第一条路,但真正放行的依据是第二条。
这个项目不是几张 CRUD 表
它是个深度 PG 项目。做静态统计的时候数出来的东西是这样的。
TIMESTAMPTZ 出现 58 次,GIN 索引 47 次。BIGSERIAL 和 SERIAL 一共 16 次,JSONB 14 次。CREATE EXTENSION 里 pgcrypto 和 uuid-ossp 共 2 次。LANGUAGE plpgsql 函数 1 次。
这些语法换到别的数据库,要么不支持,要么退化。JSONB 会退化成普通 JSON,索引能力直接丢掉。
逐项对着 PG 18 核了一遍,结论是全部兼容。序列类型还在,JSONB 还在,带时区的时间戳还在,GIN 索引还在。那两个扩展属于官方镜像自带的 contrib,plpgsql 函数也照旧。
得说一下这次迁移的硬前提。服务器上原来那个数据库容器是一个 MySQL。
那是同机另一个正在跑的线上项目,不能动,也根本不是同一种数据库。所以只能在面板里新建一个独立的 PostgreSQL 应用,没有第二条路。
光查清单为什么不够
版本跳跃不能靠一句理论兼容就放过去。
清单只能证明没有已知的不兼容,证明不了没有未知的不兼容。你核的永远是自己想得起来的那几项。真正出事的,往往是你没想起来的默认行为。
跨大版本踩的坑,多半不是语法不支持,而是行为变了。语法不支持会当场报错,行为变化不会。DDL 照样执行成功,等业务逻辑跑起来才出错。这类问题,看语法清单一个都看不出来。
还有一条更实在的。
我对自己做的静态统计已经不信任了。就在这次迁移里,我数错过两次。
一次是表总数。我算的是 25 张,实际是 33 张。只数了 init.sql,漏了另外两个脚本各自新建的 6 张和 2 张。
一次是某配置表的行数。我算的是 56 条,实际是 24 条。漏看了 seed.sql 开头那组 DELETE FROM,那是个清空重建式的种子脚本。
我自己连数数都会数错两回,凭什么相信那份兼容清单。
让执行器在第一现场叫停
判决书就是这一行参数。
bash
docker exec -i app-postgres psql -U <用户> -d <库名> -v ON_ERROR_STOP=1 < init.sql
ON_ERROR_STOP 是 psql 的一个变量,-v 就是设变量。设成 1 之后,含义是任何一条语句报错就立刻中止,打印出错语句原文,并以非 0 退出码结束。
加与不加,差别很大。
不加的时候,遇到错误只打印一行警告,然后继续往下跑。退出码是 0,看起来像成功了。交到你手上的,是一个好像成功了的表象。要是 1671 行 SQL 断在第 248 行,你不会知道。
加了之后,立刻停在当前语句,退出码非 0,你会看到第 248 行。
这不是理论推演。
同一次环境调整涉及的另一个项目就栽在这上面。六个 SQL 脚本按顺序导入,语法全对,照样断在第 248 行。最要命的是,终端最后打印出来的是命令提示符,不是错误。
没人会想到导入其实失败了。问题被一路带到运行时,以业务报错的形式重新出现。
所以「靠什么证明」的答案是:不靠我核对了 6 类语法,靠的是那个会在第一现场叫停的执行器。
清单的作用是让你知道该担心什么。ON_ERROR_STOP=1 的作用是让担心变成可观测的事实。
实测结果
6 个脚本,1671 行 SQL,按顺序导入,顺序本身不能乱。导完再跑结构校验。
表数量 33 期望 33 ✅
v2_site_config 24 期望 24 ✅
v2_page_config 3 ✅
v2_grade_config 6 ✅
v2_site_nav 3 ✅
v2_admin_user 1 ✅
33 张表一次通过。PG 18.6 和这份标注 15+ 的 SQL 完全兼容,版本跳跃的风险实测不存在。
这句话的重量在于,它和我前面那句「逐项核对兼容」不是一回事。核对兼容是我说的,一次通过是机器跑出来的。
顺带一个反例,又把这件事印证了一遍。
上面那张表里 v2_site_config = 24,最初被我判成了失败。我写的期望值是 50。追下去才发现 seed.sql 开头有一组 DELETE,它会先清空再重建。是我的判据错了,不是数据错了。
一个静态推导出来的预期值,把一个完全正确的结果判成了失败。
Redis 那半边
同一个问题在数据层的另一半上也存在。项目本地用 redis:7-alpine,面板给的是 8.10.1。
这边判断简单得多,因为先把「我到底用了什么」这个范围缩到了最小。项目只用了六个命令,SET、SETNX、GET、DEL、EXPIRE、INCR,用途是登录授权码防重放和余额分布式锁。Redis 8 全部兼容。
真正要留意的是另一件事。Redis 8 的默认持久化配置,和项目原 compose 里写的 --appendonly yes 不一样。用 AOF 会占磁盘,只用 RDB 则重启可能丢少量 key。对防重放和短期锁这类短生命周期数据来说,可以接受。结论是先按默认配置跑,观察后再决定。
这里的方法和 PG 那半边是一样的。先列清楚「我用了什么」,再谈「新版兼不兼容」。一个只用了六个命令的项目,和一个「反正装了个 Redis」的项目,面对同一个版本跳跃,风险完全不是一回事。