教学信息管理系统SQL实践:从表设计到性能优化全攻略
2026/9/17 11:48:03 网站建设 项目流程

每年都有大量学生在做“教学信息管理系统”这个题目,课程设计、毕业设计、期末项目里反复出现。作为一个见过几百个同类项目的人,我最大的感受是:大多数人的SQL水平停留在“能跑就行”,一旦要出成绩排名、统计各专业挂科率、分析选课热度,SQL就写得又长又慢,甚至跑不出来。这篇东西不讲泛泛的数据库理论,就围绕“教学信息管理系统+SQL”这个组合,把从表设计、建库SQL、业务查询到性能优化、安全防护的完整链路过一遍,所有案例都是这类系统里真正会遇到的场景。适合正在做同类系统的人,也适合想系统补一下SQL实践的读者。

1. 先搞清楚系统要管什么,再谈表怎么建

很多人的项目从网上抄一份表结构就开始写SQL,写到后面发现查询逻辑别扭、数据对不上,根子在于业务边界没理清。教学信息管理系统看起来功能多,但真正核心的就五件事:学生信息维护、教师信息维护、课程管理、选课、成绩管理。班级和专业可以归入学生信息的扩展字段或独立字典表,不需要过度设计。

1.1 五个核心业务实体,对应五类基础表

我的习惯是先把业务实体画出来,再想表结构。这套系统至少需要以下五张核心表:

数据表核心字段用途
学生表学号、姓名、性别、出生日期、专业、班级、入学年份记录学生基本信息
教师表工号、姓名、职称、所属院系教师基本信息
课程表课程号、课程名、学分、学时、课程性质课程基础数据
选课表选课ID、学号、课程号、学期、成绩、选课时间学生和课程的多对多关系
用户表用户ID、账号、密码、角色、关联学生/教师ID系统登录与权限控制

选课表是整个系统的核心,它不只记录“谁选了哪门课”,还承担了成绩存储的功能。很多人把成绩单独建一张表,选课一张表,结果两表之间要反复按学号、课程号关联,徒增复杂度。对于普通教学管理系统,成绩字段直接放选课表里就是最合理的设计,除非你要做多次考试或过程性考核。

用户表为什么独立出来?因为学生、教师、管理员都要登录系统,但角色不同权限不同。把账号密码和业务数据拆开,后续做登录验证、权限拦截时逻辑才清晰。密码字段一定要用哈希存储,这个后面安全部分细说。

1.2 表结构设计里最常犯的三个错

第一,把班级、专业直接塞进学生表当字符串。这样做也不是不能用,但后面做“统计某专业的平均绩点”时,你会发现同一个专业在不同记录里叫法不一致,比如“计算机科学”和“计科”混着写,数据没法聚合。正确做法是单独建专业表、班级表,学生表里存ID。

第二,选课表不设唯一约束。这应该是所有教学管理系统最容易出的数据问题。同一学生在同一学期选了同一门课两次,成绩存了多条,统计时数据翻倍。就算业务前台做了判断,数据库层也必须用唯一约束兜底。

第三,滥用自增ID当主键。学生表用学号做主键其实更自然,因为学号本身就是业务唯一标识。当然如果考虑到学号可能变更,用自增ID也行,那就必须给学号加唯一索引。选课表中间表必须有独立主键,但复合唯一约束(学号+课程号+学期)必不可少。

2. 建库建表SQL:约束、类型与完整性

业务模型理清后,建表SQL的写法直接决定后续查询的稳定性。我见过太多表结构没有任何约束,数据想怎么脏就怎么脏,后面排查问题查到怀疑人生。

2.1 一份可运行的核心建表SQL

以下以SQL Server为例,给出一份带完整约束的建表SQL,可以直接跑。这份SQL我在多个教学管理系统项目里改改用用过,核心结构没有大问题。

