news 2026/5/15 23:08:59

联合查询详解:内连接、外连接与自连接

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
联合查询详解:内连接、外连接与自连接

在前面几篇文章中,我们已经多次使用JOIN来将两张或更多表的数据组合在一起查询。现在是时候系统地掌握联合查询的各种类型了。联合查询是关系型数据库最强大的特性之一,它将分散在不同表中的数据通过关联条件“连接”起来,让我们能够回答跨越多个实体的业务问题。

本文将详细讲解:

  • 内连接(INNER JOIN)——只返回匹配的行
  • 外连接(LEFT JOINRIGHT JOIN)——保留不匹配的行
  • 多表连接——超过两张表的联合查询
  • 自连接(Self Join)——同一张表连接自身

实战部分将围绕我们的图书管理系统,查询每位读者的借阅记录(含图书名称),并深入练习自连接场景。


1. 什么是 JOIN?

关系型数据库设计中,我们通常将数据拆分到不同的表中,以消除冗余(范式化)。但业务查询时往往需要把这些分散的信息重新拼起来。JOIN就是用来“拼”数据的机制。

连接的核心要素

  • 连接条件:指定两张表如何关联,通常是通过外键与主键的等值比较(如a.reader_id = b.id)。
  • 连接类型:决定了当某行在对方表中没有匹配时的处理方式(保留还是丢弃)。

MySQL 中支持的连接类型包括:INNER JOINLEFT [OUTER] JOINRIGHT [OUTER] JOIN,以及CROSS JOINOUTER关键字可省略。


2. 内连接:INNER JOIN

内连接是最常用的连接,它返回两个表中满足连接条件的所有行。如果某行在对方表中没有匹配,则不会出现在结果中。

2.1 基本语法

SELECT列列表FROM表AINNERJOIN表BON连接条件[WHERE过滤条件];

ON后面是连接条件,通常使用=进行匹配。

2.2 示例:查询借阅记录及对应的图书名称

在我们的图书管理系统中,borrow_records表存储借阅的book_id,要显示书名就需要连接books表。

SELECTbr.idASrecord_id,b.title,br.borrow_date,br.due_dateFROMborrow_records brINNERJOINbooks bONbr.book_id=b.id;

只有book_idbooks表中存在的借阅记录才会返回。由于我们有外键约束,正常情况下所有记录都能匹配。但如果我们删除了某本书而没有级联删除借阅记录,那么对应的book_id就无法匹配,使用INNER JOIN这些记录会被“吞掉”(不显示)。

使用 WHERE 进一步过滤

SELECTb.title,br.borrow_dateFROMborrow_records brINNERJOINbooks bONbr.book_id=b.idWHEREbr.borrow_date>='2025-03-01';

2.3 隐式内连接(SQL-89 风格)

早期 SQL 写法也支持在FROM中列出多张表,并在WHERE中写连接条件:

SELECTb.title,br.borrow_dateFROMborrow_records br,books bWHEREbr.book_id=b.id;

这种写法功能与INNER JOIN相同,但可读性和维护性较差,容易忘记连接条件变成笛卡尔积。推荐使用显式JOIN ... ON语法


3. 外连接:保留不匹配的行

外连接与内连接的区别在于:即使对方表中没有匹配的行,也会保留“主表”中的行,并用NULL填充对方表的列。

3.1 左外连接:LEFT JOIN

LEFT JOIN保留左表(FROM 后的第一张表)的所有行,右表中无匹配的行则填NULL

语法:

SELECT列列表FROM左表LEFTJOIN右表ON连接条件;

需求:列出所有读者,包括那些没有借阅记录的读者

SELECTr.name,br.idASrecord_id,br.borrow_dateFROMreaders rLEFTJOINborrow_records brONr.id=br.reader_idORDERBYr.name,br.borrow_date;

结果中,没有任何借阅记录的读者会显示一行,其record_idborrow_dateNULL。用LEFT JOIN可以轻松找出“从未借书的读者”。

统计每位读者的借阅次数(包括 0 次)

SELECTr.name,COUNT(br.id)ASborrow_countFROMreaders rLEFTJOINborrow_records brONr.id=br.reader_idGROUPBYr.id,r.name;

COUNT(br.id)只统计非 NULL 的借阅记录,所以没有借阅的读者计数为 0。如果使用INNER JOIN,他们将完全不出现。

3.2 右外连接:RIGHT JOIN

RIGHT JOIN保留右表的所有行,左表无匹配的行填NULL

语法:

SELECT列列表FROM左表RIGHTJOIN右表ON连接条件;

实际上,RIGHT JOIN可以通过交换表顺序用LEFT JOIN实现。例如:

-- 列出所有图书及其被借阅记录(包括从未被借过的书)SELECTb.title,br.borrow_dateFROMborrow_records brRIGHTJOINbooks bONbr.book_id=b.id;

等价于:

SELECTb.title,br.borrow_dateFROMbooks bLEFTJOINborrow_records brONb.id=br.book_id;

