上一讲我们把Oracle的安装配置、实例架构、表空间和基础增删改查这些地基性的东西过了一遍。这次的内容算是“基础2”,重点会放在真正干活时躲不开的那些操作上:事务和锁的处理、子查询和集合运算的进阶玩法、PL/SQL里过程函数包的实战套路、还有当查询变慢时,从哪些角度下手定位问题,以及日常备份和权限管理的常规操作。
说白了,这本书里不少内容在官方文档里都有,但讲得比较学术,真正遇到问题的时候总感觉隔着一层。这一篇我会把我这些年实际用到的、踩过的坑、总结出来的操作习惯都糅进去,尽量让不管是刚入门的人,还是用了一段时间Oracle但总停留在select的读者,都能找到点实实在在的东西。涉及到的关键点我会配合代码示例讲,方便直接拿去做实验,毕竟Oracle这种东西,看着都会,一跑就错,环境里多敲几遍比看十篇文章都管用。
1. 这一篇到底讲什么:基础操作里最容易被忽略的硬骨头
1.1 从基础1到基础2,差距往往不在语法上
很多刚接触Oracle的人,最常犯的错觉就是把“会写SQL”当成“会操作数据库”。基础1做完之后,环境能起来、表能建、简单的增删改查没问题,这确实是个好的开始。但真到了要处理稍微复杂一点的业务逻辑,或者赶上系统里出了个锁等待、跑了个慢查询,瞬间就不知道该从哪里下手了。
基础2和基础1最大的区别在于:基础1是教你“怎么把东西放进去、取出来”,基础2则是教你“怎么在真实环境里把它玩明白”。包括事务提交的时机怎么掌控,多个会话一起操作同一行数据时锁是怎么互斥的,一条看似平常的查询背后嵌套了几层逻辑,业务需求要“分组取前几”“同比环比”时该怎么写才能既正确又高效。这些能力不靠背语法,靠的是对底层机制的理解加上大量的实操积累。
这篇文章的定位,就是把这块最容易被忽略的硬骨头啃下来。不需要你把官方文档全部背下来,但理解了这些核心机制之后,再回头看官方文档里那些概念,你会发现完全不是同一个感觉,很多内容真的就是一层窗户纸。
1.2 这篇文章适合谁读、能解决什么问题
我大概把目标读者分成两类。第一类是刚入行或者正在学习阶段的开发者,你已经过了“建表、插数据、查数据”的入门期,开始频繁接触存储过程、触发器,或者参与一些权限管理、备份恢复的日常维护工作,很需要把事务隔离、锁机制、PL/SQL异常处理这些进阶概念搞清楚。第二类是工作需要经常操作数据库,但一直缺少系统性理解的工程师,可能平时写SQL不少,但遇到死锁、遇到update卡住、需要做性能排查的时候,只能靠猜或者到处搜索。
这篇文章重点解决这么几个高频问题。为什么并发操作时数据会乱、事务到底应该在什么时候提交?嵌套子查询和集合运算什么时候用哪个,窗口函数到底比传统写法强在哪儿?一个完整可用的PL/SQL过程、函数和包,从定义到调试应该怎么设计?还有一条SQL跑得慢,拿到执行计划之后怎么判断哪里是真正的瓶颈,日常备份和权限管理有哪些安全红线不能碰。每个问题都会配上实际案例和排查思路,照着做基本能解决工作中至少八成以上的常见麻烦。
2. 事务与并发控制:多用户环境下必须理解的核心
2.1 事务的四个特性,以及提交时机怎么把握
事务,简单理解就是把一组操作打包成一个整体,要么全部成功,要么全部撤回。我经常喜欢打一个比方:事务就好比转账,从A账户扣钱和往B账户加钱这两个动作必须是一场“生死与共”的绑定,如果在扣完钱之后、加钱之前系统崩了,那这钱就等于凭空消失了,这是绝对不能接受的。
Oracle里事务的核心其实是回滚段(Undo Segment)在支撑。你执行修改操作时,数据块上会被打上事务标记,同时修改前的镜像数据会被写进回滚段。如果事务COMMIT了,回滚段里的旧数据标记为可覆盖;如果ROLLBACK了,就用回滚段里的旧数据把原数据块恢复覆盖。理解了这一层,你才能对“为什么不能随便加大事务规模”有真正的感悟——回滚段不够用了,事务就会报ORA-01555快照过旧,这个经典报错本质上就是因为回滚数据被覆盖了。
提交时机的把握,是新手和老手之间最明显的分水岭。一个常见的反面案例是这样的:某开发同学在一个循环里对一万条数据做更新,每条更新后都执行一次COMMIT。从业务上每行都立即生效了,但副作用是每次COMMIT都要同步一次日志,整体性能被拉低一大截,而且一旦中途出错,想整体撤回非常困难,前面提交的数据已经收不回来了。正确做法应该是控制事务粒度,让它匹配业务上的一个“自然单位”。比如批量处理中,每处理两百条提交一次,既能保证故障恢复时重跑的成本可控,又不会因为回滚段积累过大影响性能。
我在实际项目里常用的一个折中策略是,对大规模批处理操作,按主键区间或者按批次切分,每个批次一个事务。这样做的好处有两个,一是单事务的回滚段占用有上限,不会拖着整库的性能下水;二是后台跑批任务如果中断,能将已提交的进度保存下来,从断点直接续跑,而不需要全部从头再来。
提示:Oracle默认不会自动帮你提交事务。如果你用的是命令行工具而忘了自己执行COMMIT,数据在当前会话里看是改了,但一旦会话断开,没有提交的数据就全部回滚了。建议养成“修改数据之后尽快明确提交或回滚”的习惯,千万别把事务拖到下班。
2.2 锁的类型、互斥关系,以及锁等待的排查思路
锁这个机制刚接触的时候会觉得抽象,但理解了它的本质之后就特别简单:锁是为了一致性服务的,为了防止你改数据的同时别人也在改同一行,导致最后状态错乱。Oracle的锁分两大类,一类叫行级锁,一类叫表级锁。行级锁是Oracle的默认方案,你执行update或delete时,受影响的行会被加上排他锁,其他会话对同一行做更新时会被阻塞,但读数据完全不受影响,因为Oracle用回滚段实现了多版本读一致性。表级锁则需要显式操作,比如执行LOCK TABLE语句,日常开发中很少手动用到,但在一些特殊的批处理场景里,为了确保整表数据不被中途修改,会给表加个行共享或排他锁。
写程序的人总喜欢问一个经典问题:两个update语句互相拿住对方的资源怎么办?这就是典型的死锁。Oracle的锁管理器会自动检测这种循环等待,一旦发现,会选择一个会话作为牺牲者,抛出ORA-00060死锁错误,同时回滚它的整个事务。另一个会话则获得资源,继续执行。
如果排查锁等待,锁定目标会话后,可以通过下面这个经典SQL查到底谁在等谁:
SELECT a.sid, a.serial#, a.username, b.type, b.id1, b.id2, c.sid AS blocking_sid, c.username AS blocking_user FROM v$session a, v$lock b, v$session c WHERE a.sid = b.sid AND b.block = 1 AND c.sid = b.sid AND a.username IS NOT NULL;但说实话,实际生产环境中我更常直接查v$session里的BLOCKING_SESSION字段,Oracle 10g以后这个字段特别实用,直接能定位阻塞源头:
SELECT sid, serial#, username, status, blocking_session, event FROM v$session WHERE blocking_session IS NOT NULL;相比之下,以前用v$lock慢慢关联的方式效率低得多。再配合v$session_wait里的事件名称,比如常见的“enq: TX - row lock contention”,基本就能判断是哪条SQL在等哪条SQL的锁了。
2.3 隔离级别的差异,以及读不到已提交数据的坑
隔离级别决定了事务能看到别人改了什么。Oracle默认的隔离级别是读已提交,这个级别下,同一个事务里如果间隔做了多次查询,两次结果可能不一样,因为中间有别的会话提交了数据。如果你希望反复读取的结果保持完全一致,就需要使用可串行化级别,或者用查询级快照。Oracle做串行化不是靠表锁死不让别人写,而是靠回滚段构造出一个一致性读快照,这个设计非常巧妙,读操作从来不阻塞写操作,写操作也不会阻塞读操作。
不过有一个坑必须要注意。在可串行化隔离级别下,如果你试图更新一条本事务启动之后被其他会话修改并提交过的数据,Oracle会直接报ORA-08177错误,提示无法串行化访问。这种情形下不能干等,只能回滚后重试整个事务。实际业务里,我建议除非有明确强一致性的需求,否则别轻易把隔离级别调到可串行化,它带来的大量ORA-08177重试会让人非常头疼。
要查看当前会话的隔离级别,可以用这条语句:
SELECT name, value FROM v$sysstat WHERE name LIKE 'transaction%';或者用SET TRANSACTION ISOLATION LEVEL命令在会话级临时设置。日常开发中实际用得最多的还是默认的读已提交,它性能表现好,也足够应对绝大部分业务场景,关键是把握住事务粒度,避免一个大事务跨越多长时间,因为这种长事务是好几个并发问题的源头。
注意:事务太大、时间太长,不只是锁等待的问题,回滚段空间也可能被大量消耗。我见过一些凌晨批处理任务直接跑了几个小时不提交,最后回滚表空间直接打满,然后全库报表ORA-30036无法扩展段。解决这类问题的方向通常是拆批、提前规划好UNDO表空间大小,而不是事后加数据文件。
3. 查询能力进阶:子查询、集合运算与窗口函数的正确打开方式
3.1 子查询改写思路:什么时候该用关联子查询
子查询是“查询中嵌套查询”的统称,看起来简单,但什么时候该用、什么时候会踩坑,门道不少。一种常见的子查询是非关联子查询,它跟外层查询没有直接关系,只会执行一次,比如查出工资大于全公司平均值的人:
SELECT employee_id, last_name, salary FROM employees WHERE salary > (SELECT AVG(salary) FROM employees);这类用法几乎百无禁忌,代价只是子查询先算一次,然后外层再扫一遍表。但如果数据量巨大,里面的AVG计算本身消耗资源,外层扫描又没索引,性能就会差,这是另一个优化话题。
另一种是关联子查询,它每处理一行外层数据,都可能执行一次子查询。经典例子是查出每个部门里入职最早的那批员工:
SELECT e1.department_id, e1.employee_id, e1.hire_date FROM employees e1 WHERE e1.hire_date = ( SELECT MIN(e2.hire_date) FROM employees e2 WHERE e2.department_id = e1.department_id );这里每扫到一行e1,都要去按部门重新算一次最小入职日期,如果employees表有几十万行,这种写法性能会非常难看。大多数情况下MySQL里这类需求会用GROUP BY加JOIN改写,但Oracle里其实有一个更优的思路:用窗口函数一次性搞定。
从实战角度,我总结的经验是:子查询在小数据集或逻辑明确时非常好用,但一旦涉及大表,要尽量分析它的执行方式。那些路上慢得离谱的SQL,很多都是关联子查询或嵌套层数太深导致的。
3.2 集合运算的用途和不同写法的内在差异
Oracle里有一组主要用来比对的集合运算:UNION、UNION ALL、INTERSECT、MINUS。它们的逻辑很好理解:UNION合并去重,UNION ALL合并不去重,INTERSECT取交集,MINUS取差集。但理解逻辑不等于会用好,真正关键的是知道不同写法背后的性能差异。
举个例子,如果想要两个表中都存在的人员名单,有两种写法:
SELECT employee_id FROM employees INTERSECT SELECT employee_id FROM former_employees;或者用EXISTS:
SELECT e.employee_id FROM employees e WHERE EXISTS ( SELECT 1 FROM former_employees f WHERE f.employee_id = e.employee_id );从结果上看这两条SQL是等价的,但INTERSECT会先把两个结果集都取出来,然后做排序去重(SORT UNIQUE)才能找到交集。EXISTS则更像是半连接,Oracle优化器有机会把它改写成更高效的HASH JOIN或NESTED LOOP。数据量一大,INTERSECT往往要额外消耗大量的临时表空间,而EXISTS的写法要轻量得多。
我的经验是:数据量超过十万行以后优先考虑EXISTS/IN/JOIN,而不是集合运算。集合运算表达上直观简洁,适合小结果集、临时排查,或者是两条SQL的比对场景。比如排查两个表之间的数据差异,直接用MINUS一把梭非常爽,这比写一堆关联条件要省心得多。
3.3 窗口函数:分组排序类需求的神器
窗口函数是Oracle里提升SQL表达能力最明显的一类功能,用过之后就很难再退回旧写法。它最核心的特点是,在返回基础列的同时,还能计算基于分组、排序的聚合值,而且不会压缩行数。用传统GROUP BY能分组却没法保留明细,用关联子查询能保留明细但性能差,窗口函数则同时获得明细数据和聚合结果,效率高得不是一点半点。
比如经典的“每个部门工资排前三的人”需求,一条SQL就能写出来:
SELECT department_id, employee_id, salary, RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rk FROM employees;RANK()、DENSE_RANK()、ROW_NUMBER()这三个排序函数的区别很值得说明。RANK()遇到并列名次会跳号,比如两个人并列第一,那下一个就是第三;DENSE_RANK()则不跳号,两个人并列第一,下一个人还是第二;ROW_NUMBER()则不管并列,直接给一个连续的行号。实际业务中,如果需要的是“每个分组中前三条记录”,不管并列不并列,用ROW_NUMBER()最直接;如果需要“排名”概念,比如竞赛排名,那RANK()更合适。
窗口函数除了排序类,还有聚合类窗口,比如累计求和、移动平均。统计销售额的同比环比,用LAG和LEAD特别顺手:
SELECT sales_date, amount, LAG(amount, 1) OVER (ORDER BY sales_date) AS prev_day_amount, LEAD(amount, 1) OVER (ORDER BY sales_date) AS next_day_amount FROM daily_sales ORDER BY sales_date;这种写法传统上要自关联,或者用子查询实现,代码又长又容易出错,窗口函数可以让复杂度直线下降。只要理解“窗口子句定义了在哪个范围内计算”,剩下的大多就是套模板的问题。
提示:Oracle 12c及以上版本里,窗口函数还支持了开窗子句的增强选项,比如ROWS BETWEEN帧界,可以精确控制移动范围。这类语法特别适合做滑动平均、累计占比这类分析,建议在实验环境里多练几种帧界写法。
4. PL/SQL开发入门:过程、函数与包的实用套路
4.1 初识PL/SQL块结构,以及游标处理
PL/SQL是Oracle的编程语言扩展,强大但写起来比较容易拘泥于细节。一个最简单的块大概长这样:
DECLARE v_count NUMBER; BEGIN SELECT COUNT(*) INTO v_count FROM employees; DBMS_OUTPUT.PUT_LINE('Total employees: ' || v_count); END;DECLARE部分声明变量,BEGIN到END之间写执行逻辑,异常处理部分用EXCEPTION关键字开启。这里有一个容易让人疑惑的点:SELECT INTO只能返回一行,如果查出来是多行,立刻会报TOO_MANY_ROWS异常;如果一行都没有,又会报NO_DATA_FOUND。所以遇到不确定返回行数的查询时,不要直接用SELECT INTO,要么用游标,要么先用聚合函数包一层。
游标是PL/SQL里读取结果集的主要方式。显式游标的经典写法是声明、打开、循环取行、关闭。虽然现在FOR循环也能隐式处理游标并自动关闭,但遇到需要分批处理、或者需要控制每次取多少行时,还是得回到显式游标上来。不过很多传统写法到了Oracle 12c以后,已经可以写得更精简,在取一批数据之前先限制一下行数,有效减少内存消耗。总之,游标的核心心法很简单:用完必须关,循环别把整表数据一次性装进内存。
我在实际写存储过程时经常用“批量游标+数组绑定”的方式处理大数据量的更新,比如一次取500行,更新完再取下一批。这样做从响应时间到资源占用都能控制在一个相对稳定的水平,而不是一次性把多表关联的几十万行都塞进内存。这种批量设计在ETL或报表加工场景里几乎是标准打法。
4.2 存储过程的参数设计要点
存储过程是PL/SQL里最有代表性的对象。它的参数有三种模式:IN、OUT、IN OUT。IN是默认模式,把值传入过程;OUT是过程把值传出来;IN OUT则是既能传入也能传出。最容易理解的难点在于:过程这个“对象”本身没有返回值,所有要吐出来的数据都要通过OUT参数传递。函数则恰好相反,一定有返回值,但原则上函数最好只做计算而不做数据修改,不要在函数里执行insert或update,这点在团队协作中容易被忽视。
实战中我建议把过程参数设计得单一职责一些,能用IN绝不用IN OUT。原因很简单:参数越多、职责越模糊,后面维护成本成倍上涨。设计一堆IN OUT参数的过程,调用者根本分不清哪个是输入哪个是输出,很容易用错。更好的做法是用记录类型或集合类型把输出结果整体打包,或者干脆拆成多个小过程。
一个典型的过程定义大致长这样:
CREATE OR REPLACE PROCEDURE p_get_employee_salary ( p_emp_id IN employees.employee_id%TYPE, p_salary OUT employees.salary%TYPE ) IS BEGIN SELECT salary INTO p_salary FROM employees WHERE employee_id = p_emp_id; EXCEPTION WHEN NO_DATA_FOUND THEN p_salary := NULL; END p_get_employee_salary; /这里用%TYPE做类型声明的习惯一定要养成。表结构一旦发生变化,过程定义里对应的字段类型会自动适配,不需要手动改,能少掉很多因表结构调整引发的隐式转换问题。
4.3 包的用处,以及异常处理绝不能只写WHEN OTHERS
包是过程的容器,可以把相关的过程、函数、变量都组织在一起,比如一组关于员工薪资的逻辑放在一个包里面。包分成包头和包体,包头放公开的声明信息,包体放具体的实现。好处是调用方只跟包头打交道,即使包体的实现细节变了,只要包头接口不变,调用方的代码就不用动。
异常处理是PL/SQL开发中非常有实战意义的部分。我曾见过不少开发同学写的异常处理一律长这样:
EXCEPTION WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE('Error');这种写法危害很大。WHEN OTHERS捕获一切异常,但不做任何回滚操作,也不记录到底是哪一行、哪个错误码,出了问题完全排查不到源头。一旦事务操作上半个commit了、半个没提交,还会导致数据状态半新半旧。
正确做法是至少记录SQLCODE和SQLERRM,并且把当前事务做明确处理。保存点也是一个实用机制,比如在循环中每个批次开始前设置一个保存点,某行处理失败时只回滚到保存点,而不是整个事务全部回滚:
BEGIN FOR rec IN (SELECT ...) LOOP SAVEPOINT batch_start; BEGIN -- 业务处理 EXCEPTION WHEN OTHERS THEN ROLLBACK TO batch_start; -- 记录错误日志 END; END LOOP; END;这样既保留了出错前的数据,又不会让一个异常拖垮整个批量任务。自定义异常或业务异常建议用RAISE_APPLICATION_ERROR抛出,再配合一个专门的异常日志表来记录每次报错的上下文,这种做法在正式项目里非常有用,能让后续排查问题的响应时间大幅缩短。
4.4 触发器是隐形代码,能不用尽量别用
触发器看起来很方便,一行插入自动触发一段逻辑。但实际项目中我一直持谨慎态度。因为触发器是隐式执行的,所有通过SQL Developer、应用系统甚至编写脚本执行的DML都会被触发,出问题的时候很难定位。如果几个触发器在同一张表上互相影响,更是容易演化成不可控的连锁效应。
如果是审计数据变更,现在Oracle有更成熟的内置方案,比如Flashback Data Archive,它对应用透明,不需要手工维护触发逻辑。如果只是简单的记录操作日志,也可以在业务层统一实现。触发器真正难以替代的场景,反而是数据库层面强一致的约束,比如需要保证某条数据只能插入不能修改,而且应用层管不住所有入口时,用BEFORE INSERT触发器兜底。
如果真要用触发器,我强烈建议在触发器内部不要做太重的操作,不要写复杂的子查询,不要访问远程表,更不要在里面调用执行时间很长的过程。触发器里的异常必须自己处理完全,一旦它自己报错,会导致整个用户操作失败。
5. 查询性能优化入门:从读懂执行计划开始
5.1 一条SQL到底慢在哪:执行计划的读取方法
遇到查询慢,别急着往SQL里加各种提示,第一步永远是先看执行计划。执行计划是Oracle优化器对SQL的执行方案描述,读懂了它,你才算真正看到了一条SQL在数据库内部如何被拆解。最常用的方式是用EXPLAIN PLAN FOR加SQL,然后查PLAN_TABLE。但在较新的版本里我更喜欢直接使用DBMS_XPLAN来展示:
EXPLAIN PLAN FOR SELECT e.employee_id, e.last_name, d.department_name FROM employees e, departments d WHERE e.department_id = d.department_id; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);执行计划里每一行都是一个操作,缩进关系表示执行顺序。刚开始读的时候只需要抓三个重点:第一,看有没有TABLE ACCESS FULL;第二,看执行顺序,哪一步最先执行决定了驱动表是谁;第三,看每个操作的代价估算是否状态很大,一般看ROWS字段和实际返回行数是否差了好几个数量级。
优化器最常见的问题是统计信息不准确。如果表和索引的统计信息长期不收集,优化器就会基于一些陈旧的数据估算代价,很可能选了错误的执行路径,出现全表扫描或者多头嵌套循环。定期刷新统计信息到生产的日常操作里非常关键,我一般用DBMS_STATS.GATHER_TABLE_STATS包来完成,操作时指定估算比例,常用的是让系统自动确定采样比例。
EXEC DBMS_STATS.GATHER_TABLE_STATS( ownname => 'SCOTT', tabname => 'EMPLOYEES', estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE, method_opt => 'FOR ALL COLUMNS SIZE AUTO' );5.2 哪些操作会引起性能滑坡,索引为什么没走
索引失效是新手排查慢查询时最容易卡住的一环。最常见的原因是你在WHERE条件里对索引列做了函数运算。比如在日期列上使用TRUNC函数,此时Oracle无法直接使用该列上的普通索引:
WHERE TRUNC(hire_date) = DATE '2024-01-01';如果hire_date本身是索引列,这种写法会让索引失效。改成范围条件通常更合适:
WHERE hire_date >= DATE '2024-01-01' AND hire_date < DATE '2024-01-02';还有一种常见情况是隐式类型转换。字符类型的索引列如果被当成数值类型比较,或者反过来,都会导致索引列被隐式转换而失效。比如一个VARCHAR2列,你去跟数字比较,Oracle会默认把列转化为数字,索引当然帮不上忙。开发规范里统一使用绑定变量的数据类型的习惯,能有效规避这种问题。
关于索引本身,也要明白不是越多越好。每个索引都会拖累insert、update和delete的性能,因为表数据变更时索引也要跟着同步更新。我常用的原则是:高度频繁查询的字段建索引,区分度低的字段不如不建,超过4到5个字段的联合索引要特别慎重,宁可拆成几个单列索引,优化器自己组合使用效果往往更好。
注意:复合索引的列顺序很重要。如果你建了INDEX(col_a, col_b),查询条件只有col_b时很可能用不上这个索引;但只有col_a时一定能用。所以设计复合索引时,要把等值条件列放前面、范围条件列放后面,这是基本规则。
5.3 绑定变量与SQL共享,以及为什么不能随便格式化
绑定变量的价值在于SQL共享和执行计划复用。如果你在程序里每传一个不同的数值就动态拼一个全新的SQL,那Oracle每次都要硬解析生成新的执行计划,SQL解析本身在高并发下会大量消耗CPU和共享池。改成绑定变量后,同类SQL能共享同一条执行计划,解析开销大幅降低:
SELECT * FROM employees WHERE department_id = :dept_id;我在项目里见过一些框架层面默认帮用户拼好SQL字符串的情况,短期内看不到问题,一旦并发量上来,共享池里的缓存会被大量SQL变体挤爆,最终表现为系统整体性能骤降。所以越是访问频繁的核心查询,越要规范化SQL,统一用绑定变量。
但绑定变量也有一个反直觉的坑:它可能让优化器无法准确估算特定数值的选择性。比如某字段90%的值都是0,10%是1,查询条件等于1时数据量很小,用索引扫描更合适,但绑定变量让优化器无法知道这个值到底是多少,可能一律走全表扫描。对这类数据分布严重倾斜的场景,可行的办法是针对特定取值使用字面值,或者使用DBMS_STATS收集直方图信息,帮助优化器回顾数据的分布情况。
6. 日常操作与安全管理:备份恢复和权限控制的必修课
6.1 备份恢复里最实用的几个方案,以及恢复数据的正确姿势
日常运维中备份和恢复是绕不开的课题。Oracle的备份体系分成两大派系:用户管理的备份,也就是手工拷贝数据文件、控制文件、归档日志;还有就是使用RMAN(Recovery Manager)进行的备份。我个人的建议是主用RMAN,它做增量备份、块级别坏块检测,恢复时也方便,学习成本虽然比手工备份稍微高一点,但顺利养成习惯后绝对值得。
一个最简单的RMAN全库备份脚本大概长这样:
rman target / <<EOF BACKUP DATABASE PLUS ARCHIVELOG; DELETE OBSOLETE; EXIT; EOF这条命令的作用是备份整个数据库和所有归档日志,顺便清理掉已经过期的备份文件。加上PLUS ARCHIVELOG的原因在于,仅仅备份数据文件并不能保证恢复到任意时间点,还需要归档日志配合。归档日志相当于数据库操作的法医记录,有了它才能重放事务。
恢复操作最常见的是恢复误删除的数据或某个表空间。这个时候,如果数据库处于归档模式并且开启了闪回恢复区,Flashback Drop是一个非常快的救急方案,可以直接从回收站恢复被DROP的表:
FLASHBACK TABLE employees TO BEFORE DROP;但如果误删的数据已经超过回收站保留期限,或者执行的是TRUNCATE操作,Flashback Drop救不了你,这时就得用时间点的恢复方案。RMAN可以做到按时间恢复到某一个具体时刻。这种操作需要极其谨慎,恢复前要确认目标时间点之后的所有修改都允许丢弃,否则会造成更新的数据丢失。
重要:不管用什么方案做恢复,先做一次冷备份或者先评估恢复对当前系统的影响范围再动手。恢复通常是“覆盖式”操作,涉及的库越重要,越要先确保退路,免得损坏范围进一步扩大。
6.2 用户与权限管理的基本操作,以及最小权限原则
权限管理的核心原则只有四个字:最小权限。给用户只分配完成工作所需的最少权限,能显著减少误操作和安全风险。创建用户的常用语句很简单:
CREATE USER app_read IDENTIFIED BY strong_password DEFAULT TABLESPACE users QUOTA 10M ON users;创建之后要给用户授权,其中最常见的两个系统权限是CREATE SESSION(允许登录)和CREATE TABLE(允许建表)。但直接把权限一点一点授予比较复杂。在实际项目中,我更常用角色来管理。角色是一组权限的集合,把权限授予角色,再把角色授予用户,这样管理粒度更清晰。给某个应用核心用户统一的SELECT权限,只需要创建一个只读角色,把需要的表和视图权限全部扔进角色里,再一次性授权给用户,比逐表授权要轻松得多。
对象权限的回收也经常出现尴尬场景。比如某用户用GRANT SELECT ON scott.employees TO app_read方式获得了查询权限,但后续突然不做这部分工作了,要收权限就用REVOKE。这里要注意的是,权限回收只影响之后的操作,已连接的会话在权限回收生效前可能仍然能够继续用已建立的连接执行SQL。如果数据敏感度很高,回收权限后建议同步终止对应会话。
另外一定不要用DBA角色当默认权限包到处发。DBA权限意味着几乎可以对整个数据库做任何事,包括删除所有表的数据。在开发环境图省事可以,但生产环境绝不能这样操作,严格的权限管理是数据库安全的第一道防线。
6.3 闪回的回路由:日常维护中最实用的几条命令
闪回技术是Oracle里非常实用的一套机制,可以快速从错误操作中恢复。除了前面提到的FLASHBACK TABLE TO BEFORE DROP恢复误删表,还有几种用得特别多的闪回操作。
闪回查询用于查看过去某个时间点的数据状态:
SELECT * FROM employees AS OF TIMESTAMP (SYSTIMESTAMP - INTERVAL '30' MINUTE) WHERE department_id = 10;这条SQL查的是30分钟之前的数据,不需要备份也能查看历史状态。如果只是某一行数据被人误更新了,用闪回查询把旧值找出来,再把它更新回去,就能解决很多临时的数据修复需求。
如果在做DML事务时发现误修改了大量数据,还有一种操作叫闪回事务:
SELECT versions_starttime, versions_endtime, versions_operation FROM employees VERSIONS BETWEEN TIMESTAMP (SYSTIMESTAMP - INTERVAL '1' HOUR) AND SYSTIMESTAMP;通过查询版本信息可以定位到具体事务号,再用DBMS_FLASHBACK.TRANSACTION_BACKOUT把整个事务撤销。这类操作在库上如果启用了相应的撤销日志才可用。闪回删除、闪回查询、闪回版本查询、闪回事务,从按表级、行级到事务级的覆盖面已经非常广,真正遇到大面积丢失的时候还有闪回数据库,可以把整个库回退到某个时间点,但要求库运行在归档模式下,而且只能整体回退,后续的改动会丢。
7. 高频故障与排查技巧速查
7.1 实际踩坑记录:ORA-01555、ORA-30036、锁等待
ORA-01555快照过旧,是开发过程中被问过很多次的经典错误。它出现的原因简单说就是查询需要读旧版本数据,但回滚段里对应的旧数据已经被覆盖了。常见诱因有三个:回滚表空间太小、执行时间太长的事务、查询和更新并发跑得剧烈。解决方向比较简单,第一条是增大UNDO表空间,第二条是优化SQL避免查询耗时太久,第三条是从业务层面错峰运行大型批处理任务。
ORA-30036无法扩展UNDO表空间,这个报错通常跟深夜跑批时间过长、事务规模太大、一次性修改数据太多有关。处理时最直接的办法是给UNDO表空间加数据文件,但这只是治标。根本出路还是把事务拆小、避免长事务,或者调整UNDO_RETENTION参数,让旧版本数据更快被覆盖清理。
锁等待则是另一种高频问题,表现往往是应用界面上某个更新操作被卡住,一直转圈。先查v$session找到被阻塞的会话和它的阻塞源,再针对阻塞会话判断是直接杀掉还是等待它完成。杀会话的操作在必要时应该果断而克制,用ALTER SYSTEM KILL SESSION 'sid,serial#' IMMEDIATE。但IMMEDIATE表示立即终止,未提交事务会回滚,有些长事务回滚本身也要时间。这也是为什么我要反复强调事务粒度的重要性,控制好事务大小能成倍缩短这种故障的恢复窗口。
7.2 当事务提交后数据还是旧:会话缓存的那些坑
有些操作看起来没有报错,但其他会话查询出来的数据就是旧的,这种问题往往跟会话缓存或者查询缓存相关。如果某个连接被连接池复用,原来会话的一些上下文状态可能残留。比如会话级设置过NLS参数,或者某些临时变量、包状态还在。如果某个包里面定义了全局变量,每次调用过程后变量的值会保留到会话结束,下次新调用时如果不重新初始化,就会用旧状态,这种情况很容易在PL/SQL开发中出现,因为普通变量每次调用才初始化,但包级状态是会话级别的。
另一个跟“数据还是旧”相关的问题是物化视图或者普通视图的查询结果。物化视图默认不会自动刷新数据,需要手动刷新或者设置自动刷新策略。很多人刚用物化视图时很容易忘,查询出来的永远是旧数据,还以为自己更新没生效。跑一下REFRESH就正常了:
BEGIN DBMS_MVIEW.REFRESH('mv_emp_dept'); END;这个坑在实际项目里出现过不止一次,每次都是大半夜业务人员反馈报表数不对,最后排查半天发现物化视图没刷新。所以凡是涉及物化视图的报表体系,一定要规划好刷新时间点,甚至要做刷新成功与否的监控告警。
7.3 密码过期了怎么办,以及临时表空间的清理逻辑
数据库使用久了,经常会遇到ORA-28001密码已过期的提示。这其实是安全策略生效的结果,Oracle默认配置了密码生命周期,不主动改就会到点过期。处理这个问题的短期手段是修改密码并解锁用户:
ALTER USER scott IDENTIFIED BY new_password ACCOUNT UNLOCK;长期管理策略则是在测试环境或内部系统里修改用户的默认密码配置,比如把密码过期时间设成无限期,但在生产环境不建议这么做,合规要求和管理制度通常都有明确标准,密码新鲜期管理是数据库安全的一部分。
临时表空间的问题多发生在大批量排序或创建索引时。报ORA-01652无法扩展临时表空间时,意味着排序区不够用了。临时表空间的逻辑很简单,一旦数据库实例重启它就清空,运行期间里面的临时段则是不断分配和释放的。日常维护上要留意临时表空间的大小和增长趋势,别等到报错才发现磁盘被临时文件占满了。用下面这条语句可以实时看临时表空间的使用率:
SELECT tablespace_name, current_usage_kb, total_allocated_kb FROM v$temp_space_header;遇到个别会话占用了大量临时段又不释放的情况,可以从v$sort_usage里找出对应会话,确认安全性后再考虑断开。实际运维中这种长排序的会话很影响整体资源,需要建立一套大排序任务的监控习惯,在问题发生前提前干预。
提示:处理日常小问题时,多利用数据字典视图,不要凭感觉。比如查用户的默认表空间、临时表空间、附加权限,都只是几条SQL扫描DBA_USERS、DBA_TABLESPACES、DBA_ROLE_PRIVS就可以搞定的,准确且省事。
8. 写给自己的几点经验总结
这一篇写下来,我自己最深的体会是,Oracle的基础操作远不只是一堆SQL语法,它背后是事务、锁、优化器、权限体系这些机制的协同工作。很多人学了一段时间之后卡在瓶颈期,往往不是因为某个SQL忘了一个关键字,而是没有形成一个整体的操作思维:执行一个修改之前,先想一想事务该怎么控制;发现查询慢,先看一下执行计划再决定怎么改。
如果要从这里继续往前走,下一步比较实用的方向有这么几个:深入了解执行计划的每一步操作到底意味着什么,学会看懂谓词信息的含义;把PL/SQL的集合类型的用法吃透,比如关联数组、嵌套表、VARRAY的使用差异和应用场景;再往后可以学一下RMAN备份恢复的完整方案设计,特别是增量备份策略和恢复演练。这些和本篇的内容是连续的,你基础2的内容如果都消化得差不多,顺着这几个方向往下走会顺得多。
最后再分享一个小习惯,也是我在实际操作中受益最多的一条:每次遇到报错不要只把报错信息复制去搜,而是先稍微分析一下报错的触发条件,再结合数据字典查一下当时的运行状态。只有把报错的“上下文”弄清楚,才能在下一次碰到类似问题时更快反应过来。数据库这种东西,经验积累的本质,其实就是把一个个报错现场变成自己识别的模式,这样后来开工之前,很多坑就已经下意识避开了。