先别急着改表结构。把现有数据分成三类:必须保留原值的、可以迁移重算的、可以冻结归档的。然后为扩展选一条路径——加附属表、加键值扩展表,或重建主表并做双写过渡。判断依据不是“哪个更先进”,而是旧数据能否停机、读写方能否同步改造、回滚窗口有多长。
很多“字段不够用”其实是同一列承担了多种语义。例如一个 status 列同时表示订单阶段、支付状态和退款状态,新增业务时自然无处安放。此时加字段只会让问题更乱。
可区分的原因有三类:
只有第二、三类才值得动结构。第一类应先拆语义,否则扩展后仍会再次不够用。
当新增数据与主实体是一对多关系时,新建一张带外键的附属表,旧表完全不动。适用条件是:读取方可以接受多一次关联查询,且旧接口不必返回新字段。代价是查询变复杂,列表页容易出现 N+1 问题。
用 (entity_id, field_key, field_value) 结构存扩展属性。适用条件是:新增字段多为可选、低频读取,且团队能接受弱类型带来的校验成本。不适合需要范围查询、排序或强约束的字段,否则会退化成在应用层做数据库该做的事。
新建目标表,写入时同时写旧表和新表,读流量逐步切到新表,稳定后再停旧写。适用条件是:能接受一段时间的存储翻倍,且所有写入方都在自己控制范围内。若存在无法改造的第三方写入,这条路径基本不可行。
假设你手里有一张 article 表,原本只有标题、正文、发布时间。上线后要支持多作者署名、多分类和自定义字段。按下面顺序处理:
article_author;自定义字段是零散需求,走键值扩展表;标题正文保持不变。其中第 3 步的结果直接决定下一步:如果副本预演出现行数差异,就不要进入灰度双写,先修数据。这个顺序不能颠倒。
空值和默认值的语义。新增可空列后,“未填写”和“明确为空”往往被混为一谈。若业务需要区分,就要额外加标记列,而不是靠空值推断。
索引与查询模式。键值扩展表上的查询如果按值过滤,通常无法有效走索引。先列出真实的查询条件,再决定是否值得为某个扩展字段单独建列。
旧数据的回填成本。回填不是一次性动作,而是要在写入路径切换后仍能补齐历史行。若历史数据量大,应分批回填并记录进度,避免长时间锁表。
如果新增需求只影响展示层,例如同一份数据换一种排版,那么改模板比改表更合适。判断标准是:新需求是否需要被检索、被排序或被其他系统引用。只需要显示的内容,放在应用层的映射表里即可,不必进入数据库。
扩展字段设计的核心不是一次设计到位,而是让下一次扩展的成本可控。选择哪条路径,取决于旧数据能否停机、写入方能否同步改造、以及你愿意为回滚保留多长的窗口。