-- 学生表 CREATE TABLE dbo.Student ( StudentID NVARCHAR(20) NOT NULL, -- 学号 StudentName NVARCHAR(20) NOT NULL, -- 姓名 Gender NCHAR(1) NOT NULL DEFAULT N'男', BirthDate DATE NULL, Major NVARCHAR(50) NOT NULL, -- 专业 ClassName NVARCHAR(50) NOT NULL, -- 班级 EnrollYear INT NOT NULL, -- 入学年份 Phone NVARCHAR(20) NULL, CONSTRAINT PK_Student PRIMARY KEY (StudentID), CONSTRAINT CK_Student_Gender CHECK (Gender IN (N'男', N'女')) ); GO -- 教师表 CREATE TABLE dbo.Teacher ( TeacherID NVARCHAR(20) NOT NULL, TeacherName NVARCHAR(20) NOT NULL, Title NVARCHAR(30) NULL, -- 职称 Department NVARCHAR(50) NOT NULL, -- 所属院系 Phone NVARCHAR(20) NULL, CONSTRAINT PK_Teacher PRIMARY KEY (TeacherID) ); GO -- 课程表 CREATE TABLE dbo.Course ( CourseID NVARCHAR(20) NOT NULL, CourseName NVARCHAR(50) NOT NULL, Credits DECIMAL(3,1) NOT NULL, -- 学分,如 3.5 CourseHours INT NOT NULL, -- 学时 CourseType NVARCHAR(20) NOT NULL DEFAULT N'必修', -- 必修/选修 CONSTRAINT PK_Course PRIMARY KEY (CourseID) ); GO -- 教学班表:记录哪位教师教哪门课,替代直接在课程表里放教师ID CREATE TABLE dbo.TeachingClass ( ClassID INT IDENTITY(1,1) NOT NULL, CourseID NVARCHAR(20) NOT NULL, TeacherID NVARCHAR(20) NOT NULL, Semester NVARCHAR(20) NOT NULL, -- 如 2024-2025-1 CONSTRAINT PK_TeachingClass PRIMARY KEY (ClassID), CONSTRAINT FK_TC_Course FOREIGN KEY (CourseID) REFERENCES dbo.Course(CourseID), CONSTRAINT FK_TC_Teacher FOREIGN KEY (TeacherID) REFERENCES dbo.Teacher(TeacherID) ); GO -- 选课表(含成绩) CREATE TABLE dbo.Enrollment ( EnrollID INT IDENTITY(1,1) NOT NULL, StudentID NVARCHAR(20) NOT NULL, ClassID INT NOT NULL, Semester NVARCHAR(20) NOT NULL, Score DECIMAL(5,2) NULL, EnrollTime DATETIME NOT NULL DEFAULT GETDATE(), CONSTRAINT PK_Enrollment PRIMARY KEY (EnrollID), CONSTRAINT FK_Enr_Student FOREIGN KEY (StudentID) REFERENCES dbo.Student(StudentID), CONSTRAINT FK_Enr_Class FOREIGN KEY (ClassID) REFERENCES dbo.TeachingClass(ClassID), CONSTRAINT UQ_Enrollment UNIQUE (StudentID, ClassID, Semester), CONSTRAINT CK_Enrollment_Score CHECK (Score IS NULL OR (Score >= 0 AND Score <= 100)) ); GO

这里有几个设计点需要解释。

选课表外键指向教学班表而不是课程表,这是很多人没想到的。一个教学班包含“课程+教师+学期”,选课只需要关联到教学班,就能同时拿到课程和教师信息,不用在选课表里重复存课程号、教师ID。同时选课表里专门存Semester字段,是为了查询方便,避免每次都要通过教学班去反查学期。

选课时间的默认值用GETDATE(),这样前台即使不传时间,数据库也能自动记录。CHECK约束保证成绩只能在0到100之间,期末录入错误数据时直接报错,比业务代码里写一堆if判断靠谱得多。

2.2 字段类型选择的两个经典反例

第一个反例是字符类型的坑。学生信息的手机号、学号、身份证号这类“数字”,必须用NVARCHAR/VARCHAR,不要用INT或BIGINT。原因有两点:一是这类字段不需要参与数学计算,二是它们常常有前导零。比如学号“20231001”,用数值型存储会变成“20231001”,如果学号是“00120231001”,存储时前导零直接丢失。身份证号就更典型,如果用FLOAT存,超过15位会变成科学计数法,这也是网上很多Oracle导出身份证信息变成“1.23457E+18”的根本原因。

