1. 为什么Oracle的笔试题总让人“一看就会,一做就废”
很多人第一次接触Oracle数据库的笔试,都会有一种错觉:SQL语法我熟,选择题应该不难。结果做完一套题下来,正确率惨不忍睹。问题出在哪儿?不是语法不会,而是Oracle的笔试面试题从来不考“语法本身”,它考的是你对执行语义、边界条件、隐式转换、空值逻辑这些底层机制的理解深度。
我见过太多候选人,能写出SELECT * FROM emp WHERE deptno = 10,但遇到WHERE comm > 100就翻车——因为comm为NULL的行会被过滤掉,而很多人脑子里默认“NULL不参与比较”这件事在笔试时根本想不起来。Oracle的题目设计者非常清楚哪些地方容易产生认知偏差,他们就是专门挑这些点来出题。
这篇文章面向三类人:正在准备Oracle相关岗位笔试面试的求职者、需要带新人做SQL能力评估的技术负责人、以及想系统梳理Oracle核心知识点的DBA或开发人员。我会把Oracle笔试面试中最常出现的题型拆开揉碎,不光告诉你答案是什么,更要讲清楚为什么是这个答案,以及在实际工作中这个知识点会怎么坑你。
需要提前说明的是,Oracle的笔试面试题覆盖面很广,从SQL基础到PL/SQL编程、从索引原理到事务隔离级别、从分区表到执行计划分析,几乎每个方向都有经典题目。我会按照知识模块来组织内容,每个模块先讲核心原理,再拆解典型题目,最后补充我在实际工作中遇到的真实案例和避坑经验。
2. 单表查询与WHERE子句的隐藏陷阱
2.1 NULL值的三值逻辑:笔试第一大坑
Oracle的NULL是整个SQL体系里最容易出题的地方,没有之一。先记住一个核心原则:NULL表示“未知”,任何与NULL的比较运算结果都是UNKNOWN,而不是TRUE或FALSE。WHERE子句只返回结果为TRUE的行,所以UNKNOWN和FALSE一样都会被过滤掉。
来看一道经典题:
SELECT * FROM employees WHERE salary > 5000;如果employees表中有10行数据,其中3行的salary为NULL,那么返回几行?答案是:返回salary大于5000且不为NULL的行数。那3行NULL数据不会出现在结果中,因为NULL > 5000的结果是UNKNOWN。
更隐蔽的坑在于NOT IN和NULL的组合:
SELECT * FROM employees WHERE deptno NOT IN (10, 20, NULL);这条语句返回什么?答案是永远返回空结果集。原因在于NOT IN等价于deptno != 10 AND deptno != 20 AND deptno != NULL,而deptno != NULL的结果永远是UNKNOWN,整个AND表达式就永远是UNKNOWN或FALSE,不可能为TRUE。这个知识点在笔试中出现频率极高,很多人第一次遇到都会选错。
实际工作中的教训:我曾经在一个数据清洗项目里用NOT IN子查询排除已处理的记录,子查询的关联字段恰好有NULL值,导致整个清洗任务返回空结果,排查了两个小时才发现是NULL在作怪。后来我养成了一个习惯——只要用NOT IN,子查询里必须加WHERE column IS NOT NULL。
2.2 隐式类型转换:看不见的性能杀手和逻辑陷阱
Oracle在比较不同数据类型时会自动做隐式转换,这个特性在笔试中经常被用来出“结果是否符合预期”的题目。
SELECT * FROM orders WHERE order_no = 12345;假设order_no是VARCHAR2类型的列,存储的是字符串形式的订单号。这条语句能执行吗?能。Oracle会把12345隐式转换为字符串'12345'再比较。但问题来了——如果order_no列上建有索引,这个隐式转换可能导致索引失效,因为Oracle可能选择对每行做TO_NUMBER转换后再比较,而不是直接使用索引。
更经典的笔试题是这样的:
SELECT TO_CHAR(SYSDATE, 'YYYY-MM-DD') FROM dual; SELECT TO_DATE('2024-01-15', 'YYYY-MM-DD') FROM dual;问哪个返回的是字符串,哪个返回的是日期。这个考的是对Oracle数据类型体系的清晰认知。TO_CHAR返回VARCHAR2,TO_DATE返回DATE。看起来简单,但在复杂的嵌套函数题目中,很多人就会搞混。
还有一个高频考点是字符串和数字比较时的转换方向:
SELECT * FROM t WHERE varchar_col = 100;Oracle会把varchar_col的每一行都尝试转换成数字再和100比较。如果varchar_col中有非数字字符(比如'abc'),运行时会报ORA-01722: invalid number错误。这个错误在笔试中经常以“以下语句执行结果是什么”的形式出现,答案就是报错。
2.3 ORDER BY的排序规则与中文排序问题
ORDER BY看起来简单,但Oracle的排序有几个容易出题的点。首先是NULL值的排序位置:Oracle默认把NULL值排在最后(升序时),这和某些数据库的行为不同。如果笔试题目问“升序排列时NULL值出现在哪里”,答案就是最后。
其次是中文排序。Oracle默认的排序规则取决于数据库的NLS_SORT参数设置,可能是二进制排序也可能是拼音排序。笔试中如果出现中文排序的题目,通常会明确给出NLS_SORT的设置,或者考的是ORDER BY NLSSORT(column, 'NLS_SORT=SCHINESE_PINYIN_M')这种显式指定排序方式的写法。
还有一个常考的点是ORDER BY中可以使用列别名:
SELECT salary * 12 AS annual_salary FROM employees ORDER BY annual_salary DESC;这条语句在Oracle中是合法的,因为ORDER BY的执行顺序在SELECT之后。但WHERE子句中就不能使用列别名,因为WHERE的执行顺序在SELECT之前。这个执行顺序的知识点几乎每套笔试题都会涉及。
3. 多表连接与子查询的语义辨析
3.1 连接方式的选择:INNER JOIN、LEFT JOIN、RIGHT JOIN的等价改写
Oracle笔试中经常出现“以下哪种写法与给定语句等价”的题目。核心要掌握的是:LEFT JOIN可以通过交换表位置改写为RIGHT JOIN,FULL OUTER JOIN可以用LEFT JOIN和RIGHT JOIN的UNION来模拟。
-- 以下两条语句等价 SELECT a.id, b.name FROM table_a a LEFT JOIN table_b b ON a.id = b.a_id; SELECT a.id, b.name FROM table_b b RIGHT JOIN table_a a ON a.id = b.a_id;但要注意,Oracle特有的(+)外连接语法在笔试中也经常出现:
SELECT a.id, b.name FROM table_a a, table_b b WHERE a.id = b.a_id(+);这个(+)表示b表是可选表,等价于a表LEFT JOIN b表。(+)要放在缺少匹配行的那一侧,这个方向很容易搞反。我个人的记忆方法是:(+)放在“可能没有数据”的那张表的列后面。
3.2 子查询的关联与非关联:EXISTS和IN的性能差异
笔试中经常问“EXISTS和IN的区别是什么”,标准答案通常会说“IN适合子查询结果集小的情况,EXISTS适合外层查询结果集小的情况”。但这个答案不够精确,真正的关键在于执行计划中的驱动表选择。
-- 写法一:使用IN SELECT * FROM employees WHERE deptno IN (SELECT deptno FROM departments WHERE location = 'NY'); -- 写法二:使用EXISTS SELECT * FROM employees e WHERE EXISTS (SELECT 1 FROM departments d WHERE d.deptno = e.deptno AND d.location = 'NY');在Oracle中,优化器通常会把IN子查询转换为半连接(SEMI JOIN),把EXISTS也转换为半连接,所以两者的执行计划可能完全一样。但在子查询包含NULL值的情况下,NOT IN和NOT EXISTS的行为完全不同——NOT IN遇到NULL会返回空结果,NOT EXISTS则不受影响。
这个知识点在实际工作中的价值极高。我曾经优化过一个报表查询,原来用NOT IN子查询耗时40多秒,改成NOT EXISTS后降到2秒以内。原因就是NOT IN无法使用哈希反连接,而NOT EXISTS可以。
3.3 标量子查询和WITH子句的妙用
标量子查询是指返回单行单列的子查询,可以出现在SELECT列表、WHERE子句甚至ORDER BY中:
SELECT e.ename, e.salary, (SELECT d.dname FROM departments d WHERE d.deptno = e.deptno) AS dept_name FROM employees e;这种写法在笔试中经常出现,考的是对标量子查询返回多行时会报错(ORA-01427)的理解。
WITH子句(也叫子查询因子化)在笔试中出现的频率越来越高,因为它能体现候选人对复杂查询的组织能力:
WITH dept_summary AS ( SELECT deptno, AVG(salary) AS avg_sal, COUNT(*) AS emp_count FROM employees GROUP BY deptno ) SELECT d.dname, ds.avg_sal, ds.emp_count FROM dept_summary ds JOIN departments d ON ds.deptno = d.deptno WHERE ds.avg_sal > 5000;WITH子句的一个重要特性是:在Oracle中,WITH子句定义的临时结果集默认是内联视图,不一定会被物化。如果希望它被物化以提高性能,需要使用/*+ MATERIALIZE */提示。这个知识点在高级笔试中经常出现。
4. 聚合函数与GROUP BY的边界条件
4.1 GROUP BY的列选择规则与常见错误
GROUP BY的核心规则是:SELECT列表中出现的非聚合列,必须全部出现在GROUP BY子句中。这个规则看起来简单,但在复杂查询中很容易出错。
-- 以下语句会报错:ORA-00979: not a GROUP BY expression SELECT deptno, job, AVG(salary) FROM employees GROUP BY deptno;因为job列没有出现在GROUP BY中,也没有被聚合函数包裹。正确的写法是GROUP BY deptno, job。
笔试中经常出现的一种变体是:GROUP BY中使用了表达式或函数,SELECT列表中也必须使用相同的表达式:
SELECT TO_CHAR(hiredate, 'YYYY'), COUNT(*) FROM employees GROUP BY TO_CHAR(hiredate, 'YYYY');这条语句是合法的,因为SELECT和GROUP BY中的表达式完全一致。但如果SELECT中写的是TO_CHAR(hiredate, 'YYYY-MM')而GROUP BY中写的是TO_CHAR(hiredate, 'YYYY'),就会报错。
4.2 HAVING与WHERE的执行顺序:为什么HAVING中不能用列别名
这是一个高频考点。WHERE在GROUP BY之前执行,HAVING在GROUP BY之后执行。所以:
- WHERE中不能使用聚合函数,因为此时还没有分组
- HAVING中可以使用聚合函数,因为分组已经完成
- WHERE中不能使用SELECT中定义的列别名,因为SELECT在WHERE之后执行
- HAVING中也不能使用列别名,原因同上
-- 错误写法:WHERE中不能用聚合函数 SELECT deptno, AVG(salary) FROM employees WHERE AVG(salary) > 5000 GROUP BY deptno; -- 正确写法:用HAVING SELECT deptno, AVG(salary) FROM employees GROUP BY deptno HAVING AVG(salary) > 5000;实际工作中,我见过有人在WHERE中写WHERE COUNT(*) > 10,然后困惑为什么报错。理解执行顺序之后,这类错误就再也不会犯了。
4.3 ROLLUP、CUBE和GROUPING SETS:高级分组功能
Oracle的ROLLUP和CUBE是笔试中区分度很高的考点。简单来说:
GROUP BY ROLLUP(a, b):先按(a, b)分组,再按(a)分组,最后对全表汇总GROUP BY CUBE(a, b):按(a, b)、(a)、(b)以及全表所有可能组合分组GROUPING SETS:精确指定需要哪些分组组合
SELECT deptno, job, SUM(salary) FROM employees GROUP BY ROLLUP(deptno, job);这条语句会返回:每个deptno下每个job的薪资总和、每个deptno的薪资总和、以及全表的薪资总和。结果中会出现deptno为NULL的行(表示该deptno的汇总),以及deptno和job都为NULL的行(表示全表汇总)。
笔试中经常问“如何区分结果中的NULL是数据本身的NULL还是ROLLUP产生的NULL”,答案是使用GROUPING函数:
SELECT deptno, job, SUM(salary), GROUPING(deptno) AS g_dept, GROUPING(job) AS g_job FROM employees GROUP BY ROLLUP(deptno, job);GROUPING返回1表示该列是汇总产生的NULL,返回0表示是数据本身的NULL。这个技巧在实际做报表时非常实用。
5. 窗口函数与分析函数的实战应用
5.1 ROW_NUMBER、RANK、DENSE_RANK的区别与选择
这三个排名函数是笔试中的常客,它们的区别用一句话概括:
ROW_NUMBER():不管是否并列,序号连续递增,1、2、3、4RANK():并列时序号相同,后续序号跳跃,1、2、2、4DENSE_RANK():并列时序号相同,后续序号连续,1、2、2、3
SELECT ename, salary, ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn, RANK() OVER (ORDER BY salary DESC) AS rk, DENSE_RANK() OVER (ORDER BY salary DESC) AS drk FROM employees;如果两个员工薪资相同,ROW_NUMBER会给他们不同的序号(取决于内部排序的稳定性),RANK会给他们相同的序号但下一个序号跳跃,DENSE_RANK会给他们相同的序号且下一个序号连续。
实际工作中最常用的场景是“取每个部门薪资最高的前3名”:
SELECT * FROM ( SELECT ename, deptno, salary, ROW_NUMBER() OVER (PARTITION BY deptno ORDER BY salary DESC) AS rn FROM employees ) WHERE rn <= 3;注意这里必须用ROW_NUMBER而不是RANK,因为RANK在并列时可能返回超过3行。这个细节在笔试中经常被考察。
5.2 LAG和LEAD:跨行访问的利器
LAG和LEAD用于访问当前行之前或之后的行数据,在计算同比、环比、差值等场景中非常有用:
SELECT month, revenue, LAG(revenue, 1) OVER (ORDER BY month) AS prev_revenue, revenue - LAG(revenue, 1) OVER (ORDER BY month) AS diff FROM monthly_sales;笔试中经常考的是LAG/LEAD的第三个参数——默认值。当没有前一行时,LAG返回NULL,但可以通过第三个参数指定默认值:
LAG(revenue, 1, 0) OVER (ORDER BY month)这样第一行的prev_revenue就是0而不是NULL。
5.3 窗口帧(WINDOWING CLAUSE)的精确控制
窗口帧是分析函数中最容易被忽视但也最容易出题的部分。默认的窗口帧是RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,这意味着累计计算从第一行到当前行。
-- 累计求和 SELECT month, revenue, SUM(revenue) OVER (ORDER BY month) AS running_total FROM monthly_sales; -- 滑动窗口:当前行及前后各一行 SELECT month, revenue, AVG(revenue) OVER (ORDER BY month ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS moving_avg FROM monthly_sales;ROWS和RANGE的区别在于:ROWS按物理行数计算,RANGE按值范围计算。当ORDER BY的列有重复值时,两者的结果会不同。这个知识点在高级笔试中经常出现,很多人第一次遇到都会选错。
6. 索引、执行计划与SQL优化的笔试要点
6.1 B树索引的访问路径与索引失效场景
Oracle笔试中关于索引的题目通常围绕“什么情况下索引会失效”来出。常见的索引失效场景包括:
| 失效场景 | 示例 | 原因 |
|---|---|---|
| 列上使用函数 | WHERE UPPER(name) = 'ABC' | 索引存储的是原始值,函数转换后无法使用索引 |
| 隐式类型转换 | WHERE varchar_col = 123 | 可能对列做TO_NUMBER转换 |
| 前导通配符 | WHERE name LIKE '%ABC' | 无法定位索引的起始位置 |
| 使用NOT | WHERE status != 'A' | 优化器可能选择全表扫描 |
| 列参与运算 | WHERE salary * 12 > 100000 | 需要对每行计算后才能比较 |
但要注意,这些规则不是绝对的。比如WHERE status != 'A'在某些情况下仍然可以使用索引(当status='A'的行数很少时,优化器可能选择索引扫描加过滤)。笔试中如果出现这类题目,通常会给出具体的统计信息或执行计划,需要根据实际情况判断。
6.2 执行计划的阅读方法:从缩进看执行顺序
读懂执行计划是Oracle笔试面试中的高阶技能。执行计划的阅读要点:
- 缩进越深的操作越先执行
- 同级操作从上到下执行
- TABLE ACCESS FULL表示全表扫描,INDEX RANGE SCAN表示索引范围扫描
- NESTED LOOPS表示嵌套循环连接,HASH JOIN表示哈希连接,SORT MERGE JOIN表示排序合并连接
EXPLAIN PLAN FOR SELECT e.ename, d.dname FROM employees e JOIN departments d ON e.deptno = d.deptno WHERE e.salary > 5000; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);一个典型的执行计划可能长这样:
| Id | Operation | Name | | 0 | SELECT STATEMENT | | | 1 | HASH JOIN | | | 2 | TABLE ACCESS FULL | DEPARTMENTS | | 3 | TABLE ACCESS BY INDEX ROWID| EMPLOYEES | | 4 | INDEX RANGE SCAN | EMP_SAL_IDX |从缩进可以看出:先做INDEX RANGE SCAN(Id=4),然后回表(Id=3),同时全表扫描DEPARTMENTS(Id=2),最后做HASH JOIN(Id=1)。这个执行顺序的理解在笔试中经常被考察。
6.3 绑定变量与硬解析:为什么OLTP系统必须用绑定变量
笔试中经常问“绑定变量的好处是什么”,标准答案是“减少硬解析,提高性能”。但更深层的考点是:绑定变量在数据倾斜场景下可能导致执行计划选择错误。
-- 不使用绑定变量:每次都是硬解析 SELECT * FROM orders WHERE status = 'NEW'; SELECT * FROM orders WHERE status = 'DONE'; -- 使用绑定变量:软解析或软软解析 SELECT * FROM orders WHERE status = :status;在OLTP系统中,绑定变量可以大幅减少共享池的压力。但在数据仓库或报表系统中,由于查询模式不同,可能更适合让优化器根据具体值选择执行计划。这个权衡在高级笔试中经常出现。
7. PL/SQL编程与事务控制的笔试重点
7.1 游标与循环:显式游标和隐式游标的区别
PL/SQL笔试中关于游标的题目通常考察以下几点:
- 隐式游标(SQL%FOUND、SQL%NOTFOUND、SQL%ROWCOUNT)的使用
- 显式游标的声明、打开、提取、关闭流程
- 游标FOR循环的自动管理机制
DECLARE CURSOR c_emp IS SELECT ename, salary FROM employees WHERE deptno = 10; v_ename employees.ename%TYPE; v_salary employees.salary%TYPE; BEGIN OPEN c_emp; LOOP FETCH c_emp INTO v_ename, v_salary; EXIT WHEN c_emp%NOTFOUND; DBMS_OUTPUT.PUT_LINE(v_ename || ': ' || v_salary); END LOOP; CLOSE c_emp; END;笔试中经常问“如果忘记关闭游标会怎样”,答案是游标会一直占用资源直到会话结束。但在游标FOR循环中,Oracle会自动处理打开和关闭,所以推荐使用FOR循环而不是手动管理。
7.2 异常处理:预定义异常和自定义异常
PL/SQL的异常处理是笔试中的必考内容。需要掌握:
NO_DATA_FOUND:SELECT INTO没有返回行TOO_MANY_ROWS:SELECT INTO返回多行ZERO_DIVIDE:除零错误OTHERS:捕获所有未处理异常
BEGIN SELECT salary INTO v_salary FROM employees WHERE empno = 9999; EXCEPTION WHEN NO_DATA_FOUND THEN DBMS_OUTPUT.PUT_LINE('员工不存在'); WHEN TOO_MANY_ROWS THEN DBMS_OUTPUT.PUT_LINE('返回了多行数据'); WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE('其他错误: ' || SQLERRM); END;一个容易出错的点是:WHEN OTHERS必须放在所有其他异常处理之后,否则会报编译错误。另外,SQLCODE返回错误代码,SQLERRM返回错误信息,这两个函数在笔试中经常出现。
7.3 事务控制:COMMIT、ROLLBACK和SAVEPOINT
事务控制的笔试题目通常围绕以下几点:
- DDL语句会自动提交事务
- 正常退出会话会自动提交,异常退出会自动回滚
- SAVEPOINT可以部分回滚
BEGIN INSERT INTO t1 VALUES (1); SAVEPOINT sp1; INSERT INTO t2 VALUES (2); ROLLBACK TO sp1; -- 只回滚t2的插入,t1的插入保留 COMMIT; END;笔试中经常问“执行ROLLBACK TO sp1之后,t1的插入是否还在”,答案是还在,因为SAVEPOINT只回滚到保存点之后的操作。这个知识点在实际开发中用于实现部分回滚逻辑。
8. 我在准备Oracle笔试时总结的几条实战经验
第一,不要只刷题不写代码。Oracle的很多行为(比如隐式转换、NULL处理、执行计划)在不同版本和不同参数设置下可能有差异。我建议在本地装一个Oracle Express Edition,把每道题都实际跑一遍,观察真实结果。很多笔试中看似“应该这样”的答案,实际跑出来可能完全不同。
第二,建立自己的错题本,但不要只记答案。每道错题都要记录:题目考的是什么知识点、我为什么选错、正确的思考路径是什么、这个知识点在实际工作中怎么用。我自己的错题本里,关于NULL值的题目占了将近三分之一,后来我专门花了一个下午把所有NULL相关的行为都测试了一遍,从此这类题目再也没错过。
第三,关注Oracle版本差异。12c和19c在分区表、JSON支持、执行计划等方面有差异,笔试中如果出现版本相关的题目,一定要看清楚题目描述的是哪个版本。我遇到过一道关于IDENTITY列的题目,在12c之前根本不支持这个特性,如果没注意版本就会选错。
第四,面试中遇到不会的题目,展示思考过程比直接说不会更重要。Oracle的知识体系太庞大了,没有人能全部记住。面试官更看重的是你分析问题的思路——你会不会从执行顺序入手、会不会考虑边界条件、会不会想到用执行计划来验证。我面试别人的时候,最怕听到“这个我没背过”,最喜欢听到“我不确定,但我会这样分析……”。
第五,实际工作中的SQL优化经验比笔试技巧更有价值。笔试考的是知识点,面试考的是解决问题的能力。如果你能讲出一个自己优化过的SQL案例——原来多慢、怎么分析的、改了什么、后来多快——这比答对十道选择题都管用。我在面试中遇到过一个候选人,笔试成绩一般,但他详细讲了自己如何通过执行计划发现一个隐式类型转换导致的索引失效问题,并把查询从8秒优化到0.1秒,我当场就决定要他了。