☰
数据库实验报告拆解:SQL建表、查询与完整性复习地图
2026/10/27 3:01:31 网站建设 项目流程

简介:面向太原理工大学软件工程专业数据库概论课程的实验报告文档,适合正在学习数据库系统原理、需要完成SQL上机实验或复盘课堂操作的学生参考。内容围绕Microsoft SQL Server 2016环境展开,覆盖基本表的创建、修改与删除,索引与视图的建立,以及插入、更新、查询等数据操作,并涉及数据完整性、用户鉴别与数据控制等实验主题,能帮助读者理清实验步骤与SQL语句的对应关系。资源包共1个docx文件,大小约1.69MB,以实验报告正文形式组织,便于直接查阅实验目的、平台配置、主要实验内容及结果记录。目前已有311人学习下载。读者可从中获得较完整的实验过程样例、SQL语法示例与结果验证思路,用于对照自己的建表、索引、视图和更新操作,查漏补缺并理解数据完整性约束在实践中的落地方式。

1. 数据库实验的知识地图:一份实验报告能拆出多少考点

软件工程专业学数据库,最后大多要交一份实验报告:几十条 SQL、一个学生选课库、两三张结果截图。很多人写完就丢进硬盘深处,等到复习才发现,这东西其实是一张现成的知识地图——数据定义、数据更新、数据查询、数据完整性、访问控制,五块内容是混在一起写的,而考场上它们是分开问的。这份笔记做的事就是把那份混着写的实验报告重新切开、按模块码齐:先讲表、索引、视图这些“架子”怎么搭,再讲增删改查这条主链路,然后是约束、触发器、权限这三道完整性关卡,最后收到一张能自测的复习清单。它适合正在系统学数据库的人当地图用,也适合考前想快速查漏补缺的人对着目录翻。提前说一句:读的时候手边开着查询编辑器,看到语句就敲一遍,比读三遍管用。

2. 数据定义与对象管理:建表、改表、索引三件事

数据定义是整份实验报告的地基,也是最容易被“跑通了就行”糊弄过去的一段。建表语句写得糙,后面所有更新、查询、约束验证都会跟着别扭。这一章把三张核心表、四类 ALTER 动作、三种索引和视图放一起对照,搞清楚每一步为什么这么写。

2.1 三张表的骨架:主键、外键、CHECK 与 DEFAULT 怎么摆

学生选课库是数据库课的万能模型:Student存人,Course存课,SC存“谁选了什么、考了多少分”。三张表的难点不在字段多少,而在约束往哪儿放。

-- 学生表:实体完整性靠主键,用户定义完整性靠 CHECK 和 DEFAULT CREATE TABLE Student ( Sno CHAR(8) PRIMARY KEY, -- 主键:唯一且非空 Sname CHAR(8) NOT NULL, -- 姓名不允许为空 Ssex CHAR(2) CHECK (Ssex IN ('男','女')), -- 取值域限制 Sage SMALLINT, Sdept CHAR(20), Sclass CHAR(4) NOT NULL, Stotal SMALLINT DEFAULT 0 -- 不写值时自动填 0 ); -- 课程表:约束显式命名,后面想删才有名字可用 CREATE TABLE Course ( Cno CHAR(4) CONSTRAINT PK_Course PRIMARY KEY, Cname CHAR(40), Cpno CHAR(4), -- 先修课,指向本表 Cno Ccredit SMALLINT ); -- 选课表:复合主键 + 两个外键 + 成绩区间校验 CREATE TABLE SC ( Sno CHAR(8) REFERENCES Student(Sno), -- 列级外键的写法 Cno CHAR(4), Grade SMALLINT CONSTRAINT SC_CHECK CHECK (Grade > 0 AND Grade < 100), PRIMARY KEY (Sno, Cno), -- 同一个学生同一门课只能一条 CONSTRAINT FK_SC FOREIGN KEY (Cno) REFERENCES Course(Cno) );

三张表的角色分工值得单独记一句:Student和Course是“被引用方”,SC是“引用方”,所以外键只出现在SC上;SC的主键是复合的(Sno, Cno),这决定了它天然能防止重复选课,也决定了后面插入时两个列都不能为空。

