ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Oracle期末复习:10道典型SQL与PL/SQL实例题精讲

Oracle期末复习:10道典型SQL与PL/SQL实例题精讲

Oracle期末复习,最能拉开分差的其实就是那几道典型的SQL与PL/SQL题目。很多同学复习时习惯把教材从头翻到尾,概念背了一堆,结果到了考场上,面对“用SQL实现XX查询”“补齐下面的PL/SQL块”这类题目,还是卡壳。我带过不少学生的考前突击,也见过太多“背了语法却写不出完整语句”的例子,总结下来,与其漫无目的地刷题,不如把最常考的十类知识点做成一个闭环练习。这篇文章整理了10道有代表性的Oracle期末实例题,覆盖基础查询与排序、多表连接、分组统计、子查询、数据更新与事务回滚、建表约束、用户权限、视图、PL/SQL匿名块和索引,每道题都给出标准答案、逐步解析、易错提醒以及实际执行效果说明。适合正在准备Oracle数据库期末考试的在校学生,也适合刚入门想检验自己掌握程度的学习者。

1. 考前摸底:10道题的考点地图

1.1 题型分布与知识权重

先把这10道题的考点分布铺开,方便对照自己的薄弱环节。

题号核心考点常见题型难度建议用时
题1SELECT过滤与排序编写SQL入门3分钟
题2多表连接(ANSI语法)编写SQL入门5分钟
题3GROUP BY与HAVING聚合编写SQL基础5分钟
题4单行子查询编写SQL基础5分钟
题5UPDATE与事务回滚编写SQL+简答基础6分钟
题6建表与完整性约束编写SQL基础6分钟
题7用户创建与权限授予编写SQL+简答基础4分钟
题8视图与WITH CHECK OPTION编写SQL进阶6分钟
题9PL/SQL匿名块编程题进阶10分钟
题10索引创建与删除编写SQL入门3分钟

从这张表能看出两点:第一,期末试卷不会只考概念背诵,绝大多数分数落在“你能不能写出能跑的语句”上;第二,PL/SQL匿名块几乎是必考的压轴题,因为它能同时考察变量声明、SELECT INTO、异常处理这几个得分点。如果时间有限,优先把题1到题5滚瓜烂熟,这五道题涵盖了SQL语句的骨架,其余题目更多是在此之上的组合变化。

1.2 复习策略与刷题顺序

我的复习建议顺序很关键:先SQL,后PL/SQL,再数据库对象。SQL部分是地基,SELECT怎么写、JOIN怎么写、聚合函数怎么分组,这些不熟的话,后面PL/SQL块里的SQL语句一样写不出来。我遇到过有同学背了一大堆DECLARE、EXCEPTION的语法,结果进入模拟题后根本写不对正文,因为UPDATE语句本身就没掌握。

建议这样刷:第一轮,计时完成题1到题5,先不要看解析,把能写出来的写出来;第二轮,对照解析,把没写出来的部分重点标注,默写三遍;第三轮,再做题6到题10,重点记忆关键词的拼写和语法位置。Oracle的SQL语法只要大小写不搞错、子句顺序不错,基本能拿大部分分数,不需要把整个参考手册背下来。

2. 基础查询与多表连接:题1、题2精讲

2.1 题1:过滤与排序的固定执行顺序

原题:查询employees表中部门编号为50的员工姓名与工资,按工资从高到低显示。

这里用最典型的employees表结构,字段就是employee_id、first_name、last_name、salary、department_id等。标准写法:

SELECT first_name, last_name, salary FROM employees WHERE department_id = 50 ORDER BY salary DESC;

拆开解释一下为什么这道题是“送分题”也可能是“丢分题”。

WHERE写在FROM之后、ORDER BY之前。SQL有严格的子句顺序,这个顺序是FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY。有的同学把ORDER BY写到WHERE前面,或者在SELECT前写ORDER BY,都会语法报错,写SQL时一旦看到“ORA-00933: SQL command not properly ended”,多半就是这个原因。

