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

资讯详情

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

PLSQL 实战题目一:用 dblink + procedure + cursor + forall 写一份可复制的批量同步骨架

PLSQL 实战题目一:用 dblink + procedure + cursor + forall 写一份可复制的批量同步骨架

1. 从一次 900 万行跨库同步说起:dblink + procedure + cursor + forall 到底解决什么问题

如果你手上有一张远程库的user_login表,数据量在千万级,要求把它整表搬到本地库,还要能重复执行、能看日志、出错能回滚——这就是 PLSQL 实战题目一最典型的场景。核心检索词先摆出来:PLSQL 用 dblink 跨库批量同步数据,它指的是在 Oracle 里通过 database link 读取远端表,再用存储过程封装游标遍历和FORALL批量写入,把「取数—转换—落库」整条链路做成一个可重复调用的骨架。

它能做什么?一句话:把原来要写 Java 定时任务、或者手工insert into ... select ...卡死 undo 的活,收敛成一个存储过程,按批提交、按批记录日志。适合谁?适合正在做数据迁移、报表汇总、异构库同步的 DBA 和 PLSQL 开发,尤其是被「一次 insert 900 万行把临时表空间打爆」坑过的人。

我先把这道题拆成两个子任务,后面所有代码都围绕它们展开:

  • 子任务 A:通过dblink把远程user_login迁移到本地,字段是user_id / login / login_time。
  • 子任务 B:把user_login按「用户 + 小时」汇总,写入fact_login_cn(statedate, login_cn, userid)。

两个任务的技术骨架完全一致:cursor定义结果集 →bulk collect ... limit分批取 →forall批量插 → 循环退出条件 → 异常兜底。区别只在字段类型和汇总 SQL。下面从环境准备开始,一步步跑通。

2. TaoToken 前置准备:把模型对话和 API Key 配好再动手写 PLSQL

写 PLSQL 的过程中你会反复遇到「这段游标为什么只取到 5000 行」「forall报 ORA-06550 怎么读」这类问题,与其翻文档,不如先把一个能随时问的模型对话入口配好。我习惯在动手前把工具链准备好,这样排错时不用来回切窗口。

第一步,打开模型对话页面,地址是:

https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite

在这里可以直接贴报错信息、贴游标代码,让它帮你逐行解释。比如你把exit when cur_login%notfound or cur_login%notfound is null;这行贴进去,它会告诉你%notfound在bulk collect场景下的真实语义——这正是本题最容易写错的地方。

第二步,如果你打算把「生成建表语句 / 生成存储过程骨架 / 解释执行计划」做成脚本自动化,就需要 API Key。进入控制台创建:

https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite

然后在 API Keys 页面生成密钥:

https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite

拿到 Key 之后,接口基地址用这个(注意 API 地址不带 UTM 参数):

https://taotoken.net/api

第三步,如果你是用 Claude Code 这类编码工具来辅助写 PLSQL,可以走 Coding Plan,把长期编码任务挂上去:

https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite

接入文档在这里,里面有 Base URL、Key、Model ID 三件套的完整说明:

https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite

这里要强调一个原则:任何接入都必须写全三件套——Base URL、API Key、Model ID,缺一个都会在请求时报 401 或 model not found。下面给一份可直接复制的配置片段,以常见的settings.json形式为例(路径按你本地实际工具调整):

{ "base_url": "https://taotoken.net/api", "api_key": "sk-你的密钥", "model_id": "claude-sonnet-4-20250514", "timeout": 60 }

如果你用的是 TOML 风格的配置,等价写法:

[provider] base_url = "https://taotoken.net/api" api_key = "sk-你的密钥" model_id = "claude-sonnet-4-20250514"

配好之后,你写 PLSQL 遇到ORA-报错,可以直接把错误码和上下文丢给模型对话,让它给出排查方向。这一步不是必须,但能省掉大量翻 MOS 文档的时间。工具准备好,接下来进入正题:建表、建链、写过程。

3. 可复制配置:建表、建 dblink、写 procedure 骨架

这一节是全文的技术核心,所有代码都可以直接复制到你的测试库跑。我按「建表 → 建 dblink → 写过程 A → 写过程 B」的顺序给。

3.1 建本地目标表和日志表

先建两张表:迁移目标表user_login和日志表t_log。日志表用来记录每次同步的批次和异常信息。

-- 迁移目标表 create table user_login ( user_id number, login number, login_time date ); -- 日志表 create table t_log ( seqid number, msg varchar2(2000), createtime date ); -- 序列,供日志表主键使用 create sequence seq_log start with 1 increment by 1;

再建汇总结果表,对应子任务 B:

create table fact_login_cn ( statedate number, login_cn int, userid number );

3.2 建 dblink

dblink 的名字按题目用px_dblink,指向远程库。你需要替换成自己的远程库连接串:

create database link px_dblink connect to remote_user identified by "remote_pwd" using '(DESCRIPTION = (ADDRESS = (PROTOCOL = TCP)(HOST = 10.0.0.20)(PORT = 1521)) (CONNECT_DATA = (SERVICE_NAME = orcl)) )';

