拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Oracle 11g UPDATE和DELETE操作详解:从UNDO机制到生产环境避坑指南

Oracle 11g UPDATE和DELETE操作详解:从UNDO机制到生产环境避坑指南

1. 先搞清楚UPDATE和DELETE的底层逻辑:数据到底是怎么被改掉的

1.1 数据块里的行版本切换:UPDATE不是"原地改写"

很多Oracle开发者在刚开始接触UPDATE和DELETE时,都会有一种直觉:UPDATE就是把某个格子里的旧值擦掉,写上新的值;DELETE就是把这一整行从表里移除。这个直觉在逻辑层面是成立的,但在物理存储层面却完全是另一回事。

Oracle 11g的表数据存在数据块(Data Block)里,每个数据块由块头、表目录、行目录和空闲空间组成。当你执行一条UPDATE语句时,Oracle并不会真的去"擦掉"旧数据,而是在原数据块内生成这行的一个新版本,同时把旧版本的行镜像放进UNDO表空间。这期间,数据库要往REDO日志里写重做记录,要分配UNDO空间存放修改前的影像,还要维护行链、ITL(Interested Transaction List,事务槽)等内部结构。换句话说,修改一条记录,牵扯到的不只是那一行,而是一整套事务基础设施。

理解了这一点,你就能解释很多新手看不懂的现象:为什么一条简单的UPDATE在数据量很大的时候会慢得离谱?因为Oracle需要为每一行修改生成UNDO和REDO;为什么UPDATE之后立即回滚,数据好像"变回来了"?因为所谓的变回来,本质上是借助UNDO里的镜像把旧版本重新恢复成当前版本。这套设计是Oracle数据安全性的根基,但也正因为有了这套机制,DML操作才会比很多人想象中更"重"。

1.2 DELETE号称"标记删除":空间不释放的真正原因

DELETE的底层机制更有意思。你以为DELETE是把数据行物理移除,实际上Oracle只是把这个行的头部标志位改成了"已删除",真正的数据仍然躺在那个数据块里。块里的这些空间被标记为可重用,但在后续INSERT语句把这块空间重新填满之前,表占用的物理空间不会自动缩小。

这就解释了数据库维护里一个常见的怪现象:一张500GB的大表,DELETE掉90%的数据之后,查询SELECT COUNT(*)依然慢得像在扫描一张完整的表,表空间也一点没变小。原因有两个:第一,高水位线(HWM)不会因为DELETE而回落,全表扫描的终点仍然是那个历史最高水位;第二,块内的空间碎片需要重新整理才能被有效利用。如果你真的想快速释放空间,通常得用TRUNCATE或者是MOVE操作,但那是另一个话题了。

DELETE的另一个隐藏代价是UNDO。因为DELETE同样需要把被删除行的完整镜像写进UNDO表空间,所以删除一万行和删除一千万行,对UNDO的压力完全不是一个量级。很多生产事故就是这么来的:有人在一个大事务里跑了条DELETE,删到一半发现UNDO表空间快满了,又不敢直接取消事务,因为回滚的时间可能比删除的时间还长,最后只能硬着头皮等它跑完。所以,我一向建议:凡是生产环境的大表删除,一定要先评估数据量和UNDO容量,否则你删的是数据,烧掉的是整个数据库的健康状态。

2. UPDATE语句的正确姿势:从语法到条件设计

2.1 基础语法与WHERE子句的生死线

先给新读者把最基本的语法铺开。Oracle 11g里UPDATE的标准写法是:

UPDATE table_name SET column1 = value1, column2 = value2, ... WHERE condition;

SET子句负责指定要修改的列和新值,WHERE子句负责圈定受影响的行范围。这当中,WHERE是整个语句的灵魂,也是生产事故的第一来源。

为什么这么说?因为WHERE是可选的。一旦漏掉,Oracle就会把整张表的每一行都更新一遍。我在前公司就处理过一次真实事故:某同事在测试环境执行一条UPDATE,忘记加WHERE条件,直接把线上用户表的手机号全部刷成了同一个值。当时所有用户的登录验证都失效了,客服电话瞬间被打爆。虽然后来通过备份恢复了数据,但那次事故让大家达成了一个共识——生产环境执行UPDATE和DELETE,必须有平台准入限制,强制要求要么带主键条件,要么限制更新行数,要么在事务里先SELECT确认行数再执行UPDATE。

如果你没有这样的平台约束,也可以自己养成习惯:写UPDATE之前,先用相同的WHERE条件跑一条SELECT,确认命中行数和目标数据的样子。比如:

SELECT empno, ename, salary FROM emp WHERE deptno = 30;

确认无误后,再把SELECT换成UPDATE,这样至少能拦住大部分低级失误。

WHERE条件的写法也直接影响性能。尽量避免在条件列上套函数,像WHERE UPPER(ename) = 'SMITH'这种写法,会让ename列上的索引失去作用,Oracle只能老老实实做全表扫描。如果你的业务确实有大小写不敏感的需求,那就老老实实建函数索引,或者把数据存成统一格式再查询。

另外,UPDATE语句里有个容易被忽略的细节:多个列的赋值顺序没有影响,但列的数据类型必须匹配。比如数值列传入字符串'100',Oracle会自动做隐式转换,在大多数情况下不会报错,但在某些字符集或特殊格式下会导致性能问题,甚至ORA-01722这类转换错误。宁可多写一遍TO_NUMBER之类的显式转换,也不要把命运交给隐式转换。

2.2 用子查询做关联更新时,必须防住NULL污染

实际业务里,最常用的UPDATE场景是根据另一张表的数据,来更新当前表的字段。这种关联更新在Oracle 11g里的常见写法是:

UPDATE emp e SET e.salary = ( SELECT s.base_salary + s.bonus FROM sal_std s WHERE s.empno = e.empno ) WHERE EXISTS ( SELECT 1 FROM sal_std s WHERE s.empno = e.empno );

注意,这里我特意写了WHERE EXISTS。很多新手的写法是只写SET里的子查询,不写WHERE EXISTS,结果就是:emp表里那些在sal_std中找不到匹配项的员工的salary,全被更新成了NULL。这个错误在公司历史上至少出现过三次,因为它表面上很合理——"按关联关系更新",但Oracle对子查询的语义是:如果子查询返回NULL,就把NULL赋给目标列,而不是"什么都不做"。

我后来养成了一个固定习惯:所有关联UPDATE都必须带EXISTS或IN条件,确保子查询有匹配时才更新。如果在业务逻辑里,目标表中确实允许一部分行不参与更新,那这条铁律更要严格执行。

还有一种常见需求是更新来源和目标其实是同一张表,比如"把部门编号为30的员工工资提高10%"。这种需求用UPDATE直接写很简单:

UPDATE emp SET salary = salary * 1.1 WHERE deptno = 30;

但如果是"把销售部门下每个员工的工资,调整到该部门平均工资的1.1倍"这种,就得小心了。因为SET里引用了聚合结果,而聚合结果本身又来自目标表,Oracle不允许在同一条UPDATE里既读又写的聚合嵌套。这种场景我会推荐用MERGE,下面细说。

2.3 复杂场景优先选MERGE而不是层层嵌套

MERGE在Oracle里常被叫做"万能DML",因为它能在一个语句里同时实现UPDATE、INSERT甚至DELETE。11g版本对MERGE的支持已经非常成熟,我处理复杂数据同步任务时几乎只写MERGE。

举个实际例子:每天凌晨要把外部系统导入的临时表temp_salary,同步到正式的员工表emp里。匹配到的更新工资,匹配不到的插入新员工记录。用纯UPDATE加INSERT要写两条语句,还得处理中间可能出现的数据变化,而MERGE一条搞定:

MERGE INTO emp e USING temp_salary t ON (e.empno = t.empno) WHEN MATCHED THEN UPDATE SET e.salary = t.salary, e.update_time = SYSDATE WHEN NOT MATCHED THEN INSERT (empno, ename, salary, update_time) VALUES (t.empno, t.ename, t.salary, SYSDATE);

MERGE的优势不仅是写法简洁,更重要的是它对源数据只扫描一遍,减少了IO次数。在大数据量同步场景里,这个性能差距会被放大得非常明显。

不过MERGE也有个经典坑:ON子句里的关联字段必须唯一。如果temp_salary表里有重复的empno,MERGE会直接抛出ORA-30926错误,哪怕你只在WHEN NOT MATCHED分支里用到这些重复值也不行。所以写MERGE之前,我通常会对源表做一次去重检查:

SELECT empno, COUNT(*) FROM temp_salary GROUP BY empno HAVING COUNT(*) > 1;

有重复就先清洗,再跑MERGE。这个习惯帮我避免过很多次半夜被电话叫醒的尴尬。

3. DELETE操作:删除动作背后的UNDO与锁代价

3.1 DELETE的执行链路:从行锁到UNDO镜像

DELETE的语法比UPDATE更简单:

DELETE FROM table_name WHERE condition;