ORDER BY salary DESC表示按工资从高到低,DESC代表降序;如果漏掉DESC,Oracle默认升序ASC,会变成工资从低到高。这种细节在机考时不会立刻报错,但输出顺序和题目要求不符,一样扣分。

字符串字面量必须用单引号。number类型的50写不写引号都行,但字符串条件如果写成"IT_PROG",就可能报ORA-00904之类的错误。双引号在SQL中一般用来括对象名,初学者最容易混淆。

执行之后会看到该部门下的员工记录按salary从大到小排列。如果没有匹配行,查询会返回空结果但不会报错,这是SQL比较友好的一点。这里还有一个批改作业时反复强调的点:题目要求的是“姓名和工资”,如果写完SELECT只选了salary而漏掉姓名列,虽然查询不报错,但结果不符合要求。期末阅卷是按点给分的,经常有人因为少选列、多选了无关列被扣分,攒起来就是十来分。

2.2 题2:多表连接为什么要用ON子句

原题:查询员工姓名和对应的部门名称,要求使用ANSI标准连接语法。

这道题的完整答案:

SELECT e.last_name, d.department_name FROM employees e JOIN departments d ON e.department_id = d.department_id;

关于表别名这里多讲几句。e和d是表别名,不是可选项,而是建议项。当两个表都有last_name或department_name时,如果不写表的引用,Oracle会报ORA-00918: column ambiguously defined。这题中last_name只存在于employees表,department_name只存在于departments表,不写前缀也能跑,但为了严谨和扩展,最好每次都写前缀。这个习惯能帮你避免多表查询时的列歧义坑。

连接条件是ON e.department_id = d.department_id。employees表通过department_id外键关联到departments表的主键。有些同学会顺手写成ON e.department_id = d.department_id AND e.last_name = d.last_name,画蛇添足;或者干脆忘掉ON子句,直接JOIN后面接WHERE,虽然旧式语法(FROM employees e, departments d WHERE e.department_id = d.department_id)在很多环境也能跑,但期末题目明确要求ANSI语法时,用JOIN...ON更稳。

还有一个很容易踩的坑:JOIN不加ON会形成笛卡尔积。假设employees有107行、departments有27行,不写ON会返回2889行,而正确结果大约106行左右,因为有一个员工没有部门号。以前给同学调试时就遇到这种情况,他发现返回行数特别多,还以为数据有问题,实际上就是连接条件缺失。所以写完多表查询后,可以用SELECT COUNT(*)检查返回行数,这应该成为肌肉记忆。

3. 聚合、子查询与数据更新:题3到题5

3.1 题3:GROUP BY与HAVING的协同工作

原题:统计每个部门的员工人数,只显示人数超过5人的部门。

标准写法:

SELECT department_id, COUNT(*) FROM employees GROUP BY department_id HAVING COUNT(*) > 5;

这道题表面考聚合函数COUNT,实际考的是分组逻辑。理解分组很关键:GROUP BY department_id会把数据按部门编号分成多个组,每组一行结果;SELECT里的字段有两类,一类是分组依据本身(department_id),另一类是聚合函数(COUNT(*)、SUM、AVG、MAX、MIN)。如果在SELECT里写了department_name或者其他非聚合、非分组的列,Oracle会直接报ORA-00979: not a GROUP BY expression。这个错误在期末卷面上出现频率很高。

再解释HAVING为什么不能用WHERE替代。WHERE的执行时机是在分组之前,它过滤的是“行”;HAVING的执行时机是在分组之后,它过滤的是“组”。查询要“人数大于5的部门”,需要先算出每个部门的人数,然后对组结果做筛选。可以在WHERE里写salary > 5000来先过滤员工行,但绝不能写WHERE COUNT(*) > 5,因为COUNT是在分组后才产生的,执行到WHERE时根本没这个值。正确思路是:先用WHERE过滤不需要参与分组的行,再用GROUP BY分组,最后用HAVING过滤组。这是SQL子句执行顺序的直接体现。

