ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

数据库规范化与1NF/2NF/3NF:订单大表拆解实践

数据库规范化与1NF/2NF/3NF:订单大表拆解实践

第一次见到“范式”这个词的人,十有八九会把它和正则表达式扯到一起。我在内部分享会上就被问过:“这个正则化是不是写^[a-z]+$那种?”——不是。数据库语境里的 Normalization,标准译名是规范化或者范式化,讲的是“一张表该拆成几张表、每个字段该待在哪张表里”这件事。有些教材直译成正则化,于是就有了这个容易误解的标题,我后面统一用“范式”这个词。

这篇内容面向的是刚接手真实业务库、或者正在准备数据库相关面试的人。你大概已经能写 CRUD,也能看懂 ER 图,但面对一张六十多个字段、里面还塞着逗号分隔 ID 串的大表时,心里没底:到底该不该拆、拆到什么程度算够。UNF、1NF、2NF、3NF 这条线,就是回答这个问题的。我会把每一层“为什么要有这个规则”“违反了会出什么事”“怎么改”讲透,最后用一份订单数据从零推到 3NF,附上可以直接跑的 DDL。

1. 把数据全塞进一张表,问题不在于“装不下”

1.1 UNF 描述的其实是大多数系统的起点

严格说 UNF(Unnormalized Form,非规范化形式)不算一种范式,它描述的是“还没开始做任何规范化处理”的状态。这个状态在真实项目里出现的频率远比教科书想象的高,常见的三个来源:业务方给的 Excel 模板被原样建表、第三方接口返回的 JSON 被拍平直接落库、早期为了赶进度把多个实体的字段堆在一张宽表里。

我见过一张典型的 UNF 表长这样:

订单号客户信息商品明细下单时间客户等级
D001张三,138xxxx,杭州苹果x2;香蕉x52024-03-01金卡
D002李四,139xxxx,宁波橙子x12024-03-02银卡

这张表能存数据,也能查出来,甚至在数据量小的时候看不出任何毛病。问题在于它是一个“复合体”:一行里同时承载了订单、客户、商品三类实体的信息,而且商品明细那格还是多值的。一旦业务要求变复杂——比如“查出所有买过苹果的客户”“把张三的手机号改掉”“统计金卡客户的平均客单价”——这张表就开始处处掣肘。

1.2 三类异常才是拆表的真正动机

教材里讲范式,往往先给定义再给例子,读起来很抽象。实际工作中推动你拆表的,是三种具体的事故,学术上叫插入异常、更新异常、删除异常,我按真实感受重新描述一遍。

插入异常:来了一个新客户,还没下单,你想把他录进系统。但表的主键是订单号,没有订单就没有订单号,这条客户记录根本插不进去。只能编一个假订单号占位,或者干脆等下单时再录。这就是“实体的存在依赖于另一个实体”。

更新异常:张三的手机号变了,你得把这张表里张三出现的每一行都改一遍。如果他有 200 个订单,就改 200 行。漏改一行,系统里就同时存在两个手机号,客服打过去是空号。更麻烦的是,你没法用一条 UPDATE 语句保证一致性,只能靠人肉或者定时脚本兜底。

删除异常:D002 这单被取消了,DELETE 掉这一行,结果李四这个客户的档案、等级、联系方式一起没了。你只是想删一笔订单,却顺手删掉了一个客户实体。

这三种异常的本质是同一件事:一个事实应该只在一个地方存储。UNF 把这些事实挤在一起,于是任何一次增删改都可能牵动多个事实。

1.3 规范化的目标不是“表越多越好”

这里必须先破一个常见误解。很多人学完范式,回去看自己的库,觉得每张表都不合格,然后大刀阔斧拆成十几张,结果查询变成七个 JOIN,接口响应从 20ms 涨到 300ms,线上报警。

范式的本质目标是消除冗余和依赖异常,不是消除字段数量。判断一张表该不该拆,问自己三个问题:这个字段描述的是哪个实体?它会不会因为别的字段变化而需要同步修改?删掉一行会不会连带丢失另一类事实?三个问题里有两个答“是”,就说明该拆;都答“否”,哪怕表有八十个字段,也不一定有问题。

UNF 到 1NF 迈的是第一步,也是最容易被忽略的一步:处理多值字段。