但简洁语法背后藏着的是非常"昂贵"的执行链路。一条DELETE命中一行数据,Oracle需要做几件事:在数据块中找到该行,对该行加排他行锁,把整行的镜像写入UNDO表空间,在REDO日志中记录删除操作,然后标记行头为已删除。如果删除操作影响上万行,这些步骤全都要重复上万次。

这里的行锁是个关键。Oracle的行锁机制允许并发读到被锁定的行(通过UNDO多版本一致性读),但不允许其他事务对同一行再执行UPDATE或DELETE。也就是说,如果你在一个事务里DELETE了某一行但没有提交,其他会话的UPDATE如果也想操作这一行,就会一直阻塞,直到你COMMIT或ROLLBACK。这本来是防止并发冲突的正常机制,但在实际运维中,我见过不少事故都是因为某个开发人员跑了一条DELETE后直接下班走人,既不提交也不回滚,导致业务表上相关行的更新操作全部卡死。

所以,执行DELETE前先想清楚一个原则:删除操作必须在事务边界内尽快完成,该COMMIT就COMMIT,该ROLLBACK就ROLLBACK,绝不允许把事务挂在那边过夜。

3.2 DELETE和TRUNCATE怎么选:一张表说清楚

很多数据库新手搞不清DELETE和TRUNCATE的区别,这里我给一张对比表,非常直观:

对比项DELETETRUNCATE
本质DML,逐行标记删除DDL,直接重置表段
WHERE条件支持,可删部分行不支持,只能清空全表
UNDO生成每条被删行都生成UNDO极小,基本不消耗UNDO
触发器会触发DELETE触发器不触发触发器
空间释放不释放,高水位线不变释放全部数据块,高水位线重置
回滚能力可以ROLLBACK无法回滚(除非使用Flashback Table)
执行速度慢,受索引和行数影响极快,几乎瞬间完成

如果你要清空一张临时表,或者确定要删除全表数据且不需要回滚,没有任何理由用DELETE,TRUNCATE是更合适的选择。反过来,如果业务上只删部分数据,或者有闪回需求,那就老老实实DELETE。

这里要多说一句:TRUNCATE虽然是DDL,但在Oracle里它同样会隐式提交当前事务,而且不可回滚。所以执行TRUNCATE之前,最好先确认你的备份或闪回机制是否到位。我之前看过一个事故复盘,某同事本来只想清理分区表的一个分区,结果执行了TRUNCATE TABLE,整张表的数据全部被清空,最后只能靠RMAN恢复,折腾了整整一个通宵。

3.3 千万级大表删除的实操节奏

删除大量数据时,最忌讳的就是一条DELETE删除几千万行。这不仅会生成巨量UNDO,占用大量REDO空间,还会长时间持有行锁,导致业务表上的其他DML全部等待。真实生产环境里,我推荐把大删除拆成小批次循环执行。下面是我在Oracle 11g里常写的一个PL/SQL脚本结构:

DECLARE v_deleted NUMBER := 0; BEGIN LOOP DELETE FROM big_table WHERE id IN ( SELECT id FROM big_table WHERE create_time < SYSDATE - 90 AND ROWNUM <= 50000 ); IF SQL%ROWCOUNT = 0 THEN EXIT; END IF; v_deleted := v_deleted + SQL%ROWCOUNT; COMMIT; END LOOP; DBMS_OUTPUT.PUT_LINE('Total deleted: ' || v_deleted); END; /

这个脚本的核心思路是:每轮只删除5万行,删除完立刻COMMIT,然后重复执行,直到没有满足条件的行。这样做的好处是显而易见的:每轮的UNDO和REDO消耗都被控制在很小的范围内,单次事务持续时间短,锁持有时间短,不会阻塞其他会话。实测下来,三千万行的流水表,我用这种分批删除的方式,大约十几分钟就删完了,而直接DELETE跑了一个小时还看不到尽头。

分批删除的时候,建议每一批的删除条件都用主键或者索引字段的等值/范围条件,比如上面示例里的id,这样每轮删除都能走索引,效率更高。如果条件列上没有索引,全表扫描加嵌套循环,批量删除效率会大打折扣。

4. 事务与恢复:改错数据之后的补救体系

4.1 COMMIT和ROLLBACK的正确使用节奏

Oracle的事务模型是"隐式开启,显式提交"。什么叫隐式开启?就是你执行第一条UPDATE或DELETE时,事务就自动开始了;之后的每条DML都属于这个事务,直到你执行COMMIT或ROLLBACK才结束。Oracle 11g也支持SET AUTOCOMMIT ON这种会话级设置,但我强烈不建议在生产环境开启,因为自动提交会抹掉你最后一道"后悔药"。