第二个反例是成绩字段用INT。平时可能没事,一旦遇到0.5分制的课程,成绩就只能四舍五入,学生明明考了88.5,录进去变89。用DECIMAL(5,2)或者DECIMAL(4,1),既保证了精度,又不至于溢出。

2.3 给表结构做一次反向审视

建完表之后,有一个步骤很多人跳过,但特别有用:用数据库的图表功能把ER图画出来,或者用工具一键生成。SQL Server Management Studio自带“数据库关系图”功能,可以把所有表拖进去,外键关系一目了然。DBeaver里也能通过“ER Diagram”直接生成。

这一步的实际意义在于:图形的视觉反馈比看几十条CREATE TABLE语句更容易发现设计漏洞。某张表孤零零连一条线都没有,说明它可能是个废弃表;两张表之间的线太多,说明设计可能有冗余。我通常会审视两遍:第一遍看外键关系是否完整,第二遍看字段命名是否规范统一。命名不统一后面写JOIN时会非常痛苦,比如学生表里叫StudentID,选课表里叫StuID,多写几个查询就乱了。

3. 选课、成绩、排名:日常业务中的高频SQL

表结构定了,真正让系统跑起来的是业务SQL。这一部分我挑三类高频场景拆开讲,每一类都是教学管理系统里绕不开的。

3.1 选课的核心写入逻辑与幂等控制

选课是系统的最高频操作。一个简单的INSERT语句看起来只需要几行,但需要考虑三个问题:课程容量是否已满、学生是否重复选课、当前学期是否正确。

重复选课这件事,数据库层的唯一约束已经兜底了,但业务层面还是要先查一遍,给用户及时提示,而不是等数据库报错。完整选课流程大概是:

-- 1. 检查学生是否已经选了该教学班 IF EXISTS ( SELECT 1 FROM dbo.Enrollment WHERE StudentID = @StudentID AND ClassID = @ClassID ) BEGIN RAISERROR(N'不能重复选课', 16, 1); RETURN; END -- 2. 检查教学班容量 IF NOT EXISTS ( SELECT 1 FROM dbo.TeachingClass WHERE ClassID = @ClassID AND CurrentCount < MaxCount ) BEGIN RAISERROR(N'该课程已满', 16, 1); RETURN; END -- 3. 正式选课 INSERT INTO dbo.Enrollment (StudentID, ClassID, Semester) VALUES (@StudentID, @ClassID, @Semester);

但直接用IF EXISTS检查再插入有一个隐患:在高并发下,两个请求同时通过检查,然后都执行插入,唯一约束能拦下第二个,但用户会看到一个未经处理的数据库错误。更稳妥的做法是使用MERGE或前置唯一约束冲突处理,把“检查+插入”做成一个原子操作。SQL Server中可以用BEGIN TRAN和UPDLOCK结合,或者直接捕获唯一键冲突错误码(2627/2601),把错误转成友好提示。对课程设计来说,应用层加锁或数据库唯一约束至少要用上,比裸INSERT稳一个量级。

3.2 成绩统计:聚合、分组与条件计数

成绩录入完成后,学生最关心的是自己的总绩点,老师关心的是班级平均分、及格率、分数段分布,教务处关心的是各专业挂科率。这些都是典型的聚合统计场景。

计算学生平均分和绩点:

SELECT e.StudentID, s.StudentName, COUNT(*) AS TotalCourses, AVG(e.Score) AS AvgScore, SUM(CASE WHEN e.Score >= 60 THEN c.Credits ELSE 0 END) / SUM(c.Credits) AS PassRate FROM dbo.Enrollment e INNER JOIN dbo.Student s ON e.StudentID = s.StudentID INNER JOIN dbo.TeachingClass tc ON e.ClassID = tc.ClassID INNER JOIN dbo.Course c ON tc.CourseID = c.CourseID WHERE e.Semester = '2024-2025-1' GROUP BY e.StudentID, s.StudentName ORDER BY AvgScore DESC;

