接到过不少临时数据需求,最常见的一种就是:线上订单状态不对,要捞一个时间窗口内的异常数据,核对完再批量订正。这种需求频率不高、逻辑也不算简单,如果为了它建一个存储过程,等订正完还得记得清理,不然数据库里就多一个长期没人维护的对象。后来我都是直接把整段逻辑写成匿名查询代码,跑完即弃,连数据库对象都不留下。这就是这篇文章想聊的东西——匿名查询的过程代码。
这里说的“匿名查询”,不是指网络访问层面的匿名,而是数据库开发里很常见的一类写法:不创建存储过程、不创建函数、不创建视图,直接用一段即席的匿名块或预处理语句,完成一次完整的“查询—处理—回写”流程。它能解决的核心问题是:把临时性、一次性的数据操作,用代码的方式规范地跑完,而不是靠手动执行一堆零散的SQL。适合所有需要处理数据库临时任务的人,不管是开发、运维还是数据分析师。
1. 一次临时需求把我推到“匿名查询”面前:这个代码解决什么问题
1.1 为什么要用匿名查询,而不是直接建存储过程
先说一个我刚工作不久时踩过的场景。当时有个运营活动结束后,需要把一批参与用户的状态从“活动中”批量改成“已完成”,同时还要求把不符合参与条件的用户捞出来单独核对。需求本身不难,但整个逻辑要查三张表、做一次聚合判断、再回写两张表。当时的我第一反应是写一个存储过程,因为“逻辑复杂嘛,存下来以后还能复用”。
结果这个“以后还能复用”变成了一个包袱。活动结束后三个月,这个存储过程没人再调用,但一直挂在库里,占着一个名字。后来做数据库对象梳理,DBA问起来,没人能说清楚它是干嘛的,最后还是我翻代码才认领出来删掉的。从那以后我给自己定了一条规矩:凡是明确属于一次性任务的逻辑,一律写匿名查询代码,不建任何持久化对象。
匿名块相比存储过程有几个很实际的好处:
- 不留对象:跑完即焚,数据库里不会堆一堆没人维护的存储过程、函数。
- 迭代快:改一行代码就可以重新执行,不需要DROP再CREATE,也不存在版本覆盖问题。
- 权限干净:不需要额外授予CREATE PROCEDURE权限,只要具备表级查询和DML权限就能跑。
- 跨环境迁移容易:一段匿名块从测试库复制到生产库,只要表结构一致就能直接执行,不需要在目标库先建对象。
当然,匿名查询也有不适合的场景——如果同一段逻辑会被多个业务接口频繁调用,那就应该用正式的存储过程或服务端代码,因为有预编译缓存、执行计划复用和权限管控的需求。匿名查询只属于“临时任务”这个场景。
1.2 匿名查询过程代码的三种常见形态
很多人以为匿名查询就是“不命名”的SELECT语句,其实不止。完整的匿名查询过程代码通常分为三种形态:
第一种是匿名块(Anonymous Block),典型代表是Oracle的PL/SQL匿名块和PostgreSQL的DO块。这种形态可以声明变量、写循环、做条件判断、捕获异常,本质上是一个完整的过程化代码外壳,只是不持久化到数据库里。
第二种是预处理语句(Prepared Statement),典型代表是MySQL的PREPARE/EXECUTE语法。它可以先把一条SQL语句定义成字符串,动态拼接表名或条件后再执行,适合需要动态生成SQL的临时查询。
第三种是脚本化的即席SQL批处理,比如把一组INSERT/UPDATE/SELECT串在一个数据库会话里顺序执行。这种形态最简单,但功能也最弱,没有变量和流程控制,出现中途报错时通常只能手动处理。
我平时用得最多的是匿名块和预处理语句的组合。遇到需要判断、循环、异常处理的场景,用匿名块;遇到需要动态切换表名、动态拼接过滤条件的场景,用预处理语句。
1.3 什么场景最适合用匿名查询代码
我整理过一份自己判断“该不该用匿名查询”的清单,基本上命中两条以上就可以考虑:
- 数据订正类:批量修改线上数据,但不需要长期保留这段修改逻辑。
- 数据对账类:对比两个表或两套统计口径之间的差异,需要多步骤计算。
- 一次性调度:只跑一次的数据抽取、清洗、归档任务。
- 排障验证:怀疑数据有问题,需要一边查一边调整逻辑,快速反复执行。
不适合的场景也列一下:频繁调用的业务逻辑、需要细粒度权限控制的公共数据操作、需要事务嵌套和跨库一致性的复杂流程。这些还是应该交给正式的应用代码或存储过程来管。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 三大主流数据库匿名块写法拆解:各显其能
市面上的数据库很多,但日常开发碰到最多的还是Oracle、PostgreSQL、MySQL三种。它们的匿名查询写法各有差异,放在一起对比着看,会更容易理解每一个关键字背后为什么这么设计。
2.1 Oracle的PL/SQL匿名块:DECLARE-BEGIN-END的经典三段式
Oracle的PL/SQL匿名块结构非常固定,就是一个DECLARE、BEGIN、EXCEPTION、END的框架。下面是一个非常经典的例子,查一个订单表的异常状态数量:
sql复制DECLARE
v_count NUMBER;
v_total_amt NUMBER := 0;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM orders
WHERE status = 'PAY_CALLBACK_EXCEPTION'
AND create_time >= SYSDATE - 90;
DBMS_OUTPUT.put_line('异常订单数: ' || v_count);
IF v_count > 0 THEN
SELECT SUM(amount)
INTO v_total_amt
FROM orders
WHERE status = 'PAY_CALLBACK_EXCEPTION'
AND create_time >= SYSDATE - 90;
DBMS_OUTPUT.put_line('涉及金额: ' || v_total_amt);
END IF;
v_total_amt := v_total_amt / NULLIF(v_count, 0);
DBMS_OUTPUT.put_line('单均金额: ' || v_total_amt);
END;
/
这里要注意的是SELECT INTO必须确保只能返回一行,如果返回多行会直接抛TOO_MANY_ROWS异常,返回零行会抛NO_DATA_FOUND。所以匿名块里用SELECT INTO之前,一定要对查询结果的唯一性有把握。如果没有把握,就要先COUNT一下,或者在查询条件里用聚合函数保证返回一行。
Oracle匿名块里的异常处理是个加分项。你可以在EXCEPTION段里捕获WHEN NO_DATA_FOUND、WHEN OTHERS等,然后把错误信息打出来,而不是让整个脚本直接中断:
sql复制DECLARE
v_order_id orders.id%TYPE;
v_status orders.status%TYPE;
BEGIN
SELECT id, status
INTO v_order_id, v_status
FROM orders
WHERE id = 123456;
DBMS_OUTPUT.put_line('订单状态: ' || v_status);
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.put_line('订单 123456 不存在');
WHEN OTHERS THEN
DBMS_OUTPUT.put_line('发生错误: ' || SQLERRM);
END;
/
这个写法在排查数据问题时特别有用。跑完脚本,你会知道是哪一笔数据引发的异常,而不是面对一大片报错日志发呆。
2.2 PostgreSQL的DO块:带事务包裹的即席过程
PostgreSQL的匿名块用DO语法,里面写的是PL/pgSQL代码。和Oracle最大的区别是:PostgreSQL的DO块整体是被一个事务包裹的,如果块内任何一处抛出未捕获的异常,整个块所做的所有修改都会回滚。
sql复制DO $$
DECLARE
v_count integer;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM orders
WHERE status = 'PAY_CALLBACK_EXCEPTION';
RAISE NOTICE '异常订单数: %', v_count;
IF v_count > 0 THEN
UPDATE orders
SET status = 'SETTLED'
WHERE status = 'PAY_CALLBACK_EXCEPTION'
AND create_time >= NOW() - INTERVAL '90 days';
RAISE NOTICE '已订正订单数: %', v_count;
END IF;
END
$$;
这个“事务包裹”特性是把双刃剑。好处是安全,跑挂了自动回滚,不用手动ROLLBACK;坏处是你没法在块内部做分批提交,如果UPDATE的数据量很大,整个事务会持有大量锁和回滚段。我在做大数据量订正时,通常会选择把一批数据的主键写入临时表,然后在外部循环分批调用DO块,或者直接改用别的方式控制提交节奏。
2.3 MySQL的预处理语句:动态SQL的临时解法
MySQL本身不支持像Oracle那样的完整匿名块,PREPARE/EXECUTE就承担起了动态匿名查询的职责。比如要按不同的日期分区统计订单数据,表名是orders_202401、orders_202402这种按月拆分的物理表,就可以这么写:
sql复制SET @table_name = 'orders_202401';
SET @sql_text = CONCAT(
'SELECT COUNT(*), SUM(amount) FROM ', @table_name,
' WHERE status = ''PAY_CALLBACK_EXCEPTION'''
);
PREPARE stmt FROM @sql_text;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
MySQL预处理语句里,表名和列名不能用占位符?绑定,只能通过字符串拼接进SQL文本。这意味着动态表名场景必须非常小心,不能直接把外部输入拼进去,否则就是妥妥的SQL注入入口。后面我会专门讲安全的动态表名做法。
如果MySQL里需要做更复杂的流程控制,比如循环、判断、异常处理,通常的解法是写存储过程。但如果不想留持久化对象,也可以借用会话级别的临时表和PREPARE组合出“伪匿名块”的效果。这种方式虽然不及Oracle的匿名块优雅,但在MySQL日常运维里已经够用了。
2.4 三种写法的核心差异对照
| 对比维度 | Oracle PL/SQL匿名块 | PostgreSQL DO块 | MySQL PREPARE/EXECUTE |
|---|---|---|---|
| 有无声明区 | 有,DECLARE段 | 有,DECLARE段 | 无,用SET @var |
| 异常处理 | 支持WHEN EXCEPTION | 支持EXCEPTION | 不支持,报错即中断 |
| 事务控制 | 手动COMMIT/ROLLBACK | 自动包裹事务,异常整体回滚 | 自动提交或会话控制 |
| 动态SQL | EXECUTE IMMEDIATE | EXECUTE/USING | 拼接后PREPARE执行 |
| 适用场景 | 复杂逻辑一次跑完 | 中量数据、安全性要求高 | 动态SQL、轻量操作 |
如果你不确定选哪种,我的建议是:逻辑复杂且需要异常处理的,优先Oracle匿名块或PostgreSQL DO块;只需要动态表名和简单执行的,用MySQL预处理语句。
3. 参数绑定与动态SQL:匿名查询的安全边界到底在哪
匿名查询代码之所以容易出问题,多半不是出在流程控制上,而是出在SQL拼接上。很多人写临时脚本时图省事,直接把变量值拼进SQL字符串,这在数据量小、自己手动跑的时候看起来没事,一旦条件变成动态的,或者脚本被共享给别人用,问题就来了。
3.1 拼接SQL的典型风险
先看一个反面教材。假设要根据用户输入的订单号查订单状态,有人会写成这样:
sql复制-- 危险写法,不要模仿
SET @order_id = '10001';
SET @sql = 'SELECT * FROM orders WHERE id = ' || @order_id;
EXECUTE(@sql);
如果@order_id的值是10001,这段代码没毛病。但如果@order_id变成了10001 OR 1=1,那查询就变成了全表扫描;如果变成10001; DROP TABLE orders; --,后果就更严重了。更隐蔽的问题在后面:拼接SQL会把原本简单的查询硬生生变成动态SQL,数据库无法复用执行计划,频繁执行时解析开销非常高。
只要是匿名查询过程代码,我坚持一个原则:静态的部分写死在SQL里,动态的部分能绑定的就绑定,不能绑定的也要做白名单校验。
3.2 参数绑定的正确姿势
Oracle的PL/SQL匿名块里,动态SQL用EXECUTE IMMEDIATE加USING绑定变量,这是最安全的写法:
sql复制DECLARE
v_order_id NUMBER := 10001;
v_status VARCHAR2(20);
BEGIN
EXECUTE IMMEDIATE
'SELECT status FROM orders WHERE id = :1'
INTO v_status
USING v_order_id;
DBMS_OUTPUT.put_line(v_status);
END;
/
PostgreSQL的DO块里,EXECUTE同样支持USING绑定:
sql复制DO $$
DECLARE
v_order_id integer := 10001;
v_status text;
BEGIN
EXECUTE 'SELECT status FROM orders WHERE id = $1'
INTO v_status
USING v_order_id;
RAISE NOTICE '%', v_status;
END
$$;
MySQL预处理语句里,?占位符就是为绑定变量设计的:
sql复制SET @order_id = 10001;
PREPARE stmt FROM 'SELECT status FROM orders WHERE id = ?';
EXECUTE stmt USING @order_id;
DEALLOCATE PREPARE stmt;
绑定变量不只是安全,还能减少SQL文本的重复解析。同一个匿名块循环执行一千次时,绑定变量的写法只需要解析一次SQL文本,执行计划可以复用,性能差异非常明显。
3.3 动态表名的白名单校验
刚才说过,表名和列名没法用绑定变量处理,只能拼接。为了不把数据库暴露在注入风险下,我每次动态拼接表名之前都会加一道白名单校验。最简单的做法是用正则判断表名是否只包含字母、数字、下划线,并且以预期的前缀开头:
sql复制SET @table_name = 'orders_202401';
-- 校验表名格式:仅包含字母、数字、下划线,且以orders_开头
SELECT CASE
WHEN @table_name REGEXP '^orders_[0-9]{6}$' THEN 1
ELSE 0
END INTO @is_valid;
IF @is_valid = 0 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '非法表名';
END IF;
如果连“以orders_开头”这种前缀约束都不放心,更保险的做法是直接查information_schema.tables,确认表名真实存在再执行:
sql复制SELECT COUNT(*)
INTO @table_exists
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name = @table_name;
IF @table_exists = 0 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '表不存在';
END IF;
这套思路对Oracle和PostgreSQL同样适用,只是系统视图名不一样。动态SQL本身没有原罪,危险的是无约束的拼接。
4. 权限、事务与执行环境:匿名块运行时的隐性规则
写匿名查询代码时,大家最关注的是逻辑本身,但真正让脚本跑不起来、或者跑出错误结果的,往往是权限和事务这些“看不见的规则”。这里把我在实际执行中遇到的隐性规则集中梳理一遍。
4.1 查询权限不等于执行权限
有些匿名块会包含动态SQL。在Oracle里,普通用户执行动态SQL时,要求其拥有底层对象的直接权限,而不是通过角色间接获得的权限。我遇到过一次很典型的坑:某个用户能直接SELECT订单表,但在匿名块里用EXECUTE IMMEDIATE执行同一句SELECT时,报了ORA-01031权限不足。
原因就是:PL/SQL匿名块执行时使用的是权限调用者权限,动态SQL无法继承通过角色授予的权限,必须显式授予直接对象权限。
遇到这种情况,解决方式有三种:
- 给用户直接授予对象的SELECT、UPDATE、DELETE权限。
- 如果场景允许,将匿名块封装成存储过程,并用AUTHID DEFINER指定定义者权限。
- 使用数据库的权限管理接口统一处理,比如Oracle的DBMS_CREDENTIAL或Vault类方案。
对于临时任务,我一般倾向第一种,临时用一下,用完回收,干净利落。
4.2 事务的隐式提交与回滚边界
这个坑在Oracle里尤其明显。Oracle的PL/SQL匿名块内,如果执行了DDL语句,比如CREATE TABLE、ALTER TABLE、TRUNCATE TABLE,那么会自动提交当前事务。这意味着你在匿名块里先UPDATE了一堆数据,然后在块内执行了TRUNCATE一个临时表,前面的UPDATE就会被隐式提交,之后想ROLLBACK也回不去了。
sql复制BEGIN
UPDATE orders SET status = 'PROCESSING' WHERE create_time >= SYSDATE - 1;
-- 这一句TRUNCATE会隐式提交上面的UPDATE
EXECUTE IMMEDIATE 'TRUNCATE TABLE temp_order_snapshot';
-- 此时想ROLLBACK,上面的UPDATE已经提交了
ROLLBACK;
END;
/
这是个极其隐蔽的坑。我处理这种场景的规则很简单:在匿名块里尽量避免DDL,如果必须用,就把DDL放到所有DML完成之后,或者先用显式COMMIT把当前事务落定,再执行DDL。
PostgreSQL的DO块则完全不同。整个块自带一个事务,块内如果有DDL,也是在内部事务里执行,异常时会随整个块一起回滚。这块的体验比Oracle好很多。
4.3 锁等待与资源消耗:一次大规模UPDATE的教训
匿名块最常见的大规模操作就是UPDATE。如果一张千万级订单表里一次性UPDATE几十万行,且没有合适索引,会产生两方面的压力:
- 锁等待:UPDATE操作会锁住涉及的行,其他会话读同一行时会被阻塞。生产环境的高频访问表,一个大事务UPDATE可能直接拖垮业务。
- UNDO/回滚段膨胀:一个大事务产生的UNDO量非常大,尤其在Oracle里,如果UNDO表空间配置偏小,可能触发快照过旧错误。
我的做法是:大UPDATE一定分批次提交,每批只处理少量数据,批与批之间停顿一下。在Oracle匿名块里,可以用游标配合循环,每处理1000行提交一次:
sql复制DECLARE
CURSOR cur_orders IS
SELECT id FROM orders
WHERE status = 'PAY_CALLBACK_EXCEPTION'
AND create_time >= SYSDATE - 90
FOR UPDATE SKIP LOCKED;
v_counter NUMBER := 0;
v_batch NUMBER := 0;
BEGIN
FOR rec IN cur_orders LOOP
UPDATE orders
SET status = 'SETTLED'
WHERE id = rec.id;
v_counter := v_counter + 1;
IF MOD(v_counter, 1000) = 0 THEN
COMMIT;
v_batch := v_batch + 1;
DBMS_OUTPUT.put_line('已完成批次 ' || v_batch || ',共 ' || v_counter || ' 行');
END IF;
END LOOP;
COMMIT;
DBMS_OUTPUT.put_line('全部完成,共处理 ' || v_counter || ' 行');
END;
/
这里用FOR UPDATE SKIP LOCKED是另一个细节,它会跳过当前被其他会话锁住的行,避免自己阻塞在个别行上。分批提交虽然会损失一定的一致性,但对于一次性订正任务来说,稳定性和可用性优先级高于事务一致性。
5. 实战复盘:一次完整的匿名查询对账与订正脚本
前面讲了不少原理和坑,最后用一个完整场景把这些串起来。这是一个我在项目里实际做过的需求,属于典型的匿名查询过程代码的应用:对账后批量订正。
5.1 需求背景与表结构
业务场景是这样的:订单支付成功后,支付平台会通过回调通知业务系统。正常情况下,支付回调会更新订单状态为PAY_SUCCESS并写入结算记录。但因为网络抖动、回调接口超时等原因,有一部分订单支付实际成功,但订单状态还停留在PENDING,结算记录也没生成。运营方要求:把近90天内这类可疑订单找出来核对,确认支付平台侧已成功后将订单状态订正为PAY_SUCCESS,并补生成结算记录。
涉及两张核心表:
- orders表:订单主表,字段包括id、order_no、user_id、amount、status、create_time。
- settlement表:结算表,字段包括id、order_no、amount、status、create_time。
订正规则:orders.status = 'PENDING'且create_time在近90天内,同时在支付平台业务流水表中能查到支付成功记录。
5.2 第一步:先把差异范围圈定
最开始没有直接改数据,而是先用一段匿名查询把需要处理的订单范围确认清楚。这一步有两个目的:一是验证业务判断是否准确,二是估算这次订正会影响多少订单,避免后面脚本跑完才发现范围不对。
sql复制-- 以Oracle为例,先看条件下订单量
SELECT COUNT(*) AS pending_cnt
FROM orders
WHERE status = 'PENDING'
AND create_time >= SYSDATE - 90;
-- 再看其中在支付平台侧已成功的订单量
SELECT COUNT(*) AS should_settle_cnt
FROM orders o
WHERE o.status = 'PENDING'
AND o.create_time >= SYSDATE - 90
AND EXISTS (
SELECT 1
FROM payment_platform_log p
WHERE p.order_no = o.order_no
AND p.trade_status = 'SUCCESS'
);
这里的数据量查询是低风险的,但你能通过它提前知道后续UPDATE会处理多少行。如果这个数量跟业务方预期的数量差很多,就要停下来重新核对条件,而不是闷头处理。
5.3 第二步:写订正匿名块,带上事务控制和异常处理
范围确认没问题后,写订正脚本。我把订单查询和结算记录插入放到同一个事务里,保证订正动作的一致性,同时采取分批提交策略,避免一个大事务锁住整张表。
sql复制DECLARE
CURSOR cur_orders IS
SELECT o.id, o.order_no, o.amount
FROM orders o
WHERE o.status = 'PENDING'
AND o.create_time >= SYSDATE - 90
AND EXISTS (
SELECT 1
FROM payment_platform_log p
WHERE p.order_no = o.order_no
AND p.trade_status = 'SUCCESS'
)
AND NOT EXISTS (
SELECT 1
FROM settlement s
WHERE s.order_no = o.order_no
)
ORDER BY o.id
FOR UPDATE SKIP LOCKED;
v_counter NUMBER := 0;
v_batch_count NUMBER := 0;
BEGIN
FOR rec IN cur_orders LOOP
-- 1. 更新订单状态
UPDATE orders
SET status = 'PAY_SUCCESS',
update_time = SYSDATE
WHERE id = rec.id;
-- 2. 补生成结算记录
INSERT INTO settlement (id, order_no, amount, status, create_time)
VALUES (seq_settlement.NEXTVAL, rec.order_no, rec.amount, 'UNSETTLED', SYSDATE);
v_counter := v_counter + 1;
-- 3. 每处理500行提交一次,规避大事务
IF MOD(v_counter, 500) = 0 THEN
COMMIT;
v_batch_count := v_batch_count + 1;
DBMS_OUTPUT.put_line('已订正批次 ' || v_batch_count || ',累计 ' || v_counter || ' 行');
END IF;
END LOOP;
COMMIT;
DBMS_OUTPUT.put_line('订正完成,共处理 ' || v_counter || ' 行');
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
DBMS_OUTPUT.put_line('执行异常,已回滚当前批次。错误: ' || SQLERRM);
END;
/
注意这里我加了两个防御点:
NOT EXISTS settlement,保证同订单不会重复插入结算记录,即使脚本被重复执行也不产生重复数据。这是幂等性的基本保障。FOR UPDATE SKIP LOCKED,防止订正过程中有别的会话正在处理同一批订单时互相阻塞。
5.4 第三步:结果验证与订正前快照
订正脚本跑完后,我没有马上离开,而是立刻重新执行了第一步的查询,确认pending_cnt归零、should_settle_cnt归零,并且settlement表新增记录数与之前统计的should_settle_cnt完全一致。
更严谨的做法是:在执行订正前,先把目标订单快照导出到一个临时表。万一订正后发现数据异常,可以基于快照还原。尤其是涉及生产数据修改的场景,这一步不能省:
sql复制CREATE TABLE temp_orders_before_fix AS
SELECT o.id, o.order_no, o.amount, o.status, o.create_time
FROM orders o
WHERE o.status = 'PENDING'
AND o.create_time >= SYSDATE - 90
AND EXISTS (
SELECT 1
FROM payment_platform_log p
WHERE p.order_no = o.order_no
AND p.trade_status = 'SUCCESS'
);
有了这张快照表,就算订正逻辑有偏差,也能从容写一个反向脚本来恢复。
5.5 复盘:这段代码还能怎么改进
这次实战跑完,有三点我认为值得沉淀:
第一,脚本要留痕。 虽然匿名块不创建数据库对象,但代码本身值得保存。我会把每次临时订正的SQL按日期命名放进项目目录下的scripts文件夹,比如fix_orders_20250412.sql。这个动作的成本极低,收益却很高——半年后如果有人问“当时为什么这些订单状态不一样”,翻一下脚本就全明白了。
第二,启动前确认预估行数。 批量订正前先跑一遍SELECT COUNT,与业务方确认数量级,再执行UPDATE。这个动作能拦截掉大部分条件写错导致的误操作。
第三,异常捕获后的回滚逻辑要干净。 Oracle匿名块里的EXCEPTION WHEN OTHERS捕获后,当前未提交的记录需要手动ROLLBACK。但已经分批COMMIT的批次是回不去的。所以如果脚本中途异常,你要立即知道已经提交了多少批次,用前面建好的快照表来评估影响。
这个对账订正场景完整展现了匿名查询过程代码的价值:它不需要你创建一个存储过程然后天天惦记着什么时候删掉,也不需要你手动执行几十条SQL然后自己记着每一条跑到哪一步了。它把你所有需要的过程控制、异常处理、提交策略,压缩在了一段可以反复修改、反复执行的代码里。跑完,它就不存在了,但你的数据库状态、你的执行记录、你的复盘依据,都在。