另外给一个实用建议:COUNT()和COUNT(1)在Oracle中的执行结果基本一致,但写COUNT(department_id)会忽略department_id为NULL的行,从而导致计数不一致。如果department_id允许为空,建议明确使用COUNT()统计行数。期末题一般不会为难你考察这种细微差别,但实际工作中早晚会遇到。

3.2 题4:单行子查询的运用与边界条件

原题:查询工资高于公司平均工资的员工信息。

标准写法:

SELECT employee_id, last_name, salary FROM employees WHERE salary > (SELECT AVG(salary) FROM employees);

子查询分单行和多行。这里的AVG(salary)返回的是一个数值,因此属于单行子查询,可以用大于、小于、等于这些比较运算符。如果子查询不止返回一行,例如写成WHERE salary > (SELECT salary FROM employees WHERE department_id = 10),而部门10有多个员工,就会报ORA-01427: single-row subquery returns more than one row。

讲一个更常见的变形:如果要“查询工资大于本部门平均工资的员工”,写法会变成相关子查询:

SELECT e.last_name, e.salary FROM employees e WHERE e.salary > ( SELECT AVG(salary) FROM employees WHERE department_id = e.department_id );

其中e.department_id和外层查询联动,这就是“相关子查询”。期末考试如果加难度,往往会把题4改造成这个版本。相关子查询的执行逻辑可以理解为:外层每一行进入时,内层子查询用该行的department_id重新计算一次平均工资,再比较salary是否大于这个平均值。

做题时要养成检查子查询括号闭合的习惯。见过太多人在最后少写一个右括号,导致ORA-00907: missing right parenthesis。最简单的检查办法是:写完SQL后,从左到右数一遍括号配对,或者直接把子查询单独执行一遍,确认它确实能返回一个值。

3.3 题5:UPDATE与事务回滚的完整操作链

原题:将department_id为10的员工工资上调10%,修改后执行查询验证,最后使用ROLLBACK撤销本次修改。

这道题完整的操作过程分成四步:

-- 第一步:确认修改前状态 SELECT department_id, salary FROM employees WHERE department_id = 10; -- 第二步:执行更新 UPDATE employees SET salary = salary * 1.1 WHERE department_id = 10; -- 第三步:验证修改结果 SELECT department_id, salary FROM employees WHERE department_id = 10; -- 第四步:撤销修改 ROLLBACK; -- 验证回滚后的状态 SELECT department_id, salary FROM employees WHERE department_id = 10;

解析关键点。UPDATE的WHERE子句是安全阀门。如果不写WHERE,整个employees表所有人的工资都会被乘以1.1,这是最严重的低级错误。我建议在实验环境里养成的习惯是:手写UPDATE之前,先用相同WHERE的SELECT确认一下你要影响哪些行,执行UPDATE后立刻再跑一次SELECT确认影响行数。这条经验永远不会过时。

事务是这门课的重点概念。在Oracle中,事务从第一条DML语句开始,直到COMMIT或ROLLBACK结束。执行UPDATE之后,数据在会话内可见,但其他会话看不到,因为事务尚未提交;此时如果误操作了,可以ROLLBACK撤回。这个特性叫读一致性,Oracle靠undo表空间实现。很多同学在实验课卡在这一步:明明UPDATE执行了,SELECT也看到了结果,但换一个SQL窗口一查,数据没变,急得团团转。原因就是没COMMIT,事务没有结束,数据只是处于“改而未交”的状态。

这道题的延伸考点是:如果此时执行COMMIT,数据就永久生效,再ROLLBACK也救不回来了。期末试卷常见陷阱题就是“UPDATE之后执行COMMIT,再执行ROLLBACK,问数据是否恢复原样”,答案是“不会”。提交之后,事务结束,ROLLBACK只能影响提交前未提交的部分。理解这一点能帮你拿下一道选择题或简答题。

4. 表结构、权限与视图:题6到题8

4.1 题6:建表与三类完整性约束

原题:创建学生表student,包含student_id、student_name、age三个字段。要求student_id为主键,student_name非空,age取值范围在0到150之间。

标准写法:

CREATE TABLE student ( student_id NUMBER(10) PRIMARY KEY, student_name VARCHAR2(50) NOT NULL, age NUMBER(3) CONSTRAINT ck_student_age CHECK (age BETWEEN 0 AND 150) );

为什么age用NUMBER(3)?因为0到150最多三位数,占用空间小,也直接满足约束。注意这里用的是列级约束写法:PRIMARY KEY直接跟在列后,NOT NULL也直接跟在列后。CHECK约束一般给一个名字,格式是CONSTRAINT 约束名 CHECK (条件)。不命名的话Oracle会系统自动生成一个类似SYS_C00666的名字,以后想删除约束会很麻烦。

表级约束写法长这样:

CREATE TABLE student ( student_id NUMBER(10), student_name VARCHAR2(50) NOT NULL, age NUMBER(3), CONSTRAINT pk_student_id PRIMARY KEY (student_id), CONSTRAINT ck_student_age CHECK (age BETWEEN 0 AND 150) );

两种写法期末都认,但建议掌握表级约束写法,因为当一个主键由多个字段组成时(复合主键),只能写成表级约束。比如选课表需要(student_id, course_id)作为联合主键,就必须用CONSTRAINT pk_enroll PRIMARY KEY (student_id, course_id)。

验证约束是否生效,可以插入一条违反规则的数据。例如:

INSERT INTO student (student_id, student_name, age) VALUES (2019001, '张三', 200);

运行后会报ORA-02290: check constraint (CK_STUDENT_AGE) violated,看到这个报错就说明CHECK约束生效了。同理,插入重复的student_id会违反PRIMARY KEY约束,报ORA-00001: unique constraint violated;插入NULL的student_name会违反NOT NULL约束。这种“故意跑一个错误,再看报错信息”的方式,是学习阶段最有效的验证方法之一,期末时写SQL题如果要求“添加约束”,就可以直接回忆起报错细节。

4.2 题7:用户创建与授权的最小不犯错方案

原题:创建一个新用户testuser,并授予其连接数据库和创建数据库表的权限。

标准写法:

CREATE USER testuser IDENTIFIED BY "Pass123"; GRANT CONNECT, RESOURCE TO testuser;

这里稍微补充一下。CONNECT角色至少包含CREATE SESSION权限,用来允许用户登录;RESOURCE角色包含CREATE TABLE、CREATE SEQUENCE、CREATE VIEW等一组常用系统权限,也就是允许这个用户在自己的模式中创建对象。题目只要求“连接数据库和创建数据表”,所以这个GRANT语句组合是最稳妥、最符合常规教学库的方案。

还需要提防一个坑:Oracle 12c及更高版本引入了CDB/PDB架构。在某个PDB中创建用户时,用户名前面加不加C##?只有在CDB根容器中创建通用用户才要求C##前缀,在普通PDB内创建普通用户不需要加前缀。学校实验环境一般直接连接PDB,所以用CREATE USER testuser即可。如果连的是容器数据库根,确实会报ORA-65096: invalid common user or role name。这个报错一出现,先检查当前处于哪个容器。

权限授予之后,可以用ALTER USER testuser QUOTA UNLIMITED ON users;来配置表空间配额,否则这个用户虽然能建表,也可能因为配额为0而报ORA-01536。实验课里最容易出现的问题就是“用户已授权但仍无法建表”,原因多半在这个配额上。期末大题一般只考GRANT语句,但实际实验时一定要把配额也设上。

4.3 题8:视图的创建与WITH CHECK OPTION的行为约束

原题:创建一个视图emp_dept10,只显示部门编号为10的员工信息,要求通过视图进行DML操作时,数据仍满足部门编号为10的条件。

标准写法:

CREATE OR REPLACE VIEW emp_dept10 AS SELECT employee_id, last_name, salary, department_id FROM employees WHERE department_id = 10 WITH CHECK OPTION;

视图是一个虚拟表,不实际存储数据,底层查询引用的是employees表的行。CREATE OR REPLACE的意思是:已存在同名视图则替换,否则创建。考试可能不要求加OR REPLACE,但加上更稳妥。

