1. 项目背景与需求拆解
1.1 核心需求从哪来
先交代一下场景。某数据平台需要按天生成多张业务分表,比如按日期后缀生成TRADE_20250101、TRADE_20250102这样一张张表,接着要把源库的历史数据按日期切分后灌入对应的分表。如果用手工方式靠 SQL*Plus 一条条CREATE TABLE、一条条INSERT,几十张表、几百万数据量足够让人崩溃。于是我当时接到这个任务,首先拆解成三个核心需求:
- 批量创建一批结构相同、表名不同的表。
- 批量向这些表插入对应的数据。
- 整个过程要在存储过程中自动完成,并输出基础日志,方便追溯。
这类需求在金融、零售、物流行业非常常见。尤其是月底报表、日终批处理、数据归档场景,几乎绕不开“批量建表+批量插入”的组合拳。这个项目本身没有特别高的技术门槛,但它非常考验对 Oracle 语法细节、事务控制、性能调优的理解。写顺了能省下大量人工时间,写崩了可能把生产环境搞停。
1.2 技术选型背后的逻辑
当时可选的技术方案有不少。最朴素的方案是用 Shell 脚本循环调用 sqlplus,每次执行一条建表语句、一条插入语句。优点是好理解,缺点是每张表都要单独建立连接,编译和执行开销很大。更关键的瓶颈在于:只要中间有一张表插入失败,脚本就会中断,你得手工去排查究竟跑到哪一步了。
所以我们决定把逻辑都收拢到数据库内部,用存储过程实现。建表用动态 SQL,插入用游标 + 动态 SQL 或者直接使用INSERT ... SELECT。这样有几个好处:
- 所有操作都在数据库实例内部完成,减少客户端网络往返,执行链路更短。
- 可以在一个存储过程内通过异常处理捕获错误,记录日志。
- 通过
COMMIT的节奏控制,保证单批数据的可控性。
选型的时候还要考虑一个问题:目标表是否需要预先存在?如果表不存在,存储过程里动态建表,然后再插入。这个方案最灵活,适合“批量生成大量结构相同的表”这类场景。如果表已经由 DBA 建好,那么存储过程只负责数据搬运即可。我这个项目属于前者,所以重点放在了“建表+插入”一体化的实现。
1.3 整体架构怎么搭
我用一张图来表述整体流程(这里用文字描述):存储过程接收一个批次日期和时间范围,然后解析目标表名清单,先循环动态建表,再循环动态插入,每个表插入完成后立即提交一次,同时记录日志到一张日志表。
从这个流程可以看出,核心模块有三个:
- 批次表名解析模块:负责生成目标表名清单。
- 动态建表模块:负责按模板建表。
- 数据插入模块:负责按条件从源表抽取数据并写入目标表。
这三个模块集成在一个包PKG_BATCH_DATA中。外层只暴露一个入口RUN_BATCH,传入日期参数即可。这样设计既能满足复用,也方便后续扩展。项目实际测试下来,10 张表每张 50 万条左右的数据量,整体耗时从手工执行的 40 多分钟缩短到 7 分钟左右,效果非常直观。
2. 批量建表的核心实现
2.1 动态SQL与静态SQL的选择
在 Oracle 存储过程中,如果要建的表名是运行时才知道的,CREATE TABLE不能像普通 DML 那样直接写在过程体里。必须使用动态 SQL。常见的动态 SQL 执行方式有两种:EXECUTE IMMEDIATE和DBMS_SQL。对于建表这类简单场景,EXECUTE IMMEDIATE完全够用。
很多人会问:为什么不用ALL_TABLES判断表是否存在后,再决定是否建表?答案是可以的,而且很值得做。因为存储过程一旦设计成可重复执行,就必须考虑幂等性。如果某张表已经存在,再执行一次CREATE TABLE会直接报ORA-00955: name is already used by an existing object。
我在实际编写时是这样处理的:
BEGIN SELECT COUNT(1) INTO v_cnt FROM ALL_TABLES WHERE OWNER = 'DATA_OWNER' AND TABLE_NAME = v_table_name; IF v_cnt = 0 THEN EXECUTE IMMEDIATE 'CREATE TABLE DATA_OWNER.' || v_table_name || ' AS SELECT * FROM DATA_OWNER.TMP_MODEL WHERE 1 = 0'; END IF; END;这里用WHERE 1 = 0的目的很清楚:只要表结构,不要数据。这种方式比手工逐列拼CREATE TABLE的列定义要省事得多,而且模板表后续如果加列,建出来的新表会自动带上新列,不需要额外改代码。这个技巧非常实用,能减少大量维护成本。
2.2 批量建表的实现思路
批量建表前,先要拿到表名清单。表名来源有多种可能:根据前缀加日期、从配置表读取、或者从上游文件解析。在我这个项目里,目标表名是TRADE_YYYYMMDD的格式。假设要生成某月 1 号到 10 号共 10 张表,我直接在存储过程中用循环生成日期字符串并拼接表名。
直接贴核心代码(基于常见实践,已做脱敏处理):
CREATE OR REPLACE PROCEDURE PRC_CREATE_TABLES( p_start_date IN DATE, p_end_date IN DATE ) IS v_cur_date DATE := p_start_date; v_table_name VARCHAR2(64); v_sql VARCHAR2(4000); v_cnt NUMBER; BEGIN WHILE v_cur_date <= p_end_date LOOP v_table_name := 'TRADE_' || TO_CHAR(v_cur_date, 'YYYYMMDD'); SELECT COUNT(1) INTO v_cnt FROM ALL_TABLES WHERE OWNER = 'DATA_OWNER' AND TABLE_NAME = v_table_name; IF v_cnt = 0 THEN v_sql := 'CREATE TABLE DATA_OWNER.' || v_table_name || ' AS SELECT * FROM DATA_OWNER.TMP_MODEL WHERE 1 = 0'; EXECUTE IMMEDIATE v_sql; DBMS_OUTPUT.PUT_LINE('Created table: ' || v_table_name); -- 写入日志表 INSERT INTO BATCH_LOG(LOG_ID, MODULE_NAME, ACTION_DESC, RUN_TIME) VALUES (SEQ_LOG.NEXTVAL, 'PRC_CREATE_TABLES', 'CREATE ' || v_table_name, SYSDATE); END IF; v_cur_date := v_cur_date + 1; END LOOP; COMMIT; EXCEPTION WHEN OTHERS THEN ROLLBACK; DBMS_OUTPUT.PUT_LINE('Error at: ' || v_table_name || ', Message: ' || SQLERRM); RAISE; END;这里面有几点值得注意:
ALL_TABLES查询的是当前用户能看到的表,如果当前连接用户没有DATA_OWNER下所有表的查询权限,会查不到记录,导致重复建表时报错。所以最好在过程中加上捕获异常的逻辑。COMMIT放在循环外还是循环内,取决于是否需要保留部分结果。我的做法是全部建表成功后才提交,避免产生半成品表。- 日志表每建一张就插入一条,后续排查时能清楚知道哪些表在何时被创建。
2.3 模板表与表结构克隆
模板表TMP_MODEL是这个方案的关键。它只需要建一次,放在一个固定 Schema 下面。后续所有批量创建的表都基于它克隆结构。克隆时还可以把注释、默认值一起带过来。但要注意,CREATE TABLE ... AS SELECT这种方式不会复制主键、非空约束、索引和触发器等对象。如果目标表也需要这些约束,那么要么建完表之后用ALTER TABLE补,要么改成直接执行一套完整的CREATE TABLE语句,再执行配套的索引创建语句。
我当时的做法是“基础结构模板化 + 索引脚本化”。也就是说,核心表结构用模板克隆,主键和常用索引单独放在一个脚本里,批量建表之后再统一执行一遍:
EXECUTE IMMEDIATE 'ALTER TABLE DATA_OWNER.' || v_table_name || ' ADD CONSTRAINT PK_' || v_table_name || ' PRIMARY KEY (ID)';这种做法的优点是灵活,缺点是每个约束名都需要唯一。如果表名中包含日期,主键名直接把表名作为后缀拼接上去,基本可以避免重名问题。如果表名不是唯一的,可能还需要加一个序列来生成约束名后缀。
关于WHERE 1 = 0这里再延展一句:有些人喜欢先SELECT * FROM TMP_MODEL取出来做成CREATE TABLE语句,其实没必要,直接克隆结构更简洁。另外,模板表定期会有列变更,比如业务加了一列,旧的分表并不会自动更新。所以在批量建表之前如果有变更,需要先对模板表做ALTER TABLE,然后重建旧的表,或者用ALTER TABLE ... ADD/MODIFY去旧表上动态添加列。这个在真实项目中是避免不了的,建议提前设计好“结构同步”规则。
3. 批量插入存储过程设计
3.1 批量插入的几种常见姿势
批量插入是另一个重点。Oracle 下插入大批量数据,常见方案有:
- 单条
INSERT INTO ... VALUES循环。 - 游标循环逐条插入。
INSERT INTO ... SELECT ...一次性从源表抽取。INSERT ALL多表同时插入。- 外部表加载或
SQL*Loader。
在存储过程中做批量插入,最常用的是方案 3。它把“从源表查出数据再插入目标表”的动作合二为一,由 Oracle 优化器统一处理。如果只是从一张表搬到另一张表,用INSERT INTO target SELECT FROM source WHERE condition几乎是最优解。它的隐含问题在于:一个巨大的INSERT ... SELECT会导致回滚段/undo 空间占用很高,提交前所有变更都在一个事务里,一旦中途失败,全部回滚。所以需要控制批次大小。
方案 2 的逐条插入虽然可控,但每条INSERT都会触发独立的解析和事务开销,性能很差。只有在需要处理复杂行级逻辑时才考虑。
而方案 4INSERT ALL适合把同一份源数据同时插入到多张结构相同的表或分区表中。比如把TRADE_20250101和TRADE_20250102两张表的数据都用同一个查询结果集来填充,那么INSERT ALL可以减少对源表的重复扫描。这个技巧容易被人忽略,后面我会专门讲。
3.2 存储过程中的批量插入核心代码
先看我用的最基础版本:按日期循环,每天对应一张表,从源总表SRC_TRADE中抽取当天数据插入目标表。
CREATE OR REPLACE PROCEDURE PRC_INSERT_BATCH( p_start_date IN DATE, p_end_date IN DATE ) IS v_cur_date DATE := p_start_date; v_table_name VARCHAR2(64); v_sql VARCHAR2(4000); BEGIN WHILE v_cur_date <= p_end_date LOOP v_table_name := 'TRADE_' || TO_CHAR(v_cur_date, 'YYYYMMDD'); v_sql := 'INSERT INTO DATA_OWNER.' || v_table_name || ' SELECT * FROM DATA_OWNER.SRC_TRADE WHERE TRADE_DATE = :1'; EXECUTE IMMEDIATE v_sql USING v_cur_date; DBMS_OUTPUT.PUT_LINE('Inserted into: ' || v_table_name || ', Rows: ' || SQL%ROWCOUNT); COMMIT; v_cur_date := v_cur_date + 1; END LOOP; EXCEPTION WHEN OTHERS THEN ROLLBACK; DBMS_OUTPUT.PUT_LINE('Error at: ' || v_table_name || ', Message: ' || SQLERRM); RAISE; END;注意,SQL%ROWCOUNT在EXECUTE IMMEDIATE执行 DML 之后是可以取到的。这个值代表该条语句影响的行数。不过对于INSERT ... SELECT,它返回的是实际插入的行数。
这段代码有个问题:如果某一天没有数据,INSERT ... SELECT不会报错,只会影响 0 行。这没问题。但如果目标表有主键,且数据里存在重复键,插入会报ORA-00001,整个存储过程会停止。所以还要在异常处理上多做一层判断。
3.3 自动分批的边界处理
如果是单表数据量过大,比如一天要插入 5 百万行,直接一条INSERT ... SELECT执行时间太长,undo 空间也可能爆炸。虽然“每天一张表”这个粒度天然做了数据切分,但单表内部依然可能很大。这种情况下要再做一层循环分批。
Oracle 里没有类似 MySQL 的LIMIT,通常用ROWNUM或游标FETCH FIRST n ROWS ONLY。更常见的方式是先按某种业务键如ID的范围分片。比如每批处理 10 万行,可以写成:
v_sql := 'INSERT INTO DATA_OWNER.' || v_table_name || ' SELECT * FROM (SELECT * FROM DATA_OWNER.SRC_TRADE WHERE TRADE_DATE = :1 AND ID > :2 ORDER BY ID) WHERE ROWNUM <= 100000';但这种方法在分片时容易漏数据或重复取数,需要记录上次的最大ID,比如:
v_last_id := 0; LOOP v_sql := 'INSERT INTO DATA_OWNER.' || v_table_name || ' SELECT * FROM (SELECT * FROM DATA_OWNER.SRC_TRADE WHERE TRADE_DATE = :1 AND ID > :last_id ORDER BY ID) WHERE ROWNUM <= 100000'; EXECUTE IMMEDIATE v_sql USING v_cur_date, v_last_id; IF SQL%ROWCOUNT = 0 THEN EXIT; END IF; COMMIT; -- 获取本批最大ID作为下一批起点 SELECT NVL(MAX(ID), v_last_id) INTO v_last_id FROM DATA_OWNER.SRC_TRADE WHERE TRADE_DATE = v_cur_date AND ID > v_last_id AND ROWNUM <= 1; END LOOP;这种写法相对安全,而且每批独立提交。代价是每次都要重新排序,如果源表在(TRADE_DATE, ID)上有索引,性能问题不大。如果担心ROWNUM在半途数据新增时出现问题,还可以在批处理前先锁定源表或者把该天数据先抽到一个临时表,再从临时表分片插入目标表。
4. 性能优化与参数调优
4.1 主要瓶颈在哪里
批量建表本身耗时不高,真正的瓶颈在数据插入。插入慢的常见原因有:
- 目标表有大量索引,每条记录插入时要同步维护索引。
- 约束检查如主键、唯一键会增加开销。
- 每次
COMMIT太频繁,产生大量 redo log。 EXECUTE IMMEDIATE每次执行都需要硬解析,尤其动态 SQL 拼接的表名不同,可能导致无法复用执行计划。- 源表和目标表间存在类型转换,隐性转换会让索引失效。
参数方面,主要关注SGA_TARGET、PGA_AGGREGATE_TARGET、DB_CACHE_SIZE和UNDO_TABLESPACE。但存储过程内部不能随意修改系统参数,能做的更多是操作层面优化。
4.2 Commit 的节奏
COMMIT太频繁和太少都是问题。太频繁,比如每次插入 1 行就提交,会极大增加同步和日志写入开销;太少,比如一个事务灌几百万行,undo 表空间必然告急。
我的经验是:单批次数据量控制在 5 万到 10 万行左右。既不会让事务过长,也能让性能有基本保障。对于上面按“天”切表的场景,如果每天数据量不到 10 万行,直接一张表一个事务没问题。如果超过这个量,就按ID分批。
手动控制分批时,可以在存储过程中加一个参数p_batch_size,默认 100000:
v_sql := 'INSERT INTO DATA_OWNER.' || v_table_name || ' SELECT * FROM (SELECT * FROM DATA_OWNER.SRC_TRADE WHERE TRADE_DATE = :1 AND ID > :2 ORDER BY ID) WHERE ROWNUM <= :3'; EXECUTE IMMEDIATE v_sql USING v_cur_date, v_last_id, p_batch_size;这样灵活性更高。DBA 可以根据当前库的 redo 生成速度和磁盘 IO 调整批大小,不用改代码。
4.3 并行度控制
Oracle 的并行执行可以在表级别或语句级别启用。比如:
ALTER SESSION ENABLE PARALLEL DML; INSERT /*+ PARALLEL(target_table, 4) */ INTO target_table SELECT * FROM source_table;但存储过程中直接加并行提示,要小心资源竞争。批量插入本身就涉及多个表循环,如果每个表都开启 4 个并行,10 张表可能会把 CPU 打满。建议“小库不开并行,大库只对最大表开并行”,或者把并行度限制在 2 到 4。
另外,COMMIT之后并行 DML 的会话状态可能会重置,导致后续并行提示失效。所以在循环内如果每批都提交,需要每次循环都重新执行ALTER SESSION ENABLE PARALLEL DML或者用EXECUTE IMMEDIATE 'ALTER SESSION ENABLE PARALLEL DML'。
4.4 减少日志与索引维护
插入前临时禁用目标表的索引和约束,插完再重建,这是性能杀手锏,但也很危险。如果中途断电,可能留下一堆无效索引。实际操作时,我通常在测试环境验证后再用于生产。大致语法:
EXECUTE IMMEDIATE 'ALTER INDEX idx_name UNUSABLE'; -- 批量插入 EXECUTE IMMEDIATE 'ALTER INDEX idx_name REBUILD ONLINE';如果目标表是新建的,其实最开始根本没有索引,插入完再统一建索引是最好的。
还有一个提升性能的常用手段:在插入前关闭表的日志记录,也就是NOLOGGING。建表时可以直接加NOLOGGING,或者:
ALTER TABLE table_name NOLOGGING;这样生成的 redo 会大幅度减少。但要注意,如果后续数据库发生了介质恢复,NOLOGGING操作涉及的数据块可能无法被完整恢复。建议在可重建的临时场景下使用。
5. 实战踩坑与问题定位
5.1 权限不足引发的诡异报错
存储过程里EXECUTE IMMEDIATE 'CREATE TABLE ...'时,如果当前用户只有CONNECT角色,没有RESOURCE角色,往往在执行时提示:
ORA-01031: insufficient privileges这个问题很容易被忽略。因为你在 SQL 窗口里手动执行CREATE TABLE是成功的,但编译好的存储过程默认使用定义者权限。如果执行存储过程的用户和建表用户不是同一个,存储过程会被屏蔽掉对应的权限。解决办法是给过程加AUTHID CURRENT_USER,或者给执行用户显式授予建表权限。
CREATE OR REPLACE PROCEDURE PRC_CREATE_TABLES( p_start_date IN DATE, p_end_date IN DATE ) AUTHID CURRENT_USER IS ...另一个麻烦是目标表前缀如DATA_OWNER.TRADE_20250101,如果执行用户不能访问DATA_OWNER,在建表时会报“表或视图不存在”。用户必须拥有对DATA_OWNER的CREATE ANY TABLE权限,或者干脆用DATA_OWNER账号来跑存储过程。真实生产环境通常不允许普通应用账号持有过多权限,此时需要 DBA 单独开一个批处理账号,只授予必要的建表、DML 权限。
5.2 临时表空间不足
批量插入时经常遇到:
ORA-01652: unable to extend temp segment by 128 in tablespace TEMP原因是排序、分组或索引创建占用了大量临时表空间。比如我用ORDER BY ID分片时,整个结果集可能先在临时表空间排序,如果临时表空间太小就会爆掉。解决方法是先查询SELECT * FROM SRC_TRADE WHERE TRADE_DATE = :1 AND ID > :2,这时候如果源表在组合索引(TRADE_DATE, ID)上有索引,ORDER BY ID可以走索引消除排序,否则就要保证临时表空间足够大。
更稳妥的做法是给临时表空间增加数据文件,或者降低单批数据量,减少单次排序的数据规模。如果源表非常庞大,还可以先把当天的数据抽到中间临时表,然后再从中间临时表往目标表搬。
5.3 动态SQL与SQL%ROWCOUNT的坑
SQL%ROWCOUNT只有在最近一条 DML 执行后才有效。如果中间查询了数据字典或者执行了别的语句,它就会被覆盖。我刚接触时在循环里先SELECT COUNT(1)判断目标表是否存在,然后再执行插入,导致取到的ROWCOUNT是 0 或错误的数值。解决办法是执行完EXECUTE IMMEDIATE后立刻保存:
EXECUTE IMMEDIATE v_sql USING v_cur_date; v_rows := SQL%ROWCOUNT;不要在保存前插入其他语句。另外,EXECUTE IMMEDIATE执行非查询语句时,无法直接从绑定变量传回行数,只能靠SQL%ROWCOUNT,这个细节很容易被忽略。
5.4 常见问题速查表
我把这个项目里典型的问题整理成了一张表,方便后续复用。
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| ORA-00955 建表重名 | ALL_TABLES判断失效或权限看不全 | 用USER_TABLES或确认是否有其他 schema 同名表 |
| ORA-01031 权限不足 | 存储过程定义者权限不够 | 增加AUTHID CURRENT_USER或授CREATE TABLE |
| ORA-01652 temp 不够 | 排序或索引重建占满临时表空间 | 扩容临时表空间,降低 batch_size |
| 动态SQL执行计划差 | 表名不同导致硬解析 | 使用 bind 变量固定谓词,表名必须提前拼接,但尽量只变化日期范围 |
| ROWCOUNT 始终为0 | 执行后又被其他语句覆盖 | 执行后立刻保存到变量 |
| 插入时 ORA-00001 重复键 | 源数据重复或约束逻辑冲突 | 插入前用MERGE或先DELETE重复键 |
| 过程执行到一半失败 | 事务未分批提交 | 每批次完成检查后再COMMIT |
5.5 日志与断点续跑
批处理最怕中途失败后从头再来。所以我额外设计了一个日志表BATCH_LOG和状态表BATCH_RUN_STATUS。每次建表成功、插入完成都会记录状态。下次执行时,如果发现目标表已经存在且状态为DONE,就跳过这张表:
SELECT STATUS INTO v_status FROM BATCH_RUN_STATUS WHERE TABLE_NAME = v_table_name AND BATCH_ID = v_batch_id; IF v_status = 'DONE' THEN CONTINUE; END IF;这个逻辑看似简单,但很实用。尤其是生产环境重启、网络中断等异常情况,可以直接用同一套存储过程续跑,不用人工删表或清数据。状态表还可以用来做后续的数据质量核对。
6. 扩展玩法:从“批量”到“灵活调度”
6.1 动态表名清单的几种配置方式
上面讲的都是固定规则生成表名,比如按日期生成。实际项目中表名规则往往更复杂,比如“客户号 + 日期”“渠道 + 月份”。这时候建议把表名清单放在配置表里,由上游 ETL 生成好后入库,存储过程直接读配置表循环执行。这样改动表名规则时不需要重新编译存储过程,只需要改数据。
配置表示例:
CREATE TABLE TABLE_CONFIG ( RUN_DATE DATE, TARGET_TABLE_NAME VARCHAR2(64), SOURCE_FILTER VARCHAR2(4000), STATUS VARCHAR2(16) );存储过程读表时采用FOR ... IN ... LOOP:
FOR rec IN (SELECT TARGET_TABLE_NAME, SOURCE_FILTER FROM TABLE_CONFIG WHERE RUN_DATE = p_run_date ORDER BY TARGET_TABLE_NAME) LOOP EXECUTE IMMEDIATE 'INSERT INTO ' || rec.TARGET_TABLE_NAME || ' SELECT * FROM ' || rec.SOURCE_FILTER; END LOOP;这样把“批量”玩成了“配置驱动”,后续加表、减表都不动代码。
6.2 插入与幂等性设计
存储过程的幂等性是生产环境最重要的收敛点。第二次执行同一批日期时,数据会不会重复?插入前需不需要先清空目标表?
我实现了一个PRC_TRUNCATE_IF_EXISTS子过程:
BEGIN EXECUTE IMMEDIATE 'TRUNCATE TABLE ' || p_table_name; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;TRUNCATE 是 DDL,无法回滚,所以在执行前要确认业务允许“重跑覆盖”语义。如果源数据是按时间范围全量抽取,重跑覆盖没问题;如果源数据是增量追加,那就必须用MERGE或先查重。
这里还有一个值得注意的点:如果目标表上有外键引用,TRUNCATE 可能不被允许,会报 ORA-02266。处理方式是把外键先 disable,之后 enable。
6.3 结合调度系统做定时批处理
存储过程写好后,可以包一层 Shell 脚本,由 cron 或者调度平台触发。脚本只需要调用 SQL*Plus:
sqlplus -s user/pass@srv <<EOF EXEC PKG_BATCH_DATA.RUN_BATCH(TO_DATE('2025-01-01', 'YYYY-MM-DD')); EOF如果要把多个批次并行起来,可以在存储过程里使用DBMS_SCHEDULER。比如每个表作为一个 JOB 提交,由调度器控制并行度。需要注意:并行 JOB 坏了不容易排查,建议先用单线程跑通整体流程,再根据资源情况决定是否逐步放开并行。
我个人在实际操作中踩过一个坑:在某实例上开启多个并行 JOB 同时建表,由于大家都去查ALL_TABLES,并且对同一张模板表做CREATE TABLE ... AS SELECT,偶尔会触发字典锁竞争,导致“enqueue”等待。后来我把建表阶段固定为单线程,插入阶段再并行,问题就消失了。
6.4 后续还能怎么扩展
这套存储过程框架继续扩展的方向很多:
- 支持分区表:每张表按日期建分区,插入时直接走分区。
- 增加数据校验:插入完成后对比源表和目标表的
COUNT和SUM关键字段,生成校验报告。 - 动态索引管理:结合
DBA_INDEXES自动判断哪些索引需要重建。 - 适配其他数据库:把
EXECUTE IMMEDIATE的语法抽象成公共接口,方便迁移。
不过这些都是锦上添花,核心还是先把“批量建表+批量插入”的基础打牢,再逐步添加外围能力。
7. 最后再分享几个小经验
很多初学者会把注意力放在“如何写出一条很长的存储过程”上,但我更建议把存储过程拆成多个小模块,每个模块只做一件事。比如建表过程、插入过程、日志记录过程分开,后期维护会轻松得多。
在实际开发中,我一般会先在测试库跑一遍完整流程,观察以下几点:
- 每张表插入的行数是否和源数据一致。
- 单次提交耗时是否稳定。
- redo 日志增长是否异常。
- 临时表空间使用率是否过高。
如果某一步耗时异常,优先看执行计划,而不是盲目加并行。绝大多数性能问题都能从 SQL 是否走索引上找到答案。
还有一个很容易被忽视的点:存储过程里拼接的动态 SQL,尽量少拼接、多用绑定变量。表名和 schema 名不能绑定,但条件值一定要绑定。比如上面的WHERE TRADE_DATE = :1就比WHERE TRADE_DATE = TO_DATE('...')更有利于复用游标。我见过不少因为动态拼接导致每天硬解析、最终把共享池打爆的案例,这些问题都很隐蔽,但排查起来并不困难。
回到这个项目本身,Oracle 批量建表和批量插入存储过程的难点从来不在语法,而在于对事务边界、权限模型、资源开销的管理。把这三个点控制好,不管数据量是百万还是千万,都能稳定运行。