推荐始终使用LEFT JOIN,将“主表”放在左边,思维更直观。

3.3 全外连接(FULL OUTER JOIN)

MySQL 不直接支持FULL OUTER JOIN,但可以通过LEFT JOINRIGHT JOINUNION来模拟,它将保留两个表中的所有行。实际业务中较少使用。


4. 多表连接

连接不仅限于两张表,我们可以连续JOIN多张表来获取更丰富的信息。

需求:查询每位读者的借阅记录,包含读者姓名、图书名称、分类名称

这涉及四张表:readersborrow_recordsbooksbook_categorycategories(五张)。

SELECTr.nameASreader,b.titleASbook,c.nameAScategory,br.borrow_date,br.due_dateFROMreaders rJOINborrow_records brONr.id=br.reader_idJOINbooks bONbr.book_id=b.idLEFTJOINbook_category bcONb.id=bc.book_idLEFTJOINcategories cONbc.category_id=c.idORDERBYr.name,br.borrow_date;

注意

  • 图书可能没有分类(使用LEFT JOIN保证图书信息不丢失)。
  • 读者和借阅记录之间使用INNER JOINLEFT JOIN取决于是否要包含没有借阅的读者。

连接顺序与性能
MySQL 优化器会根据统计信息决定实际的连接顺序,不一定会按照你写的顺序执行。但作为开发者,我们可以把筛选力强的表写在前面(或在ON/WHERE中提前过滤),提升可读性。后续索引优化阶段会深入探讨连接优化。


5. 自连接:同一张表连接自身

自连接是一种特殊的连接,它把同一张表当作两张独立的表来使用,通过不同的别名区分。自连接通常用来查询表内部的层级关系或配对关系。

5.1 场景举例

  • 员工表中,每个员工有一个manager_id指向同表中经理的id
  • 分类表中,一个分类可能有父分类(parent_id指向自己表中的id)。
  • 在图书系统中,如果要找出“同一作者的其他书籍”,也可以用自连接。

5.2 语法与示例

给同一张表取两个不同的别名,然后像连接两张不同的表一样操作。

示例:找出同一作者出版的除自己外的其他图书

SELECTa.idASbook_id,a.titleASbook_title,b.idASother_book_id,b.titleASother_book_titleFROMbooks aJOINbooks bONa.author=b.authorANDa.id<>b.idORDERBYa.author,a.id;
  • ab都是books表的别名。
  • 连接条件:作者相同 (a.author = b.author),但书不同 (a.id <> b.id)。

扩展:构造一个简单的员工表练习自连接

-- 创建员工表CREATETABLEemployees(idINTPRIMARYKEY,nameVARCHAR(50),manager_idINT);INSERTINTOemployeesVALUES(1,'CEO张',NULL),(2,'经理李',1),(3,'经理王',1),(4,'员工赵',2),(5,'员工孙',2),(6,'员工周',3);-- 查询每位员工及其经理的姓名SELECTe.nameASemployee,m.nameASmanagerFROMemployees eLEFTJOINemployees mONe.manager_id=m.id;

这个查询使用LEFT JOIN确保CEO张(经理为 NULL)也能显示。

5.3 自连接处理树形结构

如果分类表有父子关系:

CREATETABLEcategory_tree(idINTPRIMARYKEY,nameVARCHAR(50),parent_idINT);-- 查询分类及其父分类SELECTc.nameAScategory,p.nameASparent_categoryFROMcategory_tree cLEFTJOINcategory_tree pONc.parent_id=p.id;

自连接配合递归 CTE(MySQL 8.0+)可以处理无限层级,这会在高阶部分涉及。


6. 实战:查询每位读者的借阅记录(含图书名称)

综合运用以上知识,为图书管理系统编写几个典型的多表查询。

6.1 读者所有借阅记录(含书名和状态)

SELECTr.nameASreader,b.titleASbook,br.borrow_date,br.due_date,br.return_date,CASEWHENbr.return_dateISNOTNULLTHEN'已还'WHENbr.due_date<CURDATE()THEN'逾期'ELSE'借阅中'ENDASstatusFROMreaders rLEFTJOINborrow_records brONr.id=br.reader_idLEFTJOINbooks bONbr.book_id=b.idORDERBYr.name,br.borrow_dateDESC;
  • 第一层LEFT JOIN保留了没有借阅记录的读者。
  • 第二层LEFT JOIN保证即使book_id无效(理论上不存在)也不会丢失借阅记录行。

6.2 找出从未借过书的读者

利用LEFT JOINIS NULL判断:

SELECTr.name,r.emailFROMreaders rLEFTJOINborrow_records brONr.id=br.reader_idWHEREbr.idISNULL;

因为对没有借阅的读者,br.id为 NULL。

6.3 找出同时借了“MySQL 从入门到精通”和“算法导论”的读者

这需要自连接或多次 JOIN 同一张表:

SELECTDISTINCTr.nameFROMreaders rJOINborrow_records br1ONr.id=br1.reader_idJOINbooks b1ONbr1.book_id=b1.idANDb1.title='MySQL 从入门到精通'JOINborrow_records br2ONr.id=br2.reader_idJOINbooks b2ONbr2.book_id=b2.idANDb2.title='算法导论';