AVG函数会忽略NULL值,所以没出成绩的选课记录不会影响平均分计算,这个点很多新手会踩坑:以为自己要先过滤NULL,其实AVG天然处理了。SUM(CASE WHEN...)是条件聚合的经典写法,想在同一个查询里同时统计总学分和通过学分,这是最简单有效的方式。

分数段分布统计是另一个常见需求,用GROUP BY配合CASE就能实现:

SELECT tc.CourseID, c.CourseName, SUM(CASE WHEN e.Score >= 90 THEN 1 ELSE 0 END) AS ExcellentCount, SUM(CASE WHEN e.Score >= 80 AND e.Score < 90 THEN 1 ELSE 0 END) AS GoodCount, SUM(CASE WHEN e.Score >= 60 AND e.Score < 80 THEN 1 ELSE 0 END) AS PassCount, SUM(CASE WHEN e.Score < 60 THEN 1 ELSE 0 END) AS FailCount FROM dbo.Enrollment e INNER JOIN dbo.TeachingClass tc ON e.ClassID = tc.ClassID INNER JOIN dbo.Course c ON tc.CourseID = c.CourseID WHERE e.Semester = '2024-2025-1' GROUP BY tc.CourseID, c.CourseName;

3.3 排名需求:为什么窗口函数比游标好用

排名是教学管理系统里最容易被写复杂的需求。很多人第一反应是游标循环:先把所有人成绩查出来,然后一条条比对、计数、赋值。且不说游标性能差,代码还长。窗口函数一行就解决了。

SELECT e.StudentID, s.StudentName, e.Score, ROW_NUMBER() OVER (ORDER BY e.Score DESC) AS RankNo, RANK() OVER (ORDER BY e.Score DESC) AS RankWithGap, DENSE_RANK() OVER (ORDER BY e.Score DESC) AS DenseRankNo FROM dbo.Enrollment e INNER JOIN dbo.Student s ON e.StudentID = s.StudentID WHERE e.ClassID = @ClassID AND e.Score IS NOT NULL;

三个排名函数的区别是面试常考题,也直接影响业务逻辑:ROW_NUMBER对相同分数的两条记录会给出不同名次;RANK会在相同分数后跳过名次,比如出现两个第1名,下一条就是第3名;DENSE_RANK不跳号,两个第1名后继续第2名。学校排名通常用RANK,因为并列名次是公平的,但中文语境里“并列第一之后是第二”也说得通,看学校规定,我接触过的多数学校用RANK。

需要按课程分组排名的场景也能同样处理,只需要把PARTITION BY加到OVER子句里:

SELECT tc.CourseID, e.StudentID, s.StudentName, e.Score, RANK() OVER (PARTITION BY tc.CourseID ORDER BY e.Score DESC) AS CourseRank FROM dbo.Enrollment e INNER JOIN dbo.TeachingClass tc ON e.ClassID = tc.ClassID INNER JOIN dbo.Student s ON e.StudentID = s.StudentID WHERE e.Score IS NOT NULL;

窗口函数的高明之处在于:它在不改变行数的情况下完成计算,不像GROUP BY会把多行压成一行。这类“既要明细又要排名”“既要分组又要对比”的需求,窗口函数是最合适的技术手段。

4. 数据量上来之后:慢SQL排查与索引优化

课程设计阶段数据量小,SQL怎么写都能秒回。但系统一旦用了两三年,选课表轻松超过几万条,加上学生表、教学班表,一次多表JOIN就可能是几十万行的扫描量。这时候“慢SQL优化”就是必须面对的问题。

4.1 一条慢成绩单SQL的完整排查过程

我在一个真实项目里遇到过这种情况:学期末出“成绩单打印”,按班级输出每个人的课程成绩和总绩点,数据量大概3万条选课记录,查询要跑十几秒。SQL大致长这样:

SELECT s.StudentID, s.StudentName, c.CourseName, e.Score FROM dbo.Enrollment e LEFT JOIN dbo.Student s ON e.StudentID = s.StudentID LEFT JOIN dbo.TeachingClass tc ON e.ClassID = tc.ClassID LEFT JOIN dbo.Course c ON tc.CourseID = c.CourseID WHERE e.Semester = '2024-2025-1' AND s.ClassName = '计算机2201' ORDER BY s.StudentID, c.CourseID;

第一步先把实际执行计划打开,在SSMS里按Ctrl+M,再执行这条SQL,或者直接看输出的Estimated Number of Rows。结果很明显:Enrollment表走了Clustered Index Scan,也就是整表扫描,代价占到90%以上。

问题出在WHERE条件的筛选字段上。Enrollment表的主键索引是EnrollID,但业务查询用的筛选条件是Semester和学生外键,这些字段上都没有索引。数据库只能把整张表读一遍,过滤出符合条件的3万行,再逐条回表匹配学生、课程。

解决办法是在筛选和关联字段上建复合索引:

CREATE NONCLUSTERED INDEX IX_Enrollment_Semester_Class ON dbo.Enrollment (Semester, ClassID) INCLUDE (StudentID, Score);

这个索引的威力在于:查询条件里的Semester可以直接走索引,只取该学期的数据子集,ClassID字段在排序键中让同一个教学班的记录物理相邻,INCLUDE字段把Score提前放进索引叶子页,避免回表查数据页。执行后这条SQL从十几秒降到1秒以内,原因就是扫描的分区从全表变成了一个学期对应的少数页。

4.2 索引不是越多越好:设计原则与验证

参加过项目评审的人都知道,评委最爱问的一句话是“你的查询为什么用索引?索引怎么建的?”很多人的回答是“加了索引能变快”,但说不清加在哪个字段上、为什么。

索引设计的核心原则是围绕WHERE条件中高频出现的字段和JOIN的关联字段。上面提到过选课表的复合索引要包含学期和教学班ID;学生表的ClassName如果经常按班级筛选,也值得建索引;但像Gender这种区分度极低的字段,建索引意义不大,SQL Server优化器自己都知道扫表更快。

索引不是越多越好。每张表建几十个索引,会让INSERT和UPDATE变得非常痛苦,因为每次写入都要同步维护所有索引树。我见过一张选课表上堆了十多个索引的极端情况,数据写入时卡到超时,这就是过度索引的代价。

验证一个索引是否有效,最直接的指标是看执行计划是否走了Index Seek而不是Table Scan,以及逻辑读(Logical Reads)次数是否明显下降。在SSMS里用SET STATISTICS IO ON查看:

SET STATISTICS IO ON; SET STATISTICS TIME ON;

执行后会输出Table ‘Enrollment’。Scan count、Logical reads等信息。看Logical reads,数值从几千变成几百,说明这次优化是真的有效,不是心理安慰。

还有一个容易被忽略的优化点:不要在索引列上使用函数或计算。比如WHERE YEAR(EnrollTime) = 2024,会导致索引列被计算,SQL Server无法使用EnrollTime的索引。改写为WHERE EnrollTime >= '2024-01-01' AND EnrollTime < '2025-01-01',索引就生效了。这是一个很典型的“写法问题”导致的慢查询。

5. SQL注入不是玄学:从万能密码到参数化查询

教学管理系统登录功能是SQL注入的重灾区,因为开发者最熟悉的就是拼接字符串。看起来人畜无害的几行代码,在数据库安全层面可能是致命的。

5.1 一条登录SQL被绕过的分析

假设登录验证的SQL是这样写的:

SELECT * FROM dbo.UserInfo WHERE UserName = 'admin' AND Password = '123456';

如果后台是字符串拼接:

string sql = "SELECT * FROM dbo.UserInfo WHERE UserName = '" + userName + "' AND Password = '" + password + "'";

当输入的密码是' OR '1'='1时,SQL就变成:

SELECT * FROM dbo.UserInfo WHERE UserName = 'admin' AND Password = '' OR '1'='1';