提示:列级外键的标准写法是Sno CHAR(8) REFERENCES Student(Sno)。把FOREIGN KEY (Sno) REFERENCES ...塞进列定义里是语法错误,FOREIGN KEY关键字只属于表级约束。这个坑在交作业时特别常见,因为两种写法看起来只差几个字。

约束命名也是一门小学问。CONSTRAINT PK_Course PRIMARY KEY和直接PRIMARY KEY效果一样,但前者在系统里留下了名字,后面ALTER TABLE ... DROP CONSTRAINT PK_Course才有东西可删。默认命名的约束(像CK__SC__Grade__1A2B3C4D这种)你根本背不下来,改约束时只能先去查系统视图。

2.2 ALTER TABLE 的四类动作与执行顺序陷阱

建完表之后结构往往还要动,实验报告里通常会出现四类动作,每一类对应一个真实需求:

动作语法形态典型场景
加列ALTER TABLE 表 ADD 列 类型学生表补一个班级字段
改列类型ALTER TABLE 表 ALTER COLUMN 列 新类型Sage从INT收窄为SMALLINT
加约束ALTER TABLE 表 ADD UNIQUE(列)/ADD FOREIGN KEY ...课程名唯一、先修课自引用
删约束ALTER TABLE 表 DROP CONSTRAINT 约束名把成绩区间从开区间改成闭区间
-- 加列:新列默认允许 NULL,所以老数据不会报错 ALTER TABLE Student ADD Sclass CHAR(4); -- 改类型:从 INT 收到 SMALLINT 属于收窄,超出范围的老数据会直接失败 ALTER TABLE Student ALTER COLUMN Sage SMALLINT; -- 课程名加唯一约束,防止同名课程重复录入 ALTER TABLE Course ADD UNIQUE (Cname); -- 先修课自引用:Cpno 指向本表的 Cno ALTER TABLE Course ADD FOREIGN KEY (Cpno) REFERENCES Course(Cno);

这几条语句单看都简单,麻烦在顺序。加了Sclass列之后才能在SELECT里引用它;SC表的Grade约束如果一开始写成Grade > 0 AND Grade < 100,那 0 分和 100 分都插不进去,必须先把旧约束DROP掉再重建:

ALTER TABLE SC DROP CONSTRAINT SC_CHECK; ALTER TABLE SC ADD CONSTRAINT SC_CHECK CHECK (Grade >= 0 AND Grade <= 100);

改类型这一步要额外留个心:ALTER COLUMN在数据量大的表上可能触发表重建,收窄类型还可能截断或报错。实验环境里几百行数据感觉不到,换到真实库就是血泪经验。

2.3 索引:唯一、聚簇、复合与排序方向

索引在实验里通常只占三五行,但它是“为什么有的查询快”的唯一答案。四类索引各有各的适用面:

-- 普通索引:加速按课程名的等值和范围查询 CREATE INDEX iCname ON Course(Cname); -- 唯一索引:既加速又保证取值不重复 CREATE UNIQUE INDEX iSname ON Student(Sname); -- 聚簇索引:决定数据行的物理排列顺序,一张表只能有一个 CREATE CLUSTERED INDEX iSnoCno ON SC(Sno, Cno DESC); -- 唯一索引也可以用约束的方式建,二者底层等价 CREATE UNIQUE INDEX iCno ON Course(Cno);

iSnoCno这条最值得拆开看。它是复合索引,索引键是(Sno, Cno DESC),意味着按学号升序、同学号内按课程号降序排列。查询WHERE Sno = '20210001'能吃到它(最左前缀原则),只查WHERE Cno = '1'就吃不到——这就是复合索引“最左列不参与就白搭”的规则。DESC影响的是扫描方向,对ORDER BY Sno, Cno DESC这类排序能省掉一次排序步骤。

聚簇索引的限制也要记牢:一张表只能有一个,因为数据行只能有一种物理顺序。SC表把它建在(Sno, Cno)上,等于让同一学生的选课记录在磁盘上扎堆,查“某学生选了哪些课”时一次页读取就够。反过来,如果聚簇索引建在别的列上,再想按学号聚簇就没机会了。

注意:索引不是越多越好。每个索引在插入、更新、删除时都要同步维护,写多读少的表上堆索引纯属自找麻烦;区分度低的列(比如性别只有两个取值)建索引基本不划算。

