【零基础入门】SQL 核心语法精讲:外键约束与多表查询全解析(进阶篇)
目录
本文承接上篇《SQL 核心语法精讲:约束(Constraint)全解析(单表约束篇)》https://blog.ZEEKLOG.net/m0_74398756/article/details/158694461?fromshare=blogdetail&sharetype=blogdetail&sharerId=158694461&sharerefer=PC&sharesource=m0_74398756&sharefrom=from_linkhttps://blog.ZEEKLOG.net/m0_74398756/article/details/158694461?fromshare=blogdetail&sharetype=blogdetail&sharerId=158694461&sharerefer=PC&sharesource=m0_74398756&sharefrom=from_link,上篇我们掌握了主键、非空、唯一、默认四大单表约束,解决了单表内数据的完整性问题。但在真实业务开发中,数据从来都不是孤立存在的 —— 商品与分类、用户与订单、学生与班级,都存在强关联关系。
本文将系统讲解外键约束(多表约束) 与多表查询两大 SQL 进阶核心,从基础原理到实战案例,再到避坑指南,零基础也能一次性吃透!
一、外键约束(Foreign Key)基础认知
1.1 什么是外键约束
外键约束是用于建立和强制两张数据表之间关联关系的约束,是保证多表之间参照完整性的核心手段。
- 主表(父表):被引用的表,提供关联的基准数据,外键引用的列必须是主表的主键或唯一键
- 从表(子表):添加外键约束的表,其外键列的值必须引用主表中已存在的记录,避免出现 “脏关联数据”
举个例子:我们有分类表category(主表)和商品表goods(从表),商品的分类 ID 必须关联分类表中已存在的分类主键,这样就不会出现 “商品归属了一个不存在的分类” 的问题,从根源上保证了多表数据的一致性。
1.2 外键约束的核心特点
- 引用限制:外键列必须引用主表的主键列或唯一约束列(只有唯一值的列才能作为关联基准)
- 类型匹配:从表外键列的数据类型、长度、字符集必须与主表引用列完全一致,否则无法创建外键
- 完整性校验:主表中不存在的记录,从表无法插入;从表中已关联的记录,主表无法随意删除 / 修改主键值
- 数量无限制:一张表可以创建多个外键,分别关联不同的主表
- 空值允许:外键列默认允许为
NULL(除非额外添加NOT NULL约束),为NULL代表该条记录未关联主表数据 - 引擎要求:MySQL 中必须使用
InnoDB引擎(MyISAM 引擎不支持外键约束),Oracle、SQL Server 等主流数据库均原生支持外键
1.3 外键约束的级联动作
当主表的记录被删除或更新时,我们可以通过级联动作定义从表关联记录的处理规则,语法为ON DELETE 动作 ON UPDATE 动作,核心支持 4 种级联行为:
| 级联动作 | 核心规则 | 适用场景 |
|---|---|---|
RESTRICT/NO ACTION | 默认行为,拒绝主表的删除 / 更新操作,只要从表存在对应关联记录 | 绝大多数生产环境,严格保证数据安全 |
CASCADE | 级联操作,主表删除 / 更新主键时,从表关联记录同步删除 / 更新外键值 | 强关联的父子表,如订单与订单详情 |
SET NULL | 主表删除 / 更新时,从表关联的外键列自动设为NULL(前提是外键列允许为 NULL) | 非强关联场景,允许记录无关联主表 |
SET DEFAULT | 主表删除 / 更新时,从表关联外键列自动设为默认值(需配合默认约束使用,MySQL 中 InnoDB 不支持该特性) | 极少使用 |
二、外键约束实战操作
2.1 环境准备
承接上篇的演示环境,保证学习连贯性:
-- 沿用上篇的演示数据库 USE day02; -- 关闭外键校验(批量操作时使用,避免约束报错,常规操作建议开启) -- SET FOREIGN_KEY_CHECKS = 0; -- 开启外键校验(默认开启,强制约束校验) SET FOREIGN_KEY_CHECKS = 1;2.2 场景 1:建表时创建外键约束(推荐)
实际开发中,建议先创建主表,再创建从表,同时显式指定外键名称(方便后续维护)。
步骤 1:创建主表(分类表)
-- 分类表:主表,提供分类基准数据 DROP TABLE IF EXISTS category; CREATE TABLE category( cid INT PRIMARY KEY AUTO_INCREMENT, -- 分类ID:主键+自增 cname VARCHAR(20) NOT NULL UNIQUE -- 分类名:非空+唯一 ); -- 插入主表测试数据 INSERT INTO category VALUES (NULL, '电子产品'), (NULL, '服装鞋帽'), (NULL, '食品生鲜'), (NULL, '家居用品');步骤 2:创建从表(商品表),添加外键约束
-- 商品表:从表,关联分类表 DROP TABLE IF EXISTS goods; CREATE TABLE goods( gid INT PRIMARY KEY AUTO_INCREMENT, -- 商品ID:主键+自增(上篇拓展练习要求) gname VARCHAR(50) NOT NULL, -- 商品名:非空约束 gcode VARCHAR(30) UNIQUE, -- 商品编号:唯一约束 stock INT DEFAULT 0, -- 库存:默认约束 cid INT, -- 分类ID:外键列,关联分类表主键 -- 表级定义外键约束,显式指定外键名,规范且便于后续管理 CONSTRAINT fk_goods_category FOREIGN KEY (cid) REFERENCES category(cid) -- 可选:添加级联动作,默认是RESTRICT -- ON DELETE CASCADE ON UPDATE CASCADE ); -- 查看表结构,验证约束 DESC goods; -- 查看表的详细创建语句,确认外键配置 SHOW CREATE TABLE goods;步骤 3:验证外键约束效果
-- 案例1:正常插入,分类ID在主表中存在,插入成功 INSERT INTO goods VALUES (NULL, 'iPhone 16', 'IP16-2026', 100, 1); INSERT INTO goods VALUES (NULL, '男士纯棉T恤', 'TS-002', 500, 2); -- 案例2:插入不存在的分类ID,触发外键约束报错 -- 报错:Cannot add or update a child row: a foreign key constraint fails INSERT INTO goods VALUES (NULL, '进口红酒', 'WINE-001', 50, 99); -- 案例3:删除从表已关联的主表记录,触发外键约束报错 -- 报错:Cannot delete or update a parent row: a foreign key constraint fails DELETE FROM category WHERE cid = 1;
2.3 场景 2:建表后追加外键约束
若表已创建完成,可通过ALTER TABLE语句追加外键约束:
-- 先删除已有的外键约束(演示用) ALTER TABLE goods DROP FOREIGN KEY fk_goods_category; -- 追加外键约束 ALTER TABLE goods ADD CONSTRAINT fk_goods_category FOREIGN KEY (cid) REFERENCES category(cid);
2.4 场景 3:级联操作实战
案例 1:级联删除(ON DELETE CASCADE)
-- ============================================= -- 测试场景:外键约束 - 级联删除(ON DELETE CASCADE) -- 执行状态:已成功(2026-03-08) -- ============================================= -- 步骤1:清理环境,解除外键关联 DROP TABLE IF EXISTS goods; TRUNCATE TABLE category; -- 步骤2:初始化主表数据 INSERT INTO category VALUES (NULL, '电子产品'), (NULL, '服装鞋帽'), (NULL, '食品生鲜'), (NULL, '家居用品'); -- 步骤3:创建从表并配置级联删除 CREATE TABLE goods( gid INT PRIMARY KEY AUTO_INCREMENT, gname VARCHAR(50) NOT NULL, gcode VARCHAR(30) UNIQUE, stock INT DEFAULT 0, cid INT, CONSTRAINT fk_goods_category FOREIGN KEY (cid) REFERENCES category(cid) ON DELETE CASCADE ); -- 步骤4:插入测试数据 INSERT INTO goods VALUES (NULL, 'iPhone 16', 'IP16-2026', 100, 1), (NULL, '无线蓝牙耳机', 'BT-001', 200, 1); -- 步骤5:触发级联删除并验证 DELETE FROM category WHERE cid = 1; SELECT * FROM goods; -- 结果应为空,验证级联删除成功
案例 2:级联更新(ON UPDATE CASCADE)
-- 重建商品表,添加级联更新规则 DROP TABLE IF EXISTS goods; CREATE TABLE goods( gid INT PRIMARY KEY AUTO_INCREMENT, gname VARCHAR(50) NOT NULL, gcode VARCHAR(30) UNIQUE, stock INT DEFAULT 0, cid INT, CONSTRAINT fk_goods_category FOREIGN KEY (cid) REFERENCES category(cid) ON UPDATE CASCADE -- 级联更新:主表主键修改,从表外键同步更新 ); -- 插入测试数据 INSERT INTO goods VALUES (NULL, '男士纯棉T恤', 'TS-002', 500, 2); -- 更新主表中分类ID,将cid=2改为cid=20 UPDATE category SET cid = 20 WHERE cid = 2; -- 查看商品表:对应商品的cid已自动更新为20 SELECT * FROM goods;
案例 3:SET NULL 级联规则
-- 重建商品表,添加SET NULL规则 DROP TABLE IF EXISTS goods; CREATE TABLE goods( gid INT PRIMARY KEY AUTO_INCREMENT, gname VARCHAR(50) NOT NULL, gcode VARCHAR(30) UNIQUE, stock INT DEFAULT 0, cid INT, -- 必须允许为NULL,否则SET NULL规则不生效 CONSTRAINT fk_goods_category FOREIGN KEY (cid) REFERENCES category(cid) ON DELETE SET NULL -- 主表记录删除,从表关联外键设为NULL ); -- 插入测试数据 INSERT INTO goods VALUES (NULL, '进口车厘子', 'FR-004', 150, 3); -- 删除主表中cid=3的分类 DELETE FROM category WHERE cid = 3; -- 查看商品表:对应商品的cid已变为NULL SELECT * FROM goods;
2.5 外键约束删除操作
-- 语法:ALTER TABLE 从表名 DROP FOREIGN KEY 外键名; ALTER TABLE goods DROP FOREIGN KEY fk_goods_category;
关键提示:删除外键必须通过外键名操作,这也是建表时显式指定外键名的核心原因。若未指定外键名,可通过SHOW CREATE TABLE 表名;查看数据库自动生成的外键名。2.6 外键约束实操避坑指南
引擎错误:MySQL 中必须使用InnoDB引擎,MyISAM 不支持外键约束,建表时需确认引擎配置。
引用列不合法:外键必须引用主表的主键或唯一约束列,否则创建外键会直接失败。
数据类型不匹配:从表外键列与主表引用列的类型、长度、是否无符号等属性必须完全一致,比如主表是INT UNSIGNED,从表是INT,会创建失败。
脏数据导致创建失败:追加外键时,若从表已存在主表中没有的外键值,会创建失败,必须先清理脏数据。
外键名重复:同一个数据库中,外键名必须全局唯一,不可重复。
级联操作风险:生产环境慎用ON DELETE CASCADE级联删除,极易因误操作导致批量数据丢失,建议通过业务逻辑控制数据删除。
数据操作顺序:插入数据时,先插主表、再插从表;删除数据时,先删从表、再删主表,否则会触发外键约束报错。
三、多表查询核心实战
单表查询只能获取一张表的数据,而真实业务中,我们几乎都需要从多张关联表中获取数据,比如 “查询商品名及其对应的分类名”、“查询用户的订单信息”,这就需要用到多表查询。
3.1 多表查询基础认知
3.1.1 核心前提
多表查询的核心是正确的关联条件,缺少关联条件会产生笛卡尔积—— 两张表的行数相乘,生成大量无效数据,这是新手最常见的错误。
举个例子:分类表 4 条数据,商品表 8 条数据,无关联条件查询会生成 32 条结果,绝大多数都是无效的错误数据。
3.1.2 多表查询分类
主流多表查询分为 4 大类,覆盖 99% 的业务场景:
内连接查询(INNER JOIN)外连接查询(LEFT JOIN / RIGHT JOIN)子查询(嵌套查询)联合查询(UNION / UNION ALL)
3.1.3 测试数据准备
为了方便演示,先重置并补全测试数据:
-- 重置主表数据 TRUNCATE TABLE category; INSERT INTO category VALUES (NULL, '电子产品'), (NULL, '服装鞋帽'), (NULL, '食品生鲜'), (NULL, '家居用品'), (NULL, '图书音像'); -- 新增无商品的分类,用于外连接演示 -- 重置从表数据 TRUNCATE TABLE goods; INSERT INTO goods VALUES (NULL, 'iPhone 16', 'IP16-2026', 100, 1), (NULL, '无线蓝牙耳机', 'BT-001', 200, 1), (NULL, '男士纯棉T恤', 'TS-002', 500, 2), (NULL, '女士运动鞋', 'SH-003', 300, 2), (NULL, '进口车厘子', 'FR-004', 150, 3), (NULL, '方便面整箱', 'FN-005', 800, 3), (NULL, '纯棉四件套', 'HT-006', 120, 4), (NULL, '无主分类商品', 'NA-007', 50, NULL); -- 无分类商品,用于外连接演示
3.2 内连接查询(INNER JOIN)
核心特点
取两张表中完全匹配关联条件的记录,也就是两张表的 “交集”,不匹配的记录不会出现在结果中。
两种语法格式
1. 隐式内连接(不推荐)
通过逗号分隔表,在WHERE子句中写关联条件,缺点是关联条件与筛选条件混在一起,可读性差。
-- 查询商品名、库存、对应的分类名 SELECT g.gname, g.stock, c.cname FROM goods g, category c -- 表别名简化代码,必备技巧 WHERE g.cid = c.cid; -- 关联条件,避免笛卡尔积 结果说明:无分类的商品、无商品的分类,都不会出现在结果中。
2. 显式内连接(推荐)
通过INNER JOIN ... ON ...语法,关联条件写在ON子句中,筛选条件写在WHERE子句中,职责分离,可读性高,是企业开发的标准写法。
-- 基础查询:INNER 可省略,直接写 JOIN 效果一致 SELECT g.gid, g.gname, c.cname, g.stock FROM goods g INNER JOIN category c ON g.cid = c.cid; -- 关联条件 -- 带筛选条件的内连接:只查询电子产品分类的商品 SELECT g.gid, g.gname, c.cname, g.stock FROM goods g JOIN category c ON g.cid = c.cid WHERE c.cname = '电子产品';3.3 外连接查询(OUTER JOIN)
核心特点
以某一张表为基表,基表的所有记录都会完整显示,另一张表匹配关联条件的记录正常显示,匹配不上的字段填充NULL。
外连接分为两大类,是业务开发中使用频率最高的多表查询语法。
1. 左外连接(LEFT JOIN)
核心规则
LEFT关键字左侧的表为左表(基表),左表的所有记录都会显示,右表匹配关联条件的显示,匹配不上的字段填充NULL。
-- 需求:查询所有商品,以及对应的分类名,包括没有分类的商品 SELECT g.gid, g.gname, c.cname, g.stock FROM goods g -- 左表:商品表,基表 LEFT JOIN category c -- 右表:分类表 ON g.cid = c.cid; 
结果说明:8 条商品全部显示,其中 “无主分类商品” 的 cname 字段为NULL,完美满足需求。2. 右外连接(RIGHT JOIN)
核心规则
RIGHT关键字右侧的表为右表(基表),右表的所有记录都会显示,左表匹配关联条件的显示,匹配不上的字段填充NULL。
-- 需求:查询所有分类,以及分类下的商品,包括没有商品的分类 SELECT c.cid, c.cname, g.gname, g.stock FROM goods g -- 左表:商品表 RIGHT JOIN category c -- 右表:分类表,基表 ON g.cid = c.cid; 
结果说明:5 个分类全部显示,其中 “图书音像” 分类的 gname、stock 字段为NULL。关键补充
- MySQL 不支持全外连接
FULL JOIN,若需实现 “两张表的所有记录都显示”,可通过LEFT JOIN UNION RIGHT JOIN实现。
外连接的超级大坑:筛选条件写在 ON 和 WHERE 中的区别
-- 错误写法:筛选条件写在WHERE中,会导致左连接失效,变成内连接 -- 需求是“查询所有商品,只显示电子产品的分类名”,结果只显示了电子产品的商品 SELECT g.gid, g.gname, c.cname FROM goods g LEFT JOIN category c ON g.cid = c.cid WHERE c.cname = '电子产品'; -- 正确写法:筛选条件写在ON中,左表所有商品完整显示,仅匹配的分类名显示 SELECT g.gid, g.gname, c.cname FROM goods g LEFT JOIN category c ON g.cid = c.cid AND c.cname = '电子产品'; 
核心区别:外连接中,ON是连接前的筛选,不影响基表的完整显示;WHERE是连接后的筛选,会过滤掉不满足条件的记录,直接导致外连接失效。
3.4 子查询(嵌套查询)
核心特点
一个查询语句的结果,作为另一个查询语句的条件、数据源,嵌套在WHERE、FROM、HAVING子句中。适合分步拆解复杂需求,零基础也能快速上手。
根据结果集的格式,分为 3 大类:
1. 单行单列子查询(标量子查询)
子查询结果只有一个值,配合=、>、<、<>等比较运算符使用。
-- 需求1:查询“电子产品”分类下的所有商品 -- 分步逻辑:先查电子产品的分类ID,再查对应商品 SELECT * FROM goods WHERE cid = (SELECT cid FROM category WHERE cname = '电子产品'); -- 需求2:查询库存大于全平台平均库存的商品 SELECT * FROM goods WHERE stock > (SELECT AVG(stock) FROM goods); 
2. 多行单列子查询
子查询结果是一列多个值,配合IN、ANY、ALL运算符使用。
-- 需求1:查询有商品的分类信息 SELECT * FROM category WHERE cid IN (SELECT DISTINCT cid FROM goods WHERE cid IS NOT NULL); -- 需求2:查询库存比所有食品生鲜分类商品都高的商品 SELECT * FROM goods WHERE stock > ALL (SELECT stock FROM goods WHERE cid = (SELECT cid FROM category WHERE cname = '食品生鲜')); 
3. 多行多列子查询(表子查询)
子查询结果是多行多列的临时表,必须放在FROM子句中,且必须给临时表设置别名。
-- 需求:查询每个分类的平均库存,以及分类名称 SELECT c.cname, temp.avg_stock FROM category c JOIN ( -- 子查询:统计每个分类的平均库存,生成临时表 SELECT cid, AVG(stock) avg_stock FROM goods GROUP BY cid ) temp ON c.cid = temp.cid; 
3.5 联合查询(UNION / UNION ALL)
核心特点
将多个SELECT查询的结果集,合并成一个结果集。核心要求:多个查询的列数、列的数据类型必须完全一致,结果的列名以第一个查询为准。
核心区别
| 语法 | 核心特点 | 性能 |
|---|---|---|
UNION | 合并结果集后自动去重,去除重复记录 | 较低(需额外排序去重) |
UNION ALL | 直接合并所有结果,不去重 | 极高(无额外开销,推荐优先使用) |
-- 需求:合并查询库存大于500的商品,和电子产品分类的商品 -- UNION 会自动去重,重复记录只保留一条 SELECT * FROM goods WHERE stock > 500 UNION SELECT * FROM goods WHERE cid = 1; -- UNION ALL 直接合并,保留所有记录,无去重开销 SELECT * FROM goods WHERE stock > 500 UNION ALL SELECT * FROM goods WHERE cid = 1;
3.6 多表查询避坑指南
缺少关联条件,产生笛卡尔积:这是新手最致命的错误,多表查询必须先写关联条件,再写筛选条件。
内连接与外连接混淆:明确需求的基表,需要完整显示某张表的所有记录,必须用外连接,而非内连接。
表别名与列归属不明确:多表查询中,若两张表有同名字段,必须通过表别名.列名明确列的归属,否则会报错或结果错误。
外连接筛选条件位置错误:外连接中,基表的筛选条件写在WHERE,关联表的筛选条件写在ON,否则会导致外连接失效。UNION与UNION ALL滥用:无去重需求时,必须用UNION ALL,避免不必要的性能开销。
子查询嵌套过深:子查询建议不超过 3 层,嵌套过深会导致可读性和性能急剧下降,可通过连接查询优化。
四、核心要点总结
- 外键约束是多表参照完整性的核心,主表提供关联基准,从表添加外键,必须使用 InnoDB 引擎,引用主表的主键 / 唯一键。
- 外键级联动作中,
RESTRICT是默认安全选项,CASCADE级联删除生产环境慎用,SET NULL需保证外键列允许为空。 - 多表查询的核心是正确的关联条件,必须避免笛卡尔积,企业开发中优先使用显式内连接和左外连接。
- 内连接取两张表的交集,左外连接以左表为基表完整显示所有记录,右外连接反之。
- 子查询适合分步拆解复杂需求,标量子查询、多行子查询对应不同的运算符,表子查询需设置别名。
- 无去重需求时,
UNION ALL的性能远高于UNION,是合并结果集的首选。
五.拓展练习(承接上篇,连贯提升)
- 创建 3 张关联表:用户表
user(用户 ID 主键自增、用户名非空唯一、手机号唯一)、订单表orders(订单 ID 主键自增、用户 ID 外键关联用户表、下单时间、订单金额)、订单详情表order_item(详情 ID 主键自增、订单 ID 外键关联订单表、商品 ID 外键关联商品表、购买数量、单价),实现多表外键关联。 - 用内连接查询用户的订单信息,包含用户名、订单 ID、下单时间、订单金额。
- 用左连接查询所有用户,以及对应的订单数量,没有订单的用户订单数量显示 0。
- 用子查询查询购买了 “iPhone 16” 的用户信息。
- 测试外键的级联删除功能,删除订单时,同步删除对应的订单详情。