当前位置: 首页 > news >正文

学生成绩管理系统的 SQL 表设计与多表查询实战

在数据库学习中,设计合理的表结构并掌握多表查询技巧是核心能力。本文将以一个学生成绩管理系统为例,详细讲解如何从创建表结构、插入测试数据到实现复杂的多表关联查询,帮助你快速掌握 SQL 实战技能。

一、需求分析:为什么需要这些表?

一个基础的学生成绩管理系统需要存储四类核心数据:

  • 学生信息(学号、姓名、生日、性别等)
  • 课程信息(课程号、课程名、授课教师)
  • 教师信息(教师号、姓名)
  • 成绩信息(学生 - 课程的分数关联)

这四类数据之间存在明确的关联关系:

  • 学生通过 “成绩” 关联课程
  • 课程通过 “教师号” 关联教师
  • 成绩表是学生和课程的中间表(多对多关系)

基于此,我们需要设计 4 张表来分别存储这些数据,并通过主键和外键建立关联。

二、表结构设计与创建过程

1. 学生表(Student)

存储学生的基本信息,主键为学号(s_id)。

CREATE TABLE `Student`(`s_id` VARCHAR(20),  -- 学号(字符串类型,长度20)`s_name` VARCHAR(20) NOT NULL DEFAULT '',  -- 姓名(非空,默认空字符串)`s_birth` VARCHAR(20) NOT NULL DEFAULT '',  -- 生日(字符串类型,存储格式如1990-01-01)`s_sex` VARCHAR(10) NOT NULL DEFAULT '',  -- 性别(男/女)PRIMARY KEY(`s_id`)  -- 主键为学号
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;  -- 引擎为InnoDB,支持事务和外键;编码为utf8mb4,支持 emoji

设计说明

  • 学号用VARCHAR而非INT,因为学号可能包含字母(如 "2023001A")
  • 所有字段设置NOT NULL DEFAULT '',避免空值(NULL)带来的查询麻烦
  • 选择InnoDB引擎,支持外键约束和事务,适合数据关联紧密的场景

2. 课程表(Course)

存储课程信息,关联授课教师,主键为课程号(c_id)。

CREATE TABLE `Course`(`c_id` VARCHAR(20),  -- 课程号`c_name` VARCHAR(20) NOT NULL DEFAULT '',  -- 课程名`t_id` VARCHAR(20) NOT NULL,  -- 授课教师号(关联教师表)PRIMARY KEY(`c_id`)  -- 主键为课程号
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

设计说明

  • t_id(教师号)未直接设置外键约束,但逻辑上关联教师表的t_id,后续可通过查询关联
  • 课程名设置为非空,确保每门课程都有名称

3. 教师表(Teacher)

存储教师基本信息,主键为教师号(t_id)。

CREATE TABLE `Teacher`(`t_id` VARCHAR(20),  -- 教师号`t_name` VARCHAR(20) NOT NULL DEFAULT '',  -- 教师姓名PRIMARY KEY(`t_id`)  -- 主键为教师号
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

设计说明

  • 结构简单,仅包含必要的教师号和姓名,如需扩展可添加职称、部门等字段

4. 成绩表(Score)

存储学生的课程成绩,是学生表和课程表的中间表,主键为 “学号 + 课程号” 的组合。

CREATE TABLE `Score`(`s_id` VARCHAR(20),  -- 学生号(关联学生表)`c_id` VARCHAR(20),  -- 课程号(关联课程表)`s_score` INT(3),  -- 分数(整数,最大3位,如100分)PRIMARY KEY(`s_id`,`c_id`)  -- 联合主键:一个学生对一门课程只有一个成绩
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

设计说明

  • 联合主键(s_id,c_id)确保不会出现重复的学生 - 课程成绩记录
  • 分数用INT(3),适合存储 0-100 的整数分数,如需小数可改为DECIMAL(5,2)

三、插入测试数据:让表 “活” 起来

表结构创建完成后,需要插入测试数据才能进行查询练习。以下是为四张表插入的模拟数据:

1. 学生表数据

insert into Student values
('01' , '赵雷' , '1990-01-01' , '男'),
('02' , '钱电' , '1990-12-21' , '男'),
('03' , '孙风' , '1990-05-20' , '男'),
('04' , '李云' , '1990-08-06' , '男'),
('05' , '周梅' , '1991-12-01' , '女'),
('06' , '吴兰' , '1992-03-01' , '女'),
('07' , '郑竹' , '1989-07-01' , '女'),
('08' , '王菊' , '1990-01-20' , '女');

2. 课程表数据

insert into Course values
('01' , '语文' , '02'),  -- 课程01(语文)由教师02授课
('02' , '数学' , '01'),  -- 课程02(数学)由教师01授课
('03' , '英语' , '03');  -- 课程03(英语)由教师03授课

3. 教师表数据

insert into Teacher values
('01' , '张三'),  -- 教师01:张三
('02' , '李四'),  -- 教师02:李四
('03' , '王五');  -- 教师03:王五

4. 成绩表数据

insert into Score values
('01' , '01' , 80),  -- 学生01的01课程(语文)成绩80
('01' , '02' , 90),  -- 学生01的02课程(数学)成绩90
('01' , '03' , 99),  -- 学生01的03课程(英语)成绩99
('02' , '01' , 70),
('02' , '02' , 60),
('02' , '03' , 80),
('03' , '01' , 80),
('03' , '02' , 80),
('03' , '03' , 80),
('04' , '01' , 50),
('04' , '02' , 30),
('04' , '03' , 20),
('05' , '01' , 76),
('05' , '02' , 87),
('06' , '01' , 31),
('06' , '03' , 34),
('07' , '02' , 89),
('07' , '03' , 98);

四、多表查询实战:7 个经典案例解析

有了表和数据,就可以开始练习多表查询了。以下是 7 个经典场景的查询实现,涵盖连接查询、分组统计、条件筛选等核心技巧。

1. 查询 “01” 课程比 “02” 课程成绩高的学生信息及分数

需求:找出同时选修了 01(语文)和 02(数学)课程,且语文成绩高于数学成绩的学生。

select s.s_id, s.s_name, s.s_sex, s.s_birth, sc1.s_score as '语文成绩', sc2.s_score as '数学成绩' 
from Student s
inner join Score sc1 on s.s_id = sc1.s_id and sc1.c_id = '01'  -- 关联01课程成绩
inner join Score sc2 on s.s_id = sc2.s_id and sc2.c_id = '02'  -- 关联02课程成绩
where sc1.s_score > sc2.s_score;  -- 筛选语文成绩>数学成绩的学生

关键思路
通过两次内连接Score表,分别获取 01 和 02 课程的成绩,再通过where子句比较分数。

2. 查询平均成绩≥60 分的学生信息及平均成绩

需求:统计所有学生的平均成绩,仅显示平均分及格(≥60 分)的学生。

select s.s_id, s.s_name, avg(score.s_score) as '平均成绩' 
from student as s
inner join score on s.s_id = score.s_id  -- 关联成绩表(只统计有成绩的学生)
group by s.s_id, s.s_name  -- 按学生分组
having avg(score.s_score) >= 60;  -- 筛选平均分≥60的组

关键思路

  • inner join确保只统计有成绩的学生
  • group by按学号和姓名分组,使每个学生为一行
  • having子句用于筛选分组后的聚合结果(where不能直接用于聚合函数)

3. 查询平均成绩 < 60 分的学生(含无成绩的学生)

需求:包含所有学生(即使未选课),平均分按 0 处理,仅显示平均分不及格的学生。

select s.s_id, s.s_name, ifnull(avg(score.s_score), 0) as '平均成绩' 
from student as s
left join score on s.s_id = score.s_id  -- 保留所有学生(包括无成绩的)
group by s.s_id, s.s_name
having ifnull(avg(score.s_score), 0) < 60;  -- 用ifnull处理空值

关键思路

  • left join保留所有学生,未选课的学生成绩为null
  • ifnull(avg(score.s_score), 0)将空值转为 0,确保未选课的学生被正确统计
  • having子句直接使用处理后的平均成绩进行筛选

4. 查询学过 “张三” 老师授课的学生信息

需求:通过教师姓名反向查找所有上过该老师课程的学生。

select distinct s.*  -- distinct去重(同一学生可能选多门该老师的课)
from Student s
inner join Score sc on s.s_id = sc.s_id  -- 学生→成绩
inner join Course c on sc.c_id = c.c_id  -- 成绩→课程
inner join Teacher t on c.t_id = t.t_id  -- 课程→教师
where t.t_name = '张三';  -- 筛选教师名为“张三”的记录

关键思路
通过四表联查(学生→成绩→课程→教师),逐层关联找到 “张三” 老师的课程对应的学生,用distinct去重避免重复记录。

5. 查询所有同学的选课总数和总成绩

需求:统计每个学生的选课数量和总分,包括未选课的学生(总分按 0 处理)。

select student.s_id, student.s_name, count(score.c_id) as '选课总数',  -- 统计课程数量(null不计数)ifnull(sum(score.s_score), 0) as '总成绩'  -- 用ifnull处理无成绩的情况
from student
left join score on student.s_id = score.s_id
group by student.s_id, student.s_name;

关键思路

  • left join确保所有学生都被统计
  • count(score.c_id)统计选课数量(未选课的学生计为 0)
  • sum(score.s_score)计算总分,用ifnull将无成绩的学生总分设为 0

6. 查询同时学过 “01” 和 “02” 课程的学生信息

需求:找出同时选修了 01(语文)和 02(数学)两门课程的学生。

select s.* 
from student as s
inner join Score sc1 on s.s_id = sc1.s_id and sc1.c_id = '01'  -- 选修了01课程
inner join Score sc2 on s.s_id = sc2.s_id and sc2.c_id = '02';  -- 同时选修了02课程

关键思路
两次内连接Score表,分别限定课程号为 01 和 02,只有同时满足两个条件的学生才会被保留。

7. 查询学过 “01” 但未学过 “02” 课程的学生信息

需求:找出选修了 01(语文)但未选修 02(数学)课程的学生。

select s.* 
from student as s
inner join Score sc1 on s.s_id = sc1.s_id and sc1.c_id = '01'  -- 确保选修了01课程
left join Score sc2 on s.s_id = sc2.s_id and sc2.c_id = '02'  -- 关联02课程(未选修则为null)
where sc2.c_id is null;  -- 筛选未选修02课程的学生

关键思路

  • 先通过inner join筛选选修了 01 课程的学生
  • 再通过left join关联 02 课程,未选修的学生sc2.c_idnull
  • where sc2.c_id is null筛选出未学过 02 课程的学生

五、总结:SQL 多表查询的核心技巧

  1. 选择合适的连接方式

    • inner join:只保留匹配的记录,适合需同时存在的数据(如 “同时选修两门课的学生”)
    • left join:保留左表所有记录,适合需包含全部主表数据的场景(如 “所有学生的成绩统计”)
  2. 灵活使用聚合函数与分组

    • 聚合函数(avgsumcount)用于统计分析
    • group by按字段分组,确保每个分组只出现在一行结果中
    • having子句用于筛选分组后的聚合结果(替代where无法处理聚合函数的问题)
  3. 处理空值(null)
    ifnull()(MySQL)或coalesce()(通用)将null转为有意义的值(如 0),避免统计失真。

  4. 多表关联的逻辑链
    复杂查询需拆解为 “表 1→表 2→表 3” 的关联链,通过共同字段(如s_idc_id)逐层连接。

通过本文的表设计和查询案例,你可以清晰地看到:合理的表结构是高效查询的基础,而多表查询的核心是理解表之间的关系,通过连接、分组、筛选的组合实现需求。勤加练习这些案例,能快速提升你的 SQL 实战能力!

http://www.dtcms.com/a/324237.html

相关文章:

  • 部署一个免费开源的博客系统
  • 库的制作和原理
  • 双亲委派机制是什么?
  • 大模型工具集成四层架构:识别、协议、执行与实现
  • reinterpret_cast and static cast
  • Lua的数组、迭代器、table、模块
  • Elasticsearch 搜索模板(Search Templates)把“可配置查询”装进 Mustache
  • 从MySQL到大数据平台:基于Spark的离线分析实战指南
  • 重学React(四):状态管理二
  • Spark执行计划与UI分析
  • 【软考中级网络工程师】知识点之 DCC 深度剖析
  • 系统架构设计师备考之架构设计高级知识
  • 企业高性能web服务器——Nginx
  • App Trace 功能详解 (开发者视角)
  • IDEA 如何导入系统设置
  • 从0到1学LangChain之Agent代理:解锁大模型应用新姿势
  • 【机器学习深度学习】Embedding 模型详解:从基础原理到实际应用场景
  • Xstream反序列化,fastjson,jcakson靶场复现
  • 刑法视野下的虚拟财产属性争议:法律风险与市场潜力解析
  • ThinkPHP8学习篇(二):路由
  • Day39--动态规划--198. 打家劫舍,213. 打家劫舍 II,337. 打家劫舍 III
  • Code Exercising Day 10 of “Code Ideas Record“:StackQueue part02
  • MVCC和日志
  • 国内外主流大模型深度体验与横向评测:技术、场景与未来展望
  • 后置定语:for + 宾语 + 被动不定式
  • CentOS 10在文本控制台模式下修改字体大小
  • 2020/12 JLPT听力原文 问题一
  • LLM多模态模型应用探索调研
  • 【0基础3ds Max】主工具栏介绍(下)
  • 故障诊断 | VMD-CNN-LSTM西储大学轴承故障诊断附MATLAB代码