Oracle期末复习,最能拉开分差的其实就是那几道典型的SQL与PL/SQL题目。很多同学复习时习惯把教材从头翻到尾,概念背了一堆,结果到了考场上,面对“用SQL实现XX查询”“补齐下面的PL/SQL块”这类题目,还是卡壳。我带过不少学生的考前突击,也见过太多“背了语法却写不出完整语句”的例子,总结下来,与其漫无目的地刷题,不如把最常考的十类知识点做成一个闭环练习。这篇文章整理了10道有代表性的Oracle期末实例题,覆盖基础查询与排序、多表连接、分组统计、子查询、数据更新与事务回滚、建表约束、用户权限、视图、PL/SQL匿名块和索引,每道题都给出标准答案、逐步解析、易错提醒以及实际执行效果说明。适合正在准备Oracle数据库期末考试的在校学生,也适合刚入门想检验自己掌握程度的学习者。
1. 考前摸底:10道题的考点地图
1.1 题型分布与知识权重
先把这10道题的考点分布铺开,方便对照自己的薄弱环节。
| 题号 | 核心考点 | 常见题型 | 难度 | 建议用时 |
|---|---|---|---|---|
| 题1 | SELECT过滤与排序 | 编写SQL | 入门 | 3分钟 |
| 题2 | 多表连接(ANSI语法) | 编写SQL | 入门 | 5分钟 |
| 题3 | GROUP BY与HAVING聚合 | 编写SQL | 基础 | 5分钟 |
| 题4 | 单行子查询 | 编写SQL | 基础 | 5分钟 |
| 题5 | UPDATE与事务回滚 | 编写SQL+简答 | 基础 | 6分钟 |
| 题6 | 建表与完整性约束 | 编写SQL | 基础 | 6分钟 |
| 题7 | 用户创建与权限授予 | 编写SQL+简答 | 基础 | 4分钟 |
| 题8 | 视图与WITH CHECK OPTION | 编写SQL | 进阶 | 6分钟 |
| 题9 | PL/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 BY | SQL语法固定顺序 |
| 字符串用双引号 | WHERE name = "张三" | WHERE name = '张三' | 双引号用于对象名 |
| UPDATE缺WHERE | UPDATE emp SET salary=... | 始终写WHERE限定行范围 | 否则更新全表 |
| 等号与赋值混淆 | salary = 100放在PL/SQL变量赋值中 | 变量 := 100 | PL/SQL赋值用:= |
| 忘记连接条件 | JOIN后不写ON | 写上等值连接条件 | 否则出现笛卡尔积 |
| 删除索引带表名 | DROP INDEX idx ON employees | DROP INDEX idx | Oracle语法不识别ON |
| 视图可更新性混淆 | 认为所有视图都能DML | 简单视图可更新,复杂视图受限 | 底层查询决定 |
这张表建议考前最后一天看一遍。刚复习时不建议背着张表硬记,而是做到那些题、撞过几次ORA报错之后再回来看,印象会深得多。
7. 期末自救锦囊:从会看到会写的最后一公里
最后聊点心得。数据库这门课,真正的难点不是“看懂”,而是“写对”。看懂SELECT和看不出自己漏了WHERE,在分数上的差别很大。复习阶段可以给自己准备一个错题本,不是抄题目,而是抄报错。ORA-01427、ORA-00979、ORA-01402、ORA-02290这几个报错代码,遇到时全部抄一遍,然后写清楚触发原因和解决思路。到考前再翻一遍,比硬背一百个知识点更有效。
我自己也是这样一路过来的:先会用,再理解原理。不要怕报错,不要怕一个命令执行三分钟。多执行、多读报错信息、多用DESCRIBE验证表结构,这比什么都重要。把这10道题写两三遍,Oracle期末的大题基本就能稳稳收住。