PostgreSQL ADD COLUMN NOT NULL DEFAULT now():这条 ALTER 为什么不该直接过审

2 阅读6分钟

上周有条变更单,SQL 就一行:

ALTER TABLE users ADD COLUMN created_at timestamptz NOT NULL DEFAULT now();

开发说加一列、有默认值、旧行不会变 NULL。工单过了,风格检查过了,有人已经约了业务低峰执行。

我拦了。不是 SQL 写错,是这条在 PostgreSQL 上可能 rewrite 整张表。大表上就是长时间锁、WAL 暴涨、磁盘再吃一份表大小。看起来像加列。跑起来像重建表。

为什么贵

PostgreSQL 加列不都便宜。

加可空列、没有 volatile 默认值,很多版本只改目录,几乎不碰堆表。

NOT NULL,并且 DEFAULTnow()clock_timestamp() 这种每行求值都不同的表达式,就没法只在目录里记一个常量。引擎可能扫全表、给每一行物化默认值,过程中拿很重的锁。relfilenode 会变。社区把这叫 table rewrite。墨天轮上也有人按版本列过哪些 DDL 会 rewrite。结论很土:别把它当普通加列。

人审如果只看语法和命名,这条很容易过。

工单和 linter 为啥放行

工单会盯权限、备份、窗口。不一定有人记得 DEFAULT now()DEFAULT 0 在 PostgreSQL 上不是一类操作。

风格检查管缩进、关键字、标识符。now() 对 linter 就是个函数调用。过了不等于评估了锁。

gh-ost、pt-osc 这类在线 DDL 解决的是怎么改,不是该不该这样改。DeltaScope 自己也写了:它不执行 SQL,只在执行前看文本和策略。跟在线 DDL 是互补。

还有一种漏:变更拆成好几条「很小」的语句。MySQL 上同一张表连续两条 ALTER TABLE,工单按条过,文件级的两次重建没人看。

离线跑一下会打哪条

DeltaScope 是离线优先的 SQL 审核,支持 MySQL / TiDB / PostgreSQL。默认不连库。只解析 SQL、套策略,给出 blocker / warning / notice,再聚合成 reject / review / pass。当前发布是 v0.490.0。规则目录跟 v0.480.0 一样:371 条,blocker 72、warning 142、notice 157。不执行你提交的 SQL,也不跑 EXPLAIN ANALYZE

发行说明里有这条命令:

deltascope audit \
  --dialect postgresql \
  --sql "ALTER TABLE users ADD COLUMN created_at timestamptz NOT NULL DEFAULT now()" \
  --format json

会打到:

项目
规则 IDddl.pg.alter.add_column.non_null_default.rewrite.warn
默认级别warning
附加事实not_nullhas_defaultdefault_kind(例如 function_call
默认结论review,不是 reject

默认 --fail-onblocker。warning 只把 verdict 打成 review,退出码还是 0。CI 里要拦住这类加列,得显式 --fail-on warning。改某条规则的级别时,enabledparams 要一起写。只写 level 会整条替换默认策略,规则可能被关掉。用 deltascope config status <rule-id> 看生效值,别猜。

我不会把这条默认设成 blocker。表很小、低峰 rewrite 一次,有人能接受。几个亿的表,同一行 SQL 就是事故。工具把风险标出来,窗口和回滚还是人定。

看规则正文:

deltascope rules explain ddl.pg.alter.add_column.non_null_default.rewrite.warn

同类我还会看

DEFAULT now() 不是孤例。下面这些默认也都是 warning。

CHECK 不加 NOT VALID。一次校验加 ACCESS EXCLUSIVE,大表等于锁表扫描。

deltascope audit \
  --dialect postgresql \
  --sql "ALTER TABLE orders ADD CONSTRAINT amount_positive CHECK (amount >= 0);"

文档里的 JSON 是:

{
  "rule_id": "ddl.pg.alter.add_check.not_valid.require",
  "level": "warning",
  "message": "ADD CHECK constraint should use NOT VALID to avoid full table scan with ACCESS EXCLUSIVE lock"
}

更稳的拆法:先 ADD CONSTRAINT … NOT VALID,再 VALIDATE CONSTRAINT。从 v0.42.0 起,只有 NOT VALID、后面没有对应 VALIDATE,还会再打 ddl.pg.alter.not_valid_constraint.validate.require

CREATE INDEX 不带 CONCURRENTLY。默认建索引会挡住读写。规则是 ddl.pg.create_index.concurrently.require

MySQL 同一张表拆多条 ALTER。首页和示例配置里的 ID 是 ddl.alter.merge.mysql.require,能力矩阵写成 ddl.alter.merge.mysql。行为一样:多条 ALTER TABLE 打同一张表,建议合成一条。跨文件检测不到,必须放进同一次 deltascope audit

这些都不是 DROP TABLE,也不是 SQL 写错。就是合法但贵。

对照:没有 WHERE 的 DELETE

ALTER 之外,DML 有条几乎都会认的红线。README 的例子:

deltascope audit --sql "delete from users"

文档里的输出:

Verdict: reject
Statements: 1
Blockers: 1
Warnings: 0
Notices: 0

Statement 1: DELETE
- [blocker] dml.where.require: UPDATE and DELETE statements must include a WHERE clause

UPDATE / DELETELIMIT、带子查询,也有对应规则。带 WHERE 的删除,离线只按 SQL 形状估行数。连上库、用只读统计(PostgreSQL 上会用 planner 的 EXPLAIN,不是 EXPLAIN ANALYZE)能收得更紧。阈值规则是 dml.impact.rows.max_countdml.impact.ratio.max_percent

我把 ALTER 放前面,是因为没 WHERE 的 DELETE 太好认,工单里本来就难混。DEFAULT now() 这种才容易被点过。

离线够用,什么时候连库

默认离线。笔记本、CI、agent 会话都不必先塞数据库账号。

连库是可选项。列在不在、索引在不在、InnoDB 索引键长超没超,离线会被跳过,不会假装过了。README 里的连库例子:

deltascope audit \
  --sql "alter table orders add index idx_status (status)" \
  --host 127.0.0.1 --port 3306 --user root --ask-password --schema app

PostgreSQL 连库要显式 --dialect postgresql,不会自动侦测。命令行不再接受 --password,改用 --ask-password--password-env--password-file

离线回答这句话本身犯不犯规。连库回答对这台实例、这张表现在合不合适。别混成「必须连生产才能审」。

方言默认是 mysql。SQL 长得像 PostgreSQL、却没加 --dialect postgresql,会打 dialect.postgresql.syntax.detected.notice,不会自动切方言。审 PG 迁移却看到一堆 MySQL 规则,先查方言。

从 v0.43.0 起,默认策略按方言隔离:--dialect postgresql 不再报缺 UNSIGNED / ENGINE;MySQL / TiDB 也不会冒出 ddl.pg.*

安装和放进流水线

v0.490.0 覆盖 macOS / Linux 的 amd64 和 arm64。archive 里有 deltascopedeltascope-serverdeltascope-mcp。README 没有 Windows 安装入口。

macOS:

brew tap Fanduzi/deltascope
brew install --cask deltascope

通用:

curl -fsSL https://raw.githubusercontent.com/Fanduzi/DeltaScope/main/install.sh | sh

钉死版本:

curl -fsSL https://raw.githubusercontent.com/Fanduzi/DeltaScope/v0.490.0/install.sh | \
  DELTASCOPE_VERSION=v0.490.0 sh

审文件:

deltascope audit --file ./migrations/20260328_add_column.sql

我通常把策略文件放进仓库,审核步放在 migrate / flyway 之前。默认 --fail-on blocker,要对 warning 失败再收紧。机器读 JSON / SARIF / GitLab Code Quality,人看 markdown。文档还写了 --format github-actions--format github-summary--format gitlab-codequality

deltascope audit --file ./migrations.sql --format json --fail-on warning

HTTP 是同一套引擎:POST /v1/audit。请求体里不能塞账号密码,只能引用服务端 runtime config 的 connection_id。中英文 README 的启动写法不一致,这里不抄启动命令。

它不是工单平台,没有审批流。也不是 schema diff,审的是你提交的 SQL。更不是执行后的审计日志。

给 agent 的 MCP

人审和 CI 先跑起来,再考虑 MCP。官方 MCP 四个工具:audit_sql、describe_rule、list_rules、get_capabilities。没有 Query Access。需要 Node.js 24 或更高,只支持 macOS 与 Linux(amd64 / arm64)。

[mcp_servers.deltascope]
command = "npx"
args = ["-y", "@fanduzi/deltascope-mcp"]
startup_timeout_sec = 20

已经装过 installer 或 Homebrew 的,可以直接跑本地 deltascope-mcp。审核契约和 CLI 相同:同一套规则 ID,同一套 verdict。

我不会用它做的事

  • 不执行 SQL,不替代 gh-ost / pt-osc。
  • 不替代人审窗口、备份、回滚。warning 默认不失败。
  • 不是完整 PostgreSQL DDL 覆盖。生成列、部分 identity、部分 parse 失败会明确说没审。「没报错」不是「没问题」。
  • Query Access 是另一条能力,不是这次说的变更审核,也不在 MCP 工具集里。

项目:github.com/Fanduzi/Del… 站点:deltascope.pages.dev/ 许可证:Apache 2.0

小结

那一行 SQL 语法没问题,工单也干净。危险是因为在 PostgreSQL 上可能 rewrite 整张表。

DeltaScope 离线会标 ddl.pg.alter.add_column.non_null_default.rewrite.warn。默认 warning,结论 review。要变成门禁,把这条升到 blocker,或者 CI 用 --fail-on warning

我自己用得很窄:本地先过变更文件,CI 用同一份 deltascope.yaml 再过一遍,连库只留给「列在不在、索引在不在」。MCP 有需要再挂。规则 ID 和级别摆到桌面上,过不过还是 DBA 签字。