2.4 视图:把复杂连接封成一个名字

视图是虚表,本身不存数据,只保存一条查询定义。实验报告里常见的三种视图分别对应三种需求:单表投影、聚合汇总、多表连接。

-- 需求一:只看信息系学生的学号、姓名、年龄 CREATE VIEW IS_Student AS SELECT Sno, Sname, Sage FROM Student WHERE Sdept = 'IS'; -- 注意原文这里少了一个右引号 -- 需求二:每个学生的平均分,列名重命名让结果更好读 CREATE VIEW S_G (Sno, Gavg) AS SELECT Sno, AVG(Grade) FROM SC GROUP BY Sno; -- 需求三:把三表连接封起来,以后查选课明细不用再写 JOIN CREATE VIEW XK_VIEW AS SELECT Student.Sno, Sname, Course.Cno, Cname, Grade FROM Student, SC, Course WHERE Student.Sno = SC.Sno AND SC.Cno = Course.Cno;

建完之后SELECT * FROM IS_Student就能当普通表用,这是视图最直接的价值:把三表连接这种一写就错的语句固定下来,调用方只需要记住一个名字。

视图的可更新性是个高频考点。单表投影、不含聚合和DISTINCT的视图一般可以增删改;带GROUP BY、聚合函数、DISTINCT、集合运算的视图(比如S_G)不可更新,因为一条视图记录对应的是多行基表数据的汇总,数据库没法反推该改哪一行。

3. 数据更新与查询:从 INSERT 到嵌套子查询的完整链路

这一章是整份实验报告里篇幅最大的部分,也是考试分值最集中的地方。按“写进去、改掉、删掉、查出来”的顺序走一遍,比零散背语法有效得多。

3.1 插入的三种写法与 SELECT INTO 建临时表

插入看似只有一条INSERT,实际有四种变体,用错了就是约束报错或者列错位:

-- 写法一:全列插入,值的顺序必须和表定义完全一致 INSERT INTO Student VALUES ('20210001','林一','男',20,'CS','1001',0); -- 写法二:指定列插入,允许空值的列可以省略,走默认值或 NULL INSERT INTO Student (Sno, Sname, Ssex, Sclass) VALUES ('20210002','周迟','女','1001'); -- 写法三:插入 NULL 要显式写 NULL,不能靠省略 INSERT INTO Student VALUES ('20210003','陈舟','女',NULL,NULL,'1001',0); -- 写法四:从查询结果批量插入,一次搬一整批数据 INSERT INTO cs_Student (学号, 姓名, 年龄) SELECT Sno, Sname, Sage FROM Student WHERE Sdept = 'CS';

写法四特别值得记,因为两个细节容易漏:目标表的列顺序和SELECT的列顺序是按位置对应的,名字对不对得上反而不重要;INSERT INTO ... SELECT不加VALUES关键字,加了就报错。

SELECT INTO是另一条路,它会在插入的同时新建表:

-- 把选课数据整体复制成一张临时表,方便做“删除演练” SELECT * INTO tmpSC FROM SC;

这条语句的特点是目标表不能事先存在,字段类型和名字从查询结果推导。实验里用它来练删除最合适——删坏了重新SELECT INTO一遍就行,不会污染原始数据。

3.2 UPDATE 与 DELETE:子查询做条件时的两种写法

单条件更新没什么好说的,真正容易出错的是“条件在别的表里”。这时候有两种写法,一种用标量子查询,一种用相关子查询:

-- 标量子查询:先把系名查出来,再当作等值条件 UPDATE SC SET Grade = Grade + 5 WHERE 'CS' = (SELECT Sdept FROM Student WHERE Student.Sno = SC.Sno); -- 相关子查询:子查询里的 SC.Sno 来自外层正在更新的那一行 UPDATE SC SET Grade = 80 WHERE Cno = '2' AND Sno = (SELECT Sno FROM Student WHERE Sname = '周迟');

两条语句的关键差别在于关联列的位置。第二条里,子查询(SELECT Sno FROM Student WHERE Sname = '周迟')返回的是单值,所以能用=;如果姓名不唯一,子查询返回多行,=直接报错,得换成IN。这是标量子查询最常见的运行时错误。