2. 1NF:单元格里塞列表,代价远高于省下的那张表

2.1 原子性怎么判定,得看业务怎么用它

1NF 的要求是:每个字段的值都是原子的(不可再分),没有重复组,每行可以被唯一标识。定义简单,难点在于“原子”是相对业务而言的。

“收货地址”存成“浙江省杭州市西湖区某路1号”,算不算原子?如果业务只需要整段打印在快递单上,它就是原子的,不用拆。但如果运营要按省份做发货时效统计、按城市做运费分区,那这个字段就是非原子的,必须拆成 province、city、district、detail 四段。

同样,“商品规格”存成“红色 XL”,如果只是展示,够了;如果要按颜色做库存汇总,就得拆成 color 和 size 两列。我踩过的一个坑是早期把“尺寸”存成了“长x宽x高”这种格式,后来要做体积筛选和三维装箱计算,只能写一堆字符串切割函数,SUBSTRING_INDEX嵌套三层,慢且容易错。

所以判断标准不是“值里有没有分隔符”,而是**“这一整块值会不会被单独检索、统计或者更新”**。会,就不是原子的。

2.2 逗号分隔的 ID 串,会在四个地方咬你

多值字段最典型的形式是标签串、ID 列表,比如tag_ids = '3,7,12'。这种设计在快速原型阶段特别爽,省掉一张关联表,写进去也方便。但它会在下面几个场景里集中爆发。

第一,精确匹配失效。查标签 ID 为 3 的文章,直觉写法是LIKE '%3%',结果把标签 13、30、131 的文章全查出来了。改成LIKE '%,3,%',又漏掉了首尾的情况,得写成CONCAT(',', tag_ids, ',') LIKE '%,3,%'。这个表达式左边包了函数,索引彻底失效,全表扫描。

第二,索引和查询优化器全面失能。FIND_IN_SET(3, tag_ids)语法上比 LIKE 干净,但它同样无法使用索引。文章表到百万级以后,一次标签筛选就是几百毫秒起步,用户点一下筛选项页面就卡住。

第三,删除和更新变成字符串手术。要把某个标签从所有文章上摘掉,得写正则替换或者应用层遍历处理。要是遇到'3,13,33'这种串,替换'3'的时候一不小心把 13 和 33 也切了。我见过线上出过这种事,一次批量脚本把标签关系搞乱,最后靠备份回滚。

第四,聚合统计几乎没法写。“每个标签下有多少篇文章”这个问题,在关联表结构里是一条GROUP BY,在字符串结构里只能全表拉出来在应用层 reduce。数据量一大,内存直接崩。

2.3 拆成关联表之后,主键怎么定

把tag_ids拆出去,标准做法是建一张中间表:

-- 反例:文章表里塞标签串 CREATE TABLE article_bad ( id BIGINT PRIMARY KEY, title VARCHAR(200), tag_ids VARCHAR(200) COMMENT '逗号分隔,如 3,7,12' ); -- 正例:拆成关联表 CREATE TABLE article ( id BIGINT PRIMARY KEY, title VARCHAR(200) NOT NULL ) ENGINE=InnoDB; CREATE TABLE article_tag ( article_id BIGINT NOT NULL, tag_id BIGINT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (article_id, tag_id), KEY idx_tag_article (tag_id, article_id) ) ENGINE=InnoDB;

几个细节值得说清楚。

主键用(article_id, tag_id)的复合主键,而不是再加一个自增 ID。原因是这两个字段的业务含义本身就是“某篇文章拥有某个标签”,天然唯一,加自增 ID 反而多一层无意义的映射,还多一个索引的维护成本。同时复合主键自带唯一约束,插入重复关系时数据库直接报错,省掉应用层的判重逻辑。

索引设计上,主键(article_id, tag_id)已经覆盖了“查某文章的所有标签”这个方向。反过来“查某标签下的所有文章”走不到这个索引,所以额外建一个(tag_id, article_id)的联合索引。注意把 tag_id 放左边,因为它是等值查询的驱动列,article_id 放右边可以直接走覆盖索引,不用回表。

这张中间表只有两个业务字段,而且两个都是主键的一部分。这意味着它不存在“非主键属性”,也就无所谓部分依赖和传递依赖——它天然满足 2NF 和 3NF。这是个很典型的规律:拆到最细的关联表,往往一步到位满足到 3NF,因为里面只剩“谁和谁有关系”这一个事实。