'1'='1'恒为真,整个WHERE条件永远为真,攻击者不需要知道任何密码就能登录管理员账号。这就是“万能密码”的经典原理。更严重的变体是把输入拼进WHERE条件后加注释符--,把后面的条件全部注释掉。这类攻击在任何一个用字符串拼接SQL的系统里都存在,跟数据库品牌没关系,MySQL、SQL Server、Oracle全都一样。

自己写项目做测试时,可以去了解注入原理,但绝不能在有真实数据的系统上尝试。正确做法是根除拼接,使用参数化查询。

5.2 参数化与最小权限的防御组合

参数化查询的原理是把SQL语句结构传给数据库预编译,然后用参数占位符传值。数据库把语句结构固定下来之后,输入内容永远只是“值”,不可能是“SQL代码”。

C#的ADO.NET写法是这样:

using (SqlConnection conn = new SqlConnection(connectionString)) { string sql = "SELECT * FROM dbo.UserInfo WHERE UserName = @UserName AND Password = @Password;"; SqlCommand cmd = new SqlCommand(sql, conn); cmd.Parameters.AddWithValue("@UserName", userName); cmd.Parameters.AddWithValue("@Password", passwordHash); // 执行 }

Java的JDBC是PreparedStatement,Python的pymysql里是cursor.execute(sql, params)。语言不同,思路完全一样:SQL结构由代码固定,变量走参数通道。

防御SQL注入不是只靠参数化就完事,还要配合权限最小化原则。连接数据库的应用账号不应该是sa或db_owner,而应该是有权限限制的账号,只能读写自己需要的那些表。这样即使万一被注入,攻击者想执行xp_cmdshell这类危险存储过程也会因为权限不足直接失败。数据库账号只给SELECT/INSERT/UPDATE/DELETE权限,不给DDL权限,应用层账号永远不建表、不删表。

另外,明文密码存储是个低级的坑。用户表的密码字段应该存哈希值而不是明文。即使数据库被拖走,攻击者拿到的是哈希,也要花代价破解。老系统导出身份信息出现科学计数法的坑,根子也是字段类型设计失误,前面已经说过用NVARCHAR解决。

6. 环境选型与常见排错记录

写SQL离不开库环境。教学管理系统项目最常用的数据库还是SQL Server,从2008R2到2022,版本跨度很大。每次在这些版本上踩坑,都能写一小段排错记录。

6.1 SQL Server版本怎么选

SQL Server的版本让人眼花缭乱,我直接给结论:

  • 学习、课程设计、毕业设计:SQL Server 2022 Developer版,免费,功能完整,可以装在任何系统上,只是不能用于生产。
  • 个人做小工具、轻量部署:SQL Server Express版,免费,数据库单个文件上限10GB,对教学管理系统完全够用。
  • 企业项目上线:Standard版,生产可用,价格不便宜,但没必要自己承担。

很多人一搜“SQL Server下载”就找到一堆需要付费激活的渠道,其实微软官方对Developer和Express版是免费提供的,不需要网上找乱七八糟的下载站点。老项目还在用SQL Server 2008 R2的属于历史遗留,因为版本太老,对Windows新版支持不佳,碰到“服务无法启动”这类问题,如果不是业务必须,尽早迁移到新版。

6.2 安装和连接中遇到的几个典型问题

安装SQL Server时报错“无法启动Windows Management Instrumentation服务”,这是被问到过很多次的经典问题。WMI服务是Windows的一个基础设施服务,提供系统和硬件管理信息。SQL Server安装程序在检查环境时依赖它,如果该服务被禁用、损坏或依赖项启动失败,安装就会中断。排查步骤一般是:先到services.msc里查看Windows Management Instrumentation服务的启动类型是否为自动,手动启动一次看报错内容;然后检查它的依赖服务DCOM Server Process Launcher、RPC Endpoint Mapper是否正常。如果WMI服务本身没坏,把这些依赖服务启动起来再重启SQL Server安装程序就能过。

连接时报“[08001]命名管道提供程序: 无法打开与SQL Server的连接”,这个问题本质是TCP/IP协议没开。SQL Server默认安装时有时不会启用TCP/IP协议,客户端用TCP连接就会被拒。解决方法是打开SQL Server配置管理器,双击“SQL Server网络配置”里的“TCP/IP”协议,右键启用,然后重启SQL Server服务。还有一个关联问题,SQL Server Express默认实例名是SQLEXPRESS,连接字符串里写成localhost或服务器名是连不上的,必须是“localhost\SQLEXPRESS”。

还有一个我每次都要提醒的细节:SQL Server 2008 R2在删除数据库时报错“无法删除数据库,因为数据库正在使用”,原因多半是有别的连接会话占着这个库。排查方式是打开活动监视器,或执行sp_who查看会话,把对应SPID杀掉再执行DROP DATABASE。如果数据库设置成了单用户模式,还要先切回多用户模式。

7. 数据维护里容易被忽略的两个SQL技巧

除了上面这些核心流程,实际维护教学管理系统时还会遇到两类很实用的SQL操作,处理得不好会非常影响日常管理效率。

7.1 数据清洗:去重与去空值

学校里休学、复学、转专业的情况很多,学生数据更新频繁,难免出现重复记录或空字段。处理这些数据时有两个SQL技巧经常用到。

按某个字段去重,比如统计学生表里是否有重名的学生:

SELECT StudentName, COUNT(*) FROM dbo.Student GROUP BY StudentName HAVING COUNT(*) > 1;

删除完全重复的记录,只保留一行:

WITH CTE AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY StudentID ORDER BY EnrollYear DESC) AS RN FROM dbo.Student ) DELETE FROM CTE WHERE RN > 1;