删除的语法更简单,风险反而更大:

-- 有条件删除:删掉某学生的某门选课记录 DELETE FROM SC WHERE Sno = '20210001' AND Cno = '1'; -- 无条件删除:清空整表,表结构还在,自增种子会重置 DELETE FROM tmpSC;

DELETE FROM 表不带WHERE就是全删,这个动作在实验里是刻意演示,在真实库上就是事故。区分一下:DELETE是逐行删、记日志、可回滚;TRUNCATE TABLE是整页释放、不记行日志、速度快但不可回滚;DROP TABLE连表结构一起没。三者不是一回事。

还有一个连锁反应值得提前知道:UPDATE Student SET Sno = '20210025' WHERE Sno = '20210021'这种改主键的操作,如果SC表里有对应外键记录,且外键没声明ON UPDATE CASCADE,语句会直接失败。想改主键,要么先删子表记录,要么建外键时就带上级联选项。

3.3 单表查询:条件写法的清单

单表查询是分值最稳的一块,题目千变万化,落到语法上就那么几类谓词。做成一张对照表,复习时扫一眼比翻书快:

需求写法常见翻车
去重SELECT DISTINCT Sno FROM SC忘了DISTINCT,选课数被算成选课记录数
表达式列SELECT Sname, YEAR(GETDATE()) - Sage AS 出生年 FROM Student硬编码年份,换一年结果全错
列别名Sname AS 姓名/Sname 姓名/姓名 = Sname三种写法混用,中文别名没加引号报错
范围WHERE Sage BETWEEN 20 AND 23以为区间开闭可配,实际两端都包含
集合WHERE Sdept IN ('IS','MA','CS')用OR拼一长串,漏括号导致优先级错乱
模糊WHERE Sname LIKE '林%'%和_混用;NOT LIKE的 NULL 行不返回
空值WHERE Grade IS NULL写成= NULL,永远返回空结果集
多重条件WHERE Sdept = 'CS' AND Sage < 20AND优先级高于OR,混用时漏括号
排序ORDER BY Sdept, Sage DESC以为DESC管全部列,其实只作用于紧跟的那一列
前 N 行SELECT TOP 5 * FROM SC ORDER BY Grade DESCTOP不带ORDER BY,结果顺序不确定

表格里最值得反复看的是IS NULL和= NULL的区别。NULL不是值,是“未知”,任何和它的比较运算结果都是未知,WHERE Grade = NULL只会返回空集。同理,COUNT(Grade)不统计 NULL 行,COUNT(*)才统计。

ORDER BY的多列排序也要说清楚:ORDER BY Sdept, Sage DESC的语义是“先按系名升序,系名相同时按年龄降序”,DESC只修饰Sage,Sdept仍然默认升序。想两列都降序,得写两遍DESC。

3.4 分组统计:GROUP BY、HAVING 与聚集函数的执行顺序

分组是查询里最抽象的一块,抽象的原因是它改变了对“一行”的定义。理解方式就一句:GROUP BY之后,每一组变成一行,SELECT里能出现的只有分组列和聚集函数。

-- 按课程分组:每组的课程号、人数、均分、最高分 SELECT Cno AS 课程号, COUNT(*) AS 人数, AVG(Grade) AS 均分, MAX(Grade) AS 最高分 FROM SC GROUP BY Cno HAVING AVG(Grade) > 85; -- 过滤的是“组”,不是“行”

这段语句的执行顺序是FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY。WHERE在分组前过滤行,HAVING在分组后过滤组,二者不能互换位置。想在分组前筛掉 NULL 成绩,写WHERE Grade IS NOT NULL;想筛掉均分不够的课程,写HAVING AVG(Grade) > 85。

聚集函数本身也有讲究:

写法含义差别
COUNT(*)统计行数含 NULL 行
COUNT(Grade)统计该列非空值个数跳过 NULL
COUNT(DISTINCT Sno)统计不重复的学号数选课人数,不是选课记录数

COUNT(*)和COUNT(DISTINCT Sno)的差别在“选课人数”这类题里是送分点也是丢分点:一个学生选三门课,前者算三条,后者算一人。

