ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Oracle 海量测试数据快速构造:SQL*Loader 与 PL/SQL 实践

2026/9/17 15:53:23 拓冰建站 浏览量
Oracle 海量测试数据快速构造:SQL*Loader 与 PL/SQL 实践 Oracle 里造测试数据这件事看着不起眼真到项目上却经常卡住人功能测试要 100 万行订单压测要 5000 万行流水数据迁移验证要一份结构和真实库长得像的样本。手工写几条 INSERT 谁都会但要在一个晚上把上亿行数据灌进表里还得保证字段分布合理、约束不冲突、第二天早上库还能正常跑那就是另一回事了。我这些年做过的性能优化和数据迁移项目里至少有三分之一的时间花在“先把数据准备好”上。这篇就把我常用的几套 Oracle 快速构造大量测试数据的做法整理出来从几十万行的小批量到上亿行的大表思路、参数、踩过的坑都写清楚刚入门的朋友照着抄也能跑通做过几年 DBA 的也能从里面捡到几个能提效的细节。1. 造数之前先想清楚的三件事很多人一上来就开始写循环 INSERT跑了一半发现表空间不够或者发现生成的数据字段全是随机字符串业务上根本没法用。造数本质上是一次小型的“数据建模性能工程”动手前先把下面三个问题答清楚后面能省掉大量返工。1.1 这份数据要满足什么“业务形态”测试数据不是随便填满就行它得支撑你要验证的场景。你要压测查询性能那数据的分布就得贴近真实比如订单表里 80% 的订单金额集中在几百元区间只有 5% 是大额订单这样索引选择性才真实执行计划才有参考价值。你要测分页接口那每页 20 条、翻到第 10 万页的响应时间才是重点数据量就必须堆到千万级。你要测报表汇总那日期字段必须跨越多个自然月和年份否则按月分组的聚合结果全挤在一个月里等于没测。我一般会要求业务方给一句话“这批数据要用来验证什么”。如果答案是“验证列表页不卡”那重点就是行数和几个过滤字段的分布如果答案是“验证对账逻辑”那重点就是金额、状态、时间的组合关系必须自洽。想清楚这一点字段生成规则基本就定了。1.2 四条技术路线各管一段场景Oracle 里堆数据量常用的就四条路它们没有绝对优劣只有场景匹配问题。我把它们的真实表现列在下面数据来自我在普通机械盘存储、单实例 11g 和 19c 上的实测区间具体数字会随硬件波动但量级关系是稳定的。方案适合量级大致速度实现难度主要限制INSERT ... SELECT 自复制100 万 ~ 1 亿快分钟级到十分钟级低字段多样性差容易整表重复PL/SQL FORALL 批量绑定10 万 ~ 5000 万中等受逻辑复杂度拖累中逻辑一复杂就慢undo 压力大SQL*Loader 直接路径100 万 ~ 数十亿最快百万行/分钟级中高需要外部文件字段要与文件对齐外部表 CTAS1000 万 ~ 上亿快中依赖目录对象文件必须规整一句话选型只是想快速把表撑大用 INSERT...SELECT 自复制字段要有业务含义用 PL/SQL 批量绑定要上亿行且能落到文件用 SQL*Loader。外部表其实是 SQL*Loader 的孪生兄弟加载机制类似但可以配合并行查询我在做超大表初始化时更偏爱它。1.3 一个真实项目的选型过程去年做一个计费系统的容量验证目标表是明细流水要求 8000 万行字段包括用户 ID、产品编码、用量、单价、金额、出账时间、状态。客户环境是一台 16 核、64G 内存、机械盘的测试库undo 表空间只有 8G。我先评估了 PL/SQL 路线8000 万行走 FORALL每批 1 万行加上随机数生成和字符串拼接实测每小时大概能跑 300 万到 500 万行跑完要十几个小时而且 undo 压力大容易撞上 ORA-01555。这个方案被否掉。再看 SQL*Loader我写了段 Python 生成 8 个 CSV 文件每个 1000 万行然后用directtrue paralleltrue起 8 个会话并行加载整体耗时压到了 40 分钟左右中间没碰 undo 的边因为直接路径加载基本不写 undo。最后选了这条路唯一的代价是磁盘上多占了约 30G 的临时文件跑完删掉就行。这个案例说明一个道理造数方案的瓶颈往往不在 Oracle 本身而在你愿不愿意把数据先落成文件。一旦落到文件就可以并行、可以断点续传、可以反复重跑比在数据库里写循环健壮得多。2. 速度瓶颈到底卡在哪里同样是一百万行插入有人跑三分钟有人跑三小时差距往往就出在几个会话级和语句级的参数上。这部分是全文最值钱的地方理解了原理后面所有优化都是顺水推舟。2.1 慢的根源commit 次数和 undo/redo 的代价Oracle 保证事务的原子性靠的就是 undo 和 redo。你每插一行就 commit 一次意味着每次都要写 redo 日志、更新 undo 段、还要等待日志写入落盘这个开销远大于插入本身。极端情况下逐行提交的速度可能只有批量提交的几十分之一。我做过一个对比测试在同一张宽表上插 100 万行提交方式耗时说明每行 commit约 52 分钟基本不可接受每 1000 行 commit约 4 分 20 秒常见默认做法每 10000 行 commit约 2 分 10 秒推荐区间每 100000 行 commit约 1 分 55 秒提升有限undo 峰值变高可以看到从每行提交到每万行提交是数量级的提升但从一万行到十万行收益就很小了反而把 undo 峰值抬高了。所以 commit 频率不是越大越好找到一个“速度够快、undo 不爆”的平衡点才是正解。2.2 批量绑定为什么快PL/SQL 里逐行执行 SQL每条语句都要经历一次上下文切换PL/SQL 引擎把语句交给 SQL 引擎、SQL 引擎解析执行、再把结果传回来。这个来回切换的固定开销在循环一万次时就会变成明显负担。FORALL和数组绑定做的事就是把一万条 INSERT 攒成一个数组一次性交给 SQL 引擎处理上下文切换从一万次降到一次效率差出好几倍是正常现象。这也是为什么我在造数脚本里几乎不用裸FOR循环里直接INSERT而是先把数据塞进集合再用FORALL一把刷进去。2.3 几个关键的建表和会话参数造数阶段有几个参数值得单独调它们直接影响写入吞吐-- 让表在造数期间不记日志减少 redo 生成 ALTER TABLE t_order NOLOGGING; -- 会话级commit 时不必等待 redo 落盘11g 常用12c 之后谨慎使用 ALTER SESSION SET COMMIT_WRITE BATCH, NOWAIT; -- 关闭约束检查的开销仅测试环境且要能确保数据本身合规 ALTER TABLE t_order DISABLE CONSTRAINT fk_order_cust; -- 造数完成后记得改回来 ALTER TABLE t_order LOGGING; ALTER TABLE t_order ENABLE CONSTRAINT fk_order_cust;注意NOLOGGING不是“不写 redo”的万能开关它只对直接路径插入、索引重建等特定操作生效。普通 INSERT 即使是 nologging 表照样会生成 redo。要真正吃到这个红利语句里必须带/* APPEND */提示走直接路径。另外外键约束在造数期间是纯负担因为每条插入都要去父表校验一次。测试环境里只要保证数据关系自洽直接禁用外键速度能提升 20% 到 50%。但记住一件事禁用外键意味着脏数据不会被拦住所以造数脚本里的父表 ID 必须提前准备好别指望数据库帮你查。3. 实操一INSERT ... SELECT 自复制法这是最省事、最快出效果的一招适合“我只想把表撑大、字段内容不重要”的场景比如测索引扫描速度、测分页性能。3.1 翻倍法一条语句把数据乘二核心逻辑只有一句把表里现有的数据再插一遍行数翻倍。反复执行行数按 2 的 N 次方增长。-- 假设 t_order 已有 1000 行作为种子数据 INSERT /* APPEND */ INTO t_order ( order_id, cust_id, product_code, amount, order_date, status ) SELECT seq_order_id.NEXTVAL, -- 主键用序列避免唯一键冲突 cust_id, product_code, amount, order_date, status FROM t_order; COMMIT;从 1000 行开始执行 10 次翻倍得到约 100 万行执行 17 次得到约 1.3 亿行。每次执行的耗时几乎一样因为每一轮的输入量都在翻倍但相对于总行数来说单轮耗时并不夸张。这里的关键点是主键必须换成序列生成如果你直接复制原主键第二轮就会撞上唯一约束报 ORA-00001。3.2 用 connect by level 造连续序列只有种子数据、没有表怎么办Oracle 的层次查询可以凭空造出连续数字序列这是造数脚本里最常用的一招。-- 生成 1 到 100 万的连续数字 SELECT LEVEL AS n FROM dual CONNECT BY LEVEL 1000000;这条语句在 11g 和 19c 上都能跑10 万行秒级返回100 万行也就一两秒。它的原理是CONNECT BY会不断递归生成行LEVEL是递归深度限制到 100 万就得到 100 万行。有了这个序列你就可以直接用 CTAS 造表不需要任何种子数据CREATE TABLE t_order NOLOGGING AS SELECT LEVEL AS order_id, TRUNC(DBMS_RANDOM.VALUE(1, 100000)) AS cust_id, P || LPAD(TRUNC(DBMS_RANDOM.VALUE(1, 9999)), 4, 0) AS product_code, ROUND(DBMS_RANDOM.VALUE(1, 99999), 2) AS amount, TRUNC(SYSDATE) - TRUNC(DBMS_RANDOM.VALUE(0, 3650)) AS order_date, CASE WHEN DBMS_RANDOM.VALUE 0.7 THEN PAID WHEN DBMS_RANDOM.VALUE 0.9 THEN UNPAID ELSE CANCELLED END AS status FROM dual CONNECT BY LEVEL 1000000;一条语句出来 100 万行带业务含义的数据全程不走 PL/SQL 循环速度非常可观。CTAS本身也可以带NOLOGGING关键字直接路径写入redo 生成大幅减少。3.3 把随机字段拼进 SELECT字段生成的“配方”我整理成了一张表直接套用就行。所有函数都写在 SELECT 里避免过程化逻辑拖慢速度。字段类型生成写法说明主键LEVEL或seq.NEXTVAL保证唯一避免约束冲突客户 IDTRUNC(DBMS_RANDOM.VALUE(1, 100000))制造重复模拟真实关联产品编码P金额ROUND(DBMS_RANDOM.VALUE(1, 99999), 2)保留两位小数日期TRUNC(SYSDATE) - TRUNC(DBMS_RANDOM.VALUE(0, 3650))分散到近十年手机号1状态CASE WHEN DBMS_RANDOM.VALUE 0.7 ...控制各状态占比18 位证件号见下方代码地区码生日顺序码证件号这种需要符合格式的字段我一般拆成几段拼接既像真的又不需要算校验位测数据不必过真实校验SELECT 110101 || TO_CHAR(DATE 1990-01-01 TRUNC(DBMS_RANDOM.VALUE(0, 9000)), YYYYMMDD) || LPAD(TRUNC(DBMS_RANDOM.VALUE(0, 1000)), 3, 0) || TRUNC(DBMS_RANDOM.VALUE(0, 10)) AS id_card FROM dual CONNECT BY LEVEL 1000;这里顺手提一个热词里经常出现的坑证件号在 SQL 客户端里被显示成科学计数法比如1.10101E17。这不是数据错了是客户端把 18 位数字当成浮点数渲染。解决办法是查询时用TO_CHAR(id_card)转成字符串或者建表时直接把该字段定义成VARCHAR2(18)从源头避免精度问题。3.4 这一招的适用边界与注意点自复制法的最大问题是数据多样性差。翻倍法本质上是把同一批数据复制 N 份除了主键以外其余字段完全重复。如果你的测试场景对字段分布敏感比如要测索引选择性、要测去重逻辑这套方法生成的数据会给出失真的结果。我的做法是混合着来先用CONNECT BY加随机函数生成 10 万行“有分布”的种子数据再用翻倍法把这 10 万行乘到 5000 万行。这样既有足够的样本多样性又能快速堆量。另外翻倍法每轮结束一定要COMMIT否则下一轮的 SELECT 读不到未提交数据等于白跑。提示自复制时如果表上有触发器每一行插入都会触发一次触发器逻辑速度可能断崖式下跌。造数前先用SELECT * FROM USER_TRIGGERS WHERE TABLE_NAMET_ORDER确认一下必要时把触发器禁用。4. 实操二PL/SQL 批量绑定造数当字段之间需要满足业务关系比如金额等于用量乘以单价或者需要从多张维表取数时纯 SELECT 就不好使了这时候交给 PL/SQL。但写法上有讲究写不好就是三小时跑一百万行。4.1 一个可以直接复用的 FORALL 模板下面这个模板是我用得最多的骨架核心是“先攒集合再批量刷”DECLARE TYPE t_num IS TABLE OF NUMBER INDEX BY PLS_INTEGER; TYPE t_str IS TABLE OF VARCHAR2(64) INDEX BY PLS_INTEGER; TYPE t_date IS TABLE OF DATE INDEX BY PLS_INTEGER; v_order_id t_num; v_cust_id t_num; v_prod t_str; v_amount t_num; v_order_date t_date; v_batch CONSTANT PLS_INTEGER : 10000; -- 每批行数 v_total CONSTANT PLS_INTEGER : 1000000;-- 目标总行数 v_rounds PLS_INTEGER; BEGIN v_rounds : CEIL(v_total / v_batch); FOR r IN 1 .. v_rounds LOOP FOR i IN 1 .. v_batch LOOP v_order_id(i) : seq_order_id.NEXTVAL; v_cust_id(i) : TRUNC(DBMS_RANDOM.VALUE(1, 100000)); v_prod(i) : P || LPAD(TRUNC(DBMS_RANDOM.VALUE(1,9999)), 4, 0); v_amount(i) : ROUND(DBMS_RANDOM.VALUE(1, 99999), 2); v_order_date(i) : TRUNC(SYSDATE) - TRUNC(DBMS_RANDOM.VALUE(0, 3650)); END LOOP; FORALL i IN 1 .. v_batch INSERT INTO t_order (order_id, cust_id, product_code, amount, order_date) VALUES (v_order_id(i), v_cust_id(i), v_prod(i), v_amount(i), v_order_date(i)); COMMIT; END LOOP; END; /这里有几个细节值得说。序列值我是在循环里先取好放进数组的而不是在FORALL的 INSERT 里直接写seq.NEXTVAL因为FORALL内部对序列的调用有一些场景限制先取出来最稳妥。批量大小v_batch取 10000 是我在多数环境下的经验值内存占用不高速度也够快。4.2 分类型字段的生成写法PL/SQL 里生成数据比纯 SQL 灵活因为可以用变量、可以做分支判断。几个我常用的写法-- 模拟真实的金额分布多数小额少数大额 IF DBMS_RANDOM.VALUE 0.9 THEN v_amount(i) : ROUND(DBMS_RANDOM.VALUE(1, 500), 2); ELSE v_amount(i) : ROUND(DBMS_RANDOM.VALUE(5000, 200000), 2); END IF; -- 状态按比例分布 v_status(i) : CASE WHEN DBMS_RANDOM.VALUE 0.70 THEN PAID WHEN DBMS_RANDOM.VALUE 0.90 THEN UNPAID ELSE CANCELLED END; -- 从维表随机挑一个有效值比随机数更像真实关联 SELECT MIN(cust_id) INTO v_min_cust FROM t_customer; SELECT MAX(cust_id) INTO v_max_cust FROM t_customer; v_cust_id(i) : TRUNC(DBMS_RANDOM.VALUE(v_min_cust, v_max_cust 1));最后这个写法很实用。如果你只是想生成一个“看起来像外键”的值直接随机数会产生大量不存在的父键后期做关联查询时结果全是空。正确做法是先查出父表主键的实际范围在这个范围内取值命中率会高很多。如果父表主键不是连续的那就把主键集合读进一个数组用v_keys(TRUNC(DBMS_RANDOM.VALUE(1, v_keys.COUNT1)))随机取这样能保证 100% 命中。4.3 commit 批次怎么算才安全批次大小不是拍脑袋定的可以用 undo 反推。假设每行数据加索引的 undo 记录平均 500 字节undo 表空间可用 4G同时还有别的业务在跑给造数留 1G 比较安全单批 undo 占用 批次行数 × 500 字节 安全批次行数 1G / 500字节 ≈ 200 万行理论上可以开很大但实际我不会超过 10 万行一批原因有三个一是单批越大回滚一次要等的时间越长二是 undo 保留策略是按时间的批次越大单事务持续越久越容易触发 ORA-01555三是中间万一报错已经提交的部分不会丢可以从中断处继续批次小一点损失更小。实测下来1 万到 5 万行一批是个比较舒服的区间既拿到了批量提交的绝大部分收益又把风险控制住了。4.4 注意事项PL/SQL 造数有几个反复踩的坑。第一个是DBMS_RANDOM不设种子时每次会话生成不同序列如果你需要结果可复现用DBMS_RANDOM.SEED(12345)显式指定。第二个是集合类型如果用VARRAY或嵌套表而不是INDEX BY关联数组行数多了会吃大量 PGA 内存务必用INDEX BY PLS_INTEGER。第三个是循环变量i在每一轮FORALL里都要从 1 重新开始如果你写成累加数组下标会越界报 ORA-06533。注意FORALL遇到任何一行报错默认整批回滚并抛异常。如果你想跳过个别坏行继续得用FORALL ... SAVE EXCEPTIONS配合SQL%BULK_EXCEPTIONS收集错误行这个在造数时一般用不上但做数据清洗时很有用。5. 实操三SQL*Loader 直接路径加载当目标行数上了千万甚至上亿PL/SQL 的短板就暴露了所有数据都要在数据库内部生成undo、redo、索引维护的开销全都压在库上。SQL*Loader 直接路径绕过了 SQL 引擎数据直接从文件灌进数据块速度快一个档次。5.1 用脚本生成规整的数据文件我用 Python 生成 CSV因为它跨平台、写起来快而且能精确控制字段格式。下面是生成 100 万行订单数据的脚本import csv, random, datetime random.seed(20240501) start datetime.date(2015, 1, 1) status_pool [PAID] * 70 [UNPAID] * 20 [CANCELLED] * 10 with open(order_data_1.csv, w, newline\n, encodingutf-8) as f: w csv.writer(f) for i in range(1, 1000001): cust_id random.randint(1, 100000) prod P%04d % random.randint(1, 9999) amount round(random.uniform(1, 99999), 2) od start datetime.timedelta(daysrandom.randint(0, 3650)) status random.choice(status_pool) w.writerow([i, cust_id, prod, amount, od.strftime(%Y-%m-%d), status])生成 8 个这样的文件每个 100 万行总耗时大概两三分钟文件总体积 400M 左右。这里的关键是日期字段一定用字符串格式YYYY-MM-DD输出加载时在控制文件里指定转换格式比让数据库自己去猜稳得多。5.2 控制文件怎么写控制文件是 SQL*Loader 的核心字段顺序、分隔符、日期格式全在这里定义LOAD DATA INFILE order_data_1.csv INFILE order_data_2.csv INFILE order_data_3.csv INFILE order_data_4.csv INFILE order_data_5.csv INFILE order_data_6.csv INFILE order_data_7.csv INFILE order_data_8.csv APPEND INTO TABLE t_order FIELDS TERMINATED BY , OPTIONALLY ENCLOSED BY TRAILING NULLCOLS ( order_id, cust_id, product_code, amount, order_date DATE YYYY-MM-DD, status )APPEND表示追加到已有数据如果想清空重来就换成TRUNCATE但要注意TRUNCATE在直接路径下会先删数据再加载速度快但不可回滚。TRAILING NULLCOLS的作用是当行尾字段为空时把剩余字段置为 NULL 而不是报错处理不规整文件时很必要。5.3 并行与直接路径的加载命令命令本身很短所有优化都在参数里sqlldr useridapp/app123orcl \ controlorder_load.ctl \ logorder_load.log \ badorder_load.bad \ directtrue \ paralleltrue \ errors100000 \ rows10000directtrue是关键它让加载走直接路径跳过 SQL 引擎基本不生成 undo。paralleltrue会为每个 INFILE 分配一个加载会话8 个文件就是 8 路并行吞吐能翻好几倍。rows10000在直接路径下其实被忽略因为直接路径是按数据块批量写入的写在这里只是为了让配置看起来完整。实测这个配置在 16 核机械盘机器上800 万行加载耗时约 4 分钟也就是每分钟 200 万行左右比 PL/SQL 快了十倍以上。5.4 注意事项直接路径加载有几个硬性限制必须提前知道。第一目标表上有触发器或外键约束且未禁用时直接路径会自动降级为常规路径速度立刻掉下来。所以加载前务必禁掉触发器和外键加载完再启用。第二直接路径加载期间表会被锁住其他会话无法对它做 DML生产库上千万慎用。第三加载完成后表的高水位线会被顶上去即使删了数据全表扫描还是扫那么多块测完了记得用ALTER TABLE t_order MOVE或者干脆 truncate 重建。提示如果加载后日志里出现大量Record N: Rejected - Error on table T_ORDER先看.bad文件里的原始行。最常见的三种原因是字段数量与文件列数不匹配、日期格式和实际内容对不上、数值字段里混进了空字符串。把.bad文件头几行拉出来一看就知道问题在哪。6. 常见问题与排查技巧实录造数过程中遇到报错是常态下面这张表是我这些年攒下来的高频问题清单基本覆盖了九成以上的现场情况。6.1 报错与现象速查表现象或报错常见原因处理方向ORA-01555 snapshot too old单事务太大undo 被覆盖减小 commit 批次扩大 undo 表空间ORA-01653 表空间不足数据量估算不准提前算容量加数据文件或开自动扩展ORA-01652 无法扩展临时段排序或哈希操作吃满 temp扩大 temp 表空间或分批处理ORA-00001 唯一约束冲突自复制时主键没换序列主键改用seq.NEXTVAL或LEVELORA-02287 此处不允许序列号在FORALL/子查询里直接调序列循环外先取序列值存入变量加载后查询结果为空串客户端把空格当 NULL 处理用TO_CHAR转换或检查 NLS 设置数字字段显示成科学计数法客户端按浮点渲染超长数字建表用VARCHAR2查询用TO_CHAR造数后查询突然变慢统计信息过期计划走偏手动收集统计信息这张表里ORA-01555 和统计信息过期是两个最容易被误判为“数据库坏了”的问题其实都是使用方式导致的。ORA-01555 你只要把批次调小八成能解决统计信息这个下面单独讲。6.2 三个最容易忽略的坑第一个坑是随机值边界。TRUNC(DBMS_RANDOM.VALUE(1, 100))生成的是 1 到 99不包含 100因为上界是开区间。如果你想生成 1 到 100得写TRUNC(DBMS_RANDOM.VALUE(1, 101))。这个细节在生成“期望值”数据时特别容易出错比如你要模拟一个枚举值只有 1 到 5 的字段写错了就会漏掉最大值。第二个坑是时区与日期截断。TRUNC(SYSDATE)会把时间部分截掉得到当天零点。如果你的应用对时间敏感比如要求订单时间分布在一整天内只用TRUNC会让所有订单都堆在零点索引选择性完全失真。正确写法是叠加随机的小数天TRUNC(SYSDATE) - TRUNC(DBMS_RANDOM.VALUE(0,3650)) DBMS_RANDOM.VALUE(0,1)这样时间能均匀落在一天内。第三个坑是造数脚本没有幂等性。很多脚本跑一半失败了重跑时又把数据从头插一遍结果总量翻倍。我的习惯是在脚本开头做一次“目标行数检查”或者干脆每轮跑之前先 truncate绝不依赖“跑失败的部分会自动消失”。批量加载文件方案天生幂等重跑最多是重复加载配合TRUNCATE选项就能保证每次结果一致。注意测试库再造数也别把NOLOGGING当成永久设置。很多团队造完数忘了改回来结果这张表后续的真正业务数据也不记日志一旦出现介质损坏恢复时这些块直接报坏得不偿失。建一个收尾检查列表造数结束逐项勾掉。7. 造完数据之后的收尾工作数据灌进去只是第一步能不能真正拿来用取决于收尾做得好不好。这一步做漏了前面省下的时间都会在排查执行计划时还回去。7.1 统计信息必须重新收集Oracle 的优化器靠统计信息估算行数和选择率造数刚刚改变了数据量如果统计信息还是空表时的旧数据执行计划会离谱到什么程度我曾经见过一张 5000 万行的表优化器以为它只有 1000 行于是选择了全表扫描而不是索引一个分页查询跑了 40 秒。造完数第一时间收集统计信息BEGIN DBMS_STATS.GATHER_TABLE_STATS( ownname APP, tabname T_ORDER, estimate_percent DBMS_STATS.AUTO_SAMPLE_SIZE, method_opt FOR ALL COLUMNS SIZE AUTO, degree 8, cascade TRUE ); END; /cascade TRUE会连同表上的索引一起收集省得再单独跑。degree 8开启并行收集大表能快很多。estimate_percent用AUTO_SAMPLE_SIZE让 Oracle 自己决定采样比例19c 上效果不错11g 上如果数据分布有明显倾斜可以手动指定 30% 甚至 100%。7.2 索引的禁用与重建如果你是在已有索引的表上批量造数插入过程会不断维护索引速度会明显下降。我的做法是造数前把非唯一索引置为不可用造完再重建-- 造数前让索引暂时不可用DML 不再维护它 ALTER INDEX idx_order_cust UNUSABLE; -- 造完数据后重建索引nologging 减少 redo ALTER INDEX idx_order_cust REBUILD NOLOGGING PARALLEL 8; ALTER INDEX idx_order_cust NOPARALLEL;这里有个细节要强调唯一索引不能随便置为 unusable因为一旦不可用任何 DML 都会报错原因是要维持唯一性却没了索引。所以唯一索引要么保留要么直接 drop 掉造完再建。另外重建索引后记得把并行度改回 1否则后续查询可能因为索引上的并行度设置而走偏。7.3 测试数据的清理与还原测试数据用完必须清理干净尤其在同一套库上要跑多轮测试时。清理优先级是这样的-- 最快整表清空高水位线一并重置 TRUNCATE TABLE t_order; -- 只想删一部分按条件删但高水位线不会降记得 MOVE DELETE FROM t_order WHERE order_date DATE 2020-01-01; COMMIT; ALTER TABLE t_order MOVE; -- 重建段降低高水位线 ALTER INDEX idx_order_cust REBUILD;TRUNCATE是不可回滚的 DDL清空前确认确实不要了。DELETE加MOVE的组合适合要保留部分数据的场景但MOVE会锁表且需要额外空间做之前算好表空间。我个人的习惯是给造数环境单独建一个用户测试表全部放在这个用户下一轮测试结束直接DROP USER ... CASCADE重建干净利落不用担心残留数据影响下一轮。这套流程跑熟之后从建表、造数到收尾一整套下来也就半小时。最后分享一个用久了才体会到的经验造数脚本一定要放进版本管理。我见过太多次同事临时写了段脚本跑完就丢了两个月后要复现同一个测试场景只能从头再写一遍而且写出来的数据分布和上次不一样两次测试结果根本没法对比。把ctl文件、Python 生成脚本、建表 DDL、统计信息收集语句放在一个目录里配一个简单的 shell 串起来下次要用改个参数就能跑这才是真正把效率沉淀下来。