本节摘要:关系型数据库用二维表组织数据,用键建立表间联系,用完整性约束守住数据质量。本节把表、元组、属性、域、主键、外键这六个底层概念逐一拆开,它们是全书每一次评审讨论的通用语言。位置:全书起点,先有这套词汇,后面的设计与优化才有讨论的地基。
2019 年我接过一个图书借阅系统的二开需求。前任开发者把读者数据存在一个文本文件里,每行一条,逗号分隔。系统跑了一年,需求来了:统计每个校区借书最多的前十名读者。就这么一个需求,让维护者折腾了整整两天——因为有人昵称里带逗号,有人手机号后面多了个空格,还有三百多条记录的"借出日期"写成"3月5号"而不是标准格式。数据能查,但不敢信;能统计,但要先人工清洗。
这个故事的教训不是"文本文件不好",而是:当数据有了结构、有了关系、有了并发修改,就需要一套比文件系统更严格的组织纪律。关系型数据库就是这套纪律的工程化实现。它用三样东西建立纪律:一张张结构统一的二维表(结构统一)、表与表之间通过键连接(关系显式声明)、字段级别的完整性约束(非法数据进不来)。MySQL 是这套纪律目前世界上最流行的开源实现。
关系模型由 E.F. Codd 在 1970 年提出,数学根基是集合论。教科书术语和日常叫法有偏差,评审会上说错了会显得不专业,这里一次对齐:
| 术语 | 日常叫法 | 准确定义 |
|---|---|---|
| 关系 Relation | 表 | 一个二维结构,行无序、列无序、行不重复 |
| 元组 Tuple | 行/记录 | 表中的一条数据 |
| 属性 Attribute | 列/字段 | 表中的一列,有名字有类型 |
| 域 Domain | 取值范围 | 属性的合法值集合,如"性别"的域是固定的两个值 |
| 候选键 Candidate Key | —— | 能唯一标识一行的最小属性组,可有多个 |
| 主键 Primary Key | 主键 | 从候选键中选定的那个,全表唯一且非空 |
两个容易被忽略的细节值得单独强调。其一,"行无序"是关系模型的数学承诺——你不能依赖"新插入的行排在最后"这种假设,第 4 章讲索引时会看到查询结果顺序由执行计划决定。其二,"行不重复"靠主键保证,没有主键的表在 InnoDB 里其实也会被偷偷塞一个隐藏列当主键,代价是你失去了主动控制数据物理位置的能力。
光有表还不够,业务数据天然是关联的:读者借书、订单包含商品、商品属于分类。关系型数据库的做法是在表里放一个外键(Foreign Key)——指向另一张表主键的列,声明"这一行的业务含义依赖那张表的某一行"。
用一个被评审会反复追问的例子说明。订单系统的最小结构:
CREATE TABLE customer ( customer_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(64) NOT NULL, phone CHAR(11) NOT NULL ) ENGINE=InnoDB; CREATE TABLE orders ( order_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, customer_id BIGINT UNSIGNED NOT NULL, amount DECIMAL(10,2) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_order_customer FOREIGN KEY (customer_id) REFERENCES customer(customer_id) ) ENGINE=InnoDB;
外键声明之后,数据库替你守住两条底线:往 orders 插一条 customer_id 等于 99999 的订单——如果客户表里没有 99999,直接报错,脏数据进不来;反过来,要删除一个还有订单的客户,默认也会被拒绝。这两条防线就是引用完整性(Referential Integrity)。
完整走一遍(背景→操作→结果→解读→变式)。背景:验证外键是否真的在工作。操作:先插客户再插订单,然后故意插一条坏订单。
INSERT INTO customer (name, phone) VALUES ('陈晓', '13800001111'); -- 结果:Query OK, 1 row affected INSERT INTO orders (customer_id, amount) VALUES (1, 59.90); -- 结果:Query OK, 1 row affected INSERT INTO orders (customer_id, amount) VALUES (88888, 59.90); -- 结果:ERROR 1452 Cannot add or update a child row: -- a foreign key constraint fails
解读:第三条语句在到达存储引擎之前就被 InnoDB 拒绝,因为 88888 在 customer 表中无对应主键值。注意报错发生在插入时而不是查询时——这正是数据库层校验和应用层校验的本质区别:它不依赖每一个写代码的人都记得做检查。
变式一:如果业务允许"先有订单、后补客户资料"(比如导入历史数据),可以声明外键但插入时临时关闭校验,或者在逻辑层改用"延迟关联"。变式二:互联网大厂常用做法是不建物理外键,只建逻辑外键(同样的列、同样的查询,但不下 FOREIGN KEY 约束),把校验放在应用层——换来的是写入性能和分库分表的自由度,代价是脏数据风险由应用承担。两种路线没有绝对对错,评审会上关键是把取舍说出口,而不是默认选一个。
误解一:"主键就是编号列"。 主键是"能唯一标识一行的最小属性组",身份证号、手机号在理论上都可作为客户表的候选键。选自增 ID 当主键的理由是稳定(业务标识会变,如换手机号)、短小(主键会被二级索引复制,越长索引越胖)、无业务含义(不暴露信息)——这是工程选择,不是定义。
误解二:"加个外键更安全,那就都加上"。 每个外键在写入时都要做一次父表检查,批量导入时这个检查会成为明显瓶颈;高并发场景下外键还引入死锁风险。标准做法:核心强一致关系(如资金流水)用物理外键,其余用逻辑外键加应用层校验。
误解三:"关系型数据库什么都能存"。 图片、视频这类大二进制对象放进表里会把缓冲池撑爆,拖垮整库性能。正确姿势是表里只存对象存储的引用键。关系模型的强项是结构化、强关联、强一致的数据,识别数据形态本身就是设计能力的一部分。
回到评审会。这一节的知识点转化成评审委员的提问就是:主键选了什么、为什么;哪些关系用了物理外键、哪些是逻辑外键、理由;有没有大二进制对象混进表里。
下一节我们离开数据模型,跟着一条 SQL 语句走一遍 MySQL 的内部结构——你优化的每一处,都对应这条路径上的某一层。