做数据库设计这些年,我最大的感受是:绝大多数系统后续的糟糕性能、难维护、不敢改表,并不是某一行的SQL写得有问题,而是从建表那一刻起,设计原则就没立住。很多初学数据库的同学问“数据库设计原则到底是什么”,网上一搜全是“三大范式、主键、索引”这类词条,但真正在面对一个订单、一套权限、一份日志时,还是不知道从哪里下手。这篇文章我会抛开教科书,用实际项目的思路,把数据库设计背后的取舍、常用原则和落地步骤一次讲清楚。不管你是刚入行的后端开发,还是需要自己维护数据结构的全栈工程师,照着这个思路去设计表,至少能避开八九成的坑。
1. 从一个糟心的历史系统讲起:没有原则会付出什么代价
1.1 曾经的“万能用户表”是怎么把项目拖垮的
很多人觉得数据库设计不过是建几张表、定几个字段的事,可一旦遇到烂设计,后期改起来真是想死的心都有。我之前接手过一个内部管理系统,它的用户表叫t_user_info,里面什么都有:姓名、手机、身份证、公司、部门、职位、入职时间、紧急联系人、紧急联系人电话、银行卡号、社保账号、工资、绩效备注……二十多个字段全塞在一张表里。
刚开始业务跑得挺好,后来要支持“一个用户可能属于多个部门”,前端做了多选,开发直接在表里加了一个字段叫department_ids,用逗号分隔。接着要按部门筛选人员,SQL里就出现了FIND_IN_SET(dept_id, department_ids)这种写法,数据量一上万,页面直接卡死。
更难受的是,业务侧要把用户分为“在职、离职、外包、实习生”四种状态,结果原设计用一个status字段,后来状态之间出现组合关系,比如“外包-离职”到底算离职还是外包?没人说得清。修Bug的人怕动老表,就在应用层用一堆if else判断。结果每个接口都背着沉重的历史包袱,加一个新功能要改六个地方。
这个系统的核心问题,不在于某一张表太宽,而在于设计时没有遵守“单一职责”和“最小冗余”这两条基础原则。数据库设计原则从来不是为了像考试一样满足范式,而是为了避免今天的一次随意设计,变成未来每一天都在偿还的技术债。
1.2 数据库设计原则的本质是管理复杂度和变化
我后来重新梳理这个系统时,把它拆成了用户主表、部门表、用户部门关联表、员工扩展资料表、状态变更记录表,每一项数据都有了明确的归属。改动用户部门关系时,只需要操作关联表;查询某部门下的员工,只需要一次join;状态变化通过独立的状态表和时间字段记录,永远能回溯。
这才体会到:所谓数据库设计原则,本质上是在回答三个问题:数据如何存放才不会重复?数据如何变更才不会出错?数据如何查询才不会低效?大部分设计原则都是围绕这三个问题的平衡。教科书里的范式、约束、索引,都是平衡的手段,而不是目的。
所以在设计任何表之前,我会逼自己先不说“用什么字段”,而是先写清楚这个表要承载什么业务对象,这个对象有哪些状态,状态之间怎么流转,谁在什么操作下修改这些状态。把这些理清,再来谈建表,思路会截然不同。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 范式设计:懂规则更要懂什么时候打破规则
2.1 三大范式的实际含义与判断方法
聊数据库设计原则,绕不开范式。但很多人的范式是背出来的,一到实际场景就判断不了。我在工作中会用一套特别朴素的判断方法:
- 第一范式(1NF):每一列都只存一个值,不存逗号分隔的“小数组”。前文那个
department_ids字段,就是典型违反1NF。 - 第二范式(2NF):表里的每个非主键字段,必须依赖完整的主键,而不是部分主键。常见于联合主键的表,比如订单明细表用
(订单id, 商品id)做联合主键,这时“商品名称”就只依赖商品id,不依赖订单id,放进去就会产生冗余。 - 第三范式(3NF):非主键字段之间不能有传递依赖。举个例子,“用户表”里放“部门名称”而不是“部门id”,看起来方便查询,可一旦部门改名,整张表都要update,就是违反了3NF。
我用一句容易记的话总结:第一范式解决“一个格子里别放一筐”的问题;第二范式解决“别让部分依赖产生重复”的问题;第三范式解决“别让字段绕弯子传递依赖”的问题。
2.2 范式不是银弹:什么时候该主动反范式
遵循范式能带来一致性,但过度范式化也会带来灾难。比如一个电商订单详情页,需要展示订单号、下单时间、用户昵称、收货人地址、商品名称、商品图片、单价、数量、实付金额。如果严格按3NF设计,你可能需要join订单表、用户表、商品表、订单明细表、地址表等五张以上才能把一页数据查出来。一次列表查询要join五个表,再叠加分页、过滤条件,数据库的压力会非常大。
所以实际业务里,我会在三个场景下主动牺牲范式:
第一是高频查询字段冗余。比如订单表直接冗余一份“下单时商品快照”,包括商品名称、主图、单价。注意要叫快照,因为商品可能改价、下架甚至删除,但订单历史必须保留当时的信息。这在数据一致性上看起来“违反范式”,但在电商业务里是刚需。
第二是统计汇总字段冗余。例如订单表里冗余“订单总金额”,每次下单时计算后写入,省去每次统计时对明细表sum。这样设计需要靠事务保证总金额与明细一致,但可以极大降低报表查询压力。
第三是频繁join的公共信息冗余。比如用户表和部门表之间,如果90%的场景都要显示用户对应的部门名称,我会在用户表里冗余一个dept_name字段,并在部门改名时通过一个明确的同步任务去更新。
范式与反范式的选择不是二选一,而是要看“写多”还是“读多”、要强一致还是允许最终一致。我的经验是:核心交易类数据(订单、支付、库存)不要轻易反范式,宁可查询时join;对外展示、分析报表类数据可以大胆冗余和宽表化。
2.3 实际判断:这条字段该不该放这张表
设计时我会做一个简单的依赖检查清单:
- 这个字段的核心主体是谁?它描述的是“当前表这一行对应实体”的属性,还是另一个实体的属性?
- 如果删除这张表,这个字段还有没有其他归属地?如果到处都有,说明放错了或者该抽出来。
- 业务后续会不会在“可选范围/多对多关系”上变化?如果会,就该做关联表而不是加冗余字段。
这套检查不一定能保证设计完美,但可以有效降低一张表上百个字段、多处数据源不一致的混乱局面。
3. 核心建表原则:字段类型、主键与约束的设计细节
3.1 主键设计:自增id、业务主键还是uuid
主键是数据库设计里最基础也最容易踩坑的部分。我用过的方案主要有三种,分别适用不同场景。
自增id(BIGINT AUTO_INCREMENT)是最省心的选择,写入性能好、索引空间小、分页方便。但它会暴露业务规模,比如订单号用自增id,竞争对手一看就知道你每天多少单。更重要的是,自增id在分库分表场景下会产生冲突,需要用号段模式或分布式id生成器。
业务主键也很常见,比如用订单号、身份证号、商品编码直接做表主键。优点是查询时少一次唯一索引定位,缺点是业务主键通常较长,InnoDB聚簇索引会自动把所有二级索引带上主键值,导致二级索引膨胀;另外业务主键一旦生成规则调整,改动极麻烦。
UUID字符串做主键是我最不推荐常规做法。字符串主键占用空间大,随机字符串插入时可能造成页分裂和索引碎片,写入性能会明显下降。如果你确实需要全局唯一且不暴露规律,我建议用雪花算法生成的long型id。
3.2 字段类型选择:小表见真功夫
很多项目后期出现空间膨胀、索引失效、查询变慢,与字段类型选择不当有直接关系。我总结了几个基本原则:
- 整数类型按需选择。能用INT不用BIGINT,能用TINYINT不用INT,但主键和关联字段尽量统一用BIGINT,避免将来数据量上来后迁移主键类型,代价极高。
- 金额不要用FLOAT/DOUBLE,精度丢失会让你对账对到怀疑人生。分系统可以用
DECIMAL(12,2),涉及汇率等更精细的,直接用DECIMAL(20,6)存最小货币单位对应的基础值。 - 字符串长度不要乱给。VARCHAR(255)还能有索引前缀优化,有人图省事全表VARCHAR(1000),结果InnoDB对过大的varchar在临时表排序和磁盘存储上都不友好,索引也有长度限制。
- 时间类型选择要统一。能用DATETIME别用VARCHAR存时间,能用TIMESTAMP/DATETIME就别用INT存时间戳。除非你的系统有明确的时区转换需求,否则DATETIME可读性更强。
- 有布尔语义的字段用TINYINT(1)就好,别用CHAR(1)的'Y'/'N',代码读起来别扭,查询也不直观。
3.3 约束与默认值:数据库能扛的事别丢给代码
设计原则里有一条常被忽略:能用数据库约束保证的数据正确性,就不要依赖上层应用自觉。
主键约束必须有,不为别的,就为防止重数据插进去;唯一约束用在业务上“不该重复”的字段组合,比如用户名、手机号、会员卡号,但不是所有字段都非得全局唯一;外键约束我一直是这个态度:交易核心库你可以用外键保证强一致,但在互联网高并发、分库分表场景下,外键会让每次写操作都检查关联表,锁竞争和死锁概率明显上升。我一般用应用层事务来保证一致性,表结构里不建物理外键,只建逻辑外键加索引。
NOT NULL和DEFAULT要花心思。可空字段在查询时容易漏掉IS NULL条件,导致统计出错。我的习惯是:字符串类型的空值用空字符串''而不是NULL,数值类型的空值可以看业务含义决定是否允许NULL,所有表都加上created_at和updated_at时间戳字段,并设置合理的默认值。
也许你会觉得这些很小,但恰恰是这些“小设计”决定了后面写SQL和排查问题的效率。真等出了问题,再去给几千万数据的表加约束、改默认值,代价早就不是编写时那几分钟了。
4. 亲手设计一套订单系统的流程拆解
4.1 需求整理:先画“实体-关系”草图再动SQL
我尽量不在拿到需求的第一时间打开Navicat建表。更稳妥的做法是先在白纸上画出核心实体:用户、商品、订单、支付流水。它们之间的关系也要写清楚:
- 一个用户拥有多个订单。
- 一个订单包含多个商品(快照)。
- 一个订单可以有多次支付尝试,最终只有一次支付成功。
- 一个订单会有关联的物流信息,但物流可能拆分成多个包裹。
根据这个关系,我规划出第一批表:用户表、商品表、订单主表、订单明细表、支付流水表、物流包裹表。
4.2 表结构设计过程:订单表字段的来源
以订单主表为例,我会把所有要和订单一起展示的信息列一遍:
- 订单编号、用户id、订单状态、商品总金额、运费、优惠金额、实付金额、收货人姓名、收货人手机、收货地址快照、下单时间、支付时间、发货时间、完成时间、取消时间。
有些信息明显属于下单时的“用户快照”冗余,比如收货人、手机、地址。这些字段不能直接从用户地址表去join,因为用户后续可能修改收货地址,但订单历史不能被修改。
订单编号我不会用自增id直接暴露,而是采用“业务编码”。每个订单先通过一张单独的发号器生成唯一id,再拼上业务前缀和日期信息,保证可读性与唯一性。
订单状态字段用TINYINT还是VARCHAR,我通常会看团队习惯。如果状态机清晰就VARCHAR(20)存可读值,比如PENDING_PAYMENT、PAID、SHIPPED、COMPLETED、CLOSED,代码里放枚举,避免出现“数字1代表什么”的歧义。
4.3 DDL示例与常见字段说明
下面是一张简化但完整的订单主表建表语句,可以参考我的注释理解每个字段的取舍。
sql复制CREATE TABLE `t_order` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主键,仅用于物理唯一',
`order_no` VARCHAR(32) NOT NULL COMMENT '业务订单号,全局唯一、展示用',
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID,逻辑外键',
`order_status` VARCHAR(20) NOT NULL DEFAULT 'PENDING_PAYMENT' COMMENT '订单状态',
`total_amount` DECIMAL(12,2) NOT NULL DEFAULT '0.00' COMMENT '商品总金额',
`discount_amount` DECIMAL(12,2) NOT NULL DEFAULT '0.00' COMMENT '优惠金额',
`pay_amount` DECIMAL(12,2) NOT NULL DEFAULT '0.00' COMMENT '实付金额',
`receiver_name` VARCHAR(50) NOT NULL COMMENT '收货人快照',
`receiver_phone` VARCHAR(20) NOT NULL COMMENT '收货电话快照',
`receiver_address` VARCHAR(255) NOT NULL COMMENT '收货地址快照',
`remark` VARCHAR(255) DEFAULT '' COMMENT '用户备注',
`paid_at` DATETIME DEFAULT NULL COMMENT '支付时间',
`shipped_at` DATETIME DEFAULT NULL COMMENT '发货时间',
`completed_at` DATETIME DEFAULT NULL COMMENT '完成时间',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
KEY `idx_user_id` (`user_id`),
KEY `idx_order_status` (`order_status`),
KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表';
这张表的设计有三个关键点:第一,订单号虽然唯一,但没有直接做物理主键,因为order_no的长度为32字节,作为聚簇索引会拖累所有二级索引,所以单独用自增id做主键,再给order_no建立唯一索引。第二,user_id只建立普通索引,不建外键约束,目的是在分库分表时减少约束限制,同时保留通过索引快速关联的能力。第三,时间字段都允许为NULL,因为订单创建成功后这些时间点天然为空,等业务推进再更新。
订单明细表的核心则是“商品快照”:
sql复制CREATE TABLE `t_order_item` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`order_id` BIGINT UNSIGNED NOT NULL COMMENT '订单主表id/order_no',
`product_id` BIGINT UNSIGNED NOT NULL COMMENT '商品id',
`product_name` VARCHAR(128) NOT NULL COMMENT '商品名快照',
`product_image` VARCHAR(255) DEFAULT '' COMMENT '商品图快照',
`product_price` DECIMAL(12,2) NOT NULL COMMENT '下单时单价快照',
`quantity` INT NOT NULL DEFAULT 1 COMMENT '购买数量',
`subtotal_amount` DECIMAL(12,2) NOT NULL COMMENT '小计金额',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_order_id` (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表';
为什么明细表不直接记商品全表的外键?因为商品信息是动态的——改名、改价格、删除,而订单里的商品信息必须冻结在下单那一刻。这是“数据库设计要服务业务不可变性”的典型例子。
4.4 设计评审:表之间关系是否干净
表建完以后,我会站在查询和变更两个角度复查一遍:
从查询角度,订单列表页要展示用户昵称、订单号、金额、状态,可能要join用户表,此时应该检查t_order的user_id上有没有索引;从变更角度,用户申请退货,状态变化要经过哪几张表、有没有字段需要同步更新,这些都要写清楚,不能只靠脑子记。
做完表设计,及时把ER图和数据字典保存到团队文档里。我见过太多项目上生产之后,只有建表语句没有设计文档,半年后谁都不清楚一个字段是干什么的。维护数据字典几乎和建表一样重要。
5. 索引设计原则:让查询不慢的金线
5.1 索引是数据库的“目录”,但不是越厚越好
数据库设计原则里,多数人最先接触到的是索引,但理解得最浅。我曾经听说“所有where条件的字段都要建索引”,结果一张表建了二十多个单列索引,写入慢、占空间,查询优化器也经常用错索引。
索引不是越多越好,每一次写入都要同步维护索引树。如果一张表大量插入而只有少量查询,过度的索引就是在为极其低频的查询付出高频的写放大代价。所以我在实际工作中会先明确查询场景,再决定需要哪些索引。
判断一个字段是否需要索引,我会看三点:
- 这个字段是否经常出现在where、join、order by或group by里。
- 这个字段的选择性高不高,比如“性别”就不适合单独建索引,因为一个值可能筛出一半数据。
- 这个字段是否很大,大文本建索引不划算,必要时用前缀索引。
5.2 联合索引设计原则:最左前缀优先
一个高频查询可能是“订单状态 + 支付时间范围”,那么(order_status, paid_at)联合索引要优于单独两个索引。但是要注意最左前缀原则:如果查询条件只用了paid_at没带order_status,那张联合索引就派不上用场。
设计联合索引的字段顺序也很讲究。我的一句话经验是:“区分度高的放前面,等值查询的放前面,范围查询的放后面”。这里的区分度是指某个字段不同值占行数的比例,比如订单状态可能只有10种,区分度低;支付时间却是连续不断的,区分度高。但如果查询经常按“状态+时间”过滤,状态虽然区分度低,却是等值条件,适合放前面;时间范围条件放后面,可以充分利用索引的有序性直接范围扫描。
下面是我常用的索引规划步骤:
- 把核心查询列出来,先找出最高频的5~10条SQL。
- 根据SQL里的where与order by字段,统计被使用字段。
- 优先为高频组合建联合索引,不要一上来就为每个字段建单列索引。
- 用
EXPLAIN看实际执行计划,重点关注key、rows、Extra等字段,确认没有出现Using filesort或Using temporary。
5.3 索引失效的常见原因
就算建好了索引,也不代表所有情况都能命中。我踩过太多这种坑,现在排查SQL慢时会先检查几类问题:
- 对索引列做了函数运算,如
WHERE DATE(created_at)=...,会让索引失效。应改成created_at >= ? AND created_at < ?形式。 - 隐式类型转换,如手机号字段是varchar,但查询时用了数字,MySQL会先转成数字再匹配,索引失效。
LIKE '%关键字%'前置通配符导致无法走索引,改成LIKE '关键字%'就能走。- OR条件连接了不同字段,其中一列无索引,整个查询可能不走索引。
- 排序字段与查询条件不在同一个联合索引上,会触发文件排序。
每次整理索引时我都会提醒自己:索引是给数据库优化器看的“目录提示”,而写SQL的人要尽量保证条件写法能与目录结构匹配,不能想当然。
6. 数据完整性与一致性的设计防线
6.1 数据库能提供的几层保证
一个成熟系统在写入数据时可能同时发生很多事:扣库存、生成订单、减少优惠券。如果每一步都靠开发自己判断,总会有漏掉的地方。数据库设计原则会强调“在数据库层就提供防线”,但不同场景用的防线不同:
- 事务能保证多条DML语句要么都成功要么都失败。所以在涉及强一致的订单和库存场景,要把减库存与生成订单放到同一个事务里,并设置合适的隔离级别。
- 唯一约束可以拦截重复提交。例如支付回调可能被重复调用,支付流水表给“订单号+支付渠道流水号”加唯一索引,重复插入直接报错,再从代码层面感知即可。
- 触发器我并不常用,因为隐式逻辑很难排查,但偶尔可以用它做审计日志,代价是降低写入性能。
6.2 乐观锁与悲观锁:并发场景下的取舍
当两个用户同时购买最后一件库存时,怎么保证不超卖?本质上需要从数据层面设计约束。
悲观锁方式是在查询库存时执行SELECT ... FOR UPDATE,锁住库存行直到事务结束。这种方式能稳,但并发量高时会导致大量线程阻塞等待。
乐观锁方式更符合多数互联网场景——在库存表或订单表加一个version字段,更新时带上旧版本号:
sql复制UPDATE t_inventory
SET stock = stock - 1, version = version + 1
WHERE product_id = ? AND stock >= 1 AND version = ?;
这种写法把“检查库存是否充足”和“扣减库存”合并成一个原子语句,不需要显式加锁,在高并发下性能好很多。如果affected rows为0,说明库存已经被改过,业务层再提示用户重新尝试。
6.3 唯一约束之外的“软唯一”:删除标记带来的麻烦
现实中为了可恢复、防误删,很多表会采用逻辑删除,也就是加一个is_deleted字段,删除时置为1,而不是物理删除。但这会带来一个很有意思的坑:用户名原本要求唯一,逻辑删除后同一个用户名可能出现在多条历史记录里。
如果业务要求删除后释放用户名,我建议用“UNIQUE KEY (username, is_deleted)”就不太可行,因为同一个用户名只能被删除一次。常见的两种解法是:保留物理删除但用独立的回收站日志表;或者在删除时把用户名改写为带时间戳的回收值,例如old_name#20240511120000,确保唯一约束不被命中。具体选哪种要结合运维规范和数据量判断。
6.4 一致性永远要考虑最终一致
跨库、跨服务的系统没法依赖本地事务保证一致性时,数据设计要留出对账的字段和能力。比如每笔支付流水都会有“本地金额”“支付渠道金额”“回调时间”“结算状态”,定时任务去拉取渠道账单与本地流水比对,发现不一致主动告警。数据库设计原则在这种场景下的表现,更多是“为将来的对账留好字段”,而不是指望一个数据库解决所有一致性问题。
7. 常见问题排查与设计原则落地清单
7.1 我经常被问到的几个设计问题
结合平时答疑的经验,整理几个典型问题,大家可以对照自查。
为什么我的一张表字段很多、业务跑起来也很快,有必要拆表吗? 如果一张表字段很多但都是同实体属性,且没有大量重复和null,就未必需要拆。但一旦出现多个类型的联系方式、多个部门归属、大段JSON存业务明细,就要警惕了。JSON字段虽然在MySQL 5.7以后很常用,但只能用作非核心扩展,别把整个订单快照全塞进一个大JSON里,导致无法按明细统计。
要不要给时间字段建立索引? 要看需求。如果你频繁按时间范围做列表查询,时间字段可以作为联合索引的第二个字段;如果全局只查这一次,完全不用。不要因为“以后可能按时间查”就给每张表的时间字段加索引,这样浪费磁盘且拖慢插入。
分库分表之后,数据库设计原则还适用吗? 适用,但约束与事务的粒度会变。以前一张表能完成的唯一约束,现在可能要依赖全局发号器;以前一个事务能解决多表一致,现在要依赖分布式事务。所以我建议核心原则不变,只是实现方式需要适当演进。
7.2 一张表设计完成后,我会做的十项检查
为了方便落地,我给自己总结了一个检查清单,通常建完表后逐项打钩:
- 是否有明确主键,且主键类型是BIGINT或等价的整数,避免超长字符串主键。
- 唯一业务键是否有唯一索引。
- 常用外键关联字段是否建了索引,避免join全表扫描。
- 字段是否设置了合适的默认值,update频繁的地方是否有updated_at。
- 是否存在逗号分隔或JSON数组这种多值字段,如有则评估是否需要子表。
- 字段类型是否选择合理,如金额用DECIMAL,时间用DATETIME。
- 是否存在大量可为NULL却又没有业务含义的列,若有调整为NOT NULL+DEFAULT。
- 是否有保留状态历史的需求,如果有,是否有state_log表。
- 是否有硬删除风险,业务是否需要逻辑删除,以及逻辑删除时唯一约束的处理。
- 表注释和字段注释是否已经写全,尤其是枚举含义。
7.3 一次重建表结构的实际复盘
最后说一个我真实的复盘经历。之前我做过一个营销活动系统,活动与商品是多对多关系。第一版设计时我在活动表里直接存了商品id逗号串,例如“1001,1002,1003”,因为当时运营配置的活动绑定商品数量基本只有三五个,查询也非常简单,整个列表读出来才能知道哪些商品参加了活动。
后来活动升级成“导入上万商品”,运营希望按“某个商品查它参加的所有活动”,原来的设计直接把数据库查崩了。我把它重构成“活动-商品关联表”,主键是自增id,业务唯一键是(activity_id, product_id),再为product_id建立索引。改造完以后,关联表记录了上百万条关系,单次查询耗时从数秒降到几十毫秒。
对比来看,一开始用逗号串设计就是贪图当时的简单,没有考虑“关系会有动态变化”这个未来场景。现在我会坚持一个原则:凡是业务上会“多对多”且可能扩展的关系,一律建独立关联表,不要用逗号分隔字段偷懒。
这个原则同样适用于标签体系、用户角色、组织成员关系等场景。关联表的成本非常低,却能在未来帮你躲开几乎所有“中间关系”引发的数据一致性问题。
我个人在实际操作中的体会是:数据库设计原则不是一堆僵硬的规则,而是一套围绕业务与时间的风险管理方法。你越认真对待每一张表、每一个字段、每一条约束,后续你在排查慢查询、处理脏数据、做系统重构时就越从容。真希望能早点用这套思路去拯救当初那个“万能用户表”,但更希望看到这篇文章的你们,能从下一张表就开始把每一步都设计得稳妥一点。
