数据库子查询题集(10题)
以下题目基于典型的"学生-课程-成绩"数据库模式,并引入复杂场景、性能陷阱及高级SQL特性,旨在深入考察对子查询执行原理、优化策略及逻辑转换的理解。
| 题号 | 题目描述 | 考察核心 |
|---|---|---|
| 1 | 多层嵌套与聚合:查询"平均成绩高于全校学生平均成绩,且其最高单科成绩高于该科目平均分20分以上"的学生的学号和姓名。 | 多层聚合子查询、HAVING子句中的复杂条件。 |
| 2 | 相关子查询与极值 :找出每门课程中,成绩不是最高也不是最低的学生名单(即排除每门课的最高分和最低分获得者)。 | 相关子查询、NOT IN与极值查询(MAX, MIN)的结合。 |
| 3 | EXISTS 与 三表关联:查询从未选修过"张老师"所授课程,但选修过所有"李老师"所授课程的学生信息。 | EXISTS, NOT EXISTS 与全称量词的逻辑转换(通过双重否定或计数实现)。 |
| 4 | ALL/ANY 与除法运算 :查询选修了计算机系 开设的全部课程的学生学号。 | 使用 NOT EXISTS 模拟关系代数中的"除法"运算。 |
| 5 | 标量子查询与更新 :将每位学生的总成绩更新到学生表的一个新增字段total_score中,要求使用单条UPDATE语句配合标量子查询完成。 |
在UPDATE的SET子句中使用相关标量子查询。 |
| 6 | FROM 子句中的子查询与窗口函数:查询每个班级中,总成绩排名前10%的学生。要求使用FROM子句中的子查询先计算排名和百分比,再筛选。 | 子查询生成派生表(Derived Table)并与窗口函数(RANK(), NTILE())结合。 |
| 7 | 条件子查询与 CASE 表达式:生成一份报告,列出所有学生。对于每个学生,如果其有任何一门课成绩低于60分,则显示"有不及格";如果所有成绩都在85分以上,则显示"优秀";否则显示"普通"。 | 在SELECT子句和CASE WHEN中使用EXISTS和聚合子查询。 |
| 8 | 横向连接 (LATERAL JOIN) 问题:对于每个学生,找出其成绩最高的两门课程的名称和分数。 | 使用 LATERAL 子查询(或特定数据库的等效语法)实现行间关联。 |
| 9 | 集合运算与子查询性能 :查询本学期有成绩记录,但上学年没有任何成绩记录的学生。分别用NOT IN、NOT EXISTS和LEFT JOIN...IS NULL三种方法实现,并分析其执行计划可能存在的差异。 |
不同逻辑写法对数据库优化器(如He3DB的子连接上拉)的影响及性能对比。 |
| 10 | 递归子查询(CTE) :给定一个"员工-经理"表(employee_id, manager_id, salary),找出每个员工的所有间接下属(不限层级)中,薪资最高的那位员工的薪资。 |
使用递归公共表表达式(CTE)遍历树形结构,并在外层查询中进行聚合。 |
部分难题的参考解答与深度解析
题目 2:相关子查询与极值
sql
-- 方法:找出每门课成绩既不是最大值也不是最小值的学生
SELECT DISTINCT s.student_id, s.name, c.course_name, sc.score
FROM student s
JOIN score sc ON s.student_id = sc.student_id
JOIN course c ON sc.course_id = c.course_id
WHERE sc.score <> (
SELECT MAX(score) FROM score sc2
WHERE sc2.course_id = sc.course_id -- 相关子查询,找当前课程最高分
)
AND sc.score <> (
SELECT MIN(score) FROM score sc3
WHERE sc3.course_id = sc.course_id -- 相关子查询,找当前课程最低分
);
解析 :此查询为每个成绩行执行了两次相关子查询以获取当前课程的极值,可能产生性能问题。优化器可能尝试将其转换为JOIN,但相关性子查询限制了上拉优化。
题目 4:ALL/ANY 与除法运算(使用 NOT EXISTS 实现)
sql
-- 查询选修了计算机系全部课程的学生
SELECT s.student_id
FROM student s
WHERE NOT EXISTS (
-- 找出计算机系开设的、该学生没选的课程
SELECT c.course_id
FROM course c
WHERE c.dept = '计算机系'
AND NOT EXISTS (
SELECT 1
FROM score sc
WHERE sc.student_id = s.student_id
AND sc.course_id = c.course_id
)
);
解析 :这是经典的关系除法问题的NOT EXISTS解法。它通过双重否定("不存在一门计算机系的课是他没选的")来表达全称量词。外层查询遍历学生,内层第一重NOT EXISTS检查是否有一门计算机系的课不在该学生的选课列表中(由最内层NOT EXISTS判断)。这种嵌套相关子查询是优化器的重点优化对象,在He3DB等数据库中可能会被重写为更高效的semijoin或antijoin。
题目 9:集合运算与性能分析
sql
-- 方法1: NOT IN (需注意NULL值问题)
SELECT student_id FROM score WHERE term = '2024-春'
AND student_id NOT IN (
SELECT student_id FROM score WHERE term = '2023-秋'
);
-- 方法2: NOT EXISTS (通常更优)
SELECT s.student_id
FROM score s
WHERE s.term = '2024-春'
AND NOT EXISTS (
SELECT 1 FROM score s2
WHERE s2.term = '2023-秋'
AND s2.student_id = s.student_id
);
-- 方法3: LEFT JOIN ... IS NULL
SELECT DISTINCT s.student_id
FROM score s
LEFT JOIN score s_prev ON s.student_id = s_prev.student_id AND s_prev.term = '2023-秋'
WHERE s.term = '2024-春'
AND s_prev.student_id IS NULL;
性能分析:
NOT IN:如果子查询返回结果集包含NULL值,则整个NOT IN条件可能返回UNKNOWN(即无结果),这是逻辑陷阱。性能上,数据库可能将其实现为Hash Anti Join。NOT EXISTS:是表达此逻辑最清晰和安全的方式,通常能利用关联字段的索引。现代数据库优化器(如MySQL 8.0+、He3DB)擅长将NOT EXISTS重写为高效的antijoin。LEFT JOIN...IS NULL:这是一种更"过程化"的写法,明确指示数据库先进行连接操作。在拥有良好统计信息时,优化器可能生成与NOT EXISTS相似的执行计划。但在复杂情况下,它可能比NOT EXISTS产生更大的中间结果集。
核心优化点 :数据库优化器在查询重写阶段,会尝试将子查询(特别是EXISTS/IN类)"上拉"为JOIN,以减少嵌套迭代。例如,He3DB会将满足条件的EXISTS_SUBLINK转换为semijoin,将NOT EXISTS转换为antijoin,从而可以利用高效的连接算法(如Hash Join)和并行执行。能否成功上拉取决于子查询是否"相关"(引用外层列)以及其复杂程度。
题目 10:递归子查询(CTE)
sql
WITH RECURSIVE SubordinateTree AS (
-- 锚点:直接下属
SELECT employee_id, manager_id, salary
FROM employee
UNION ALL
-- 递归:间接下属
SELECT st.employee_id, e.manager_id, e.salary
FROM SubordinateTree st
JOIN employee e ON st.manager_id = e.employee_id
)
SELECT
emp.employee_id,
(SELECT MAX(salary) FROM SubordinateTree st WHERE st.employee_id = emp.employee_id) as max_subordinate_salary
FROM employee emp;
解析:此CTE先递归构建出每个员工的所有下属关系(包括间接)。外层查询通过一个标量子查询,从递归结果中找出每个员工下属的最高薪资。递归CTE是解决层次查询的强有力工具,但其性能依赖于递归深度和表的索引情况。