1. 为什么Oracle的笔试题总让人“一看就会,一做就废”
但凡在数据库这条线上摸爬滚打过几年的人,手里多半都攒着几套Oracle的笔试面试题。我见过太多同行,简历上写着“精通Oracle”,结果一上来被一道SELECT语句的执行顺序问得哑口无言,或者被一个NULL值的比较逻辑绕进去半天出不来。这事儿不丢人,因为Oracle的题目有个很鲜明的特点:表面考语法,实际考的是你对数据行为的理解深度。
我整理这套题解,不是为了给你一份标准答案去背。市面上的题库已经够多了,背答案这条路走不通——面试官稍微换个条件,或者追问一句“为什么”,背下来的东西立刻就露馅。我真正想做的是,把每一类题目背后的数据逻辑、执行机制和常见陷阱拆开揉碎,让你下次看到类似问题时,脑子里能自动浮现出“它在考我什么”的判断。
这篇文章适合三类人看:第一类是正在准备数据库方向求职的应届生或转行者,你需要一套能帮你建立知识框架的题解;第二类是有一定工作经验但基础不够扎实的开发者,你可能天天写SQL,但从来没认真想过WHERE和HAVING到底差在哪;第三类是做运维或数据管理的同行,你不需要刷题,但你需要理解那些容易踩坑的细节,避免在生产环境里翻车。
接下来的内容,我会按照查询逻辑、事务与锁、对象管理、性能相关这几个维度来组织。每一道题我都会先给出典型问法,然后拆解考点,再补充我实际工作中遇到的变体和坑。你不需要按顺序读,但建议至少把每一节的“注意事项”部分看完,那里面往往藏着面试官真正想听的东西。
2. 查询逻辑类题目:别被语法顺序骗了
2.1 执行顺序才是理解SQL的第一道门槛
几乎所有Oracle笔试题里都会出现这样一道:给出一个包含SELECT、FROM、WHERE、GROUP BY、HAVING、ORDER BY的完整查询,问你哪个子句最先执行、哪个最后执行。很多人凭写SQL的习惯,脱口而出“从左到右”,这就掉坑里了。
Oracle实际的处理顺序是这样的:
FROM:先确定数据来源,包括表、视图、子查询,以及它们之间的连接方式。WHERE:对FROM阶段产生的行进行过滤,注意这时候还没有分组。GROUP BY:把过滤后的行按指定列分组。HAVING:对分组后的结果进行过滤。SELECT:选择需要的列,这时候才处理列别名和表达式。ORDER BY:对最终结果排序。
这个顺序解释了很多“反直觉”的现象。比如为什么WHERE子句里不能用SELECT中定义的列别名?因为WHERE执行的时候,SELECT还没跑呢,别名根本不存在。而ORDER BY可以用别名,因为它是在SELECT之后执行的。
我实际面试别人的时候,喜欢追问一句:“如果WHERE和HAVING都能过滤,那什么时候必须用HAVING?”答案很简单:当过滤条件里包含聚合函数的时候。比如HAVING COUNT(*) > 5,这个条件没法放在WHERE里,因为WHERE执行时还没有分组,COUNT(*)没有意义。
注意:很多初学者会把
HAVING当成“分组后的WHERE”来记,这个理解方向是对的,但不够精确。更准确的说法是:WHERE过滤的是行,HAVING过滤的是组。行和组是两个不同层级的概念。
2.2 NULL值比较:那个让无数人翻车的三值逻辑
Oracle的NULL表示“未知”,它不是空字符串,也不是0。这个“未知”带来了一套三值逻辑:真、假、未知。任何与NULL的比较运算,结果都是未知,而WHERE子句只保留结果为“真”的行。
来看一道经典题:
SELECT * FROM employees WHERE salary != 5000;如果salary列里有NULL值,这些行会不会被查出来?答案是不会。因为NULL != 5000的结果是未知,不是真。很多人在这里翻车,以为“不等于5000”应该把空值也算进去,但Oracle不这么认为。
正确的写法应该是:
SELECT * FROM employees WHERE salary != 5000 OR salary IS NULL;或者用NVL函数把NULL转换成一个具体值再比较:
SELECT * FROM employees WHERE NVL(salary, -1) != 5000;但这里有个性能陷阱:在WHERE条件里对列使用函数,会导致该列上的索引失效。如果salary列有索引,NVL(salary, -1) != 5000这种写法就没法走索引了。所以更推荐用IS NULL的写法,或者根据业务逻辑判断是否真的需要把NULL纳入结果。
还有一个更隐蔽的坑:NOT IN子查询里如果包含NULL,整个查询会返回空结果。比如:
SELECT * FROM departments WHERE dept_id NOT IN (SELECT dept_id FROM employees);如果employees表的dept_id列里有任何一行是NULL,这个查询就一行都查不出来。原因是NOT IN等价于对每一行做!=比较,而!= NULL的结果是未知,整个条件就永远不为真。这个坑我在生产环境里见过不止一次,排查起来非常费劲,因为语法完全正确,逻辑上却“莫名其妙”没结果。
实操心得:写
NOT IN之前,先确认子查询的列有没有NULL。如果有,要么加WHERE dept_id IS NOT NULL过滤掉,要么改用NOT EXISTS。NOT EXISTS对NULL的处理更符合直觉,不会出现整个查询为空的情况。
2.3 连接查询:内连接、外连接和那些“看起来一样”的写法
Oracle支持两种连接语法:ANSI标准的JOIN ... ON和Oracle传统的(+)外连接符号。笔试里经常两种都考,而且会故意出一些“看起来等价但实际不等价”的题目。
先看一个典型的左外连接:
-- ANSI写法 SELECT e.name, d.dept_name FROM employees e LEFT JOIN departments d ON e.dept_id = d.dept_id; -- Oracle传统写法 SELECT e.name, d.dept_name FROM employees e, departments d WHERE e.dept_id = d.dept_id(+);这两种写法在功能上是等价的,但有几个细节需要注意。第一,(+)符号要放在“可能为空”的那一侧。左外连接是右表可能为空,所以(+)放在右表的连接条件上。第二,如果连接条件里有多列,每一列都要加(+),漏掉一个就变成了内连接。第三,(+)不能和OR、IN等条件混用,否则会报错。
笔试里还喜欢考“外连接加过滤条件”的题目。比如:
SELECT e.name, d.dept_name FROM employees e LEFT JOIN departments d ON e.dept_id = d.dept_id WHERE d.dept_name = '研发部';这个查询和把条件放在ON子句里有什么区别?
SELECT e.name, d.dept_name FROM employees e LEFT JOIN departments d ON e.dept_id = d.dept_id AND d.dept_name = '研发部';区别很大。第一种写法,WHERE是在连接之后执行的,它会过滤掉所有d.dept_name不为“研发部”的行,包括那些左表中存在但右表没有匹配的行(因为那些行的d.dept_name是NULL,NULL = '研发部'为未知,不保留)。结果就是左外连接退化成了内连接。
第二种写法,条件放在ON子句里,是在连接过程中判断的。左表的每一行都会保留,只是右表不匹配的时候,右表的列显示为NULL。这才是真正的左外连接语义。
注意:这个区别在实际开发中非常容易搞混。我的建议是,外连接的条件尽量都写在
ON子句里,WHERE只用来过滤最终结果。如果确实需要在WHERE里过滤右表的列,一定要想清楚是否要把NULL行也保留下来。
3. 事务与锁:那些笔试爱考、工作中常踩的坑
3.1 事务隔离级别与并发现象
Oracle的默认隔离级别是读已提交。这个级别下会出现不可重复读和幻读,但不会出现脏读。笔试里经常给一个并发场景,让你判断最终结果或者问会出现什么现象。
我整理了一个对照表,把隔离级别和并发现象的关系列清楚:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 | 可能 | 可能 | 可能 |
| 读已提交 | 不可能 | 可能 | 可能 |
| 可重复读 | 不可能 | 不可能 | 可能 |
| 串行化 | 不可能 | 不可能 | 不可能 |
Oracle只支持读已提交和串行化两种。串行化通过SET TRANSACTION ISOLATION LEVEL SERIALIZABLE来设置,但实际工作中很少用,因为它会大幅降低并发性能。
有一道题我印象很深:两个会话同时更新同一行数据,第一个会话更新后不提交,第二个会话会怎样?答案是第二个会话会被阻塞,直到第一个会话提交或回滚。如果第一个会话一直不提交,第二个会话会一直等下去,直到达到DISTRIBUTED_LOCK_TIMEOUT或者手动取消。
这里有个容易忽略的点:Oracle的读操作不会阻塞写操作,写操作也不会阻塞读操作。这是因为Oracle用回滚段来保证读一致性。当一个会话在更新数据但还没提交时,另一个会话读到的仍然是更新前的数据。这个机制叫多版本并发控制,是Oracle区别于很多其他数据库的重要特性。
实操心得:在生产环境里,长事务是灾难。我见过一个案例,某个开发人员在会话里更新了几行数据,然后去开会忘了提交,结果其他会话全部被阻塞,业务直接停摆。所以写完
UPDATE或DELETE之后,要么立刻提交,要么立刻回滚,千万别开着事务去干别的事。
3.2 锁的类型与死锁排查
Oracle的锁分为很多种,笔试里最常考的是行级锁和表级锁。行级锁通过SELECT ... FOR UPDATE或者UPDATE、DELETE语句自动加锁。表级锁的级别比较多,从ROW SHARE到EXCLUSIVE,不同级别的锁兼容性不同。
死锁是另一个高频考点。Oracle检测到死锁后,会自动回滚其中一个会话的语句,并报ORA-00060错误。笔试里经常给两个会话的操作序列,让你判断会不会死锁。
比如:
- 会话A:更新表1的第1行,然后更新表2的第1行。
- 会话B:更新表2的第1行,然后更新表1的第1行。
如果两个会话同时执行到第二步,就会互相等待对方释放锁,形成死锁。Oracle会检测到这种情况,回滚其中一个会话。
避免死锁的方法其实很简单:让所有会话按相同的顺序访问资源。如果所有会话都先更新表1再更新表2,就不会出现交叉等待。这个原则在实际开发中非常实用,尤其是在批量处理多个表的场景下。
排查死锁的时候,可以查V$LOCK和V$SESSION视图,找到被阻塞的会话和阻塞它的会话。更直接的方法是查看告警日志,Oracle会在日志里记录死锁的详细信息,包括涉及的两个会话和各自的SQL语句。
-- 查看当前被阻塞的会话 SELECT blocking_session, sid, serial#, wait_class, seconds_in_wait FROM v$session WHERE blocking_session IS NOT NULL;这个查询能快速定位到谁在等谁。如果blocking_session列有值,说明这个会话正在被那个会话阻塞。顺着链条找下去,就能找到根源。
4. 对象管理与DDL:那些“看起来简单”的陷阱
4.1 表、视图、同义词的创建与删除
笔试里关于DDL的题目往往不难,但细节很多。比如CREATE TABLE ... AS SELECT和CREATE TABLE ... LIKE的区别,TRUNCATE和DELETE的区别,DROP和TRUNCATE的区别。
我列一个表来对比DELETE、TRUNCATE和DROP:
| 操作 | 可回滚 | 触发器 | 高水位线 | 空间释放 | 日志量 |
|---|---|---|---|---|---|
| DELETE | 可以 | 触发 | 不重置 | 不释放 | 大 |
| TRUNCATE | 不可以 | 不触发 | 重置 | 释放 | 小 |
| DROP | 不可以 | 不触发 | 重置 | 释放 | 小 |
TRUNCATE是DDL操作,不能回滚,不触发DELETE触发器,会重置高水位线。DELETE是DML操作,可以回滚,触发触发器,不重置高水位线。这个区别在数据量大的时候非常明显:一张几千万行的表,DELETE全部数据可能要跑几个小时,而TRUNCATE只需要几秒钟。
但TRUNCATE有个限制:如果表被外键引用,就不能直接TRUNCATE,除非先禁用外键约束。这个细节笔试里经常考。
视图的创建也有讲究。CREATE VIEW和CREATE OR REPLACE VIEW的区别在于,后者可以覆盖已有的视图定义,而前者如果视图已存在会报错。另外,如果视图定义里包含GROUP BY、DISTINCT、聚合函数等,这个视图就是只读视图,不能进行INSERT、UPDATE、DELETE操作。
同义词在笔试里出现得不多,但实际工作中很有用。CREATE SYNONYM可以给表、视图、序列等对象起别名,简化SQL书写,也可以用来隐藏对象的真实名称和位置。
4.2 序列与自增列
Oracle在12c之前没有自增列的概念,需要用序列加触发器来实现。12c之后引入了IDENTITY列,语法类似其他数据库。
序列的CACHE参数是个容易踩坑的地方。CACHE的作用是预先在内存中生成一批序列值,减少对数据字典的访问,提高性能。但如果有多个会话同时使用序列,或者数据库发生异常关闭,缓存中的序列值会丢失,导致序列出现“跳号”。
比如CREATE SEQUENCE seq_test CACHE 20,如果数据库突然崩溃,内存中还没用掉的序列值就丢了,下次从新的位置开始。这在大多数业务场景下没问题,但如果业务要求序列必须连续,就不能用CACHE,或者把CACHE设为1。
注意:
NOCACHE和CACHE 1在行为上有一点区别。NOCACHE每次取序列值都要访问数据字典,性能较差。CACHE 1会在内存中缓存一个值,性能稍好,但跳号的可能性仍然存在。如果业务对连续性要求极高,建议用NOCACHE,或者干脆不用序列,用其他方式生成编号。
4.3 索引的创建与使用
索引是笔试里的重头戏。B树索引、位图索引、函数索引、复合索引,每种都有适用场景和限制。
B树索引是默认的索引类型,适合高基数列(不同值多的列)。位图索引适合低基数列(不同值少的列),比如性别、状态这种只有几个值的列。但位图索引在并发DML场景下容易产生锁竞争,所以OLTP系统里很少用,更多用在数据仓库里。
复合索引的列顺序很重要。CREATE INDEX idx_test ON table_name(col1, col2)和CREATE INDEX idx_test ON table_name(col2, col1)是两个不同的索引。复合索引遵循最左前缀原则:如果查询条件里只包含col2,这个索引就用不上;如果包含col1或者col1加col2,就能用上。
函数索引是另一个考点。如果查询里经常对某列使用函数,比如WHERE UPPER(name) = 'ABC',可以在UPPER(name)上建函数索引:
CREATE INDEX idx_upper_name ON employees(UPPER(name));这样查询就能走索引了。但函数索引要求查询里的函数和索引定义的函数完全一致,包括大小写和参数个数。
索引的另一个常见问题是索引失效。除了前面提到的对列使用函数,还有几种情况会导致索引失效:隐式类型转换、NOT操作符、IS NULL或IS NOT NULL(取决于索引类型和列的可空性)、前导模糊查询LIKE '%abc'等。
实操心得:判断索引是否失效,最直接的方法是看执行计划。用
EXPLAIN PLAN FOR加上你的SQL,然后查PLAN_TABLE,看访问路径是TABLE ACCESS FULL还是INDEX RANGE SCAN。如果该走索引的没走,再回头检查SQL写法。
5. 性能相关题目:从执行计划到SQL优化
5.1 执行计划的阅读方法
执行计划是SQL优化的基础。笔试里经常给一个执行计划,让你判断性能瓶颈在哪里,或者问某个操作的含义。
执行计划里常见的操作有:
TABLE ACCESS FULL:全表扫描,通常意味着没有可用的索引,或者优化器认为全表扫描更快。INDEX RANGE SCAN:索引范围扫描,通常是比较理想的情况。INDEX UNIQUE SCAN:索引唯一扫描,效率最高。NESTED LOOPS:嵌套循环连接,适合驱动表结果集小的情况。HASH JOIN:哈希连接,适合大表之间的连接。SORT MERGE JOIN:排序合并连接,适合已经排序的数据。
看执行计划的时候,要从上往下看,但实际执行顺序是从最内层、缩进最多的行开始。Cost列表示优化器估算的成本,Cardinality表示估算的行数。如果估算行数和实际行数差距很大,说明统计信息可能过期了,需要重新收集。
-- 收集表的统计信息 EXEC DBMS_STATS.GATHER_TABLE_STATS('SCHEMA_NAME', 'TABLE_NAME');统计信息对优化器选择执行计划至关重要。如果统计信息过期,优化器可能选错索引,导致性能急剧下降。生产环境里建议定期收集统计信息,尤其是在数据量变化较大的表上。
5.2 SQL改写与优化技巧
SQL优化是笔试里最能拉开差距的部分。同样一个查询,不同的写法性能可能差几十倍。
一个经典的例子是UNION和UNION ALL的区别。UNION会去重,需要排序,性能较差;UNION ALL不去重,直接合并结果,性能好很多。如果业务逻辑允许重复行,优先用UNION ALL。
另一个例子是EXISTS和IN的选择。一般来说,如果子查询的结果集小,用IN;如果主查询的结果集小,用EXISTS。但这个规则不是绝对的,还要看具体的执行计划和数据分布。
分页查询也是笔试常考的点。Oracle里分页通常用ROWNUM或者12c之后的OFFSET ... FETCH。用ROWNUM分页的时候要注意,ROWNUM是在结果集生成过程中分配的,所以不能直接写WHERE ROWNUM > 10,必须嵌套一层子查询:
SELECT * FROM ( SELECT a.*, ROWNUM rn FROM ( SELECT * FROM employees ORDER BY hire_date ) a WHERE ROWNUM <= 20 ) WHERE rn > 10;这个写法的逻辑是:最内层排序,中间层取前20行并分配ROWNUM,最外层过滤掉前10行。如果直接在最内层写ROWNUM > 10,会一行都查不出来,因为ROWNUM从1开始,第一行不满足> 10,第二行也不满足,永远没有满足条件的行。
注意:12c之后的
OFFSET ... FETCH语法更直观:SELECT * FROM employees ORDER BY hire_date OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY。但老版本的Oracle不支持这个语法,笔试里如果没说明版本,建议用ROWNUM的写法,兼容性更好。
5.3 绑定变量与硬解析
绑定变量是OLTP系统性能优化的关键。如果SQL语句里直接用字面值,每次执行都会产生一个新的SQL ID,导致硬解析,消耗大量CPU和共享池内存。用绑定变量可以让相同的SQL结构复用执行计划,减少解析开销。
比如:
-- 不用绑定变量,每次都是新SQL SELECT * FROM employees WHERE employee_id = 100; SELECT * FROM employees WHERE employee_id = 101; -- 用绑定变量,SQL结构相同 SELECT * FROM employees WHERE employee_id = :emp_id;笔试里经常问:为什么用了绑定变量之后,某些查询反而变慢了?这是因为绑定变量窥探。Oracle在第一次执行带绑定变量的SQL时,会窥探绑定变量的值,并据此生成执行计划。如果后续传入的值数据分布差异很大,这个执行计划可能就不适用了,导致性能下降。
解决方法是使用自适应游标共享,或者对数据分布差异大的列禁用绑定变量窥探。但这些都属于比较高级的优化手段,笔试里一般不会深究,知道有这个现象就行。
6. 常见问题与排查技巧实录
6.1 笔试高频错题速查表
我把这些年见过的、最容易出错的题目整理成了一个速查表,你可以对照着检查自己的理解有没有偏差:
| 题目类型 | 常见错误答案 | 正确答案 | 核心考点 |
|---|---|---|---|
| WHERE和HAVING的区别 | 都是过滤,可以互换 | WHERE过滤行,HAVING过滤组 | 执行顺序 |
| NULL比较 | NULL = NULL为真 | 结果为未知 | 三值逻辑 |
| NOT IN子查询 | 正常返回不匹配的行 | 子查询有NULL时返回空 | NULL传播 |
| 左外连接加WHERE | 保留左表所有行 | 可能退化为内连接 | 连接语义 |
| TRUNCATE和DELETE | 都可以回滚 | TRUNCATE不可回滚 | DDL与DML |
| 复合索引使用 | 任意列都能用 | 遵循最左前缀 | 索引结构 |
| ROWNUM分页 | 直接写ROWNUM > 10 | 必须嵌套子查询 | ROWNUM分配时机 |
| UNION和UNION ALL | 性能差不多 | UNION ALL快很多 | 去重开销 |
这张表里的每一行,我都见过有人在面试里答错。有些错误甚至在工作多年的人身上也会出现,因为平时写SQL的时候没注意,形成了错误的直觉。
6.2 排查问题的通用思路
遇到一个陌生的SQL问题,我通常按这个顺序排查:
- 看执行计划:先搞清楚Oracle打算怎么执行这条SQL。是全表扫描还是索引扫描?连接方式是什么?有没有排序或哈希操作?
- 看统计信息:统计信息是否过期?表的行数、列的不同值数量是否准确?
- 看数据分布:数据是否有倾斜?某些值特别多,某些值特别少?这会影响优化器的选择。
- 看SQL写法:有没有对列使用函数?有没有隐式类型转换?有没有用
NOT IN加子查询? - 看系统资源:CPU、内存、I/O有没有瓶颈?是不是因为并发太高导致资源争抢?
这个顺序不是固定的,但大体上是从SQL本身往外扩。先确认SQL写法没问题,再看统计信息和数据分布,最后看系统层面。
实操心得:我见过很多人一遇到慢SQL就去加索引,结果加了索引反而更慢。原因是索引不是越多越好,每个索引都会增加DML操作的开销,而且优化器可能选错索引。加索引之前,先用执行计划确认瓶颈在哪里,再决定加什么索引。
6.3 几个容易被忽略的细节
第一个细节是字符集。Oracle的字符集在创建数据库时确定,之后很难修改。如果字符集设置不当,中文可能会出现乱码,或者长度计算出现偏差。比如VARCHAR2(10)在AL32UTF8字符集下,最多只能存10个字节,而一个中文字符通常占3个字节,所以实际只能存3个中文字。这个细节在笔试里偶尔会考,实际工作中如果涉及多语言数据,一定要提前确认字符集。
第二个细节是空字符串和NULL。在Oracle里,空字符串''和NULL是等价的。INSERT INTO table_name(col) VALUES('')实际上插入的是NULL。这个行为和其他数据库不一样,比如MySQL里空字符串和NULL是两回事。笔试里如果考到字符串处理,要注意这个区别。
第三个细节是日期格式。Oracle的日期类型包含日期和时间,但显示格式取决于NLS_DATE_FORMAT参数。如果直接用字符串比较日期,可能会因为格式不匹配导致错误。推荐用TO_DATE函数显式转换:
SELECT * FROM orders WHERE order_date > TO_DATE('2024-01-01', 'YYYY-MM-DD');这样不管数据库的默认日期格式是什么,都能正确比较。
第四个细节是隐式类型转换。如果employee_id是数字类型,你写WHERE employee_id = '100',Oracle会自动把字符串转换成数字。这个转换本身没问题,但如果列是字符类型,你写WHERE employee_id = 100,Oracle会把列值转换成数字再比较,这会导致索引失效。所以写SQL的时候,尽量保证比较两边的类型一致。
7. 从笔试到实战:我的几点体会
笔试题目再刁钻,终究是有限的。真正让我在数据库这条路上走得更远的,不是刷了多少题,而是养成了几个习惯。
第一个习惯是写SQL之前先想执行计划。我现在写稍微复杂一点的查询,脑子里会自动过一遍:这个查询会走索引吗?连接方式是什么?有没有排序操作?如果预估性能不好,就换一种写法。这个习惯让我避免了很多“写完才发现慢”的情况。
第二个习惯是对NULL保持警惕。每次写NOT IN、!=、外连接的时候,我都会下意识地问自己:这里有没有NULL?NULL会不会导致结果不符合预期?这个习惯帮我躲过了好几次生产事故。
第三个习惯是用绑定变量。不管是写应用代码还是临时查询,只要涉及重复执行的SQL,我都会用绑定变量。这不仅是为了性能,也是为了让共享池里的SQL更容易管理。
第四个习惯是定期看告警日志。很多问题在爆发之前,告警日志里已经有征兆了。比如死锁、空间不足、归档失败,这些信息都会记录在告警日志里。定期翻一翻,能提前发现隐患。
最后再分享一个小技巧:如果你在面试里被问到不会的题目,不要硬编答案。你可以说“这个点我平时接触不多,但根据我的理解,可能是……”,然后说出你的推理过程。面试官往往更看重你的思考方式,而不是标准答案。数据库这个领域太广了,没有人什么都会,但一个清晰的排查思路,比背下来的答案更有说服力。