建完先验证链路通不通,这一步别跳过:

select count(1) from user_login@px_dblink;

如果这里就报ORA-12154或ORA-12541,说明连接串或监听有问题,先解决链路再写过程,否则后面所有报错都会混在一起。

3.3 过程 A:dblink 取数 + cursor + forall 批量写入

这是题目一的核心骨架。注意几个关键点:用bulk collect ... limit 5000分批取,用forall批量插,循环退出条件要同时判断%notfound,异常块里回滚并写日志。

create or replace procedure p_login as cursor cur_login is select user_id, login, login_time from user_login@px_dblink; type type_user_id is table of number; v_user_id type_user_id; type type_login is table of number; v_login type_login; type type_login_time is table of date; v_login_time type_login_time; lv_errinfo varchar2(2000); begin open cur_login; loop fetch cur_login bulk collect into v_user_id, v_login, v_login_time limit 5000; forall i in 1 .. v_user_id.count insert into user_login values (v_user_id(i), v_login(i), v_login_time(i)); commit; insert into t_log(seqid, msg, createtime) values (seq_log.nextval, 'batch inserted: ' || v_user_id.count, sysdate); commit; exit when cur_login%notfound; end loop; close cur_login; exception when others then rollback; lv_errinfo := '错误信息:' || SQLERRM; insert into t_log(seqid, msg, createtime) values (seq_log.nextval, lv_errinfo, sysdate); commit; end p_login;

这里有个细节值得单独说:原题里写的是exit when cur_login%notfound or cur_login%notfound is null;,实际上%notfound是布尔值,不会为 null,or ... is null是冗余的。但更重要的是——bulk collect取完最后一批后,%notfound才为 true,所以退出判断放在forall和commit之后是对的,能保证最后一批数据被写入。如果你把exit when放到fetch之后、forall之前,最后一批就会丢。

3.4 过程 B:按小时汇总 + forall 写入

子任务 B 的骨架和 A 几乎一样,只是游标 SQL 换成了group by,字段类型变成number / int / number。

create or replace procedure p_fact as cursor cur_fact is select to_char(login_time, 'yyyymmddhh24') as statedate, count(1) as login_cn, user_id from user_login group by user_id, to_char(login_time, 'yyyymmddhh24'); type type_statedate is table of number; v_statedate type_statedate; type type_login_cn is table of int; v_login_cn type_login_cn; type type_userid is table of number; v_userid type_userid; begin open cur_fact; loop fetch cur_fact bulk collect into v_statedate, v_login_cn, v_userid limit 5000; forall i in 1 .. v_statedate.count insert into fact_login_cn values (v_statedate(i), v_login_cn(i), v_userid(i)); commit; exit when cur_fact%notfound; end loop; close cur_fact; end p_fact;

注意to_char(login_time, 'yyyymmddhh24')返回的是字符串,而fact_login_cn.statedate是number,Oracle 会做隐式转换。如果你不想依赖隐式转换,可以在游标里显式to_number(...),更稳妥。

3.5 关于批量大小的选择

limit 5000不是随便定的。太小,循环次数多、commit 频繁;太大,单次 PGA 占用高。经验值在 1000 到 10000 之间,900 万行用 5000 大约 1800 批。你可以用表格对照一下:

limit 值批次数(900万行)适用场景
10009000单行宽、内存紧张
50001800通用推荐
10000900行窄、追求吞吐

4. 验证请求与成功结果:怎么确认批量同步真的跑通了

代码写完不代表跑通,必须验证。这一节给一套可执行的验证步骤,从执行过程到核对数据量。

4.1 执行存储过程

在 SQL*Plus 或 PLSQL Developer 里执行:

set timing on exec p_login; exec p_fact;

set timing on会打印耗时,900 万行用 5000 批量,正常在几分钟级别,取决于网络和 IO。

4.2 核对数据量

执行完立刻核对源和目标行数是否一致:

-- 远程源表行数 select count(1) from user_login@px_dblink; -- 本地目标表行数 select count(1) from user_login;

两个数字必须相等。如果本地少了,大概率是最后一批没写入,回去检查exit when的位置。

4.3 查看日志表

日志表能告诉你每批插了多少行、有没有异常:

select * from t_log order by createtime desc;

正常情况你会看到一串batch inserted: 5000,最后一条可能是batch inserted: 剩余行数。如果看到错误信息:ORA-xxxxx,说明异常块被触发,需要根据错误码排查。

4.4 验证汇总结果

对子任务 B,验证汇总是否正确:

select statedate, sum(login_cn) from fact_login_cn group by statedate order by statedate;

再和源表直接汇总对比:

select to_char(login_time,'yyyymmddhh24'), count(1) from user_login group by to_char(login_time,'yyyymmddhh24') order by 1;

两边每个小时的计数应该一致。如果对不上,检查group by里是否漏了user_id——题目要求是按「用户 + 小时」汇总,不是只按小时。