3. 2NF:只有当主键是复合的,这个坑才会显形

3.1 部分依赖到底在说什么

2NF 的前提是这张表已经满足 1NF,并且它的候选键是复合的。在这个前提下,要求所有非主键属性都完全依赖于整个候选键,而不能只依赖其中一部分。

“完全依赖”这个词有点绕,用订单明细讲最直观:

-- 违反 2NF 的订单明细表 CREATE TABLE order_detail_bad ( order_id BIGINT, product_id BIGINT, product_name VARCHAR(100), -- 只依赖 product_id product_price DECIMAL(10,2), -- 只依赖 product_id quantity INT, -- 依赖 (order_id, product_id) order_date DATE, -- 只依赖 order_id PRIMARY KEY (order_id, product_id) );

主键是(order_id, product_id),一行代表“某订单里的某个商品”。逐个字段看依赖关系:quantity必须知道是哪个订单的哪个商品才有意义,这是完全依赖;但product_name只要知道product_id就能确定,跟订单号没关系,这就是部分依赖;order_date只要知道order_id就能确定,也是部分依赖。

部分依赖带来的直接后果是冗余和更新不一致。同一个商品被 5000 个订单买过,product_name就在这张表里存了 5000 遍。商品改名,得同步更新 5000 行,漏一行就出现两个名字。同一个订单有 10 个商品,order_date就存了 10 遍,改订单日期同样要改 10 行。

3.2 自增单列主键的表,为什么几乎不会违反 2NF

这是很多人的困惑点。翻遍自己的库,发现几乎每张表主键都是id BIGINT AUTO_INCREMENT,那是不是说明所有表都满足 2NF?

答案是:单列主键的表不可能违反 2NF,因为部分依赖的前提是“主键由多个属性组成”。只有一列,谈不上“部分”。但这不代表这些表就没问题——它们的问题可能出在 3NF 上,也就是非主键字段之间的传递依赖。所以很多团队的实践是“统一用代理主键,把 2NF 这一关直接跳过,重点管 3NF”。

这个做法有代价。用自增代理主键会掩盖业务上的自然键,导致重复数据可以插进去。比如(order_id, product_id)这个自然唯一约束,如果不额外加UNIQUE KEY,同一个订单里同一个商品就能插两条。所以我个人的习惯是:代理主键可以有,但业务上的自然键必须单独建唯一索引兜底。

CREATE TABLE order_detail ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, product_id BIGINT NOT NULL, quantity INT NOT NULL, UNIQUE KEY uk_order_product (order_id, product_id), KEY idx_order (order_id) ) ENGINE=InnoDB;

3.3 一个中间表返工的实例

早年我做过一个权限系统,角色和权限的关联表是这么设计的:

CREATE TABLE role_permission_bad ( role_id BIGINT, perm_id BIGINT, role_name VARCHAR(50), -- 部分依赖 perm_code VARCHAR(50), -- 部分依赖 granted_by BIGINT, -- 完全依赖 PRIMARY KEY (role_id, perm_id) );

当时图省事,觉得反正查询要展示角色名和权限编码,干脆冗余进来少一次 JOIN。跑了一年多,出过一次事故:某个角色改名之后,代码只更新了role主表,忘了同步这张关联表,导致权限校验页面上同一个角色出现了两个名字,运营以为建了重复角色,手工删了一批关联关系,最后花了大半天恢复数据。

后来拆成三张表:role、permission、role_permission(role_id, perm_id, granted_by)。关联表只保留关系本身和关系上的属性(谁授权的、什么时候授权的),不再携带任何实体的描述性字段。查询确实多了一次 JOIN,但那张关联表的数据量在十万级,走主键 JOIN 成本可以忽略,换来的是改名这件事只需要改一个地方。

4. 3NF:非主键字段之间的“私下传递”

4.1 传递依赖的识别,靠画依赖链

3NF 在 2NF 基础上多要求一条:所有非主键属性都不能传递依赖于候选键。所谓传递依赖,就是A → B → C这样的链条,A 是主键,C 通过 B 间接依赖 A,而不是直接依赖 A。

学生表是最经典的例子:

CREATE TABLE student_bad ( sno VARCHAR(20) PRIMARY KEY, sname VARCHAR(50), dept_no VARCHAR(10), dept_name VARCHAR(50) -- 传递依赖:sno → dept_no → dept_name );

sno确定dept_no,dept_no确定dept_name,所以dept_name传递依赖于sno。它描述的不是学生这个实体,而是院系这个实体。院系改名,所有该院系的学生行都要改。

识别方法很机械:盯着主键,问每个非主键字段“它是由主键直接决定的,还是由另一个非主键字段决定的”。如果是后者,链条就成立了。稍微复杂一点的情况是链条更长,比如订单 → 客户 → 客户等级 → 折扣率,三级传递,这种表拆起来要按链条一层层剥。

不是所有冗余都算传递依赖。有一类字段叫派生字段,比如订单表里的total_amount,它是由明细表汇总而来的。严格从函数依赖看,它确实依赖于主键之外的东西,但它跨表了,所以通常单独讨论,叫“派生冗余”。处理方式和传递依赖不同——传递依赖该拆,派生冗余看场景,后面会讲。

4.2 冗余字段引发的三类事故

第一类是数据对不上。两个地方存同一个事实,只要有一个写入路径漏了同步,就会不一致。这类 bug 最恶心的地方在于它不是必现的——正常流程都对,只有某条边缘路径(后台批量导入、运维手工改数据、某个老接口)漏更新,然后在几个月后对账时才发现。

第二类是更新放大。一个院系有 8000 名学生,改一次院系名要 UPDATE 8000 行。如果表上有多个索引,每次更新还要维护索引,锁的范围大,高峰期执行容易造成主从延迟。

第三类是删除信息丢失。新建了一个院系,还没有学生,在这个结构里根本存不进去。等第一个学生录入时,院系名才“顺便”被记下来。这又回到了插入异常。

拆解方式是把院系抽成独立表:

CREATE TABLE department ( dept_no VARCHAR(10) PRIMARY KEY, dept_name VARCHAR(50) NOT NULL ) ENGINE=InnoDB; CREATE TABLE student ( sno VARCHAR(20) PRIMARY KEY, sname VARCHAR(50) NOT NULL, dept_no VARCHAR(10) NOT NULL, KEY idx_dept (dept_no), CONSTRAINT fk_student_dept FOREIGN KEY (dept_no) REFERENCES department(dept_no) ) ENGINE=InnoDB;

外键这个约束在实际项目里经常被“性能考虑”关掉,我觉得要看场景。高并发写入的核心表确实可以不用数据库外键,改由应用层保证;但配置类、主数据类的表,把外键打开能挡掉大量脏数据,收益远大于那点开销。

4.3 什么时候可以故意违反 3NF

有几类场景我明确不建议硬套 3NF。

报表和统计表。数据仓库里的宽表、物化视图、日报周报明细表,这些表的存在意义就是“一次查询少 JOIN”,冗余是设计目标而不是缺陷。它们的写入通常由 ETL 批量完成,不存在并发更新导致的不一致问题,只需要保证每次重算覆盖全量。

高频读低频写的主数据快照。订单表里通常会冗余一份下单时的收货地址和商品名称。这不是没做范式,而是刻意的快照——商品后来改名了、地址后来改了,历史订单必须保留下单那一刻的信息。这种情况下必须冗余,而且要明确标注字段含义,避免后来的人以为是没拆干净又去“优化”掉。

需要原子读取的聚合值。比如账户余额,它理论上可以由所有流水汇总得出,但每次查余额都去 SUM 一遍流水表显然不现实。做法是保留余额字段,同时在流水表上记录每次变动,用事务保证两者同步更新,再配一个定期的对账任务校验。这是典型的“用受控冗余换性能”。

判断能不能违反,我的标准是三条:这个冗余是有意为之还是历史遗留?写入路径是否唯一且可控?有没有对账或重建机制?三条都过,可以留;有一条不过,趁早拆。

5. 把一张订单大表从 UNF 推到 3NF 的完整过程

5.1 起点:一张什么都往里装的表

假设接手的是这么一张表,来源是早期从 Excel 导入的历史数据:

CREATE TABLE order_raw ( order_no VARCHAR(32) PRIMARY KEY, customer_str VARCHAR(300) COMMENT '姓名,电话,城市', items_str TEXT COMMENT '商品名x数量;商品名x数量', order_date DATE, cust_level VARCHAR(20), level_rate DECIMAL(4,2) COMMENT '等级对应折扣率' );

