让 AI 设计数据库表,为什么上线三个月就改不动了
数据截至 2026-07,各产品的额度与报错口径以官方最新说明为准。
**多数团队把”AI 设计的表不好用”归因成模型能力不行,其实错因在交接点:你给它的是一段业务描述,它还你的是一份能跑通的 DDL,中间那层——不变量是什么、哪些字段会变、这张表半年后要长成什么样——从来没人写下来,模型也就没法凭空补上。**能跑通和能长期改,是两个验收标准。AI 稳定地满足前者,后者需要你把判断说清楚。
这篇讲的是设计环节本身。站内已有的 数据库迁移不可逆 讲的是改坏之后怎么止损,AI 代码评审工具 讲的是用什么工具兜住产出质量;本篇夹在两者中间,讲的是 DDL 还没合并进主干、你还有机会用极低成本改掉它的那段时间该干什么。
一、先划清这个环节的能力边界
把建表这件事拆成四层,你会发现 AI 的成功率是断崖式分布的。
**第一层,语法与样板。**给定字段清单生成 DDL、写出对应的 ORM 模型、补上迁移脚本的上下行、生成一份带注释的字段说明——这层可以全交出去,你只做通读。模型对主流数据库的语法差异掌握得相当稳,跨库改写也基本可靠。
**第二层,常规范式与命名。**拆一对多、抽出关联表、统一时间字段命名、决定用自增还是 UUID——这层 AI 给的方案通常是教科书答案,能用,但它会默认你要的是通用解。它不知道你这张表每天写入量的量级,也不知道哪个字段是运营每天要按它筛选的。你需要把这些补进去再让它改。
**第三层,不变量与约束。**哪两个字段的组合必须唯一、哪条外键必须级联删除而哪条必须禁止删除、金额字段允不允许为负、状态字段的合法取值集合是什么——这层是本篇的重点,也是漏得最多的地方。原因不难理解:这些信息不在你给它的业务描述里,模型只能猜,而它猜的时候倾向于宽松,因为宽松的约束不会让示例数据插不进去。
**第四层,演进路径。**这张表半年后会加什么、哪个字段大概率要拆成独立实体、分库分表的键预留在哪——这层没法外包。它依赖的是你对业务方向的判断,不是任何模型能从当前需求推出来的。
一个可操作的分界:**能从你给出的文字里推导出来的,交给 AI;需要从业务未来、线上流量、团队习惯里取的,必须你来定。**判断不清时,就把这个字段的疑问明确写出来让它列选项和代价,而不是让它直接拍板。
二、现象到成因的判别表
线上出问题的时候,先别急着加索引。同一个现象背后可能是完全不同的成因,处置动作也相反。
| 现象 | 大概率成因 | 怎么验证 | 处置动作 |
|---|---|---|---|
| 出现语义上不该存在的重复行 | 缺唯一约束,业务侧靠”先查后插”防重,并发下失效 | 按业务键分组统计出现次数大于 1 的行;查代码里是否有 select 后 insert 的模式 | 先清洗重复数据,再加唯一索引;写入侧改成依赖数据库唯一约束的插入冲突处理 |
| 删了主表,子表变成孤儿数据 | 外键约束缺失,或建了外键但删除行为选了不校验 | 用左连接反查子表:以子表外键列关联主表主键,取主表侧为空的行 | 补外键,删除行为按业务显式选级联或禁止;存量孤儿数据单独归档 |
| 状态字段出现从没定义过的值 | 状态用了自由文本或宽整型,没有取值约束 | 对状态字段做去重统计,看有没有超出预期的值 | 加检查约束或枚举,加完必须用一条越界值插入验证它真的生效(部分数据库的老版本会接受检查约束的语法但不实际执行);同时查上游哪个写入路径漏了校验 |
| 单表查询突然变慢,数据量涨了一截 | 缺索引,或索引字段顺序与查询条件不匹配 | 用数据库自带的执行计划命令看是否全表扫描 | 按实际高频查询补复合索引,注意最左匹配顺序 |
| 加了索引但没变快 | 查询条件对字段做了函数运算或隐式类型转换,索引失效 | 对比执行计划中索引是否被选中 | 改查询而不是继续加索引;字段类型与参数类型对齐 |
| 写入变慢,读没变化 | 索引建太多,每次写入都要维护 | 统计该表索引数量与各索引使用情况 | 删掉长期零命中的索引,合并前缀重叠的索引 |
| 金额或数量算出诡异的小数 | 用了浮点类型存钱 | 查字段类型定义 | 改成定点数类型。注意两件事:已经写进去的值精度已经丢了,转换不会把它变回来,要单独对账;这类改动通常要重写整表数据,大表上按停机窗口或影子列双写来做,别就地一条 alter 了事 |
| 中文写进去变问号或截断 | 字符集与排序规则不匹配 | 查库、表、连接三层的字符集设置 | 三层对齐后重建表,历史数据需要转码回填 |
后两行的具体处置细节,站内的 浮点精度问题 和 数据库字符集乱码 讲得更细,这里不重复。
判别表的用法是从上往下过一遍,而不是直接跳到最像的那行。约束类问题和索引类问题会互相伪装:唯一约束缺失导致的重复行,会让统计查询变慢,你以为是索引问题,加了索引之后查询确实快了一点,但重复数据继续在长。
三、把三件事补回去:约束、索引、演进
约束:让数据库替你兜底
写下这张表的不变量,一句一条,越具体越好。比如”同一个用户在同一个活动里只能有一条报名记录""订单金额不能为负""订单必须属于一个存在的用户”。这几句话本身就是唯一索引、检查约束、外键的直接对应物。
把这些句子给 AI,让它翻译成约束语句,这一步它做得很好。**关键在于句子是你写的。**你写不出来的不变量,任何工具都补不上。
验收动作:对每一条不变量,写一条一定会失败的插入语句,跑一遍,确认数据库拒绝它。这比读 DDL 可靠得多——读的时候你只会确认”哦这里有个 unique”,跑的时候你才知道它到底约束的是哪几个字段的组合。
存量表补约束前必须先做数据清洗,否则加约束的语句会直接失败。顺序是:查违反行 → 决定怎么处理这些行 → 处理 → 加约束。中间任何一步都别省。
索引:从查询倒推,不是从字段正推
AI 建索引的常见做法是给每个看起来会被查的字段单独建一个。这个策略在读多写少的小表上没什么坏处,规模上来之后就是纯负担。
正确的顺序是先收集真实查询。上线前用你自己写的接口清单倒推,上线后用慢查询日志。把高频查询的 where 条件、排序字段、连接字段列出来,再让 AI 按最左匹配原则设计复合索引。你给的输入从”这张表有哪些字段”换成”这张表要承受哪些查询”,产出的质量会完全不同。
验收动作:对每条高频查询跑执行计划,确认走了预期的索引,且扫描行数与返回行数在一个量级上。点查和小范围查询扫一万行只返回十行,说明索引选得不对;聚合与大范围统计是例外,那类查询本来就要扫过一大片数据,看的是扫描量有没有被条件收窄到预期区间。
还有一个容易被跳过的检查:执行计划要在接近生产的数据量上跑。空表或几百行的表上,优化器往往直接选全表扫描,因为那样确实更快,你会误判成”索引没生效”。同理,索引刚建完统计信息可能还没更新,看到走错索引先手动更新一次统计信息再看。
演进:预留改动的余地,而不是预留字段
预留一堆 ext1、ext2 空字段是上一代的做法,现在没必要。更有用的是三件事:
一是给可能独立出去的实体留出干净的边界。如果你判断”收货地址”半年内会从订单表里拆成独立表,那现在就别把省市区街道摊在订单表里,先按一个整体存。
二是给所有表加上创建时间和更新时间,并且保证更新时间由数据库或统一的写入层维护。数据出问题时,这两个字段决定了你能不能定位到是哪一批写入引入的。
三是软删除还是硬删除,全库统一一个策略。混着来的代价是每个查询都要记得带上过滤条件,漏一次就是已删数据重新出现在页面上,或者统计口径虚高。这类 bug 的特点是不报错、上线很久才被业务方发现。
四、什么情况下别再折腾
排查这类问题最大的浪费是在错误的路径上加班。给几个明确的止损信号。
**信号一:同一张表的迁移脚本改了三轮还是跑不通。**这时候问题多半不在脚本,而在这张表的设计本身有内在矛盾——比如你想加的唯一约束和现有业务逻辑允许的数据状态是冲突的。停下来重新看不变量,而不是继续调脚本。
**信号二:存量数据里违反约束的行超过总量的一个显著比例。**具体阈值看业务,但如果清洗工作量已经超过重建表的工作量,那就走新表 + 双写 + 切换的路线,别在原表上打补丁。
**信号三:改动开始外溢到你没预期的模块。**本来只想给一张表加约束,结果发现三个服务都在往里写、其中一个还是别的团队维护的。这时候立刻停,把范围重新框一遍。AI 辅助改动特别容易在这里失控,站内 AI 改动范围失控 那篇讲的场景在数据库变更里放大得更厉害,因为数据库的错误是不可逆的。
**回滚点怎么留。**任何结构变更前,至少要有这三样:变更前的表结构导出、变更影响行数的预估、以及一条能把结构改回去的语句。前两样十分钟能做完,第三样对加索引、加字段这类操作是现成的,对删列、改类型这类操作则要提前说清楚”回不去”——回不去的操作就不该在没有数据备份的情况下执行。
# 结构快照(MySQL 写法;PostgreSQL 换成 pg_dump --schema-only)
# 变更前跑一次,产物进版本库
mysqldump --no-data -u USER -p DBNAME > schema_before.sql
# 变更后再跑一次,用 diff 确认改动范围与预期一致
mysqldump --no-data -u USER -p DBNAME > schema_after.sql
diff schema_before.sql schema_after.sql
导出结果里会带上自增计数器一类每次都在变的值,diff 时会形成噪声,看的时候跳过这类行,只盯字段、类型、约束、索引四类差异。这一步花不了两分钟,但它是唯一能机械发现”改动范围超预期”的手段——靠人回忆改了哪些表是不可靠的。
这个 diff 的价值在于它能抓住 AI 顺手改掉的东西。让模型改一张表的时候,它有时会同时调整相邻表的字段类型或默认值——不是恶意,是它认为那样更一致。你不 diff 就发现不了。
**什么时候该换条路。**如果你发现自己在反复解释业务规则、模型每次都能生成语法正确但语义偏移的方案,那说明这个问题的复杂度已经超过了自然语言描述能承载的量。换成先自己画一张实体关系图、把关键约束标在图上,再让 AI 从图生成 DDL。输入的精度决定输出的精度,这一点在数据建模上比在写业务代码时明显得多。这也是 规格驱动开发 的思路在建模环节的应用。
五、避坑清单
**坑一:把示例数据能插进去当成设计通过。**为什么会踩——AI 生成 DDL 时通常会附几条示例插入语句,跑通了让人产生完成感。怎么避——把验收从”能插进去”改成”该失败的必须失败”,为每条不变量准备一条反例插入。
**坑二:直接采纳生成的默认值。**为什么会踩——默认值藏在 DDL 中间,通读时容易滑过去,而模型倾向于给每个字段配一个”安全”的默认值。怎么避——把可空性和默认值单独拉一张表出来逐行确认。字段该不该为空,是业务问题不是技术问题。
**坑三:用生成的假数据做性能验证。**为什么会踩——AI 造的测试数据分布过于均匀,索引选择性看起来完美,上线后真实数据的倾斜暴露一切。怎么避——性能验证用脱敏后的真实数据分布,至少要保证高基数字段和低基数字段的区分度贴近实际。造数据混进生产的连带风险,站内 模拟数据进入生产 有专门一篇。
**坑四:迁移脚本只写了上行没写下行。**为什么会踩——模型默认输出的是”怎么改成新样子”,回滚不在它的任务描述里。怎么避——把下行脚本写进同一次改动的验收项,并且在预发环境实际跑一次上行加下行的往返。
**坑五:外键的删除行为一律选级联。**为什么会踩——级联删除让示例跑得最顺,不会因为约束报错卡住。怎么避——逐条问”主记录删了,这些子记录到底该消失还是该阻止删除”,多数业务场景下阻止删除才是对的,真正需要级联的比你想的少。
**坑六:一次变更同时改结构和改数据。**为什么会踩——AI 生成的迁移脚本习惯把加列、回填、加非空约束写在一个事务里,看上去简洁。怎么避——拆成三次独立发布:先加可空列,再后台回填,确认无残留后再加非空约束。合在一起的那版在大表上会长时间持锁。
**坑七:把索引设计交给自动建议工具后不再复核。**为什么会踩——建议工具只看单条查询的局部最优,不看整表的写入代价。怎么避——每次加索引前先看现有索引清单,能靠调整已有复合索引字段顺序解决的,就不要新建。
**坑八:跨库改写时相信类型是一一对应的。**为什么会踩——模型做跨数据库 DDL 转换时语法层面几乎不出错,但时间类型的时区语义、字符串的比较规则、自增的并发行为在不同产品间差别很大。怎么避——对时间、字符串比较、自增这三类字段单独做行为验证,别只看类型名对上了。
收束
这个环节的分工其实相当清楚:AI 负责把你的判断快速变成正确的语法,你负责提供它推不出来的那部分判断。团队里出问题的往往是没人意识到后半句也是活儿,以为把需求描述丢进去就该出一份能用十年的设计。
合并 DDL 前过一遍这张清单:
- 这张表的不变量我能用几句话说清楚吗,每一句都有对应的约束吗
- 每条约束我跑过反例验证吗,确认了它拒绝该拒绝的数据吗
- 索引是从真实查询倒推的,还是从字段清单正推的
- 每条高频查询的执行计划我看过吗,扫描行数合理吗
- 金额用的是定点数吗,时间字段的时区语义确定吗
- 迁移脚本的下行写了吗,在预发跑过一次往返吗
- 结构 diff 我看过吗,有没有模型顺手改掉的相邻表
- 这次改动的影响范围有没有超出我最初框定的模块
八条里有一条答不上来,就先别合。数据库这层的错误,修复成本比其他任何一层都高一个数量级。