上期文章我们初步学习了 SQL 基础,完成了一些入门级的单表查询。但实际业务中,数据会分散在多张关联表中(如用户表、订单表、商品表)。要高效处理这类数据,就必须掌握多表关联 (解决"如何拼接多表数据")和约束(解决"如何保证数据正确")两大核心技能。
1.多表关联与约束的作用
假设将不同数据集中存储在同一张表(如合并用户、订单和商品信息),我们会遇到三个主要问题:首先,数据冗余 会导致用户手机号等字段被重复存储;其次,数据修改变得繁琐 ,比如更新手机号需要修改所有相关记录;最后,可能出现数据不一致的情况,例如订单关联到不存在的用户。
采用多表关联方案可以解决这些问题:多表关联 通过共享字段实现多表连接查询,有效整合分散的数据;同时,约束设置表级和字段级的规则,能够确保数据的完整性、一致性和唯一性,从根本上避免脏数据的产生。
2.约束核心子集
在学习多表关联之前,我们需要先了解**"主键+外键+非空"**这三个核心子集,这三个约束可看作"表的基本规矩",也是关联的"连接器"。
|------|-------------|-----------|-------|-----------------|-------------------------------|
| 约束类型 | 关键字 | 核心作用 | 是否唯一 | 允许NULL | 典型场景 |
| 主键约束 | PRIMARY KEY | 唯一标识每行数据 | 表内仅1个 | 禁止 | user_id、order_id |
| 外键约束 | FOREIGN KEY | 保证多表关联一致性 | 表内可多个 | 允许(需配合NOT NULL) | orders.user_id关联users.user_id |
| 非空约束 | NOT NULL | 字段必须有值 | 表内可多个 | 禁止 | username、product_name |
2.1 主键约束:表的"身份证"
一张表必须有主键,否则无法唯一定位记录。主键是约束的核心。
示例:
sql
sql
-- 方式1:单字段主键(最常用)
CREATE TABLE users (
user_id INT AUTO_INCREMENT PRIMARY KEY, -- 自增主键
username VARCHAR(50) NOT NULL
);
-- 方式2:复合主键(适用于中间表或无自增场景)
CREATE TABLE order_items (
order_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL,
-- 复合主键:一个订单的一个商品仅一条记录
PRIMARY KEY (order_id, product_id)
);
-- 建表后添加主键(不推荐,易遗漏数据)
ALTER TABLE users
ADD CONSTRAINT pk_users PRIMARY KEY (user_id);
其中复合主键需合并声明,而非单独加多个主键。
2.2 外键约束:多表的 "连接器"
外键是建立从表与主表关联 的关键字段,确保从表数据始终与主表记录对应。从表外键必须引用主表的主键或唯一键,并通过操作规则(ON DELETE/ON UPDATE)定义主表数据变更时的级联处理方式。
示例:
sql
-- 主表:商品表(被关联方)
CREATE TABLE products (
product_id INT PRIMARY KEY AUTO_INCREMENT,
product_name VARCHAR(100) NOT NULL
);
-- 从表:订单明细表(关联方)
CREATE TABLE order_items (
item_id INT PRIMARY KEY AUTO_INCREMENT,
order_id INT NOT NULL,
product_id INT NOT NULL, -- 外键关联商品表
-- 外键约束+操作规则
FOREIGN KEY (product_id) REFERENCES products(product_id)
ON DELETE RESTRICT -- 商品有订单明细时,禁止删除商品
ON UPDATE CASCADE -- 商品ID更新时,订单明细同步更新
);
外键操作重要规则
| 规则 | 解释 | 适用场景举例 |
|---|---|---|
| RESTRICT | 主表有从表关联数据 → 禁止删 / 改(默认) | 商品有订单,不能删商品 |
| CASCADE | 主表删 / 改 → 从表同步删 / 改 | 用户删除,同步删除其订单 |
| SET NULL | 主表删 / 改 → 从表外键设为 NULL | 部门解散,员工的部门 ID 设为 NULL |
2.3 非空约束:字段必填
有些信息不能缺失(比如用户名、商品名称),非空约束就是强制"必须填值",避免关键信息为空。
示例:
sql
-- 创建时添加
CREATE TABLE users (
username VARCHAR(50) NOT NULL, -- 用户名必须填
phone VARCHAR(20) -- 手机号可选填(默认允许NULL)
);
-- 修改表添加/删除非空约束
ALTER TABLE users MODIFY phone VARCHAR(20) NOT NULL; -- 手机号改为必须填
ALTER TABLE users MODIFY phone VARCHAR(20) NULL; -- 手机号改回可选填
3.基础多表关联实践
学习完上文的约束核心子集,我们来进行初步的实践。
我们可以创建两张表,先"不用外键"做关联查询,看看会发生什么。
sql
-- 1. 创建无外键的订单表
CREATE TABLE orders (
order_id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL, -- 没有外键约束
product_name VARCHAR(100) NOT NULL
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4; -- 兼容中文的字符集设置
-- 2. 插入数据:故意插一个不存在的user_id=3(用户表只有1和2)
INSERT INTO orders (user_id, product_name)
VALUES (1, '手机'), (3, '耳机');
-- 3. 关联查询:查用户和订单
SELECT u.username, o.order_id, o.product_name
FROM users u
JOIN orders o ON u.user_id = o.user_id;
在这之前我已手动创建好 users 表,并插入了两份数据。


之后向 order 表中插入两组数据,分别是(1, '手机') 和 (3, '耳机'),但是 user 表中是没有user_id 为3的用户的,然而由于订单表没有外键约束, (3, '耳机')这组数据能被成功插入。

此时以 user_id 相同为条件查询 user 表数据,只能得到 user_id 等于1的对象。

现在删除原 order 表,建立新的以 user_id 为外键约束的订单表。
sql
-- 重建订单表,加外键约束,设置utf8mb4字符集
CREATE TABLE orders (
order_id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
product_name VARCHAR(100) NOT NULL,
-- 外键约束:关联用户表,禁止插入无效user_id
FOREIGN KEY (user_id) REFERENCES users(user_id)
ON DELETE RESTRICT -- 有订单的用户不能删(避免孤儿订单)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4; -- 兼容中文的字符集设置`
再插入 (3, '耳机')一定会报错。如此便可以杜绝混乱数据。

4.关联语法JOIN
上面的实践代码里我们已经用了 JOIN(内连接)。实际应用中我们需先掌握四种常见 JOIN 类型。
| JOIN类型 | 作用 | 代码示例 | 结果特点 | |
|---|---|---|---|---|
| INNER JOIN(内连接) | 只取"两张表都匹配"的记录 | FROM users u JOIN orders o ON u.user_id=o.user_id | 无匹配记录全丢弃(如无订单的用户不显示) | |
| LEFT JOIN(左外连接) | 左表全保留,右表只取匹配的 | FROM users u LEFT JOIN orders o ON u.user_id=o.user_id | 左表完整,右表无匹配补NULL(如无订单用户的order_id为NULL) | |
| RIGHT JOIN(右外连接) | 右表全保留,左表只取匹配的 | FROM users u RIGHT JOIN orders o ON u.user_id=o.user_id | 右表完整,左表无匹配补NULL(如无效订单的username为NULL) | |
| FULL JOIN(全外连接) | 两张表全保留,不匹配补NULL | MySQL用UNION模拟(见下方) | 无遗漏记录,MySQL需用UNION拼接左右连接 |
sql
SELECT u.username, o.order_id
FROM users u LEFT JOIN orders o ON u.user_id=o.user_id
UNION -- 合并结果并去重
SELECT u.username, o.order_id
FROM users u RIGHT JOIN orders o ON u.user_id=o.user_id;
5.多表关联关系
复杂的多表场景都是以下三种关系的组合:
-
一对多(1:N):这是最常用的关系,例如一个用户可下多个订单,一个订单只属于一个用户。它的实现方式是"一"的表(用户表)设主键,"多"的表(订单表)加外键关联主键。
-
一对一(1:1): 例如一个用户只有一份身份证信息,一份身份证只对应一个用户。用子表加"唯一外键"关联主表主键实现。
-
多对多(N:N) :例如一个学生可选多门课,一门课可被多个学生选。这种关系较为复杂,两张表之间无法直接关联,而是需要创建"中间表",含两个外键分别关联两张主表主键,且外键组合为复合主键。
"中间表"示例:
sql
-- 中间表:学生-课程关联表
CREATE TABLE student_course (
student_id INT NOT NULL,
course_id INT NOT NULL,
-- 复合主键:保证一个学生选同一门课仅一次
PRIMARY KEY (student_id, course_id),
-- 双外键关联两张主表
FOREIGN KEY (student_id) REFERENCES students(student_id) ON DELETE CASCADE,
FOREIGN KEY (course_id) REFERENCES courses(course_id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 兼容中文的字符集设置
6.其他约束
最后再补充一些其他的约束。
-
唯一约束(UNIQUE):字段"不能重复"。比如手机号、邮箱不能重复注册,用唯一约束实现。(ADD COLUMN phone VARCHAR(20) UNIQUE NOT NULL; -- 手机号唯一且必填)
-
检查约束(CHECK):字段"符合条件",限制字段值的范围。(ADD CHECK (price >= 0); -- 价格不能为负)
-
默认约束(DEFAULT):字段"自动填值",插入数据时若未传值,自动填默认值。(ADD COLUMN status VARCHAR(20) DEFAULT '待付款'; -- 默认待付款)