数据长这样:

D001 | 张三,13800000001,杭州 | 苹果x2;香蕉x5 | 2024-03-01 | 金卡 | 0.90 D002 | 李四,13800000002,宁波 | 橙子x1 | 2024-03-02 | 银卡 | 0.95

5.2 第一步:打散多值与分段字段,到达 1NF

1NF 要处理两件事:customer_str和items_str都是多值或复合值,得先拆成独立字段。同时要给表一个明确的、能唯一标识一行的主键——order_no可以,但订单明细一行代表多个商品,一张表装不下,必须分成主表和明细表。

-- 订单主表 CREATE TABLE orders ( order_no VARCHAR(32) PRIMARY KEY, cust_name VARCHAR(50) NOT NULL, cust_phone VARCHAR(20) NOT NULL, cust_city VARCHAR(50) NOT NULL, order_date DATE NOT NULL ) ENGINE=InnoDB; -- 订单明细表 CREATE TABLE order_item ( order_no VARCHAR(32) NOT NULL, line_no INT NOT NULL COMMENT '行号,同一订单内唯一', product_name VARCHAR(100) NOT NULL, quantity INT NOT NULL, PRIMARY KEY (order_no, line_no) ) ENGINE=InnoDB;

这里用line_no而不是product_name作为明细的主键组成部分,是因为同一订单里可能出现同名的不同规格商品(比如“苹果”的两种产地),用商品名做主键会撞。行号是最稳妥的行内唯一标识。

5.3 第二步:拆掉明细表里的部分依赖,到达 2NF

order_item的主键是(order_no, line_no)。检查每个非主键字段:

  • product_name依赖的是商品本身,而不是订单或行号。但当前设计里商品没有独立标识,先补一个。
  • quantity依赖整行,没有问题。

引入商品表,明细表只保留商品 ID 和数量:

CREATE TABLE product ( product_id BIGINT PRIMARY KEY AUTO_INCREMENT, product_name VARCHAR(100) NOT NULL, category_id BIGINT, price DECIMAL(10,2) NOT NULL ) ENGINE=InnoDB; CREATE TABLE order_item ( order_no VARCHAR(32) NOT NULL, line_no INT NOT NULL, product_id BIGINT NOT NULL, quantity INT NOT NULL, PRIMARY KEY (order_no, line_no), KEY idx_product (product_id) ) ENGINE=InnoDB;

此时order_item只剩product_id和quantity两个非主键字段,都完全依赖整行,2NF 达成。顺带说一句,把price放在product表里有个隐患——商品调价后,历史订单的金额计算会错。正确做法是在order_item里冗余一个unit_price作为下单快照,这是前面说的“受控冗余”,属于故意为之。

5.4 第三步:剥掉订单表里的传递依赖,到达 3NF

orders表里的cust_name / cust_phone / cust_city描述的是客户实体,和订单只有“谁下的单”这一层关系。多个订单属于同一客户时,这些字段会重复存储,客户换手机号要改多行。这是典型的传递依赖:order_no → 客户标识 → 客户信息。

CREATE TABLE customer ( cust_id BIGINT PRIMARY KEY AUTO_INCREMENT, cust_name VARCHAR(50) NOT NULL, cust_phone VARCHAR(20) NOT NULL, cust_city VARCHAR(50) NOT NULL, level_id BIGINT ) ENGINE=InnoDB; CREATE TABLE customer_level ( level_id BIGINT PRIMARY KEY AUTO_INCREMENT, level_name VARCHAR(20) NOT NULL, discount DECIMAL(4,2) NOT NULL ) ENGINE=InnoDB; CREATE TABLE orders ( order_no VARCHAR(32) PRIMARY KEY, cust_id BIGINT NOT NULL, order_date DATE NOT NULL, KEY idx_cust (cust_id) ) ENGINE=InnoDB;

注意level_rate这一列的处理。它同时存在问题:一是它描述的是客户等级,不是订单;二是它又依赖于cust_level,构成两级传递。拆成独立的customer_level表后,折扣率只存一份,调整金卡折扣只需要改一行。

5.5 拆完之后的查询对比