跨表分组时要多留意一句:SELECT里的非聚集列必须出现在GROUP BY里。想按班级统计每门课的选课人数,就得GROUP BY Sclass, Cno,两个列都写上,少写一个在很多数据库上直接报错。

3.5 连接查询:WHERE 连接、JOIN ON、外连接与自连接

连接是把散在两张表里的信息拼成一行。同一件事有三种写法,语义等价,风格不同:

-- 写法一:连接条件写在 WHERE 里(隐式连接) SELECT Student.Sno, Sname, Cno, Grade FROM Student, SC WHERE Student.Sno = SC.Sno; -- 写法二:JOIN ON,连接条件和过滤条件分家(推荐) SELECT Student.Sno, Sname, Cno, Grade FROM Student JOIN SC ON Student.Sno = SC.Sno WHERE SC.Grade > 85; -- 过滤条件单独放 WHERE -- 写法三:外连接,没选课的学生也要出现在结果里 SELECT Student.Sno, Sname, Cno, Grade FROM Student LEFT OUTER JOIN SC ON Student.Sno = SC.Sno;

三种写法里,JOIN ON最值得养成习惯,因为连接条件(ON)和过滤条件(WHERE)分开了,读语句时一眼能看出哪部分是“拼表”、哪部分是“筛行”。隐式连接混着写的时候,少写一个连接条件就会退化成笛卡尔积,行数瞬间爆炸——这是最典型的一种“结果看着不对劲”。

外连接和它的位置强相关:LEFT OUTER JOIN保留左表全部行,右表没匹配上的补 NULL;RIGHT OUTER JOIN反过来。上面第三条语句的结果里,没选课的学生Cno和Grade都是 NULL,这正是“查没选课的学生”这类题的标准解法。

自连接是连接里的进阶用法,用来处理“表自己引用自己”的结构。课程表里的Cpno指向本表Cno,要求“每门课的间接先修课”,就得让课程表和它自己连一次:

-- first 是当前课程,second 是它的先修课;再往下找一层就是间接先修课 SELECT first.Cno AS 课程号, second.Cpno AS 间接先修课 FROM Course first, Course second WHERE first.Cpno = second.Cno;

自连接必须给同一张表起两个别名,否则列名没法区分。first.Cpno = second.Cno的意思是“第一张表的先修课号,等于第二张表的课程号”,也就是顺着先修关系往上走一层。

3.6 嵌套查询与集合运算:IN、比较、ALL/ANY、EXISTS

嵌套查询是把一条SELECT的结果当作另一条的条件。按引出方式分四类,难度递增:

-- 一、IN 引出:同系学生,子查询可能返回多行 SELECT Sno, Sname, Sdept FROM Student WHERE Sdept IN (SELECT Sdept FROM Student WHERE Sname = '林一'); -- 二、比较运算符 + 聚集函数:每个学生超过自己平均分的课程 SELECT Sno, Cno FROM SC x WHERE Grade >= (SELECT AVG(Grade) FROM SC y WHERE y.Sno = x.Sno); -- 三、ALL:比计算机系所有学生年龄都小 SELECT Sname, Sage FROM Student WHERE Sage < ALL (SELECT Sage FROM Student WHERE Sdept = 'CS') AND Sdept <> 'CS'; -- 四、EXISTS:只关心“有没有”,不关心具体值 SELECT Sname FROM Student WHERE EXISTS (SELECT * FROM SC WHERE SC.Sno = Student.Sno AND Cno = '1');

第二条是相关子查询的典型:y.Sno = x.Sno里的x来自外层,子查询对外层每一行都重新算一次。这类语句的性能陷阱在于,很多人会写成不相关的形式然后发现结果不对——AVG(Grade)到底是全表平均还是每人平均,全看有没有那个关联条件。

第四条里EXISTS的子查询SELECT *不是真的取所有列,数据库只判断有没有行,所以写SELECT *和SELECT 1没区别。NOT EXISTS常和“全称量词”的题绑在一起(比如“选修了全部课程的学生”),解法是“不存在一门他没选的课”,句式要绕一下。

集合运算把两条独立查询的结果按行合并:

运算关键字语义注意
并UNION两结果集合并默认去重,要保留重复用UNION ALL
交INTERSECT两边都有的行列数和类型必须一一对应
差EXCEPT左边有、右边没有顺序敏感,A EXCEPT B ≠ B EXCEPT A
-- 差集:计算机系里年龄大于 19 的学生 SELECT * FROM Student WHERE Sdept = 'CS' EXCEPT SELECT * FROM Student WHERE Sage <= 19;

EXCEPT的顺序一定不能反。同一道题写成SELECT * FROM Student WHERE Sage <= 19 EXCEPT ...,结果就变成“年龄小的非计算机系学生”,语义完全变了。集合运算对列的要求也比连接严格:列数相同、对应列类型兼容,SELECT *在两边表结构不一致时会直接报错。

4. 数据完整性与访问控制:约束、触发器、权限三道关

前两章解决“怎么建、怎么改、怎么查”,这一章解决“改坏了怎么办”。完整性是数据库课里最像工程实践的一块,也是实验报告里最容易只截图不解释的部分。

4.1 实体完整性、参照完整性与用户定义完整性怎么落地

教材上的三类完整性,落到 SQL 上就三种手段,一一对应:

完整性类型保护什么SQL 手段违约时的默认行为
实体完整性每行可唯一识别PRIMARY KEY拒绝插入或更新
参照完整性外键必须指向存在的行FOREIGN KEY ... REFERENCES拒绝,或按级联选项处理
用户定义完整性业务规则NOT NULL、UNIQUE、CHECK、DEFAULT拒绝

实体完整性靠主键,主键天然带唯一和非空两个属性,不需要再单独写UNIQUE和NOT NULL。参照完整性靠外键,外键的默认行为是“拒绝”——想删被引用的父行,先删子行。如果业务上希望自动跟着删,就在外键后面加ON DELETE CASCADE,这一项在实验里不常用,但在真实建模里几乎每张关联表都要想一遍。

用户定义完整性最灵活,也最容易被忽略边界。CHECK (Grade > 0 AND Grade < 100)看着合理,实际把 0 分和 100 分都挡在外面;改成>= 0 AND <= 100才和百分制对得上。约束写错方向(比如Ssex IN ('男','女')写成NOT IN)不会报错,只会让所有正常数据插不进去。

4.2 完整性验证:五种故意写错的语句各报什么错

实验里最有价值的一步不是建约束,而是故意违反约束看系统反应。五类典型操作,报错信息各不相同:

操作预期结果原因
插入已存在的Sno失败主键唯一性冲突
INSERT INTO SC VALUES('20210001', NULL, 78)失败主键列Cno不允许 NULL
UPDATE Student SET Sno = '20210021' WHERE Sname = '某学生'失败新学号已被占用,主键冲突
插入两门同名课程失败UNIQUE(Cname)冲突
插入Grade = 105失败CHECK越界
插入时省略Stotal成功,值为 0DEFAULT 0生效

第三行值得多看一眼,因为它和“改学号”那类题是同一件事的两种结果:改成一个没被占用的新值能成功,改成已存在的值就冲突。判断依据只有一条——改完之后主键集合里有没有重复。

第二行是复合主键的经典陷阱。SC的主键是(Sno, Cno),两个列合起来构成主键,而主键的每一列都不允许为 NULL。所以哪怕Sno有值,Cno写 NULL 一样被拒。很多人以为“复合主键可以让某一列为空”,恰恰相反,复合主键的每一列都是强制非空。

4.3 触发器:约束表达不了的时候用它

约束只能管单行、单表、静态条件。一旦规则跨表或者需要“发生之后再做一件事”,就轮到触发器:

-- 选课记录插入后,把该学生的总学分同步累加 CREATE TRIGGER trg_SC_Insert ON SC AFTER INSERT AS BEGIN UPDATE Student SET Stotal = ISNULL(Stotal, 0) + (SELECT SUM(Ccredit) FROM inserted i JOIN Course c ON i.Cno = c.Cno WHERE i.Sno = Student.Sno) WHERE Sno IN (SELECT Sno FROM inserted); END;

AFTER INSERT表示插入成功之后触发,inserted是系统提供的虚拟表,装着这次插入的所有新行。触发器里必须按“批量”思维写,一次插入多行时inserted里就有多行,用=去等一个值会漏数据——这是触发器最常见的翻车点。

