上周有条变更单,SQL 就一行:
ALTER TABLE users ADD COLUMN created_at timestamptz NOT NULL DEFAULT now();
开发说加一列、有默认值、旧行不会变 NULL。工单过了,风格检查过了,有人已经约了业务低峰执行。
我拦了。不是 SQL 写错,是这条在 PostgreSQL 上可能 rewrite 整张表。大表上就是长时间锁、WAL 暴涨、磁盘再吃一份表大小。看起来像加列。跑起来像重建表。
为什么贵
PostgreSQL 加列不都便宜。
加可空列、没有 volatile 默认值,很多版本只改目录,几乎不碰堆表。
加 NOT NULL,并且 DEFAULT 是 now()、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
会打到:
| 项目 | 值 |
|---|---|
| 规则 ID | ddl.pg.alter.add_column.non_null_default.rewrite.warn |
| 默认级别 | warning |
| 附加事实 | not_null、has_default、default_kind(例如 function_call) |
| 默认结论 | review,不是 reject |
默认 --fail-on 是 blocker。warning 只把 verdict 打成 review,退出码还是 0。CI 里要拦住这类加列,得显式 --fail-on warning。改某条规则的级别时,enabled 和 params 要一起写。只写 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 / DELETE 带 LIMIT、带子查询,也有对应规则。带 WHERE 的删除,离线只按 SQL 形状估行数。连上库、用只读统计(PostgreSQL 上会用 planner 的 EXPLAIN,不是 EXPLAIN ANALYZE)能收得更紧。阈值规则是 dml.impact.rows.max_count 和 dml.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 里有 deltascope、deltascope-server、deltascope-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 签字。