WITH CHECK OPTION是重点。它的作用是:所有通过该视图执行的INSERT和UPDATE操作,都必须满足视图定义中的WHERE条件。举例来说:

-- 这条语句会成功,因为department_id仍然是10 UPDATE emp_dept10 SET salary = salary + 100 WHERE employee_id = 100; -- 这条语句会报ORA-01402: view WITH CHECK OPTION where-clause violation UPDATE emp_dept10 SET department_id = 20 WHERE employee_id = 100;

第二条更新的意思是想把某条记录的部门号改成20,但如果这条数据来自emp_dept10视图,修改后视图的WHERE条件(department_id=10)就不再成立,所以Oracle直接拒绝。这能防止用户绕过视图的限制修改底层数据。在期末简答题里,只要把“WITH CHECK OPTION用于保证通过视图修改的数据仍满足视图定义条件”这句话写清楚,就能拿到关键分。

视图还有一个常考考点:删除视图的语句是DROP VIEW emp_dept10,删除表格的语句是DROP TABLE student,千万别把单词混了。另外,视图上做DML有很多限制,比如使用了聚合函数、GROUP BY、DISTINCT的复杂视图通常不能直接做DML操作。期末一般不会考到这么深,但选择题偶尔会问,记住“简单视图可更新,复杂视图一般不可更新”即可。

5. PL/SQL与索引:题9、题10精讲

5.1 题9:PL/SQL匿名块的完整骨架

原题:编写一个PL/SQL匿名块,声明一个变量保存公司最高工资。如果最高工资对应的员工存在,则将该最高工资上调300元,并输出调整前的最高工资;如果表为空,则输出提示信息“暂无可更新员工”。

标准答案:

SET SERVEROUTPUT ON; DECLARE v_max_salary employees.salary%TYPE; BEGIN SELECT MAX(salary) INTO v_max_salary FROM employees; IF v_max_salary IS NULL THEN DBMS_OUTPUT.PUT_LINE('暂无可更新员工'); RETURN; END IF; UPDATE employees SET salary = salary + 300 WHERE salary = v_max_salary; DBMS_OUTPUT.PUT_LINE('调整前最高工资为: ' || v_max_salary); EXCEPTION WHEN NO_DATA_FOUND THEN DBMS_OUTPUT.PUT_LINE('暂无可更新员工'); END; /

这是PL/SQL题的经典骨架。DECLARE部分声明变量,变量类型使用了employees.salary%TYPE,这是一个很加分的写法,表示变量类型和表中salary列一致,避免了写错类型。BEGIN开始执行体,SELECT...INTO把查询结果放进变量,UPDATE更新数据,DBMS_OUTPUT.PUT_LINE输出信息,EXCEPTION处理异常,END结束,末尾的斜杠/意味着提交这个PL/SQL块并执行。

几个容易扣分的点。第一,PL/SQL中的赋值符号是:=,不是等号。SELECT...INTO也属于赋值输入。第二,DBMS_OUTPUT.PUT_LINE的输出默认不显示,需要在SQLPlus中先执行SET SERVEROUTPUT ON,否则只看到“PL/SQL procedure successfully completed”,看不到输出的文字。很多同学考试时输出不出来,以为是程序写错,其实是没打开serveroutput。第三,END后面要写分号;块主体外层是BEGIN...END;,如果要让SQLPlus识别并执行整个块,末尾还要加上斜杠/。少了分号或斜杠,执行时会一直显示continue提示,像一个没写完的句子。

题目还要求“如果表为空则输出提示”。这里有一个容易混淆的细节:SELECT MAX(salary) INTO v_max_salary在employees表为空时,MAX聚合函数会返回一行NULL值,大多数Oracle版本中SELECT INTO对返回的NULL不会触发NO_DATA_FOUND。所以我用了IF v_max_salary IS NULL THEN直接判断,这样最稳妥;EXCEPTION块负责处理没有返回行等异常情况。两种可能的场景都覆盖到了,这道题就能稳稳拿满分。