拆完是五张表:orders、order_item、product、customer、customer_level。原来一条查询能拿到的订单详情,现在要 JOIN 四张表:

SELECT o.order_no, o.order_date, c.cust_name, c.cust_phone, l.level_name, l.discount, p.product_name, i.quantity, p.price, i.quantity * p.price AS line_amount FROM orders o JOIN customer c ON c.cust_id = o.cust_id JOIN customer_level l ON l.level_id = c.level_id JOIN order_item i ON i.order_no = o.order_no JOIN product p ON p.product_id = i.product_id WHERE o.order_no = 'D001';

看着复杂了,但每一跳都是主键或索引等值查找,实际耗时在毫秒级。而拆之前那条“一个字段里塞客户信息”的查询,想按城市统计订单量得靠字符串切分,根本没法走索引。性能的账不能只看 JOIN 次数,要看执行计划。

一个实践中的判断方法:如果拆表后某个查询的 JOIN 超过五张,先别急着怀疑范式,去看是不是少建了索引或者查询本身写得不对。真实业务里超过五表 JOIN 通常意味着报表需求,应该走单独的宽表或者读模型,而不是硬从主模型里查。

6. 范式化的边界:哪些表我劝你别硬拆

6.1 日志和事件流表天然是宽表

埋点日志、操作审计、消息流水这类表,写入量极大、几乎不更新、查询以时间范围扫描为主。它们的字段往往是“一次事件发生时的全部上下文快照”,比如用户当时的设备、版本、地理位置、页面路径。这些信息如果按范式拆成十几张维度表,写入时要做的 JOIN 和查询会非常重,而且事件本身不可变,根本不存在更新异常。

这类表的正确做法是:宽表落库,配合分区(按日期)、列式存储(如果用分析型数据库),以及定期的维度退化处理。不要在写入链路上做规范化。

6.2 拆表带来的读放大是真实成本

规范化的收益主要在写入侧(消除冗余更新、保证一致性),成本主要在读取侧(更多 JOIN)。如果你的系统读远多于写,比如内容型产品、商品详情页,那么适度的反范式是划算的。

常见的折中手段有三个。一是冗余展示字段,在列表页主表里冗余商品名、店铺名,避免列表页 JOIN;这些字段允许短暂不一致,用异步任务修正。二是读模型,用 CDC 或者定时任务把规范化的主模型同步成一张宽表供查询,主模型保持规范,查询走宽表。三是物化视图,如果数据库支持,直接把聚合结果物化下来,按需刷新。

这三种手段有个共同前提:主模型必须是规范化的。因为宽表、读模型、物化视图都是从主模型派生出来的,主模型一旦有冗余不一致,派生出来的东西全是错的,而且排查起来极其困难。

6.3 一份可以直接照着走的判断清单

下面这张表是我自己用的简化版判断流程,对着看就行:

现象大概率违反处理方式
字段里存逗号分隔的 ID 或列表1NF拆关联表,复合主键
字段里有固定分隔符的复合值且要单独检索1NF拆成多列
表用了复合主键,且存在只依赖其中一列的字段2NF把该字段连同其依赖列拆成新表
单列自增主键的表,存在描述另一个实体的字段3NF抽出实体表,原表存外键
存在 A 决定 B、B 决定 C 的字段链3NF沿着链逐层拆开
派生字段(汇总值、计算值)派生冗余看场景,能加对账就留,否则改为实时计算
历史快照类字段(下单时的价格、地址)不属于违规明确注释,保留

还有一点值得单独提醒。很多人学完 1NF 到 3NF,会好奇 BCNF、4NF、5NF 在哪。BCNF 处理的是“主键内部还有函数依赖”的边角情况,比如一张表有多个候选键且它们互相重叠;4NF 处理多值依赖,5NF 处理连接依赖。这几层在真实业务里遇到的概率比前三层低得多,绝大多数业务表做到 3NF 就够了。真遇到了,通常也能从 ER 模型的实体识别阶段就规避掉,不需要等到建表后再去套定义。

最后分享一个我在评审表结构时的小习惯:拿到一张新表,先不看字段名,只看主键。主键是单列自增的,我重点看有没有描述其他实体的字段;主键是复合的,我逐个字段问它依赖主键的哪一部分。这个动作花不了两分钟,但能在建表阶段挡掉大部分后面要返工的坑。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进