这里将borrow_records连接了两次(通过不同别名br1br2),各自关联不同的图书,确保同一读者既有这两本书的借阅记录。

6.4 图书推荐:同一作者的其他书

使用自连接生成推荐列表:

SELECTb1.titleASthis_book,b2.titleASrecommended_book,b1.authorFROMbooks b1JOINbooks b2ONb1.author=b2.authorANDb1.id<>b2.idORDERBYb1.title;

7. JOIN 选择的注意事项

  • 数据完整性:如果子表外键列缺少索引,连接会变慢,尤其是在大表上。通常我们应该为外键列建立索引,MySQL 创建外键约束时会自动添加(如果未手动建立)。
  • NULL 的语义:外连接产生 NULL 时,要注意在 WHERE 条件中对 NULL 的判断(IS NULL而非= NULL),以及在聚合函数中的行为。
  • 可读性:即使查询优化器可能重排连接,但我们在书写时应考虑逻辑清晰度,把主表放左边,用LEFT JOIN单向串联。

8. 小结

联合查询是 SQL 的精华所在,今天的内容可以分为四大块:

  • INNER JOIN:返回两表匹配的行,最常用。
  • LEFT JOIN / RIGHT JOIN:保留左表(或右表)的全部行,没有匹配则填 NULL,用于发现缺失数据。
  • 多表连接:链式 JOIN 多张表,实现跨越多实体的复杂查询。
  • 自连接:一张表当作两张用,解决层级、配对、推荐等问题。

在实战部分我们利用图书管理系统,从读者借阅详情、未借书读者,到“同作者推荐”,将各种连接技术融会贯通。

下一篇将是第二阶段第 5 篇——子查询与合并查询,我们将学习如何使用子查询来进一步过滤和计算,以及用UNION合并多个查询的结果。

思考题

  1. 有两张表A (id, val)B (id, val),如何使用LEFT JOIN找出 A 中有而 B 中没有的id
  2. 如果我们有一个商品表和一个订单明细表,如何用 JOIN 找出从未下过单的用户?(提示:用户表 LEFT JOIN 订单表)
  3. 自连接时,如果不加a.id <> b.id条件,会发生什么?

参考资料

  • MySQL 8.0 Reference Manual - JOIN Syntax
  • MySQL 8.0 Reference Manual - LEFT JOIN / RIGHT JOIN

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/15 23:08:58

【稀缺首发】Midjourney v6批量生成工作流白皮书:含12个行业场景参数矩阵与失败率压降至0.8%的关键阈值

更多请点击&#xff1a; https://intelliparadigm.com 第一章&#xff1a;Midjourney v6批量生成工作流的范式演进与核心价值 Midjourney v6 的发布标志着AIGC图像生成从“单次提示驱动”迈向“结构化任务编排”的关键转折。其批量生成能力不再依赖人工重复提交&#xff0c;而…

作者头像 李华
网站建设 2026/5/15 23:08:19

LZ4代码尺寸终极优化指南:-Os编译与功能裁剪技巧

LZ4代码尺寸终极优化指南&#xff1a;-Os编译与功能裁剪技巧 【免费下载链接】lz4 Extremely Fast Compression algorithm 项目地址: https://gitcode.com/GitHub_Trending/lz/lz4 LZ4作为一款Extremely Fast Compression algorithm&#xff0c;在追求极致压缩速度的同时…

作者头像 李华
网站建设 2026/5/15 23:06:22

Armv9 A-profile架构寄存器系统详解与实战应用

1. Arm A-profile架构寄存器系统概述在Armv9时代&#xff0c;A-profile架构的寄存器系统已成为现代处理器设计的核心枢纽。作为指令执行和数据处理的神经中枢&#xff0c;寄存器在芯片性能、安全隔离和能效管理方面发挥着不可替代的作用。与x86架构不同&#xff0c;Arm采用精简…

作者头像 李华
网站建设 2026/5/15 23:04:07

【软考高级架构】论文范文10——论基于ABSD方法的架构设计

论基于ABSD方法的架构设计 摘要 基于架构的软件设计(Architecture-Based Software Design,ABSD)是一种以架构为驱动中心的软件开发方法,强调在需求分析阶段就引入架构概念,通过场景捕获、需求约束分析、架构设计、文档化、评审与迭代等过程,实现需求与架构的紧密耦合。…

作者头像 李华
网站建设 2026/5/15 23:02:37

为ClaudeCode配置Taotoken作为稳定后备API解决封号困扰

&#x1f680; 告别海外账号与网络限制&#xff01;稳定直连全球优质大模型&#xff0c;限时半价接入中。 &#x1f449; 点击领取海量免费额度 为ClaudeCode配置Taotoken作为稳定后备API解决封号困扰 对于依赖Claude Code进行编程辅助的开发者而言&#xff0c;服务稳定性是保…

作者头像 李华