4.5 用模型对话辅助读执行计划

如果你想确认forall是否真的走了批量绑定,可以把执行计划贴到模型对话里让它解读:

https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite

把explain plan for insert ...的输出贴进去,问「这里有没有发生逐行绑定」,能帮你判断批量是否生效。

5. 本篇常见错排查:401、ORA-02019、ORA-06550 逐个拆

这一节按真实报错来,每个错误给现象、原因、修法。

5.1 ORA-02019: connection description for remote database not found

现象:执行select count(1) from user_login@px_dblink时报这个错。

原因:dblink 名字拼错,或者 dblink 建在了别的 schema 下,当前用户看不到。

修法:查一下当前用户能看到的 dblink:

select owner, db_link, host from all_db_links;

确认px_dblink存在且 owner 正确。如果不存在,重新执行 3.2 的建链语句。

5.2 ORA-06550 / PLS-00306: wrong number or types of arguments

现象:调用p_login时报参数类型不匹配。

原因:游标select的字段顺序和bulk collect into的变量顺序不一致,或者类型不兼容。比如login_time是date,你却声明成了number。

修法:逐个核对游标字段和集合类型。建议用%type锚定,减少手写类型出错:

type type_login_time is table of user_login.login_time%type;

5.3 ORA-01555: snapshot too old

现象:长时间跑批量同步时中途报快照过旧。

原因:undo 表空间不够,或者同步时间太长,游标一致性读需要的 undo 被覆盖。

修法:调大 undo 表空间或延长undo_retention;同时把limit调小、commit 更频繁,缩短单次事务跨度。

5.4 401 Unauthorized(API 侧)

现象:调用模型接口时返回 401。

原因:API Key 没带、带错,或者 Base URL 写成了带路径的形式。

修法:确认三件套齐全——Base URL 用https://taotoken.net/api,Key 从 API Keys 页面复制,Model ID 填对。检查配置里有没有多余空格。相关文档:

https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite

5.5 local proxy failed

现象:请求发出后报本地代理失败。

原因:本地网络配置里挂了不可用的代理,或者代理端口写错。

修法:检查系统代理设置,把 API 请求走直连;确认base_url没有被本地代理规则拦截。

5.6 reading choices 相关报错

现象:解析响应时报reading 'choices'之类的字段缺失。

原因:返回体结构和预期不符,通常是 Model ID 填错导致返回了错误结构,或者请求根本没成功。

修法:先用模型对话页面确认模型可用,再核对配置里的 Model ID 拼写。

5.7 OAuth 相关报错

现象:用 Claude Code 类工具时报 OAuth 失败。

原因:认证方式选错,或者 token 过期。

修法:改用 API Key 方式接入,参考接入文档重新配置。Claude Code 的接入说明在:

https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite

5.8 排错速查表

报错大概率原因第一步动作
ORA-02019dblink 不存在查 all_db_links
ORA-06550类型/参数不匹配核对游标字段顺序
ORA-01555undo 不足调小 limit、加 undo
401Key/URL 错核对三件套
local proxy failed代理拦截检查系统代理
reading choicesModel ID 错核对模型名

6. 把骨架用起来:从跑通一次到长期批量同步

跑通一次只是开始。真正落地时,你会需要把p_login挂到定时任务里,或者用 Coding Plan 把「生成过程骨架 + 解释报错」做成日常流程:

https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite

几个实战经验,都是踩过坑总结的:

第一,先小批量试跑再放全量。把游标加个where rownum <= 10000,确认逻辑对了再放开。900 万行跑一半发现字段错位,回滚都来不及。

第二,日志表别省。t_log里记录每批行数和时间,出问题时能精确定位到哪一批断了。我见过有人为了省事不写日志,结果同步少了几万行,查了一整天。

第三,forall的索引从 1 开始。forall i in 1 .. v_user_id.count,如果集合是空的,count为 0,1 .. 0不会执行,这是安全的。但如果你写成forall i in v_user_id.first .. v_user_id.last,空集合时first和last都是 null,会报错。

第四,commit 频率和性能要平衡。每批 commit 一次,1800 批就是 1800 次 commit,redo 写入频繁但安全;如果改成每 10 批 commit 一次,性能好一点,但异常时回滚范围大。按你的数据重要性选。

第五,汇总过程注意隐式转换。to_char出来的字符串插进number列,数据量大时隐式转换会拖慢速度,建议在游标里就to_number转好。

最后给一个可以直接复用的调用模板,把两个过程串起来:

begin p_login; -- 先迁移 p_fact; -- 再汇总 dbms_output.put_line('sync done at ' || to_char(sysdate,'yyyy-mm-dd hh24:mi:ss')); end; /

这套骨架的价值在于:它不依赖任何外部调度框架,纯 PLSQL 就能完成跨库批量同步,改改游标 SQL 就能复用到别的表。你把px_dblink换成自己的链名,把字段换成目标表的字段,剩下的循环、批量、日志、异常处理逻辑原样保留即可。

返回列表