触发器能做的事很多,但用它的门槛也该高一点:它隐式执行、难调试、嵌套触发容易形成链式反应,排错时基本属于黑匣子。能用约束表达就别上触发器,这句话在实验报告里写不出来,在真实项目里是硬规矩。

4.4 用户鉴别与数据控制:登录名、数据库用户与 GRANT/REVOKE

实验题目里常出现“用户鉴别与数据控制”这一项,正文往往只做了约束部分。这一块按通用 SQL 标准补齐,逻辑分三层:登录身份进服务器、数据库用户映射进来、角色和权限决定能干什么。

-- 第一层:服务器级登录名 CREATE LOGIN stu_reader WITH PASSWORD = 'Abc@123456'; -- 第二层:映射为当前库的数据库用户 CREATE USER stu_reader FOR LOGIN stu_reader; -- 第三层:授权,只给查询和插入,且允许继续转授 GRANT SELECT, INSERT ON Student TO stu_reader WITH GRANT OPTION; -- 收回权限,CASCADE 连带收回对方转授出去的部分 REVOKE INSERT ON Student FROM stu_reader CASCADE;

权限语句的重点是几个后缀:WITH GRANT OPTION表示被授权人还能把权限转给别人,形成授权链;REVOKE ... CASCADE会顺着这条链一路收回,不写CASCADE而对方已经转授过,语句会报错。DENY是另一条独立路径,它优先级高于GRANT,用来做“整体放开、个别人禁止”的场景。

语句作用关键后缀
GRANT授予权限WITH GRANT OPTION允许转授
REVOKE收回权限CASCADE连带收回下游授权
DENY显式拒绝优先级最高,压过GRANT

最容易被忽略的是“登录名”和“数据库用户”的区别:CREATE LOGIN是服务器层面的身份,CREATE USER才是某个库里的账户,两步都做完,权限才有落点。

5. 易错点复盘:五个最容易翻车的地方

实验报告里的错误大体分两类:一类会直接报错,改起来反而快;另一类不报错但结果是错的,交上去自己都看不出来。下面五条都是第二类居多,按「易漏点 → 为什么漏 → 怎么补」拆开写。

5.1 易漏点一:ALTER 加列与后续语句的先后顺序

为什么漏:实验报告是分节写的,建表一节、查询一节,中间隔着半页纸。加Sclass列的那条ALTER TABLE如果放在查询之后,或者干脆漏写,所有引用Sclass的分组查询都会报“列名无效”。更隐蔽的是先建了表却没加列,然后INSERT里带着 6 个值去插 7 列的表,报的是列数不匹配,和真正的原因隔了一层。

怎么补:把脚本按“建表 → 改表 → 插数据 → 查询”固定成四段,任何时候重跑都从头执行一遍。写完立刻用SELECT * FROM 表看一眼实际结构,别凭记忆判断有哪些列。引用某个列之前,先在脚本里能搜到它被创建的那一行。

5.2 易错点二:CHECK 的边界值把 0 分和 100 分挡在门外

为什么漏:写约束时脑子里的语义是“成绩在 0 到 100 之间”,手上一顺手就写成> 0 AND < 100。开区间和闭区间差两个符号,测试数据又恰好都是 80 多分,一路绿灯,直到某天要插一个满分记录才炸。

怎么补:凡是区间约束,先问一句“端点算不算”。成绩、年龄、折扣率这类业务量,端点通常都要包含,写>= ... AND <= ...。建完约束立刻用边界值试三次:最小值、最大值、越界值,三次结果都对才算这条约束写完了。

5.3 易错点三:主键列写 NULL 与“复合主键能不能部分为空”

为什么漏:直觉上“复合主键是两个列合起来唯一”,于是很自然地以为单个列可以为空、只要组合不重复就行。实际规则是主键的每一列都强制非空,INSERT INTO SC VALUES('20210001', NULL, 78)会直接被拒。

怎么补:记住一句话——主键 = 唯一 + 非空,复合主键是每个分量都唯一非空。如果业务上确实需要“某一列可空”的唯一性,那是UNIQUE约束的活,不是主键的活。测试时专门插一条带 NULL 主键的记录,确认系统拒绝。

5.4 易错点四:UPDATE/DELETE 少了 WHERE 或者关联列写错

