周五上午9点15分。Françoise端着印有自己头像的马克杯——圣诞礼物——坐在三屏工位前。左边是用了15年的Excel考勤表,右边是Sage财务系统,中间是上线三周的Rembrandt。

她转过来问我:"Michel,开学报名多少人?"

打开网易新闻 查看精彩图片

我敲下SELECT COUNT(*) FROM inscriptions WHERE statut='inscrit',报出数字。她记在Excel里,逐行手工核对。45秒后,隔壁传来她的声音:"等等,咱俩得谈谈,对不上。"

问题出在Céline身上:9月报名,一次签约、一次分期付款,选了7门课。但我的inscriptions表里有7行记录——Céline被算成了7个人。

这不是bug。这是表名与真实业务对象脱节的瞬间。一个法国陶艺培训网络的ERP重构故事,关于命名如何绑架思维,以及如何用三条SQL让数据重新说真话。

【正方】表名即契约:改名或拆分才是正道

经典方案摆在桌面。当inscriptions实际存储的是"课程席位"(places)而非"报名行为"时,三条路清晰可见。

选项A:维持现状

成本最低,风险最高。Françoise的Excel成为事实上的主数据源,数据库降级为辅助工具。每次统计都需要人工二次校验,"技术债"以人力利息的形式持续偿还。

审计显示:4月11-12日核对Google Sheet与Supabase数据库,81个多课程席位在Rembrandt中"失踪",60个联系人游离于课程之外。现状的代价是明确的。

选项B:重命名

将inscriptions改为places,语义与数据对齐。所有查询、报表、API文档同步更新。一次性工程量大,但消除歧义。

支持者认为:命名是软件设计中最难的事,一旦错配,每个读代码的人都会被误导。长痛不如短痛。

选项C:拆分表结构

引入contracts表存储签约行为,places表存储具体席位。一对多关系显式化,Céline的1次报名对应7条place记录,逻辑清晰。

这是教科书式的规范化(normalisation)路径。第三范式(3NF)的拥趸会为此鼓掌:消除冗余,保证引用完整性。

【反方】改名是奢侈的暴力:schema即历史文档

反对声音来自生产环境的冷酷现实。

Rembrandt已运行三周。Françoise的Excel公式、财务部门的Sage对接脚本、面向家长的邮件通知系统——全部建立在现有schema之上。重命名意味着协调六处站点、数百名学员的并行系统迁移。

更深层的问题:inscriptions这个名字并非"错误",而是历史快照

系统上线初期,业务确实是一对一:一个学员、一门课、一条记录。多课程需求是后期演化而来。表名记录了业务模型的版本史,强行抹除等于销毁考古层。

「Michel, combien on a d'inscrits pour la rentrée, dis-moi ?」——Françoise的问题本身揭示了关键:她问的是"报名数",而非"席位数"。业务语言与数据模型之间存在天然张力,这不是技术能单方面裁决的。

拆分表的代价同样沉重。contracts与places的关联引入JOIN复杂度,报表查询从单表扫描变为多表导航。对于非技术背景的运营人员,理解成本陡增。

【判断】第四种方案:不变量即文档

作者选择了更隐蔽的路径——保留schema,显式声明约束

核心洞察:表名是约定(convention),而非契约(contract)。真正可靠的是数据库约束(constraint)和应用层不变量(invariant)。

三条SQL重构了查询语义,无需改动表名或结构:

第一:区分"报名人"与"席位"

原查询:SELECT COUNT(*) FROM inscriptions WHERE statut='inscrit' → 返回7(Céline被计7次)

修正:SELECT COUNT(DISTINCT contact_id) FROM inscriptions WHERE statut='inscrit' → 返回1

关键认知:COUNT(*)回答"多少条记录",COUNT(DISTINCT contact_id)回答"多少独立个体"。业务问题决定技术选择。

第二:显式处理多课程场景

新增视图或封装查询:按contact_id聚合,SUM(cours_count)得总席位数,COUNT(DISTINCT contact_id)得总人数。一份数据,两种口径,各自标注用途。

第三:约束即文档

保留原有的UNIQUE(contact_id, cours_id)索引,但添加注释说明其业务含义:"防止同一学员在同一课程重复报名,不限制跨课程报名"。

应用层封装核心操作:enroll_student(contact_id, cours_list) 自动处理1→N的展开,hide_complexity_from_francoise() 确保她的三屏工位不再需要心算核对。

为什么这是更好的工程决策

对比三种经典方案,第四种路径的优势在于风险可控的渐进性

重命名或拆分属于"大爆炸式"重构,需要停服窗口、回归测试、跨部门协调。对于六站点、数百学员的陶艺网络,这意味着周末加班和电话支持热线。

不变量方案则是代码层面的修正:查询重写、视图封装、API适配。Françoise的Excel可以继续指向同一个数据库端点,只是返回的数字突然对了。

更深层的价值:承认命名即权力

inscriptions这个名字承载了创始阶段的业务假设。强行改名是对历史决策的否定,而保留并注释是对演化过程的尊重。好的系统架构不是"正确设计"的结果,而是"持续适应"的能力。

作者的原话值得刻在任何数据库设计者的工位上:

「Ce n'est pas une anecdote. C'est le moment où j'ai compris que le nom de ma table et l'objet qu'elle stockait avaient cessé de se correspondre. Et cette divergence m'a appris quelque chose sur ce que c'est, modéliser un métier.」

(这不是轶事。这是我理解到表名与存储对象已然脱节的时刻。这种错位教会我:建模一门生意,究竟意味着什么。)

可迁移的方法论

这个故事的通用性远超陶艺ERP。任何业务系统的schema漂移都遵循相似模式:

1. 识别语义债务:当业务人员开始用Excel"纠错"时,就是模型与语言分离的信号。Françoise的45秒手工核对是免费的诊断工具。

2. 区分命名与约束:表名服务于人,约束服务于机器。人可以适应歧义,机器需要明确规则。让注释承担解释责任,而非追求字面完美。

3. 封装而非暴露:底层schema的复杂性应当被API层消化。Françoise问"多少人报名",系统应当理解她需要的是DISTINCT COUNT,而非原始行数。

4. 审计即治理:4月的两日审计发现了81+60处数据不一致。没有这次人工核对,问题会持续累积。定期对账是技术债的利息支付。

最终,Rembrandt的inscriptions表保留了原名。但每个查询它的开发者都会看到注释:「此表存储席位(places),非报名行为。统计人数用DISTINCT contact_id。」

Françoise的马克杯还在。三屏工位还在。只是那个45秒的核对环节,终于可以从她的早晨 routine 里删除了。

而Céline,这位热爱陶艺、时间充裕、选了七门课的学员,在系统里依然是7行记录——但只要查询写对,她就只是1个人。数字终于说真话了,尽管表名仍在撒谎。