AI agent为email列加唯一约束的SQL正确,但Postgres会持ACCESS EXCLUSIVE锁扫描全表,导致API超时、连接池耗尽,真实案例警示数据库权限和锁机制的重要性。
你见过这类新闻了。Cursor 的一个 agent 在大约九秒内抹掉了一家公司的生产数据库,连备份都没放过。Replit 的 agent 炸掉了另一家公司的生产环境。每次都是一样的套路:agent 信心满满,SQL 语法正确,没人及时踩一脚说"等等"。
这些都是闹得大的失败。解决办法大家都知道,也很无聊:不要给 agent 生产环境的写权限,默认只读,提议而不是直接执行,必须有人把关。
但还有一种更隐蔽的情况,权限策略防不住,而你的 agent 很可能现在就在做这件事。它在 diff 里看起来完全没问题。
让 agent 给 email 列加唯一约束,它会写成:
ALTER TABLE users ADD CONSTRAINT users_email_unique UNIQUE (email);
SQL 正确,执行结果正是你要的。它能顺利通过 review,因为看不出任何问题。然后当 users 表有一定数据量时,它会获取 ACCESS EXCLUSIVE 锁并扫描每一行来构建唯一索引,在整个扫描期间,表上其他任何读写操作都无法进行。API 开始超时,连接池打满。就这样,一个所有人都在 review 时放行的一行迁移,引发了一次事故。
这个 agent 做的事,一个还算合格的初级工程师也会做出来。这才是陷阱所在。危险的不是 SQL 本身,而是 SQL 拿到的锁,阅读语句本身是看不到锁的。你必须对 Postgres 锁机制非常熟悉才能知道:哪些 DDL 会获取哪种锁、持有多久、持有期间会阻塞什么。凌晨两点你还是会漏掉它。
我受够了漏掉它们。于是我实测了一个。
我用真实 Postgres 18 跑了同样的 schema 变更,两种方式。五千万行数据,二十个连接在做普通查询。不安全的方式是一个普通的 SET NOT NULL,它同样在 ACCESS EXCLUSIVE 下扫描。安全的方式是用 NOT VALID 再 VALIDATE 的组合,把扫描过程放到一个更温和的锁下进行。
不安全的方式持有排他锁 2.2 秒,二十个连接全部堆在锁队列后面。工作负载的 p99 达到了 2028 毫秒。安全的方式:排他锁只持有 3 毫秒,队列里没有堆积,p99 为 0.57 毫秒。同样的变更,schema 最终状态一样,但在到达终态的过程中,对其他人的影响相差约 3500 倍。
跟踪记录和复现脚本都在仓库里,感兴趣可以自己跑。不要纠结于具体数字,它会随硬件变化。重点是:"正确的 SQL"和"安全的迁移"是两回事,它们之间的差距就是故障发生的地方。
如果锁是阅读语句时看不到的东西,那你需要一种能在 agent 和 DDL 之间读取锁的东西。
所以这是我做的。MigrationPilot 用 libpg-query 解析迁移——这是 Postgres 实际的解析器( Postgres 本身用的同一个 C 库,而不是一堆正则表达式)——算出每条语句获取的锁,然后对照 112 条规则检查已知的大坑。有 CLI、GitHub Action,还有针对这个问题的 MCP server。
这里关键工具是 check_before_apply。这是一个通过/失败的关卡,agent 在写入或运行任何 DDL 之前调用它,给出和 CI 一样的判定结果,因为它读取的是同样的配置。在 Claude Code 里,PreToolUse hook 把它们串起来,当检查失败时直接阻断调用,所以 agent 根本上不可能把那个有问题的迁移写入文件或交给 runner 执行。它故意设计为失败时开放通过。如果检查因某种原因无法运行,调用会带着一条备注放行,因为一条动不动就卡住你工作流的护栏,到周五你就会拆掉它。
来看看之前那个 ADD CONSTRAINT 会返回什么:
✗ [MP027] CRITICAL
Adding a UNIQUE constraint scans the whole table under ACCESS EXCLUSIVE.
Create the index concurrently first, then attach it:
CREATE UNIQUE INDEX CONCURRENTLY users_email_unique_idx ON users (email);
ALTER TABLE users ADD CONSTRAINT users_email_unique UNIQUE USING INDEX users_email_unique_idx;
不是"这个看起来有风险"。它指出了锁的名称、为什么会有危害,还给出了能做同样工作又不会引发故障的 SQL。agent 拿到这个后会重试,这回它自己的建议能通过检查了。第一次可没通过——我不得不修了工具,让它自己的建议不再触发自己的规则。有点尴尬。
这是静态分析。不连接数据库,它能知道语句获取的锁,但不能知道你的表有多大或者有多繁忙,所以它会说"这会持有排他锁",而不是"这会耗时 40 分钟"。传入 --database-url 它会读取表大小和查询统计来做出更精准的判断。那是只读访问 catalog,是免费的。这里没有任何付费墙。它只看它看到的 SQL,所以如果你的框架渲染了模板或者从应用代码里跑 DDL,把渲染后的输出给它。另外自带的解析器目前说的是 Postgres 17 语法,所以一些全新的 PG18 专有语法形式还解析不了。
仓库里也有基准测试。56 条带标签的迁移,针对 Squawk 和 pgfence 打过分,语料库和具体命令都包含在内,还列出了什么都没catch到的大坑,包括我自己的。我更愿意告诉你它薄弱的地方,而不是假装它无懈可击。
npx migrationpilot analyze migration.sql
无需安装,无需账号,发现严重问题会非零退出,所以可以直接塞进 CI。要给 agent 接线?MCP server 是 npx migrationpilot-mcp。
Agent 会继续写迁移,说实话这也没问题。SQL 部分它们很擅长。只是不擅长知道哪条语句会锁表。我们大多数人也一样。给它们一个擅长的东西。