为什么漏:UPDATE SC SET Grade = 80这样的语句在语法上完全正确,执行也不报错,只是把整张表的成绩全改成了 80。另一种是相关子查询里关联列写错,比如把Student.Sno = SC.Sno写成Student.Sno = Student.Sno,子查询退化成常量判断,结果要么全中要么全不中。

怎么补:养成“先 SELECT 后 UPDATE”的习惯——把UPDATE的WHERE原样搬到SELECT里跑一遍,看行数和内容对不对,再执行修改。改之前先SELECT COUNT(*)记下目标行数,改完对比受影响行数。涉及子查询的语句,把子查询单独拎出来跑一次,确认它返回的是单值还是多行。

5.5 易错点五:GROUP BY 里塞进非分组列、列名拼写与硬编码常量

为什么漏:SELECT Sname, AVG(Grade) FROM SC GROUP BY Sno这类语句在部分数据库上不报错,只是Sname的取值毫无意义(每组随机返回一行)。再叠加两类低级错误:表名拼成Sudent、列名Grade写成Score、出生年份写死成2004 - Sage——拼写错误会报错还算好,硬编码年份不报错,只是每年都算错。

怎么补:SELECT里的非聚集列,逐个核对是否出现在GROUP BY中,缺一个就补一个,别指望数据库替你选。年龄和出生年份一律用YEAR(GETDATE()) - Sage这类函数算,不写死常量。整段脚本跑之前先在编辑器里过一遍红色波浪线,把表名、列名和实际结构对齐。

6. 复习清单与查漏补缺:把实验报告变成一张自测表

到了收尾这一步,最有效的做法不是重读一遍报告,而是把它改造成一张能自测的表。下面这张清单按模块列了自测问题和掌握标准,遮住右列自问一遍,答不上来的就是当天要补的洞。

模块自测问题掌握标准
数据定义三张表的主键、外键、CHECK、DEFAULT 分别在哪能默写出建表语句并说清每条约束防的是什么
ALTER加列、改类型、加约束、删约束各一条语法能说清执行顺序为什么不能颠倒
索引聚簇索引为什么只能有一个,复合索引的最左前缀能判断给定查询能不能吃到某个索引
视图哪类视图可更新,哪类不可能举出GROUP BY视图不可更新的理由
更新INSERT ... SELECT与SELECT INTO的区别能说清前者要求目标表已存在
查询WHERE和HAVING的分工,COUNT(*)与COUNT(列)的差别能写出带HAVING的分组语句并解释执行顺序
连接内连接三种写法、外连接补 NULL、自连接要别名能判断一条语句会不会退化成笛卡尔积
嵌套IN、比较、ALL、EXISTS各适合什么题能识别相关子查询的关联列在哪
完整性三类完整性对应的 SQL 手段能预测五种违约操作分别报什么错
权限GRANT、REVOKE、DENY的优先级能说清WITH GRANT OPTION和CASCADE的连带效果

比表格更实用的是一个十五分钟的动手流程:新建一个空库,不看报告,凭记忆把三张表建出来;跑五条故意违规的插入,逐条预测报错再验证;写一条三表连接加分组的统计语句;最后用LEFT JOIN找出没选课的学生。四步做完,哪一步卡住,就翻回对应那一章。这比从头读一遍原文高效得多,因为读的时候大脑会骗你说“这个我会”,敲的时候不会。

还有一个收尾细节:把整份脚本从头到尾连续跑一遍。分段执行时,前面的DROP、ALTER造成的状态变化会被掩盖;连续跑才会暴露顺序依赖——比如某条SELECT引用了三步之后才加的列。连续跑通之后,把脚本按模块加注释分段保存,以后复习直接按段执行,哪段报错就说明哪块忘了。

我自己第一次整理这份东西的时候,是考前一天把报告从头翻到尾,觉得每条语句都眼熟,合上电脑却写不出一条完整的三表连接。后来改了个习惯:每次复习数据库,先关掉所有资料,白纸上默写建表和一条分组查询,写完再对照。默写不出来的地方才是真正的盲区,看着眼熟的那些,多半只是眼熟。从那以后我每次碰数据库相关的复习,都强制走一遍“空库建表 + 违规插入 + 三表查询”这三步,比刷十页选择题踏实。希望这套拆法帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询