这里的PARTITION BY指定去重键,ORDER BY决定保留哪一条,实际数据维护里非常常用。

去空值可以直接用COALESCE或者IS NULL过滤。比如打印选课名单时,天气影响导致有电话的学生没填手机号,空值填充成“未填写”:

SELECT StudentName, COALESCE(Phone, N'未填写') AS ContactPhone FROM dbo.Student;

COALESCE是SQL标准函数,返回参数列表中第一个非NULL值。它对后面的查询报表输出很有用:不管空值出现在哪列,统一输出不会导致前台页面出现空白。

7.2 把查询结果导出到报表的SQL写法

期末要交成绩册时,很多人会用程序循环去查每个班级的成绩再拼成一个Excel。其实直接在SQL Server里就能完成大部分报表数据的准备工作。

用一个查询把成绩册数据一次取出来,再在报表工具中直接引用这个查询,效率高得多:

SELECT s.ClassName, s.StudentID, s.StudentName, c.CourseName, tc.Semester, e.Score, CASE WHEN e.Score IS NULL THEN N'未考试' WHEN e.Score >= 90 THEN N'优秀' WHEN e.Score >= 80 THEN N'良好' WHEN e.Score >= 70 THEN N'中等' WHEN e.Score >= 60 THEN N'及格' ELSE N'不及格' END AS ScoreLevel FROM dbo.Enrollment e INNER JOIN dbo.Student s ON e.StudentID = s.StudentID INNER JOIN dbo.TeachingClass tc ON e.ClassID = tc.ClassID INNER JOIN dbo.Course c ON tc.CourseID = c.CourseID ORDER BY s.ClassName, s.StudentID, c.CourseID;

这样取数一次成型,报表工具里只需要渲染展示,不需要再做二次计算。数据源层的SQL写好,比在应用层写一堆循环逻辑处理数据干净得多,也更容易排查问题。

8. 我个人的几点体会

做了这么多个类似系统,最深的体会是:SQL写得好不好,在数据量小的时候体现不出来,但系统一旦跑起来、数据积累起来,差别就非常明显。一个教学管理系统核心就二三十张表、十来个业务查询,把基础的表设计、索引、参数化查询这三件事做扎实了,后面基本不会有大问题。

还有一点值得说:往数据库里写记录之前,先想想如果数据重复会发生什么,如果数据为空会发生什么,如果有人恶意构造输入会发生什么。把这三件事想明白,系统就稳了一大半。很多人看起来是SQL写不好,其实是对数据本身的思考不够。很多看起来是SQL的问题,根子都是设计阶段埋下的雷。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询