正确使用节奏是这样的:一条UPDATE或DELETE执行后,先不要急着提交,立刻用SELECT查一下受影响的数据是否正确。比如你执行了:

UPDATE emp SET salary = salary * 1.1 WHERE deptno = 30;

接着应该先验证:

SELECT deptno, salary FROM emp WHERE deptno = 30;

确认数据显示正确,再执行COMMIT。如果发现数据不对,直接ROLLBACK就能回到更新前的状态。这个"先验证再提交"的习惯,在开发环境也许还能随意一点,在生产环境则是一条生命线。

还要提醒一点:COMMIT之后,当前事务就结束了,再想回滚就得靠其他手段,比如Flashback;而ROLLBACK之后,本次事务里的所有DML都会被撤销,但这条ROLLBACK本身会保留在重做日志里,所以如果你在走归档模式的库里,依然可以根据归档日志做更细粒度的恢复。

4.2 用Flashback Query找回被改坏的数据

哪怕你没有备份,Oracle 11g还提供了一条非常实用的退路——Flashback Query。它利用UNDO表空间里保存的数据镜像,让你能查询到过去某个时间点之前的数据状态。这个功能在恢复误操作场景里简直是救命稻草。

我讲个真实经历。有次运营同事跑了一个定时脚本,原本应该只更新部分数据,结果因为脚本逻辑写错了,把一个业务表里近一周的所有记录都改了。等发现的时候已经是三个小时之后,正常渠道的备份恢复要停机,影响面太大。当时我做的第一件事就是用Flashback Query查出改之前的数据:

SELECT * FROM business_table AS OF TIMESTAMP (SYSTIMESTAMP - INTERVAL '3' HOUR) WHERE business_id = 'A10001';

查询能正常返回,说明UNDO里还有三个小时前的版本。然后我用这个时间点的数据拼了一条反向UPDATE,把被改坏的行一条条修正回来。整个过程没有停机,业务影响被控制在了最小的范围内。

Flashback Query的前提条件是UNDO表空间保留的时间足够长。Oracle 11g里默认的UNDO_RETENTION参数是900秒,也就是15分钟。如果你的数据库UNDO表空间足够大,这个参数可以调大一些,比如3600或7200,这样你能闪回的时间窗口就更充裕。当然,UNDO_RETENTION只是个目标值,如果UNDO表空间满了,Oracle依然可能提前覆盖更早的版本。

4.3 从UNDO里面翻旧账:Flashback Table的边界

如果被误操作的表在误操作期间没有被大量其他DML干扰,还有个更直接的办法:整表闪回。Oracle 11g里执行Table Flashback需要两步。第一步,开启行移动:

ALTER TABLE business_table ENABLE ROW MOVEMENT;

第二步,把整张表恢复到某个时间点:

FLASHBACK TABLE business_table TO TIMESTAMP (SYSTIMESTAMP - INTERVAL '3' HOUR);

这个操作的代价是:它会修改表数据的物理存储位置,所以必须先开ROW MOVEMENT;它回收的是整张表在指定时间点之后做的所有DML,包括合法的数据变更,所以使用前必须和业务方确认;而且如果指定时间点之后表结构发生过DDL变更,比如加列、改列类型,闪回会失效,报ORA-01466之类错误。

Flashback Table适合的场景是:一张表被一批错误DELETE或UPDATE波及,而且波及的范围基本上是全表或大范围数据,业务方也确认这段时间内没有其他合法写入。如果只是零星几行数据错了,建议优先用Flashback Query精确恢复,而不是整表闪回,因为整表闪回的影响面大得多。个人经验是,遇到误操作先别慌,按优先级来:能ROLLBACK就ROLLBACK,能Flashback Query就Flashback Query,最后才考虑Flashback Table和RMAN恢复。

5. 生产环境最常踩的坑与对应解法

5.1 ORA-01555快照过旧:UNDO配置的世纪难题

ORA-01555可以说是Oracle数据库里最著名的错误之一,也是很多DBA的噩梦。你可能会在一条很长的UPDATE或DELETE执行过程中遇到它,然后整条SQL直接失败。这个错误的本质是:Oracle在执行查询(或者DML里的读操作)时,需要根据UNDO里的镜像去构造某个时间点的一致性读版本,但那个时间点的UNDO已经被覆盖了,于是数据库只能报错。

最容易触发ORA-01555的场景就是大批量UPDATE/DELETE。比如你开启了一个大事务,先删除了一部分数据,然后又去查询之前某个时间点的数据,这时就需要非常早的UNDO版本,但那些UNDO可能已经被这个大事务自身的新UNDO挤掉了。还有一种场景是表上有长时间运行的查询,同时另一个事务在大量修改这张表,查询和修改的时间差超过了UNDO的保留能力。

