DDD教你破解数据库表设计难题
上周有位工作五年的朋友,好不容易得到一个面试机会。面试时被面试官问了个很常见的问题:“电商系统,用户可以维护多个收货地址,下单时选一个。订单表和地址表你怎么设计?"

他很自信地说:”按数据库三范式啊,设计订单表和地址表两张表,主外键关联呗。“
面试官又接着问:”那查订单详情是不是要关联查询?他说对啊。
接下来面试官又继续问:“如果用户下单后修改了地址信息,历史订单的地址会不会变?"
这时候他愣住了,想了想说:“那...把地址字段冗余到订单表?”
没想到面试官根本没给他犹豫的机会,又接着说:“好,那用户信息、商品信息、商户信息这些是不是也一样要冗余?那订单表得要有多少个字段?”他彻底懵了...
你看,这就是凭经验做设计的问题。平时开发,你可以参考老代码,可以问问同事。但面试的时候呢?临时想方案,总是漏洞百出。为什么?因为你脑子里没有一套稳定的方法论。这种方法论,虽然比较虚,但是越是重要的工作就越需要方法论。因为,开发团队里重要的决定,都是要让整个团队理解的。你要只是当一个程序员,那你自己写好打码就行了。但是如果你真要当架构师,那么如何说服团队理解并支持你的设计,很多时候比设计本身更重要。
但更要命的是,这种既要经验又要方法论的东西,网上几乎没人能讲明白。如果没真正做过复杂项目,那么他们总结的所谓'经验',其实就是背答案。遇到变种题,立马露馅。
今天这个视频我将来点不一样的。我会用DDD——领域驱动设计,这个在大厂和复杂项目中被广泛验证的方法论,带你看看真正的架构师是怎么思考这个问题的。更重要的是,掌握一套可以应对各种业务场景设计的思维框架。
【经典三范式设计】
"咱们先快速回顾一下数据库三范式,这是数据库设计中的一种规范标准,旨在提高数据存储和使用的性能。具体来说,
第一范式要求每一列都是不可分割的原子值
- 比如地址不能存成'北京市朝阳区xxx'一整个字符串,要拆成省、市、区
第二范式要求每一列都完全依赖于主键
- 订单表里不能出现商品的库存信息,因为库存依赖商品ID,不依赖订单ID
第三范式要求每一列都直接依赖主键,不能间接依赖
- 订单表里有用户ID,就不应该再冗余用户的注册时间,因为注册时间依赖用户ID
好,理论清楚了。按三范式,标准设计应该是这样:

看起来很规范对吧?一个最明显的问题就是数据一致性问题
数据一致性问题:用户下单时地址是'北京市朝阳区',过了一个月他把这个地址改成了'上海市浦东新区'。你再查历史订单,发现当时寄到北京的订单,现在显示的地址是上海!用户投诉,你怎么解释?
所以很多人会说,那我冗余呗。把地址信息直接冗余到订单表里。