5.2 题10:索引的创建、删除与性能代价

原题:为employees表的salary列创建一个索引,写出创建语句;如果该索引不再需要,写出删除语句。

标准答案:

CREATE INDEX idx_emp_salary ON employees(salary); DROP INDEX idx_emp_salary;

索引在Oracle中的作用类似书的目录。没有索引时,SELECT语句查询salary可能需要扫描整张表的所有行,索引可以让我们用B-Tree结构快速定位对应记录。但索引不是越多越好:每次INSERT、UPDATE、DELETE都要同步维护索引,写入性能会下降;索引也占表空间。期末考试最常考的就是CREATE INDEX和DROP INDEX两个语法。

DROP INDEX这里有个高频错误:很多人会写成DROP INDEX idx_emp_salary ON employees,或者在删除时又带上表名。正确写法就是DROP INDEX 索引名,不需要再写表名,Oracle通过索引名就能定位。另外,复合索引的创建格式可以是CREATE INDEX idx_emp_dept ON employees(department_id, salary);查询条件同时包含这两个字段时,复合索引通常更有效。

还有一类延伸题:创建主键或唯一约束时Oracle会自动创建唯一索引,所以不需要手动为唯一列再创建一个普通索引。手贱再去建,会得到重复索引,浪费空间。理解“自动索引”这个概念,对答选择题很有帮助。

6. 考场答题技巧与高频扣分点

6.1 写SQL之前先在心里跑一遍执行顺序

我做题时积累了一个非常实用的习惯:无论题目多简单,先在草稿纸上或者脑子里过一遍SQL子句执行顺序:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY。这个顺序不只是规则,它决定了能不能在正确的位置写过滤条件。比如题3,如果忘掉HAVING,就一定不会在GROUP BY之后过滤组,自然做不对。ORDER BY虽然是逻辑上的倒数第二步,但它在物理上写在整个语句的最后,这两个“位置”不要搞混。

还有一个加分习惯:在关键语句前加注释标注意图。比如题5的UPDATE前面写一行注释“WHERE强制限定范围”,阅卷老师看到也知道你是清楚得分点的。考试不是写论文,能展示你对考点有充分意识,分数往往更稳。

6.2 高频扣分点速查表

把我在批改和实际指导中反复看到的错误汇总成一张表:

错误类型错误示例正确做法原因
子句顺序错误ORDER BY写WHERE前FROM → WHERE → SELECT → ORDER BYSQL语法固定顺序
字符串用双引号WHERE name = "张三"WHERE name = '张三'双引号用于对象名
UPDATE缺WHEREUPDATE emp SET salary=...始终写WHERE限定行范围否则更新全表
等号与赋值混淆salary = 100放在PL/SQL变量赋值中变量 := 100PL/SQL赋值用:=
忘记连接条件JOIN后不写ON写上等值连接条件否则出现笛卡尔积
删除索引带表名DROP INDEX idx ON employeesDROP INDEX idxOracle语法不识别ON
视图可更新性混淆认为所有视图都能DML简单视图可更新,复杂视图受限底层查询决定

这张表建议考前最后一天看一遍。刚复习时不建议背着张表硬记,而是做到那些题、撞过几次ORA报错之后再回来看,印象会深得多。

7. 期末自救锦囊:从会看到会写的最后一公里

最后聊点心得。数据库这门课,真正的难点不是“看懂”,而是“写对”。看懂SELECT和看不出自己漏了WHERE,在分数上的差别很大。复习阶段可以给自己准备一个错题本,不是抄题目,而是抄报错。ORA-01427、ORA-00979、ORA-01402、ORA-02290这几个报错代码,遇到时全部抄一遍,然后写清楚触发原因和解决思路。到考前再翻一遍,比硬背一百个知识点更有效。

我自己也是这样一路过来的:先会用,再理解原理。不要怕报错,不要怕一个命令执行三分钟。多执行、多读报错信息、多用DESCRIBE验证表结构,这比什么都重要。把这10道题写两三遍,Oracle期末的大题基本就能稳稳收住。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进