在前面几篇文章中我们已经多次使用JOIN来将两张或更多表的数据组合在一起查询。现在是时候系统地掌握联合查询的各种类型了。联合查询是关系型数据库最强大的特性之一它将分散在不同表中的数据通过关联条件“连接”起来让我们能够回答跨越多个实体的业务问题。本文将详细讲解内连接INNER JOIN——只返回匹配的行外连接LEFT JOIN、RIGHT JOIN——保留不匹配的行多表连接——超过两张表的联合查询自连接Self Join——同一张表连接自身实战部分将围绕我们的图书管理系统查询每位读者的借阅记录含图书名称并深入练习自连接场景。1. 什么是 JOIN关系型数据库设计中我们通常将数据拆分到不同的表中以消除冗余范式化。但业务查询时往往需要把这些分散的信息重新拼起来。JOIN就是用来“拼”数据的机制。连接的核心要素连接条件指定两张表如何关联通常是通过外键与主键的等值比较如a.reader_id b.id。连接类型决定了当某行在对方表中没有匹配时的处理方式保留还是丢弃。MySQL 中支持的连接类型包括INNER JOIN、LEFT [OUTER] JOIN、RIGHT [OUTER] JOIN以及CROSS JOIN。OUTER关键字可省略。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_idb.id;只有book_id在books表中存在的借阅记录才会返回。由于我们有外键约束正常情况下所有记录都能匹配。但如果我们删除了某本书而没有级联删除借阅记录那么对应的book_id就无法匹配使用INNER JOIN这些记录会被“吞掉”不显示。使用 WHERE 进一步过滤SELECTb.title,br.borrow_dateFROMborrow_records brINNERJOINbooks bONbr.book_idb.idWHEREbr.borrow_date2025-03-01;2.3 隐式内连接SQL-89 风格早期 SQL 写法也支持在FROM中列出多张表并在WHERE中写连接条件SELECTb.title,br.borrow_dateFROMborrow_records br,books bWHEREbr.book_idb.id;这种写法功能与INNER JOIN相同但可读性和维护性较差容易忘记连接条件变成笛卡尔积。推荐使用显式JOIN ... ON语法。3. 外连接保留不匹配的行外连接与内连接的区别在于即使对方表中没有匹配的行也会保留“主表”中的行并用NULL填充对方表的列。3.1 左外连接LEFT JOINLEFT JOIN保留左表FROM 后的第一张表的所有行右表中无匹配的行则填NULL。语法SELECT列列表FROM左表LEFTJOIN右表ON连接条件;需求列出所有读者包括那些没有借阅记录的读者SELECTr.name,br.idASrecord_id,br.borrow_dateFROMreaders rLEFTJOINborrow_records brONr.idbr.reader_idORDERBYr.name,br.borrow_date;结果中没有任何借阅记录的读者会显示一行其record_id和borrow_date为NULL。用LEFT JOIN可以轻松找出“从未借书的读者”。统计每位读者的借阅次数包括 0 次SELECTr.name,COUNT(br.id)ASborrow_countFROMreaders rLEFTJOINborrow_records brONr.idbr.reader_idGROUPBYr.id,r.name;COUNT(br.id)只统计非 NULL 的借阅记录所以没有借阅的读者计数为 0。如果使用INNER JOIN他们将完全不出现。3.2 右外连接RIGHT JOINRIGHT JOIN保留右表的所有行左表无匹配的行填NULL。语法SELECT列列表FROM左表RIGHTJOIN右表ON连接条件;实际上RIGHT JOIN可以通过交换表顺序用LEFT JOIN实现。例如-- 列出所有图书及其被借阅记录包括从未被借过的书SELECTb.title,br.borrow_dateFROMborrow_records brRIGHTJOINbooks bONbr.book_idb.id;等价于SELECTb.title,br.borrow_dateFROMbooks bLEFTJOINborrow_records brONb.idbr.book_id;推荐始终使用LEFT JOIN将“主表”放在左边思维更直观。3.3 全外连接FULL OUTER JOINMySQL 不直接支持FULL OUTER JOIN但可以通过LEFT JOIN和RIGHT JOIN的UNION来模拟它将保留两个表中的所有行。实际业务中较少使用。4. 多表连接连接不仅限于两张表我们可以连续JOIN多张表来获取更丰富的信息。需求查询每位读者的借阅记录包含读者姓名、图书名称、分类名称这涉及四张表readers、borrow_records、books、book_category、categories五张。SELECTr.nameASreader,b.titleASbook,c.nameAScategory,br.borrow_date,br.due_dateFROMreaders rJOINborrow_records brONr.idbr.reader_idJOINbooks bONbr.book_idb.idLEFTJOINbook_category bcONb.idbc.book_idLEFTJOINcategories cONbc.category_idc.idORDERBYr.name,br.borrow_date;注意图书可能没有分类使用LEFT JOIN保证图书信息不丢失。读者和借阅记录之间使用INNER JOIN或LEFT 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.authorb.authorANDa.idb.idORDERBYa.author,a.id;a和b都是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_idm.id;这个查询使用LEFT JOIN确保CEO张经理为 NULL也能显示。5.3 自连接处理树形结构如果分类表有父子关系CREATETABLEcategory_tree(idINTPRIMARYKEY,nameVARCHAR(50),parent_idINT);-- 查询分类及其父分类SELECTc.nameAScategory,p.nameASparent_categoryFROMcategory_tree cLEFTJOINcategory_tree pONc.parent_idp.id;自连接配合递归 CTEMySQL 8.0可以处理无限层级这会在高阶部分涉及。6. 实战查询每位读者的借阅记录含图书名称综合运用以上知识为图书管理系统编写几个典型的多表查询。6.1 读者所有借阅记录含书名和状态SELECTr.nameASreader,b.titleASbook,br.borrow_date,br.due_date,br.return_date,CASEWHENbr.return_dateISNOTNULLTHEN已还WHENbr.due_dateCURDATE()THEN逾期ELSE借阅中ENDASstatusFROMreaders rLEFTJOINborrow_records brONr.idbr.reader_idLEFTJOINbooks bONbr.book_idb.idORDERBYr.name,br.borrow_dateDESC;第一层LEFT JOIN保留了没有借阅记录的读者。第二层LEFT JOIN保证即使book_id无效理论上不存在也不会丢失借阅记录行。6.2 找出从未借过书的读者利用LEFT JOIN和IS NULL判断SELECTr.name,r.emailFROMreaders rLEFTJOINborrow_records brONr.idbr.reader_idWHEREbr.idISNULL;因为对没有借阅的读者br.id为 NULL。6.3 找出同时借了“MySQL 从入门到精通”和“算法导论”的读者这需要自连接或多次 JOIN 同一张表SELECTDISTINCTr.nameFROMreaders rJOINborrow_records br1ONr.idbr1.reader_idJOINbooks b1ONbr1.book_idb1.idANDb1.titleMySQL 从入门到精通JOINborrow_records br2ONr.idbr2.reader_idJOINbooks b2ONbr2.book_idb2.idANDb2.title算法导论;这里将borrow_records连接了两次通过不同别名br1、br2各自关联不同的图书确保同一读者既有这两本书的借阅记录。6.4 图书推荐同一作者的其他书使用自连接生成推荐列表SELECTb1.titleASthis_book,b2.titleASrecommended_book,b1.authorFROMbooks b1JOINbooks b2ONb1.authorb2.authorANDb1.idb2.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合并多个查询的结果。思考题有两张表A (id, val)和B (id, val)如何使用LEFT JOIN找出 A 中有而 B 中没有的id如果我们有一个商品表和一个订单明细表如何用 JOIN 找出从未下过单的用户提示用户表 LEFT JOIN 订单表自连接时如果不加a.id b.id条件会发生什么参考资料MySQL 8.0 Reference Manual - JOIN SyntaxMySQL 8.0 Reference Manual - LEFT JOIN / RIGHT JOIN