🔗 数据库多表查询全攻略:告别笛卡尔积,玩转各种 JOIN 与高级嵌套!
引言:数据孤岛?不存在的!
在真实的业务世界里,数据从来都不是孤立存在的。用户信息存在 user 表,订单信息存在 order 表,商品详情又在 product 表......如果只会单表查询,你就只能看到一堆零散的碎片。
这时候,就需要**多表查询(Multi-table Query)**登场了!它就像一位高超的"红娘",能把分散在不同表里的数据完美地牵线搭桥,组合成我们想要的完整视图。今天,我们就来彻底搞懂多表查询的各种姿势,让你在面对复杂业务时游刃有余!
🕸️ 第一章:多表关系的"前世今生"
在写 SQL 之前,先要搞清楚表与表之间是什么关系。通常只有以下三种:
-
一对多(1:N) :最常见。比如一个部门有多个员工,但一个员工只能属于一个部门。实现方式:在"多"的一方(员工表)建立外键,指向"一"的一方(部门表)。
-
多对多(N:M) :比如一个学生可以选多门课,一门课也能被多个学生选。实现方式:必须建立一个中间表(如选课表),里面至少包含两个外键,分别关联两张主表。
-
一对一(1:1) :比较少见,通常是为了拆分大表优化性能。比如用户基础信息和用户隐私详情。实现方式:在任意一方加外键并设置唯一约束(UNIQUE)。
💥 第二章:连接查询------JOIN 的四大门派
多表查询的核心就是"连接"。如果不加任何条件直接把两张表拼在一起,会产生恐怖的笛卡尔积 (A表100行,B表100行,结果变成了10000行!)。为了避免这种情况,我们需要用 JOIN 来精准匹配。
1. 内连接(INNER JOIN):只要"两情相悦"
内连接只返回两张表中完全匹配的数据。就像找对象,必须双方都看对眼了才行。
sql
-- 查询有部门的员工姓名和部门名称
SELECT
e.name,
d.dept_name
FROM emp e
INNER JOIN dept d ON e.dept_id = d.id;
💡 提示 :INNER 可以省略,直接写 JOIN 默认就是内连接。
2. 外连接(OUTER JOIN):总有一方是"备胎"
外连接会保证某一张表的数据全部显示出来,另一张表没匹配上的就填 NULL。
-
左外连接(LEFT JOIN) :以左表 为主。左表数据全都要,右表匹配不上拉倒。场景 :查询所有员工及其部门信息(哪怕有些新员工还没分配部门,也要把人名查出来)。
sqlSELECT e.name, d.dept_name FROM emp e LEFT JOIN dept d ON e.dept_id = d.id; -
右外连接(RIGHT JOIN) :以右表 为主。逻辑和左连接相反,但在实际开发中,大家习惯统一用
LEFT JOIN,把想保全的表放在左边就行。
3. 自连接(SELF JOIN):自己和自己谈恋爱?
当一张表里既有数据又有层级关系时(比如员工表里存了 manager_id 领导编号),就需要把这张表当成两张表来用。场景:查询每个员工的上级领导名字。
sql
-- 把 emp 表看成两份:a代表员工,b代表领导
SELECT
a.name AS '员工',
b.name AS '领导'
FROM emp a
LEFT JOIN emp b ON a.manager_id = b.id;
⚠️ 注意:自连接必须给表起别名(如 a 和 b),否则数据库会晕头转向。
🪆 第三章:子查询------SQL 里的"套娃"艺术
如果不想用 JOIN,或者逻辑太复杂,我们还可以在 SQL 语句里再嵌套一个 SELECT 语句,这就是子查询。
根据子查询返回的结果不同,可以分为以下几类:
1. 标量子查询(返回单个值)
子查询的结果只有一个数,通常配合 =, >, < 使用。场景:查询工资高于公司平均工资的员工。
sql
SELECT *
FROM emp
WHERE salary > (
SELECT AVG(salary)
FROM emp
);
2. 列子查询(返回一列数据)
子查询返回一列多行,通常配合 IN, ANY, ALL 使用。场景:查询在"销售部"或"市场部"工作的所有员工。
sql
SELECT *
FROM emp
WHERE dept_id IN (
SELECT id
FROM dept
WHERE name IN ('销售部', '市场部')
);
3. 进阶玩法:带 ANY 和 ALL 的子查询
当子查询返回多个值时,我们可以用这两个关键字进行更精细的比较:
-
ANY(任意一个) :只要满足子查询结果中的任意一个值即可。场景 :查询比"研发部"任意一个 员工工资高的其他部门员工(即只要比研发部最低工资高就行)。
sqlSELECT name, salary FROM emp WHERE salary > ANY ( SELECT salary FROM emp WHERE dept_id = (SELECT id FROM dept WHERE name = '研发部') ) AND dept_id != (SELECT id FROM dept WHERE name = '研发部'); -
ALL(所有) :必须满足子查询结果中的所有值。场景 :查询比其他部门所有 员工工资都高的员工(即比所有非本部门员工的最高工资还要高)。
sqlSELECT name, salary FROM emp WHERE salary > ALL ( SELECT salary FROM emp WHERE dept_id = 30 );
4. 降维打击:带 EXISTS 的子查询
EXISTS 不关心子查询的具体内容,只判断子查询是否有结果返回 (有结果则为真,无结果则为假)。它的执行效率通常在大数据量下优于 IN。场景:查询所有选修了 1 号课程的学生姓名(相关子查询)。
sql
SELECT name
FROM student s
WHERE EXISTS (
SELECT 1
FROM score sc
WHERE sc.sno = s.sno AND sc.cno = '1'
);
💡 技巧 :子查询里写 SELECT 1 或 SELECT * 都可以,因为数据库只在乎"有没有",不在乎"是什么"。与之对应的还有 NOT EXISTS,用来查找"不存在"的记录。
5. 派生表(放在 FROM 后面)
把子查询的结果当成一张临时表来用。场景:统计各部门的平均薪资,并筛选出平均薪资大于 10000 的部门详情。
sql
SELECT
d.*,
temp.avg_sal
FROM dept d
JOIN (
SELECT dept_id, AVG(salary) as avg_sal
FROM emp
GROUP BY dept_id
) temp ON d.id = temp.dept_id
WHERE temp.avg_sal > 10000;
⚠️ 避坑 :放在 FROM 后面的子查询,一定要给它起个别名(如上面的 temp)!
⚖️ 第四章:集合查询------横向拼接的魔法
前面讲的 JOIN 都是横向拼接(增加字段),而集合查询是纵向拼接(增加行数)。它可以把两条或多条 SELECT 语句的结果合并起来。
-
UNION:合并后会自动去重,效率较低。
-
UNION ALL:直接合并,不去重,效率高(推荐使用)。
-
INTERSECT(交集):返回两个查询结果中共同拥有的部分(MySQL 8.0+ 支持,老版本可用 INNER JOIN 替代)。
-
EXCEPT / MINUS(差集):返回第一个查询有,但第二个查询没有的部分(Oracle 叫 MINUS,SQL Server 叫 EXCEPT,MySQL 可用 LEFT JOIN 替代)。
场景:要把中国城市和外国城市合并到一个列表展示。
sql
SELECT name, country FROM cn_cities
UNION ALL
SELECT name, country FROM en_cities;
⚠️ 强制规范 :参与合并的查询语句,它们的字段数量和数据类型必须一致。
📝 总结:多表查询速查宝典
为了让大家不再混淆,我把核心知识点整理成了表格:
| 查询方式 | 核心关键词 | 适用场景 | 注意事项 |
|---|---|---|---|
| 内连接 | INNER JOIN | 只要两边都有的数据 | 最常用的连接方式 |
| 左外连接 | LEFT JOIN | 保左表,右表没有补NULL | 生产环境最常用,替代右连接 |
| 自连接 | JOIN ... ON | 同一张表内的层级关系 | 必须给表起不同的别名 |
| 子查询 | ANY / ALL | 与子查询结果集进行比较 | ANY满足其一,ALL需满足全部 |
| 子查询 | EXISTS | 判断子查询是否有结果 | 大数据量下效率通常优于 IN |
| 联合查询 | UNION / ALL | 合并结构相同的多张表 | 字段数量和类型必须对齐 |
掌握了这些多表查询的技巧,你就不再是那个对着单表发愁的新手了。下次遇到复杂的报表需求,试着拆解一下表之间的关系,灵活运用 JOIN、高级子查询和集合运算,你会发现 SQL 的世界原来如此广阔!
如果觉得这篇干货对你有用,别忘了点赞、在看、转发三连哦!我们下期见!👋