新服务器上,数据初始化脚本第一次实跑,自检输出里有六项。
五项通过。唯一没过的那一项,恰好是我最看重的,站点配置表的条数。我预期 33 条,实跑出来 24 条。
表数量 33 期望 33 √
v2_site_config 24 期望 >=50 ×
v2_page_config 3 √
v2_grade_config 6 √
v2_site_nav 3 √
v2_admin_user 1 √
表数量 33 一次通过,这件事本身是个好消息。项目 SQL 头部写着 -- 引擎: PostgreSQL 15+。服务器上是 18.6-alpine,跨了 3 个大版本。我原本做好了「某个函数或扩展行为有差异」的心理准备,结果一条都没报错。
所以真正的问题只剩一个。24 到底对不对。
那 33 和 50 是怎么来的
脚本需要一个放行判据,我就去翻 SQL。项目一共 6 个脚本,我做了静态统计,数的是 INSERT。
数完发现,我此前在四份文档里写下的三个数字,全错了。
表总数我说 25 张,实际是 33 张。25 只是 init.sql 的数量。v2.4.0_admin.sql 另建 6 张,v2.6.0_marketing_page.sql 另建 2 张。
v2_site_config 我说 33 条,实际插入 56 条。init 32 条,seed 11 条,营销页脚本 4 条,另一个版本化增量脚本 9 条,UPSERT 去重后略少。
第三个数最离谱。我把一张配置表列成了检查项,那张表根本不存在。那个脚本只往 v2_site_config 做 UPSERT,再 UPDATE 一张导航表,它不建表。跑起来只会报错,说关系不存在。
前两个错误暴露的是同一个坏习惯。我在给 56 这个数拍脑袋打折。
56 个 INSERT 元组,UPSERT 会去重,我以为实际应该少一些。少多少,我没有依据。于是我凭感觉先拍了 33,后来又拍了「≥50」。
两个数字,两次都是猜的。而猜出来的东西,被我当成了判据。
后果很直接。如果我坚持 25 张表,一个完全正确的 33 会被我判成失败。而 ≥50 这个下限,会让一个完全正确的 24 永远无法放行。
追到 seed.sql 的开头
实跑结果摆在眼前,表数量 33 通过,配置表 24 没过。
我当时第一反应是,是不是哪个脚本没跑完。于是按顺序追了一遍,一直追到 seed.sql 的开头。
第 2 到第 9 行,是 8 条 DELETE。
sql
DELETE FROM v2_product_reward_config;
DELETE FROM v2_product;
DELETE FROM v2_user_balance;
DELETE FROM v2_withdraw_account;
DELETE FROM v2_invite_relation;
DELETE FROM v2_user;
DELETE FROM v2_site_config; ← init.sql 插的 32 条,在这里被清掉
DELETE FROM v2_site_nav;
我数 INSERT 的时候,眼睛是从 INSERT 这个词开始看的。这 8 行,我一个字都没看见。
它不是普通的种子脚本,它是清空重建
把执行链拼起来是这样。init.sql 往 v2_site_config 插入 32 条。seed.sql 先 DELETE 清空,再插入 11 条。v2.6.0_marketing_page.sql 加 4 条。另一个版本化增量脚本加 9 条。最终 24 条。
现在做一道小学减法。
sql
静态统计的 INSERT 总数 56
减去被 DELETE 清掉的 init 部分 -32
──────────────────────────────────
实跑真值 24
一减就对上了。24 从来不是需要被解释的异常值,它就是这套脚本唯一可能的答案。
留下的那句话是:静态统计 SQL 时,必须连 DELETE、TRUNCATE 一起看,否则算出来的预期值根本不成立。
看漏它的代价,不止是判据写错。这 8 条 DELETE 引出了这次部署最硬的一条红线。
seed.sql 会清空 8 张业务表,站点配置、站点导航、用户、商品都在里面。它是清空重建式的种子脚本,只允许在空库初始化时执行一次。在有真实数据的库上重跑,会直接删掉线上用户、商品与账户数据。
脚本里虽然有「库非空时需输入 YES」的二次确认,但那只是最后一道防线。更该记住的是这条铁律本身。
固化成判据
我把这次的教训压成一组动作,做 SQL 统计时照着做。
先数删操作,再数写操作。用 grep -nE '^\s*(DELETE|TRUNCATE|DROP)\b' *.sql 数一遍。不然得出的预期值会虚高。按脚本逐个统计,别只算总和。用 for f in *.sql; do echo -n " f";grep−cE′\s∗INSERT\s+INTO′"f"; done,不然看不出是哪个脚本清的表。
UPSERT 不等于新增,带 ON CONFLICT 的语句会覆盖,条数不增,把它当成 56 就直接错了。检查项里的表名必须来自 CREATE TABLE。用 grep -hoE 'CREATE TABLE \^( *' *.sql | sort -u 反查一遍。不然会把不存在的表写进检查清单。
预期值必须由实跑校准,不能靠推算,做法是空库跑一遍取实测值。判据本身要写死,并且注明推导链,不然下次又有人顺手改一下。
我最后定稿的判据长这样,每条都带上了来源。
csharp
表数量 33 ← init 25 + admin 6 + 营销页 2
v2_site_config 24 ← seed DELETE 清空后重建:11 + 4 + 9
v2_page_config >=3
v2_grade_config >=6
v2_site_nav >=3
v2_admin_user >=1
还有一个统计命令,能把建表、写数据、清数据一次分开。
bash
grep -nE '^\s*(CREATE TABLE|INSERT INTO|DELETE FROM|TRUNCATE|DROP TABLE)\b' *.sql
看这五类语句的相对位置,比看任何一类的数量都重要。
判据是我算出来的,而算就可能错。所以还需要一个不依赖我判断的机制,导入时带上 ON_ERROR_STOP。
bash
psql -v ON_ERROR_STOP=1 -f init.sql
它的作用是,第一条报错的语句立刻停下并打印原文,而不是被静默跳过。没有它,一条失败的语句只会让最后的计数看起来不对,于是我又回到猜的阶段。
判据告诉你结果对不对,ON_ERROR_STOP=1 保证结果没有被偷偷改写。两者不是替代关系。
我给自己留的提醒只有一句。下次再算出某个预期值,先去数一遍 DELETE。