做数据库设计这些年,我踩过最大的坑,往往不是SQL写不出来,而是一张表的结构从一开始就没设计对。特别是刚接触关系数据库的开发者,最常见的习惯就是把所有字段塞进一张大表里,看着方便,后面改需求的时候才开始后悔。关系数据库规范化这个主题,说白了就是在表结构设计阶段建立一套纪律:让每个字段待在它该待的地方,让数据只存一份,让更新、插入、删除操作不发生意外。这不仅是理论考试里的必考知识点,更是日常建表、评审他人表结构、重构老旧系统时真正能派上用场的判断标准。
这篇文章我会从实际问题出发,讲清楚规范化的核心思想、三大范式的判断方法和拆分步骤,还会补充工程实践中的反规范化权衡、表结构扫查方法,以及围绕规范化的文档评审和缺陷管理习惯。无论你是刚入门的新手,还是写过几年业务系统的后端开发,应该都能在这里面找到可以直接拿去用的东西。
1. 关系数据库规范化到底在解决什么问题
1.1 一张“万能大表”引发的连锁灾难
先看一个我在实际项目里见过很多次的场景。假设你在为一个在线教育平台设计订单表,为了图省事,把所有相关字段都放在一张表里:
CREATE TABLE order_info ( order_id VARCHAR(32) PRIMARY KEY, order_date DATETIME, customer_id VARCHAR(16), customer_name VARCHAR(50), customer_level VARCHAR(20), course_id VARCHAR(16), course_name VARCHAR(100), course_price DECIMAL(10,2), category_id VARCHAR(16), category_name VARCHAR(50), teacher_id VARCHAR(16), teacher_name VARCHAR(50), payment_status VARCHAR(20) );这张表看着结构清晰,字段齐全,甚至能直接满足报表需求。但只要你往深处想一想,问题就全出来了。
首先是数据冗余。同一个客户买了三门课,那么customer_name和customer_level就要在这张表里重复存储三遍。同一门课的讲师信息、分类信息也会随着订单数量增长而无限重复。磁盘空间浪费只是小问题,真正可怕的是数据一致性失去保障。某个客户升了级,你要 UPDATE 这张表里所有该客户的记录,只要漏掉一行,同一个客户在系统里就会同时存在“普通会员”和“VIP会员”两个版本。
然后是更新异常。客户改名、课程调价、讲师换人,这些看起来很普通的业务操作,在冗余表里都变成了高风险操作。你得记得所有相关记录都要同步改,用一条条件不准确的 UPDATE 就会造成数据错乱。
接着是插入异常。新建了一门课程但还没人下单,课程信息应该往哪存?因为order_id是主键,你没法插入一条没有订单编号的课程记录。想把新讲师录入系统?也做不到,因为讲师信息依附在订单上。这就导致很多“基础数据”只能人为地塞进别的业务流程里,系统设计从一开始就埋下了隐患。
最后是删除异常。反过来看,如果一个客户取消了他唯一的订单,删除这条订单记录的同时,这个客户的基本信息、课程信息、讲师信息也会跟着全部消失。业务上只是退了一单,数据上却像发生了数据灾难。
1.2 范式的本质:不断解除数据依赖
面对上面这些问题,最直观的解决方案就是“拆表”。但拆表不是凭感觉随便拆,规范化理论提供的是一套严谨的拆法。
范式的核心思想可以理解成:数据之间存在依赖关系,我们要把这些依赖梳理清楚,让每一张表只描述一个独立的事实。
打个比方,你的衣柜里如果所有衣服都乱堆在一起,找一件外套要翻半天。规范化的过程,就是给衣服分类:外套挂一区,裤子叠一区,袜子毛巾各归各位。每个区域只放一类东西,找起来方便,也不会丢。
在关系数据库中,这个“分类”的依据叫函数依赖。简单说,就是“通过 A 能唯一确定 B”,那 B 就函数依赖于 A。比如通过customer_id能确定customer_name,通过course_id能确定course_price。规范化的工作,就是逐层解除“不该存在的依赖”——非主属性对主键的部分依赖、传递依赖、以及其他违反语义的依赖关系。
每一种范式解决的是特定层次的依赖问题:第一范式解决字段原子性,第二范式解决部分依赖,第三范式解决传递依赖。一层层往上,约束越来越强,表拆得越来越细,数据冗余越来越少。这就是为什么你一定要搞清楚理论里面的定义是怎么来的,因为它背后其实就是这些非常实在的工程问题。
2. 三大范式:判断标准与实战拆解
2.1 第一范式(1NF):字段原子性这条底线
第一范式的定义非常朴素:表中的每一列都是不可再分的原子值。简单说,就是一个字段里不能塞一个“小集合”。
最常见的反面案例是这样的:
CREATE TABLE course_tag ( course_id VARCHAR(16) PRIMARY KEY, course_name VARCHAR(100), tags VARCHAR(200) -- 存储格式:'Python,数据分析,入门' );tags字段用逗号分隔存了多个标签。这在不少项目里都是常态,因为写的时候省事,读的时候用FIND_IN_SET或LIKE也能查。但从规范化的角度看,这个设计在查询和统计时会非常别扭。你想统计“包含‘入门’标签的课程有多少”,只能靠字符串匹配,一旦标签里出现相似的词,统计结果就不可靠。LIKE '%入门%'会把“进阶入门”这种无关数据也算进去。
解决方式有两种。第一种做法是拆行,一个标签一行:
CREATE TABLE course_tag ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_id VARCHAR(16), tag_name VARCHAR(50) );第二种做法是拆列,但如果标签数量不确定,列数无法固定,所以并不推荐。实际工程里更常见的做法是把标签单独建一张tag表,再建一张course_tag_relation关系表,这就是标准的三表关联模型。
这里必须补充一点:现代数据库已经支持 JSON、数组等复杂类型。比如 PostgreSQL 的JSONB、MySQL 5.7 之后的JSON,它们允许一个字段存储结构化数据,从严格的关系理论角度看这违反了 1NF 的原子性要求。
但在实际业务里,JSON 字段如果只是用于存储展示类信息、不参与频繁的关联查询和统计聚合,是可以接受的。理论规范是底线,不是枷锁,关键是你要清楚自己在做什么取舍。如果某个 JSON 字段将来要高频参与查询和过滤,那它在设计之初就应该被规范成独立的关系表,而不是等到数据量大了再回炉改造。
2.2 第二范式(2NF):找出只依赖部分主键的字段
第二范式建立在第一范式之上,它的定义是:在满足 1NF 的基础上,表中的每一个非主属性都必须完全依赖于主键,而不能只依赖于主键的一部分。
这个定义只有在表的主键是复合主键(由多个字段组成)时才可能被违反。如果表的主键是单字段,那么这张表自动满足 2NF。
回到开头那个order_info表。假设业务上一个订单可以包含多个课程,那这个表的主键就应该是复合主键(order_id, course_id)。现在看这些字段:
course_name、course_price、category_id、category_name、teacher_id、teacher_name:这些字段只和course_id挂钩,不依赖order_id。这就是部分依赖,违反了 2NF。customer_name、customer_level:这些字段只和customer_id挂钩,而customer_id虽然是业务字段,但在订单上下文里它只是一个普通属性,同样存在依赖问题。payment_status、order_date:这两个字段依赖整个主键(order_id, course_id),因为一个订单里的每个课程条目都有各自的支付状态和日期(理论上),所以它们符合完全依赖。
在工程判断上,一个非常实用的技巧是:凡是只和主键一半有关系的字段,都要拆出去。于是我们把表拆成下面几张:
-- 课程信息表 CREATE TABLE course ( course_id VARCHAR(16) PRIMARY KEY, course_name VARCHAR(100), course_price DECIMAL(10,2), category_id VARCHAR(16), teacher_id VARCHAR(16) ); -- 客户信息表 CREATE TABLE customer ( customer_id VARCHAR(16) PRIMARY KEY, customer_name VARCHAR(50), customer_level VARCHAR(20) ); -- 订单主表 CREATE TABLE order_main ( order_id VARCHAR(32) PRIMARY KEY, order_date DATETIME, customer_id VARCHAR(16), payment_status VARCHAR(20) ); -- 订单明细表 CREATE TABLE order_item ( order_id VARCHAR(32), course_id VARCHAR(16), quantity INT, PRIMARY KEY (order_id, course_id) );拆完之后再看:课程的属性只存在课程表里,客户的属性只存在客户表里,订单的汇总属性只存在订单主表里。这四张表各自的主键都是单字段或复合主键,且所有非主属性都完全依赖主键,2NF 就满足了。
这里要特别强调一个新手容易犯的错:拆表的时候,千万不要把业务外键关系搞丢。比如order_item表里的order_id和course_id,它们既是复合主键,又是外键,指向order_main和course。这是业务关系的一种数据化表达,拆表后必须通过外键或至少通过应用层约束把关系维护住,否则拆表就变成了丢数据。
2.3 第三范式(3NF):斩断非主属性之间的传递依赖
第三范式的定义:在满足 2NF 的基础上,非主属性之间不能存在传递依赖。
什么叫传递依赖?就是“A 决定 B,B 决定 C,于是 A 间接决定了 C”。在我们上面拆出来的course表里就典型存在:
course_id -> category_id -> category_name course_id -> teacher_id -> teacher_namecategory_name依赖category_id,而category_id又依赖course_id。也就是说course_id能推导出category_name,但这个依赖是“绕了一圈”得来的,不是直接依赖主键。这就违反了 3NF。
违反 3NF 的后果,和之前一样:冗余和更新异常。如果 100 门课都挂在同一个分类下,category_name就重复了 100 遍。哪天分类名字从“编程入门”改成“零基础编程”,你得 UPDATE 100 条记录,漏一条就是数据不一致。
正确做法是继续拆分:
-- 课程信息表 CREATE TABLE course ( course_id VARCHAR(16) PRIMARY KEY, course_name VARCHAR(100), course_price DECIMAL(10,2), category_id VARCHAR(16), teacher_id VARCHAR(16) ); -- 分类表 CREATE TABLE category ( category_id VARCHAR(16) PRIMARY KEY, category_name VARCHAR(50) ); -- 讲师表 CREATE TABLE teacher ( teacher_id VARCHAR(16) PRIMARY KEY, teacher_name VARCHAR(50) );这时候,每个字段都只直接依赖主键course_id,没有任何一个字段由“另一个非主键字段”决定。3NF 达成。
很多时候,人们把 3NF 俗称为“每一列都与主键直接相关,而不是间接相关”。这句话虽然不够精确,但在工程实践中非常好用。你建表之前,对着每一列问一遍:这个字段是否直接描述主键对应实体本身的属性?如果它描述的是另一个实体的属性,那它就应该放到另一张表里去。
2.4 再往上:BCNF 和更高的范式
在 3NF 之上还有 BCNF(巴斯-科德范式)、4NF、5NF。这些在实际开发中用得很少,但 BCNF 值得了解。
BCNF 的本质是:在 3NF 的基础上,消除“主属性对候选键的部分依赖和传递依赖”。说人话就是,3NF 只管非主属性,但如果主属性之间的依赖关系也有问题,就需要 BCNF 来兜底。
举一个经典例子。有一个选课场景:
CREATE TABLE student_course ( student_id VARCHAR(16), course_id VARCHAR(16), teacher_id VARCHAR(16), PRIMARY KEY (student_id, course_id) );业务规则是:一门课由多个老师教,一个学生选某门课时,由特定的老师授课;同时,一个老师只教一门课。那么存在依赖关系teacher_id -> course_id。在这个表里teacher_id是主属性(复合主键的一部分),但它却决定了另一个主属性course_id,这就违反了 BCNF。这种设计在实际业务里很少见,遇到时再深入研究也不迟。作为开发者,能把表规范化到 3NF,已经能解决绝大多数数据冗余和异常问题。
3. 范式级别怎么选:三范式之外的设计权衡
3.1 该规范到第几范式
很多人学完理论,会有一个疑问:是不是表拆得越细越好?答案是否定的。
规范化程度越高,表数量越多,查询时需要 JOIN 的表也越多。数据量大的时候,JOIN 的成本会显著上升,尤其是多张大表关联,查询性能可能下降一个数量级。所以实际业务里的经验法则是:
- OLTP(在线事务处理)系统:面向日常业务操作,以增删改查为主,推荐规范化到 3NF。因为这类系统最怕数据不一致,每次更新操作尽量只影响一行或少部分行。
- OLAP(在线分析处理)系统:面向数据分析和报表统计,读取频繁,写入较少。这种情况反而会刻意保留一些冗余,用空间换查询效率。比如数据仓库里的宽表模型,就是把多个维度的字段冗余在事实表里,避免分析时大量 JOIN。
- 混合负载系统:一个系统同时支撑业务操作和数据查询,常见的做法是业务库用规范化的表结构,数据分析走独立的宽表或汇总表。这些冗余表可以定时从主库同步,形成“可控的冗余”。
我个人的实践标准是:主业务流水表、配置表、用户表必须做到 3NF;数据报表、搜索索引、统计缓存可以大胆反规范化。关键是你要知道自己在反规范化,并且知道冗余字段的更新策略是什么。
3.2 反规范化不是“不规范化”
有时候在项目里看到一张表的字段冗余得像开头的order_info,问开发同学为什么要这么设计,回答往往是“当初觉得查着方便”。这就是无意识的乱设计,和“有意识的反规范化”之间有天壤之别。
有意识的反规范化通常发生在以下几种场景:
场景一:订单快照字段
电商订单里存product_name冗余字段。商品可能会改名,但订单是历史事实,用户在“我的订单”里看到的历史商品名应该是下单那一刻的名字,而不是当前商品的名字。所以订单明细表即使已经关联了商品表,也会冗余一份product_name快照。这是业务语义决定的合理冗余。
场景二:统计计数冗余
用户表里冗余一个order_count字段,每次下单成功后对这个字段做+1更新。如果不冗余,就要每次用COUNT(*)去查订单表。在频繁需要展示用户订单数的场景里,这个冗余能省下大量查询成本。当然代价是更新时要保证一致性,解决方案通常是事务或消息队列异步更新。
场景三:热字段冗余
在读多写少的场景,把高频查询的字段直接冗余到主表中,避免每次 JOIN。比如文章列表页只需要标题和摘要,那就把这两个字段冗余在列表主表里,而不是每次联查内容明细表。
这三个场景的共同点是:冗余字段是经过讨论、有明确更新策略、并且承担了明确业务职责的。这才是反规范化的正确姿势。
3.3 实际工程中的折中:JOIN 成本与索引调优
分层设计是理论,落到数据库引擎上就得谈性能。一个不能忽略的事实是:在互联网业务里,数据库服务器的性能瓶颈往往不在磁盘空间,而在查询响应时间和连接数。
有一段时间我执着于把所有表都拆到满足 3NF,但上线后查询耗时从 20ms 涨到 200ms。原因很简单:列表页一屏要加载用户信息、商品信息、门店信息,原来单表一次查询,现在要 JOIN 三张表,索引稍没建好就慢得让人抓狂。
后来在项目里形成了这样的调整策略:
- 先按 3NF 设计表结构,保证数据完整性;
- 压测发现慢查询后,用 EXPLAIN 分析执行计划,优先通过索引优化解决;
- 如果索引优化解决不了,再考虑增加冗余字段或中间表;
- 每次反规范化操作必须写入设计文档,说明原因、更新方案和预计收益。
这套流程坚持下来之后,数据库很少出现“设计时很优雅、上线后很痛苦”的情况。规范化是起点,不是终点,我们对它做一个动态的工程平衡。
还有一点经验:JOIN 本身不是洪水猛兽,适度的 JOIN(两三张小表关联)在合理索引下性能完全可以接受。真正需要警惕的是在千万级大表上做多表 JOIN,或者 JOIN 条件没有索引可用。所以拆表首先考虑表的数据量和关联频率。
4. 规范化扫查:如何审查一张表现有的设计
4.1 五步扫查法:迅速定位违反范式的位置
光知道理论还不够,面对一个现成的数据库或者别人设计的表,你得有一套快速扫查的方法。经历过一次老系统重构之后,我整理了一套非常实用的五步走流程,即便你完全不记得范式定义,按步骤走也能发现大部分问题。
第一步:确认主键。
先看表的主键是单字段还是复合字段。这决定了第二步和第三步的检查方向。如果主键是自增 ID 这种和业务无关的字段,那部分依赖的风险会小很多;但如果主键是自然字段(比如身份证号、订单号),就要小心业务依赖关系埋雷。
第二步:列出所有非主属性与主键的依赖关系。
对每一个非主字段问自己:这个字段是描述主键所代表的实体的属性吗?如果是,检查它是否直接依赖完整的整个主键。
第三步:查找“谁的属性放错了位置”。
如果一个字段明显属于另一个实体,比如订单表里放了客户等级、课程名、讲师名,那基本可以断定它至少违反 2NF 或 3NF。这一步靠的是对业务实体的直觉,做多了就非常快。
第四步:查找同一信息在不同表中重复出现。
在同一个库里,如果发现customer_name出现在五张表里,那就得追问:这五处是必须的快照冗余,还是无意识的重复?没有明确更新策略的重复信息就是坏味道。
第五步:测试更新操作的波及范围。
闭上眼睛模拟一个更新场景:客户改了手机号,课程改了价格,讲师改了名字。在现有表结构下,这个操作需要 UPDATE 多少行?需要 UPDATE 的行数越多,说明这张表的规范化程度越低。
这套流程不依赖任何工具,纯靠看表结构和业务理解就能执行,在代码评审和表结构评审里非常实用。
4.2 用 SQL 发现表中的不良设计信号
除了人工审表,还可以用一些 SQL 查询来辅助发现数据层面的异常信号。例如,要检查一张表里是否存在冗余的重复数据,可以用GROUP BY配合HAVING COUNT(*) > 1来定位。比如:
SELECT customer_id, customer_name, COUNT(*) FROM order_info GROUP BY customer_id, customer_name HAVING COUNT(*) > 1;如果返回了大量记录,说明customer_name在订单明细层面存在严重重复,冗余信号非常明显。这张表的设计很有可能需要拆表。
同样的思路还可以用在其他字段上,找出那些“本应唯一却重复出现”的信息。项目里如果积累了记录数比较大的宽表,还可以统计一下各字段的重复率,字段重复率超过一定阈值,就该怀疑它能被拆出去。
还有一种信号是超宽表。一张表如果字段数超过 20 个甚至更多,就要警惕它是不是把多个实体的属性强行揉在一起了。MySQL 单表字段数上限很大,不意味着这么用是对的。宽表往往意味着职责不清晰、索引效率低、维护困难。我是用下面这样的查询定期扫系统里的宽表:
SELECT table_name, COUNT(*) AS column_count FROM information_schema.columns WHERE table_schema = 'your_db' GROUP BY table_name ORDER BY column_count DESC LIMIT 20;发现宽表之后,再结合前面的五步法逐表分析该拆还是该留。这个扫描习惯养成后,那些“设计不良”的表会在第一时间暴露出来。
4.3 规范化扫查的尺度:别把“重构成强迫症”
扫查方法有了,还要注意别把规范化的原则用过头。有一次我在项目里评审一张配置表,它只有config_key和config_value两个字段,完全符合 1NF、2NF、3NF,但业务含义非常模糊,不同团队往里面塞了各种格式的 value,结构倒是规范,语义却乱成一锅粥。
这个例子告诉我们:规范化解决的是数据依赖问题,不是业务语义问题。一张表即使完全满足三大范式,也有可能因为设计者对业务理解不足而变得无法维护。所以在扫查的时候,不能只盯着范式规则,还要结合实体关系图、字段注释和业务文档去判断是否“名实相符”。
建议在项目的设计规范里明确这样的尺度:
- 核心业务表:必须达到 3NF,如有冗余必须写明原因;
- 中间表、汇总表:允许反规范化,但必须设计同步机制;
- 日志类、流水类表:字段尽量精简,避免可变长字段过多,按时间分表或分区。
这样的尺度不会限制设计灵活性,但会让扫查时有据可依。
5. 规范化习惯的养成:文档、评审与缺陷管理
5.1 设计文档该写什么:表结构注释也是一种规范
很多人把“规范化”仅仅理解为“拆表”,这其实是很片面的。我看到过太多数据库表,字段名和业务含义之间的对应关系全靠猜。接手一套老系统时,你看着一张t_user表里的flag字段,完全不知道flag=1代表什么,这就是另一种不规范。
一个负责任的设计文档,或者说一个负责任的表结构,至少应该包含以下内容:
- 表的功能说明:这张表存的是哪一类业务数据;
- 每个字段的业务含义:字段名、类型、是否为空、默认值、含义说明;
- 主键和外键的说明:为什么选这个字段做主键,外键关联到哪张表;
- 与实体关系的对应关系:这张表对应真实世界中的哪个对象,是否有拆表的理由说明;
- 冗余字段的说明:如果是反规范化设计,必须在这里写清楚冗余的目的和更新策略。
MySQL 里的COMMENT就是一个很方便的落盘载体。下面就是一个合格的字段注释示例:
CREATE TABLE order_item ( order_id VARCHAR(32) COMMENT '订单号,关联 order_main.order_id', course_id VARCHAR(16) COMMENT '课程编号,关联 course.course_id', quantity INT COMMENT '购买数量,默认为1', price DECIMAL(10,2) COMMENT '下单时课程成交价,冗余快照字段,不受课程改价影响', PRIMARY KEY (order_id, course_id) ) COMMENT='订单明细表,记录一个订单中包含的课程明细';在代码工程里,我们讲究可读性,讲究命名规范,到了数据库这里,字段注释就是数据库的“代码可读性”。养成把设计决策写进注释的习惯,比单独维护一份没人看的 Word 文档实际得多。
5.2 评审清单:给表结构设计做一次“规范化扫查”
在团队协作中,表结构变更往往需要评审。但评审如果只靠口头说明,非常容易流于形式。把规范化的检查点固化成一个清单,让每一次评审都有统一的尺度,是我在实践中觉得最有价值的动作。下面这份清单可以按需调整:
- 主键是否明确?是否存在业务字段当主键的高风险设计?
- 是否存在复合主键?如果存在,是否有部分依赖字段?
- 是否存在字段重复出现在多张表的情况?如果是,是否有更新策略?
- 是否有字段存了逗号分隔的多值?是否能拆表或拆行?
- 是否有字段明显属于其他实体却被放在当前表?
- 是否有任何字段可由其他字段推导得出?如有,是否有使用必要性?
- 表注释和字段注释是否完整?
- 反规范化设计是否写明了原因和更新方案?
评审时逐条打勾,附上评审结论和修改建议。这张清单平时还能用来审查新同事提交的表结构,减少很多来回沟通的成本。我自己的经验是,比起泛泛地说“这个表不规范”,拿着清单逐条指出“这个字段属于课程实体,应该挪到 course 表”,对方更容易接受,也更容易学到东西。
5.3 把 Schema 变更和缺陷管理关联起来
数据库表结构设计是一个持续演进的过程,随着需求变动,表结构大概率会调整。很多团队都有代码版本管理,却没有数据库结构版本管理。两个开发同学同时对同一张表做了不同的变更,合并代码时冲突了,结果只有一个人记得去生产库执行自己的 DDL,另一个人就怎么都跑不通了。
所谓“养成规范化文档与缺陷管理习惯”,落到数据库层面,就是三件事:
第一,所有表结构变更必须通过迁移脚本(Migration)管理。每个变更脚本包含前向迁移和回滚迁移(如果数据库支持),按版本编号存储。这个习惯能在一个 SQL 文件里清晰地看到数据库的演进历史,出现问题也能快速回滚到上一个版本。
第二,表结构变更必须关联对应的需求或缺陷编号。比如,为了修复某个数据不一致的缺陷,需要拆表,那么拆表脚本的提交信息里就要关联这个缺陷编号。后续排查问题时,能通过缺陷编号快速找到对应变更,理解当时的修改背景。
第三,线上变更必须走预发布验证。在测试环境先执行一遍迁移脚本,确认没有语法错误和锁表风险,再在低峰期操作生产库。对超过一定数据量的表,变更前评估锁表时长,必要时使用在线 DDL 工具。
5.4 一个小习惯:给每个字段写“理由”
最后分享一个我坚持了很多年的小习惯:新建表的时候,对每一个非主键字段,在注释里写清楚它“为什么存在”。
这不是要求写长篇大论,而是用一句话说明这个字段服务的业务场景。比如订单明细表里的price字段,注释写着“下单时课程成交价,冗余快照字段,不受课程改价影响”。以后任何人看到这列,都不会困惑它和course.course_price的关系,也不会在做需求时误用这个字段。
规范化从来不是一件一蹴而就的事。它更像是一种贯穿设计、开发、评审、变更全流程的意识,在每次建表时多想一步,每次评审时多问一句,每次文档里多写一行。等你把这些都变成习惯,那些冗余、异常、不一致的数据库设计问题,会从源头上减少一大半。这套东西我在好几个项目里反复验证过,越到后期维护阶段越能体会到它的价值。