几种更高效、更健壮的写法,并对比它们的性能差异和适用场景👇
一、原 SQL 的问题分析
select Student.s_no, Student.sname from Student, Sc where Student.s_no=Sc.s_no group by Student.s_no, Student.sname having count(c_no) < (select count(*) from Course);存在两个潜在问题:
- 内连接会丢失未选课的学生:完全没选课的同学,不会出现在结果里。
- 性能一般:全表聚合 + 子查询,在数据量大时开销较高。
二、高性能写法推荐(按性能从高到低排序)
✅ 写法 1:NOT EXISTS(性能最优,推荐)
利用NOT EXISTS的半连接特性,避免全表聚合,能高效利用索引:
sql
SELECT s.s_no, s.sname FROM Student s WHERE NOT EXISTS ( -- 核心:不存在“学生未选的课程” SELECT 1 FROM Course c WHERE NOT EXISTS ( SELECT 1 FROM Sc sc WHERE sc.s_no = s.s_no AND sc.c_no = c.c_no ) );优势:
- 无需
GROUP BY和聚合函数,减少排序开销。 - 能利用
Sc(s_no, c_no)的联合索引,快速判断学生是否选了某门课。 - 包含完全没选课的学生。
✅ 写法 2:LEFT JOIN + COUNT(兼容 NULL,性能次优)
用左连接保留所有学生,统计有效课程数:
sql
SELECT s.s_no, s.sname FROM Student s LEFT JOIN Sc sc ON s.s_no = sc.s_no GROUP BY s.s_no, s.sname HAVING COUNT(DISTINCT sc.c_no) < (SELECT COUNT(*) FROM Course);优化点对比原 SQL:
LEFT JOIN替代内连接,保留了未选课学生。COUNT(DISTINCT sc.c_no)避免重复选课导致的计数错误。- 可利用
Sc(s_no, c_no)索引加速分组。
✅ 写法 3:相关子查询 (易读,适合小数据量)
直接在WHERE中计算每个学生的选课数:
sql
SELECT s.s_no, s.sname FROM Student s WHERE ( SELECT COUNT(DISTINCT sc.c_no) FROM Sc sc WHERE sc.s_no = s.s_no ) < (SELECT COUNT(*) FROM Course);特点:
- 逻辑直观,易理解。
- 数据量小时性能尚可,大数据量下不如
NOT EXISTS高效。
三、关键性能优化技巧
必建索引
Sc(s_no, c_no):覆盖索引,用于快速统计学生选课数和判断课程是否已选。Course(c_no):主键索引,加速总课程数统计。
避免陷阱
- 不要用
NOT IN:当Sc.s_no存在 NULL 值时,NOT IN会返回空结果,且无法利用索引。 - 不要省略
DISTINCT:若Sc表存在重复选课记录,会导致COUNT(c_no)计数偏大。
- 不要用
四、性能对比表
表格
| 写法 | 性能 | 可读性 | 适用场景 |
|---|---|---|---|
NOT EXISTS | ⭐⭐⭐⭐⭐ | 中等 | 大数据量、生产环境 |
LEFT JOIN + COUNT | ⭐⭐⭐⭐ | 高 | 中等数据量、兼容 NULL |
| 相关子查询 | ⭐⭐⭐ | 高 | 小数据量、教学场景 |