深入解析SQL多表查询:核心技巧与实战示例
深入解析SQL多表查询:核心技巧与实战示例
本文系统梳理SQL多表查询的核心方法,涵盖广义笛卡尔积、内连接(等值/自然连接)、外连接、子查询(相关/不相关)及集合运算。通过代码示例解析连接查询的陷阱与优化策略,探讨如何用子查询简化复杂逻辑,并实战“选修所有课程的学生查询”实现。适合希望提升SQL编写效率、深入理解多表交互逻辑的开发者,提供从基础到进阶的清晰学习路径。
文章目录
- 深入解析SQL多表查询:核心技巧与实战示例
- 广义笛卡尔积(交叉连接)
- 内连接( θ \theta θ连接)
- 等值连接
- 自然连接
- 给数据表起别名
- 自身连接
- 外连接
- 子查询
- 不相关子查询
- 相关子查询
- 查询结果的集合运算
广义笛卡尔积(交叉连接)
交叉连接会产生大量无效的行,实际中一般不常用,执行语句如下:
SELECT
*
FROM
student,
course ;
内连接( θ \theta θ连接)
通过一定的连接条件来连接两个表,常见的连接条件是比较运算,比如<
或者>
等等,如果比较运算符是=
,就是等值连接。
等值连接
等值连接有两种方式:
SELECT
*
FROM
student,
score
WHERE student.`sno` = score.`sno` ;
SELECT
*
FROM
student
INNER JOIN score
ON student.`sno` = score.`sno` ;
建议使用第二种。
这样,就将学生表和成绩表合并到了一起,但是新表会有两列sno
,如果只希望一列,使用自然连接:
自然连接
自然连接也有两种方式:
SELECT
*
FROM
student
JOIN score USING (sno) ;
SELECT
*
FROM
student NATURAL
JOIN score ;
这会自动合并相同列。
给数据表起别名
SELECT
stu.sno,
sname,
c.`cno`,
`grade`
FROM
student stu
JOIN score sc
ON stu.`sno` = sc.`sno`
JOIN course c
ON sc.`cno` = c.`cno` ;
只需要在FROM
中的数据表后面加上别名即可。
[!note]
小练习:
查询所有选修了“数据结构”课程的学生学号、姓名和成绩:
SELECT stu.sno, sname, `grade` FROM student stu JOIN score sc ON stu.`sno` = sc.`sno` JOIN course c ON sc.`cno` = c.`cno` WHERE cname = '数据结构' ;
自身连接
别名的一个重要用途就是自身连接,通过对同一个数据表起不同的别名,可以将不同的别名看作不同的副本,例如想找年龄大于“吴强”的学生:
SELECT
b.sname
FROM
student a,
student b
WHERE a.`sname` = '吴强'
AND a.`birthday` > b.`birthday` ;
外连接
只需要在JOIN
前面加上LEFT [OUTER]
、RIGHT [OUTER]
、FULL [OUTER]
分别表示左外连接、右外连接、完全外连接。
子查询
不相关子查询
子查询可以很好地简化逻辑,允许一个查询结果作为返回值作为判定条件。例如,判断查询所有选修“数据结构”的学生姓名:
SELECT
sno
FROM
score
WHERE cno =
(SELECT
cno
FROM
course
WHERE cname = '数据结构') ;
比起等值连接,这样的逻辑更容易让人理解。再比如自身连接:
SELECT
sname
FROM
student
WHERE birthday <
(SELECT
birthday
FROM
student
WHERE sname = '吴强') ;
上述的两个例子都是返回单值,子查询返回结果也可以作为集合:
SELECT
sname
FROM
student
WHERE sno IN
(SELECT
sno
FROM
score
WHERE cno =
(SELECT
cno
FROM
course
WHERE cname = '数据结构')) ;
在子查询返回多值时,可以根据需要添加谓词:
SELECT
*
FROM
student
WHERE birthday < ALL
(SELECT
birthday
FROM
student
WHERE dept = '软件工程') ;
上述语句ALL
表示生日小于所有查询结果(等价于小于最小值),再比如:
SELECT
*
FROM
student
WHERE birthday < ANY
(SELECT
birthday
FROM
student
WHERE dept = '软件工程') ;
上述ANY
表示生日小于其中一个查询结果(等价于小于最大值)。上述两个例子也可以分别写成:
SELECT
*
FROM
student
WHERE birthday <
(SELECT
MIN(birthday)
FROM
student
WHERE dept = '软件工程') ;
SELECT
*
FROM
student
WHERE birthday <
(SELECT
MAX(birthday)
FROM
student
WHERE dept = '软件工程') ;
上述例子中,我们都是将子查询语句放在了WHERE
语句中,但它也可以放在SELECT
和FROM
中:
SELECT
sno,
totalcredit,
(SELECT
MAX(totalcredit)
FROM
student) AS '最高学分'
FROM
student ;
这样返回结果的每一行末尾都是相同的最高学分了
SELECT
*
FROM
(SELECT
*
FROM
student
WHERE dept = '软件工程') AS app
ORDER BY totalcredit DESC ;
注意,如果是将子查询结果作为表必须要为其设置别名。
[!note]
也可以用子查询结果创建、更新、插入、删除表,道理很简单,不再赘述。
相关子查询
相关子查询常用来实现关系代数中的除运算,比如要查找选修了所有课程的学生学号。
不相关子查询的子查询是独立的,也就是说子查询并不依赖于父查询,而相关子查询的部分信息依赖于父查询:
SELECT
sno
FROM
student
WHERE NOT EXISTS
(SELECT
cno
FROM
course
WHERE NOT EXISTS
(SELECT
*
FROM
score
WHERE (
sno = student.`sno`
AND cno = course.`cno`
))) ;
上述代码中,最内层子查询的==student.sno
==依赖于最外层查询,==course.cno
==依赖于中间层查询,执行过程中,会先利用父查询的一行执行子查询,子查询执行结束后利用父查询下一行数据进行子查询。
NOT EXISTS
指的是:如果查询为空,返回True,有结果返回False。
最内层查询比较score
表中的学号和课程号是否能在学生表和课程表中找到对应内容,如果找不到说明该课程和该学生没有关系,也就是没有选修的意思,
中间查询表示:如果内层查询有结果(NOT EXISTS
返回False),那么查询结果就是空,如果内层查询无内容,那么结果就是这个课程号,反复执行子查询后就能找到一个学生没有选修的所有课程号。如果没有未选修课程那么返回就是空。
最外层就是检查是否为空,如果为空说明选修了所有课程,查询到结果。最后可以查询到选修了所有课程的学生。
查询结果的集合运算
SQL中对查询结果的运算仅支持并运算:
SELECT
*
FROM
student
WHERE birthday < ALL (
(SELECT
birthday
FROM
student
WHERE dept = '软件工程')
UNION
(
(SELECT
birthday
FROM
student
WHERE dept = '计算机科学')
)
) ;
这样会默认去掉重复元组,如果想要保留只需要在UNION
后面加上[ALL]
即可。
``sql
SELECT
*
FROM
student
WHERE birthday < ALL (
(SELECT
birthday
FROM
student
WHERE dept = ‘软件工程’)
UNION
(
(SELECT
birthday
FROM
student
WHERE dept = ‘计算机科学’)
)
) ;
这样会默认去掉重复元组,如果想要保留只需要在`UNION`后面加上`[ALL]`即可。