做数据库压测的人迟早会碰到Swingbench。它默认带的那套订单基准(Order Entry schema)用来压OLTP确实挺顺手,但真到要模拟业务侧的特殊读写比例时,你多半会对着OracleSQLBuilder犯嘀咕——这玩意儿到底能怎么自定义SQL脚本?这篇文章就把SQLBuilder功能的完整用法、配置细节和我在实际压测中踩过的坑一次性讲清楚,内容包括脚本编写规范、事务处理方式、配置参数逻辑、启动验证方法,以及几个很容易让人卡半天的问题排查思路。如果你是第一次接触Swingbench,或者已经用过默认基准但想切到自定义SQL场景,这篇文章都值得看完。
1. SQLBuilder的定位:它解决的到底是什么问题
1.1 Swingbench负载架构与SQLBuilder的角色
Swingbench默认压测是围绕OE(Order Entry)这个业务模型跑的,后台有一整套预置的业务表,包括客户、订单、商品、库存这些。这套基准负载非常成熟,适合压测典型的OLTP读写混合场景,官方文档里也给出了不少参考指标。但问题在于,生产环境里的业务没那么规整,有的是大批量报表查询,有的是定时归档任务,有的是APP里固定几条SQL反复执行。这些非常规场景用默认基准模拟不了,你总不能为了测一条慢查询而去造几万条订单。
SQLBuilder就是干这个的。它是Swingbench的负载生成模块之一,核心能力就是让用户直接丢一个SQL脚本进去,由Swingbench建一批并发会话去执行这个脚本。脚本可以是单个SQL语句,也可以是一整段PL/SQL匿名块。你不需要装额外的插件,也不用写Java代码,只要把脚本文件放到指定目录,配置好LoadModule就能跑。
1.2 非共享连接模式为什么是SQLBuilder的标配
SQLBuilder在默认设计上走的是无状态连接,也就是每次执行脚本时新建一个物理连接,执行完就释放。这个设计容易被忽略,但它恰恰决定了自定义脚本写法的很多细节。无状态的好处是隔离性强,每个会话之间互不干扰,不会因为上一个脚本设置了alter session或修改了NLS参数而影响下一个脚本的行为。
如果你用共享连接模式,多个线程会复用同一个session,这时候脚本里一旦出现session级别的状态变化,比如alter session set nls_date_format,后续所有复用到这个连接的会话都会受影响,测试结果基本没法看。所以Swingbench给SQLBuilder默认推荐非共享连接,目的就是让每个线程的脚本执行环境尽可能干净。
1.3 为什么推荐用SQLBuilder而不是直接改OE基准
有人会想,既然OE基准也是表结构,那我直接往OE表里插自定义数据、改SQL不就行了?理论上可以,但实际维护成本很高。OE基准的负载逻辑和它的存储过程、触发器等绑定得很紧,改一处可能带动一片。而且OE基准自带的负载生成器在压测时会不断调整数据分布,你和它抢同一批表会导致结果互相污染。
SQLBuilder的好处是彻底脱离OE表,你可以完全掌控脚本里的SQL操作对象。想压测哪张表就压哪张,想怎么操作就怎么写。我通常会在压测库单独建一个业务用户,再按生产环境的结构创建自己需要的测试表,然后把这些DML和查询逻辑写进脚本。这样既干净,也能精准模拟目标业务的访问模式。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手前的准备:目录结构、数据库账号与测试对象
2.1 Swingbench目录里必须搞清楚的位置
Swingbench安装完成后,有一部分目录是你日常一定会碰到的。不提前分清,后面配置的时候很容易把脚本放错位置,然后花半小时排查为什么找不到文件。
| 目录/文件 | 作用 |
|---|---|
| $SWINGBENCH_HOME/bin | 存放swingbench、spawn、wizard等可执行脚本 |
| $SWINGBENCH_HOME/config | 存放Swingbench.xml等主配置文件 |
| $SWINGBENCH_HOME/sql | SQLBuilder默认加载SQL脚本的目录 |
| $SWINGBENCH_HOME/logs | 运行时日志输出目录 |
重点记住sql目录。自定义脚本默认放这里,配置里的脚本名通常也是相对于这个目录的路径。图形界面加载脚本时,如果文件名没写完整,它会在sql目录下找,找不到就报错。这个问题在Windows和Linux上都有,而且大小写敏感,一定要按实际文件名写。
2.2 数据库账号和权限怎么规划
自定义SQL脚本里通常会有INSERT、UPDATE、SELECT这些操作,如果脚本涉及建表、删表、建索引,那还需要DDL权限。我的建议是不要在Swingbench里用SYS或SYSTEM账号跑压测,虽然配置连接时可以通过sysdba方式连上去,但生产压测用超级账号会掩盖真实业务权限下的性能问题,而且一旦脚本写错,影响面太大。
最稳妥的做法是单独建一个压测专用用户,例如loadtest,授予足够的表空间配额,再根据脚本内容给DML权限。如果需要DDL,就临时授权,压测完再收回去。这样既安全,又能保证脚本在权限边界内正常运行,不会出现权限过大导致的行为与生产不一致。
2.3 创建测试对象:以一张某业务订单表为例
SQLBuilder本身不负责建表,表结构得提前准备好。你可以用sqlplus手动建,也可以把所有建表语句写进一个初始化脚本单独执行。下面是一个简单的示例结构,后面所有演示都基于这张表。
sql复制CREATE TABLE loadtest.app_order
(
order_id NUMBER PRIMARY KEY,
customer_id NUMBER,
amount NUMBER(10,2),
order_time DATE DEFAULT SYSDATE
);
CREATE INDEX idx_app_order_cust ON loadtest.app_order(customer_id);
这张表结构很简单,但足够说明问题。真实场景里你可能还有状态字段、城市字段、渠道字段等,用于模拟业务过滤条件。建立测试对象时我习惯把表和索引的初始化语句单独放一个文件,比如init_ddl.sql,先用sqlplus执行一次,后面压测脚本里就不再重复建表,避免Oracle DDL隐式提交造成事务混乱。
3. 核心实操:SQL脚本编写、配置与启动验证全流程
3.1 SQL脚本书写规范:set、spool、commit、exit一个都不能少
SQLBuilder执行的不是那种“拼接SQL字符串然后发送”的动态语句,它拿到的就是纯文本文件,脚本内容必须能被SQLPlus逐行解析。所以脚本里的每一行都得符合SQLPlus的语法规则。
下面是一份我常用的脚本模板,功能是循环向app_order表插入50行数据,并提交事务。
sql复制-- my_custom_load.sql
set echo off
set verify off
set feed off
spool my_custom_load.log append
PROMPT load_start
DECLARE
v_order_id NUMBER;
BEGIN
FOR i IN 1..50 LOOP
SELECT NVL(MAX(order_id),0) + 1 INTO v_order_id FROM loadtest.app_order;
INSERT INTO loadtest.app_order(order_id, customer_id, amount, order_time)
VALUES (v_order_id, MOD(DBMS_RANDOM.VALUE(1,1000),1000) + 1,
ROUND(DBMS_RANDOM.VALUE(10,1000),2), SYSDATE);
END LOOP;
COMMIT;
END;
/
PROMPT load_end
COMMIT;
EXIT;
脚本前半部分的set命令很多人不理解,觉得可有可无。这里我解释一下:set echo off是不在输出里回显脚本内容,set verify off是关闭变量替换确认信息,set feed off是关闭行数统计输出。如果不关掉这些,spool日志会被大量无关内容刷屏,压测完你想看执行情况,日志几十万行根本没法定位。
核心逻辑在BEGIN块里。通过PL/SQL匿名块循环插入,相比在脚本里写50条独立的INSERT,能显著减少SQL语句在客户端与数据库之间的网络往返,更接近真实批量处理场景。块结束后必须COMMIT,否则事务一直不提交,一方面UNDO压力大,另一方面多个并发session同时操作同一张表时容易相互阻塞。
spool和PROMPT也是关键。spool会把执行结果输出到日志文件,PROMPT通过在日志里打标记,让你能快速看到脚本执行到哪一步、每步耗时多久。EXIT务必放在脚本末尾,如果漏了,会话可能不主动断开,SQLBuilder等待连接释放时会超时或产生一堆残留session。
3.2 把脚本放进sql目录并初始化测试数据
脚本写好后,放到$SWINGBENCH_HOME/sql目录下。文件名建议用全小写加下划线,避免不同系统之间大小写敏感的问题。我一般会在sql目录下建一个子目录,比如sql/custom/,这样默认OE基准脚本和自定义脚本分开,配置时脚本路径写成custom/my_custom_load.sql即可。
表结构初始化建议先手动执行。用sqlplus连到loadtest用户,执行之前写好的init_ddl.sql。这样做的目的是把DDL和DML分开,防止在同一个脚本里出现“先建表后插入”时,DDL的隐式提交打乱后面事务的预期边界。初始化完成后,可以随手查一下表里有没有数据,确认表空间和权限都正常。
3.3 配置SQLBuilder:图形界面和spawn两种路径
图形界面配置比较直观。启动swingbench后,在Load Options里选择Connection Mode为非共享连接,然后在load generator类型里选SQLBuilder。系统会要求指定脚本路径、JDBC连接串、用户名密码、并发数Scale等参数。填完以后点运行,SQLBuilder就会按Scale设定的会话数并发执行脚本。
如果是要无人值守压测,或者同时起多组压测进程跑不同场景,spawn方式更可靠。spawn通过XML配置文件定义压测进程的启动参数。一份典型的spawn配置里,和SQLBuilder相关的模块大致长这样:
xml复制<Load>
<Group>
<LoadModule name="SQL_Builder" class="oracle.sem.sqlbuilder.OracleSQLBuilder">
<Parameter name="Script" value="custom/my_custom_load.sql"/>
<Parameter name="SqlUrl" value="jdbc:oracle:thin:@//localhost:1521/ORCL"/>
<Parameter name="SqlUser" value="loadtest"/>
<Parameter name="SqlPassword" value="secret"/>
<Parameter name="Scale" value="5"/>
</LoadModule>
</Group>
</Load>
不同小版本的Swingbench在XML结构上会略有差异,但核心参数基本一致。如果你不确定class属性和参数名,建议先在图形界面里手动配好一份,用“导出配置”功能生成XML,再照着改,比从零手写要稳得多。我在实际使用中经常遇到版本升级后旧配置直接失效的情况,这时候图形界面生成的配置文件就是最可靠的参照。
3.4 关键参数含义:Scale、Interval、Iterations怎么填
SQLBuilder最常用的参数是Script路径、JDBC URL、用户名密码、Scale(并发连接数)、Interval(每次迭代间隔毫秒数)、Iterations(迭代次数)。这些参数互为配合,直接决定压测流量形态。
- Scale:并发连接数。非共享连接模式下,每个连接都是一个独立session,Scale越高对数据库连接数和CPU的压力越大。
- Interval:每个迭代之间的等待时间,单位毫秒。设置后可以让事务流更平滑,避免所有session同时启动造成尖峰。
- Iterations:每个连接执行脚本的轮数。如果设置为0,表示一直循环执行直到手动停止。
- Silent:是否抑制SQL输出。建议开启,减少日志量。
图形界面里这些参数都有对应输入框,但spawn配置文件里不一定都包含,需要看版本。我的做法是先按最小配置跑通,再逐步加参数,不要上来就把Interval设成0加高Scale,否则数据库瞬间被流量打满,你根本分不清是脚本慢还是连接开销大。
3.5 启动后怎么确认SQLBuilder真的在跑你的脚本
很多人配置完就点运行,结果压力跑了半天,回头一看跑的其实是默认基准,这种情况太常见了。验证方法其实很简单。第一,看spool日志,脚本里只要有PROMPT,日志里就会有对应的load_start和load_end标记。第二,在数据库会话层面查当前正在执行的SQL:
sql复制SELECT sql_id, sql_text, machine, program, action
FROM v$session
WHERE username = 'LOADTEST'
AND status = 'ACTIVE';
如果能看到app_order相关SQL或者脚本里的PL/SQL块内容,说明SQLBuilder确实在工作。如果查出来的全是OE基准表,那说明配置里load generator没切换干净。这一步一定不要省,尤其是用了spawn方式启动时,配置文件多了容易搞混。
4. 参数背后的逻辑:脚本粒度、绑定变量与并发调优
4.1 脚本粒度怎么设计:长事务还是短事务
脚本里那段逻辑执行多久、提交频率如何,直接影响压测结果。如果脚本太短,比如一条快速SELECT加上立即提交,那么测出来的指标主要反映数据库处理短事务的效率;如果脚本很长,比如一个PL/SQL循环里做了几十次DML,最后才提交一次,那测出来的就更接近批量处理场景。
所以写脚本前要先想清楚目标是什么。如果目的是模拟在线交易系统,脚本粒度要短,事务尽快提交,Scale要适当高一些,Interval可以设很小。如果目的是模拟夜间批量任务,单个脚本事务周期就要拉长,每次迭代做更多操作,提交频率降低。实际操作中我会把同一套逻辑写成两个版本,一个短事务版一个长事务版,分别跑一遍,再对比数据库的等待事件差异,这样能更清楚系统瓶颈在IO还是锁竞争还是SQL解析。
4.2 绑定变量与随机值:为什么不能所有线程执行相同的SQL
脚本如果写得死板,比如所有session都执行同一个固定ID的查询,很容易把目标行集中在同一个数据块上,造成严重的热块竞争。这不是真实业务行为,压测结果自然失真。因此脚本里最好引入随机值。上面示例中用了DBMS_RANDOM,每个连接执行插入时customer_id和amount都是随机的,这样能更好地分散IO和锁竞争。
但要注意,DBMS_RANDOM本身有CPU开销,并发越高开销越明显。如果你压测的目的是考察数据库整体性能,而不是测随机函数本身,也可以在应用侧预先准备一批参数文件,或者用更规律的伪随机方式控制取值范围。总之,绑定变量和随机值的意义是让SQL形态稳定、数据分布分散,两者缺一不可。
4.3 并发数、间隔和迭代数量的调优顺序
我调并发参数时遵循一个原则:先用最低配验证正确性,再翻倍调。具体来说:
- 先用Scale=1,Interval=1000,Iterations=2跑一遍,确认脚本本身没有语法错误,spool日志正常输出,数据库v$session里能看到对应用户的活跃会话。
- 然后Scale设为5,Interval设500,Iterations设10,观察TPS和响应时间曲线是否平滑。
- 最后根据目标,逐步调高Scale、调低Interval,直到数据库关键指标接近预期水位。
这样做是因为,如果脚本本身存在性能问题,比如某条SQL忘了绑定变量导致大量硬解析,高并发下压出来的TPS没有参考价值,只会掩盖数据库真实能力。先低并发跑通,相当于先把基础质量确认好了,再上高并发才有意义。
5. 常见问题与排查技巧实录
5.1 现象:脚本没有执行,日志里什么都没有
这个是最常见的开局问题。排查顺序如下:先看$SWINGBENCH_HOME/logs下的运行日志,有没有明确报错;再确认脚本路径写的是相对路径还是相对sql目录路径,文件名大小写是否完全一致;然后把脚本用sqlplus手动跑一遍,确认脚本本身没有SQL语法错误;如果都正常,看spool文件是否生成,如果文件生成但内容是空的,说明脚本里的输出被set echo off和set feed off屏蔽了,可以临时在脚本里加一行PROMPT test,看日志里是不是出现test字样。
5.2 现象:ORA-04043或其他对象不存在的报错
脚本里如果写了DROP TABLE,但表正被其他会话锁住,DROP就会失败并中断脚本。解决建议:尽量用TRUNCATE代替DROP,TRUNCATE对锁的依赖更小;如果必须DROP,把DROP语句放到独立初始化脚本里,确保没有其他会话占用目标表;还可以在DROP之前查一下v$session里有没有阻塞会话,有就先处理。另一个相关问题是建表脚本和DML脚本混在一起,DDL隐式提交打乱事务边界,导致后续DML报错。这种情况把DDL单独拆到init脚本就能解决。
5.3 现象:配置了SQLBuilder但Swingbench仍然在跑默认基准
这种情况多半是Load Options里同时启用了多个负载模块,默认的OrderEntry load generator没被禁用。Swingbench允许同时挂多个load generator,这是设计功能,但很多人不知道。打开配置界面,把除了SQLBuilder以外的模块全部禁用或删除,再重新启动压测。用spawn方式时也要检查XML配置里是不是只保留了一个LoadModule。这个坑很隐蔽,因为Swingbench图形界面往往不会主动提示你还有默认模块在运行。
5.4 现象:并发压测过程中出现大量锁等待或死锁
自定义脚本如果多个session同时UPDATE同一批数据,且事务不及时提交,非常容易产生行锁竞争。排查时看v$lock或v$session里阻塞事件的类型,如果大量enq: TX - row lock contention,基本可以断定是锁竞争而不是SQL本身慢。缓解办法是调整Interval,给事务之间留出时间间隔;或者修改脚本更新逻辑,让每个session更新不同的数据分片;如果业务允许,把UPDATE改为INSERT提高并发度。总之要记住,SQLBuilder的并发模型就是多个独立连接,它不会帮你做任何锁协调,锁管理完全靠脚本自己控制。
5.5 现象:ORA-12516 TNS监听程序找不到可用处理器
这个是数据库连接数被打满的典型报错。SQLBuilder非共享连接下,Scale设多少就会建多少物理连接,如果数据库的processes参数或sessions上限不够,就会出现这个错。启动压测前先查一下:
sql复制SELECT resource_name, current_utilization, max_utilization, limit_value
FROM v$resource_limit
WHERE resource_name IN ('processes','sessions','sessions_per_user');
根据limit值调整Scale,或者适当放宽数据库参数。另外,生产环境如果应用本来就用连接池,你可以把SQLBuilder的Connection Mode改成共享连接来模拟真实应用行为,这样连接数不会无限增长,但要注意之前说过的session状态影响问题。
5.6 一个容易忽略的坑:脚本里不要忘记EXIT
很多从sqlplus转过来的朋友写脚本习惯写SQL和COMMIT,但忘了EXIT。在SQLBuilder的场景里,脚本末尾不EXIT,那这个连接在脚本执行完以后不会主动断开会话,SQLBuilder会认为脚本还没结束,一直等在那里。后果就是压测跑了一轮后,连接迟迟释放不了,如果Scale又高,连接池被迅速撑满。判断方法很简单,脚本执行完以后,查v$session里是不是有一批INACTIVE状态的LOADTEST用户会话迟迟不消失。如果是,把EXIT补上,重新启动就好。
最后分享一个我个人的小习惯:无论脚本多简单,我都会在开头加PROMPT load_start,结尾加PROMPT load_end,并且全程开spool。这样每次压测完,打开日志文件就能快速看到脚本执行了多长时间、在哪一步失败,即使出错也能直接定位。另一个经验是,脚本里如果既有DDL又有DML,一定严格按“先DDL后DML”的顺序拆成多个脚本分别执行,避免Oracle隐式提交把事务边界搅乱。SQLBuilder这套东西跑熟以后,你会发现它几乎能模拟任何你想要的业务负载场景,远不止跑跑TPS那么简单。