但新问题又来了:
- 如果地址的字段结构发生变化了怎么办?
- 订单表里冗余了地址信息,那用户信息、商品信息、商户信息等等,要不要也冗余?
如果全冗余,订单表会变成一个五六十个字段的巨无霸,维护起来就是灾难。那到底哪些该冗余,哪些不该冗余?这就是90%的开发都答不好的地方——因为他们靠的是感觉,而不是方法论。"
【DDD方法论登场】
这时候,DDD领域驱动设计就能给我们一个非常清晰的答案。
DDD是什么?它是Eric Evans在2004年提出的一套软件设计方法论,核心思想是:让软件设计贴近业务领域,用业务语言来指导技术实现。听起来有点抽象?没关系,咱们直接看它怎么解决表设计问题。
在DDD中,对象有两种不同的设计,实体和值对象。
先来看实体(Entity)
"在DDD中,实体是指具有唯一标识和独立生命周期的领域对象。
实体有独立的业务含义。
实体有完整的生命周期,需要唯一的ID进行标识。
实体需要被独立管理和追踪。
比如:用户、商品、商户。这些对象的信息都可以单独进行维护。并且都有很明显的,独立的业务含义。这就是很明显的实体。
然后是值对象(Value Object)
值对象是指那些没有独立含义,只是描述实体的某个属性特征的对象。
值对象的意义完全依附于某个实体。实体没了,值对象也就没意义了。
值对象一旦创建,就不应该改变。
值对象不需要独立的ID。
比如:订单的收货地址。不管他形式上可以有多少个字段但是本质上,他只是一个和订单的总支付金额一样的属性。描述”这个订单要寄到哪里“。他就是一个很明显的值对象。
关键洞察:领域边界决定对象身份
"这里有个非常重要的认知:同一个东西,在不同的业务领域中,身份是不一样的。
什么叫业务领域?简单说,就是不同的业务场景或业务模块。
拿地址来说:
在用户中心领域,地址就应该是一个实体。因为
第一、它有完整的增删改查的生命周期。用户可以随时调整自己的收获地址。
第二、它有独立的业务含义。用户可以维护多个收获地址。同一个收获地址也可以被多个用户使用。
第三、它的变化需要被追踪。用户改了地址信息,下单页面可选择的地址信息也及时更新。
所以在用户中心领域,地址就应该被设计成一个单独的有ID的表。
在订单领域,地址却是一个值对象。因为
第一、它只是订单的一个属性
第二、它没有生命周期。下单那一刻就固化了,不会也不应该随用户中心的地址变化而变化
第三、它描述的是'当时下单时的收货地址'。如果订单没了,送货地址也就没意义了。
所以在订单领域,地址就应该直接冗余到订单表中。
你看,同样是地址,在不同领域中的设计完全不同。这就是DDD的精髓。不要孤立地看对象,要看它在具体业务领域中扮演的角色。"
【实战设计方案】
"有了DDD的理论指导,表设计就有章可循了:
设计原则
- 实体之间用ID关联:保持引用关系,需要最新数据时再查询
- 值对象直接冗余:把所有属性字段冗余到实体表中
- 关键快照适当冗余:某些实体属性如果需要保留历史状态,也可以冗余
所以,对于之前面试问到的下单场景,当中,订单是实体,地址是值对象,那么地址信息可以很直接的冗余到订单表当中。像这样

甚至,你可以更大胆一点。地址信息不管他是个什么形式,最终他就是一个和订单总价格一样的值而已。那就干脆用一个字段,直接保存JSON格式的地址信息,也无不可。

甚至这种设计在某些特殊场景,还能带来一些意料之外的好处。比如地址的结构不稳定时。这个你可以看看淘宝的下单页面,他的地址信息的结构就是不一样的。选择不同的地区,如“中国大陆”或者“中国台湾”或者“美国”,地址信息的相关字段都是不同的。
比如这是淘宝上中国大陆的地址信息

这是淘宝上中国台湾的地址信息

这是淘宝上美国的地址信息

用一个JSON来存地址,连冗余的字段都不需要改了。
当然,有了这样的表结构之后,你就要想好,如何在整个项目中保持设计的一致性。比如地址,在订单领域里有一个值对象与之对应;而到了用户中心领域,又需要有一个实体与之对应。如何保持这些不同的对象设计都是一致的,不至于产生混乱,那就需要DDD一整个理论体系出马了。
如果你觉得这次分享对你有帮助,想要了解更多我对于DDD的整个思考以及实战的经验,可以点赞,收藏。后面我会继续拆解DDD的核心思想。
【方法论总结 】
所以你看,数据库表设计不是死记硬背三范式,也不是凭感觉随便冗余。有了DDD这套方法论,你就有了一个稳定的思维框架:
第一步:识别业务领域
- 这个表是在哪个业务场景下使用的?
第二步:区分实体和值对象
- 这个对象在当前领域中,有独立生命周期吗?
第三步:应用设计原则
- 实体用ID关联,值对象直接冗余
这套方法不仅适用于订单和地址,也适用于任何复杂的表设计场景。
最后,如果你都看到这里了,那么我必须告诉你一句你在其他视频里都听不到的话。
这次分享的东西,其实只是DDD的冰山一角。甚至整个DDD理论体系,也只是架构师众多秘密武器中的一个。
这些具体的技术,包括现在这个环境下,越来越重要的面试八股,都只是用来解决一些具体问题的“术”。而在这些术的背后,是不管什么时候,都能帮你走得更远的“道”。术是学不完的,但是哪怕现在整个IT环境已经如此艰难,但是程序员依然是现在最能赚钱的职业,这靠的就是道。
但这些不是一两个视频,尤其是这种三五分钟的短视频可以讲明白的。如果你感兴趣,我们再继续交流。