解决办法分几个层面:最直接的是调大UNDO表空间,或者提高UNDO_RETENTION;其次是优化你的UPDATE/DELETE,让每个事务的持续时间变短,比如我们前面讲的分批提交;再次是把大查询切成小段执行,缩短一致性读的时间窗口。如果错误是出现在批处理脚本里,考虑把脚本拆小,而不是一把梭。

5.2 一条UPDATE卡住整个业务:锁竞争的排查链路

锁竞争是我在运维里接到的最多的"紧急故障"类型。表象通常是:应用报错,某条UPDATE语句一直执行中,等待事件是enq: TX - row lock contention。背后的原因大概率是另一个会话持有了某几行的行锁,迟迟没有提交。

排查链路其实很清晰。先找到阻塞者和被阻塞者:

SELECT blocking_session, sid, serial#, username, sql_id, wait_class, seconds_in_wait FROM v$session WHERE blocking_session IS NOT NULL;

通过v$session里的blocking_session字段,能一眼看出谁在阻塞谁。然后看被阻塞会话正在执行的SQL:

SELECT sql_text FROM v$sql WHERE sql_id = '被阻塞会话的SQL_ID';

通常到这里就能定位问题:某个开发人员开了个事务,改了数据没提交,导致其他会话全都堵在那几行上。处理方法也很直接:联系相关人,确认事务是否还需要继续,不需要就杀掉会话:

ALTER SYSTEM KILL SESSION 'sid,serial#' IMMEDIATE;

我见过很多次这种故障的根源不是SQL写错了,而是业务流程里缺少"事务必须尽快结束"的规范。所以现在的项目里,我会在数据库层面加监控:事务持续时间超过10分钟的会话自动告警;超过30分钟自动通知DBA介入。这套机制比任何SQL调优都管用。

5.3 索引数量与DML性能的权衡

初学者经常以为索引越多查询越快,但在UPDATE和DELETE面前,索引过多是实打实的负担。为什么?因为UPDATE每一行时,如果修改的列上有索引,Oracle得同步维护这个索引;DELETE每一行时,所有相关索引的条目也得逐一清理。这些索引维护操作,全部要写REDO,全部要持锁,自然比纯粹修改表数据要慢。

我处理过一张订单表,上面挂了12个索引,单条DELETE走了索引,理论上应该很快,实测却需要三秒钟。后来把7个低使用率的索引去掉之后,同样条件下DELETE耗时降到了0.1秒。所以,索引设计的正确思路是:只为高频查询条件建索引,定期通过统计信息观察索引的使用情况,长期没有读命中的索引该删就删。

如果你改的是大批量数据,而且你知道这些数据里的索引字段日后不会马上被查询,还有一个操作可以省下一些索引维护成本:禁用索引再重建。比如删除一个千万级分区的数据前,可以先:

ALTER INDEX idx_name UNUSABLE;

删除完成后再重建:

ALTER INDEX idx_name REBUILD;

这与大批量导入时的常规操作一致,本质上就是避免DML期间逐条维护索引的巨额开销。当然,这里面有个权衡:索引失效期间,针对这张表的所有查询都会走全表扫描,必须在业务低峰期做。

5.4 上线前必须养成的三个习惯

最后的最后,分享三个我在生产环境摸爬滚打总结出来的习惯。第一个习惯:所有UPDATE和DELETE脚本,都必须带事务控制,并且执行前自动生成SELECT预览;如果是手动在PL/SQL Developer或SQL*Plus里执行,先输入一个SELECT语句看清楚即将影响的行,再决定是否执行DML。第二个习惯:改数据前先把对应的备份打出来,哪怕是只需要改几十行的小需求,也要有"出问题能快速还原"的心理准备。第三个习惯:上线脚本必须让第二个人评审,写完UPDATE/DELETE之后,让同事帮你检查一遍WHERE条件、关联子查询和COMMIT位置,多一双眼睛真的能避免很多事故。

Oracle 11g的UPDATE和DELETE本身并不难写,难的是把它放进生产环境的上下文里,充分考虑并发、锁、UNDO、索引这些因素。我个人做了这么多年数据库相关工作,最大的体会是:对SQL语法熟练只是第一层,真正拉开差距的是你对数据库底层机制的理解,以及在故障场景中能快速、冷静地把机制用起来。希望这篇围绕Oracle 11g的UPDATE和DELETE操作详解,能帮你少走一些弯路。

返回列表