☰
Oracle数据卸载模板:Shell脚本编排与SQL执行实践
2026/10/3 10:54:31 网站建设 项目流程

简介:这是一份面向数据仓库与ETL开发人员的Oracle数据卸载Shell脚本模板,适合需要将库内数据按批次导出为文本并完成后续传输的运维与开发场景。使用者只需在SQL模板文件中填写待卸载的查询语句,并在文件名配置中指定对应输出名称,即可灵活控制卸数逻辑,无需改动脚本主体。压缩包共4个文件,包含2个txt配置模板、1个sh主脚本和1个config环境配置文件,整体约4KB,体积轻巧但功能完整。脚本覆盖数据卸载、GBK转UTF8编码转换、批次号获取、尾行追加行数、FTP上传等环节,并附有文件切割语句注释,便于针对大文件拆分处理。目前已有868人学习下载,可帮助读者快速搭建可复用的卸数流程,理解批次管理与编码转换的落地写法,同时通过环境配置项掌握部署要点,减少重复开发成本。

1. 卸载数据模板这件事,为什么值得单独写个 shell 脚本

给 Oracle 做数据卸载,很多团队一开始都是手工敲 SQL*Plus 命令,导出几张表、删几个用户、清一批临时数据,做完就算完。等到环境多起来、要反复跑、还要在半夜无人值守执行的时候,手工操作就开始翻车了:有人忘了加WHERE条件把生产表清空,有人DROP USER没加CASCADE卡在半路,有人导出的 CSV 里全是乱码。所谓「卸载数据模板」,本质就是把「从 Oracle 里把数据抽出来、按约定格式落地、再清理掉中间产物」这一整套动作固化成可重复执行的脚本模板,让 shell 负责调度和编排,让 SQL 负责数据本身。它适合做数据迁移、离线分析、测试环境造数、定期归档的工程师,尤其是那些手里有一堆 Oracle 实例、又不想每次都手动点 SQL Developer 的人。下面这套东西,是我在真实环境里反复改出来的,能抄,但参数得按你的库改。

2. 先想清楚:shell 编排 + SQL 执行,这套模板的边界在哪

2.1 为什么是 shell 而不是纯 PL/SQL 或 Python

卸载数据这件事,核心动作其实只有三类:连库、跑 SQL、处理文件。纯 PL/SQL 能做前两件,但处理文件很别扭,UTL_FILE要配目录对象、要 DBA 权限,跨机器搬运更麻烦。Python 当然能做全套,cx_Oracle或oracledb都很成熟,但很多生产环境里 Python 版本、依赖包、网络策略都是坑,装个驱动能耗掉半天。shell 的优势在于它几乎一定存在,sqlplus客户端配好之后,剩下的就是文本处理和调度,crontab直接挂上去就能跑。

我一般把职责这么分:shell 负责参数解析、日志、文件命名、错误码判断、清理;SQL 负责真正的查询和 DML。两者之间用sqlplus的静默模式和spool衔接。这样做的代价是 shell 对 SQL 结果集的解析能力弱,所以模板里要约定好分隔符和列顺序,别指望在 shell 里做复杂的数据变换。

提示:如果你的环境里已经有成熟的调度平台(比如 Airflow、XXL-JOB),shell 脚本依然可以作为被调度的最小单元,不必推翻重来。

2.2 模板要解决的四个具体问题

第一是可重复:同一个脚本跑十次,结果一致,不会因为残留文件或残留数据导致第二次失败。第二是可观测:每次执行留下日志,出错时能定位到是哪条 SQL、哪个文件出的问题。第三是可配置:库连接、表名、输出路径、日期范围这些都不写死在脚本里,用参数或配置文件传进去。第四是安全退出:任何一步失败就停,不要带着错误继续往下删数据。这四点听起来像废话,但手工脚本翻车基本都翻在这四条上。

2.3 一个最小可用的目录结构

我习惯把模板拆成三部分:主脚本、SQL 目录、配置目录。主脚本只做流程控制,SQL 文件按用途命名,配置用env文件加载。这样换一个库,只改配置和 SQL,主脚本不动。

# 目录结构示例 oracle_unload/ ├── unload_main.sh # 主调度脚本 ├── conf/ │ └── prod.env # 连接信息、路径、表清单 ├── sql/ │ ├── export_data.sql # 导出查询 │ └── cleanup_data.sql # 清理语句 └── log/ # 运行日志,脚本自动创建

这个结构不复杂,但能避免所有东西堆在一个.sh里。下面几章就按这个骨架往里填。

3. 把连接、导出、清理拆成可复用的 SQL 与 shell 片段

3.1 连接信息怎么放才不泄露又不难改

连接信息绝对不要硬编码在脚本里,也不要用sqlplus user/pass@sid这种命令行明文,ps -ef一眼就能看到。常见做法是写一个权限 600 的 env 文件,脚本source进来,再用sqlplus的登录方式传参。

# conf/prod.env export DB_USER="unload_user" export DB_PASS="your_password" export DB_SID="ORCLPDB1" export DB_HOST="10.0.0.12" export DB_PORT="1521" export EXPORT_DIR="/data/unload/export" export LOG_DIR="/data/unload/log" export RETENTION_DAYS=7
# unload_main.sh 片段:加载配置并校验 set -euo pipefail CONF_FILE="${1:-conf/prod.env}" if [[ ! -f "$CONF_FILE" ]]; then echo "配置文件不存在: $CONF_FILE" >&2 exit 1 fi source "$CONF_FILE" # 校验关键变量 for var in DB_USER DB_PASS DB_SID EXPORT_DIR; do if [[ -z "${!var:-}" ]]; then echo "缺少必要配置: $var" >&2 exit 1 fi done mkdir -p "$EXPORT_DIR" "$LOG_DIR"

set -euo pipefail这三件套是 shell 脚本的后悔药:命令失败就退出、未定义变量报错、管道中任一环节失败都算失败。${!var:-}是间接引用,用来动态检查变量名对应的值是否为空。source加载配置比解析文本简单,但要注意 env 文件里不要有交互式命令。

3.2 用 sqlplus 静默模式导出数据并控制格式

导出这一步,核心是让sqlplus别输出一堆横幅和提示,只吐数据。-S静默、-L只登录一次、set系列命令控制格式,这几样配好,输出就是干净的文本。

-- sql/export_data.sql set echo off set feedback off set heading off set pagesize 0 set linesize 32767 set trimspool on set termout off set colsep '|' set null 'NULL' spool &1 select id || '|' || name || '|' || to_char(created_date,'YYYY-MM-DD HH24:MI:SS') from orders where created_date >= to_date('&2','YYYY-MM-DD') and created_date < to_date('&3','YYYY-MM-DD'); spool off exit
# unload_main.sh 片段:调用 sqlplus 导出 EXPORT_FILE="${EXPORT_DIR}/orders_$(date +%Y%m%d_%H%M%S).dat" START_DATE="${2:-$(date -d 'yesterday' +%Y-%m-%d)}" END_DATE="${3:-$(date +%Y-%m-%d)}" sqlplus -S -L "${DB_USER}/${DB_PASS}@${DB_HOST}:${DB_PORT}/${DB_SID}" \ @"sql/export_data.sql" "$EXPORT_FILE" "$START_DATE" "$END_DATE" \ >> "${LOG_DIR}/unload_$(date +%Y%m%d).log" 2>&1 if [[ ! -s "$EXPORT_FILE" ]]; then echo "导出文件为空: $EXPORT_FILE" >&2 exit 2 fi

&1、&2、&3是sqlplus的位置参数,对应脚本后面传进去的文件名和日期。set colsep '|'指定列分隔符,配合 SQL 里手动拼||更可控,避免字段里本身含分隔符时错位。set trimspool on去掉行尾空格,set pagesize 0去掉分页。-s判断文件非空,空文件说明查询没结果或连接失败,直接退出。

注意:linesize设太大在某些终端会截断,32767 是常见上限,如果单行超长,考虑用CLOB分段或改用其他导出方式。

3.3 清理动作要幂等,别让第二次执行失败

清理分两种:清中间文件、清库里的临时数据。文件清理用find按时间删,库清理用DELETE或TRUNCATE,但一定要幂等——重复执行不报错、不误删。

-- sql/cleanup_data.sql set echo off set feedback off set heading off delete from unload_staging where batch_date < trunc(sysdate) - &1; commit; exit
# unload_main.sh 片段:清理旧文件与临时表 find "$EXPORT_DIR" -type f -name '*.dat' -mtime +"$RETENTION_DAYS" -delete sqlplus -S -L "${DB_USER}/${DB_PASS}@${DB_HOST}:${DB_PORT}/${DB_SID}" \ @"sql/cleanup_data.sql" "$RETENTION_DAYS" \ >> "${LOG_DIR}/unload_$(date +%Y%m%d).log" 2>&1

trunc(sysdate) - &1表示当前日期往前推 N 天,&1是传入的保留天数。find -mtime +N删除 N 天前的文件,-delete直接删,不用-exec rm。这里没有用TRUNCATE TABLE,因为TRUNCATE不能带WHERE,要按条件删只能用DELETE,数据量大时记得分批提交,别一次性删几百万行把 undo 撑爆。

3.4 日志和错误码:出问题时能一眼定位

日志不要只写「成功」「失败」,要带时间戳、步骤名、影响行数。sqlplus的set feedback on会输出行数,但导出时我们关了,所以清理步骤可以单独开一个日志文件记录行数。

# unload_main.sh 片段:带时间戳的日志函数 log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "${LOG_DIR}/unload_$(date +%Y%m%d).log" } log "开始导出,日期范围 ${START_DATE} 至 ${END_DATE}" # ... 导出逻辑 ... log "导出完成,文件 ${EXPORT_FILE},大小 $(du -h "$EXPORT_FILE" | cut -f1)"

tee -a同时输出到屏幕和日志文件,方便调试时直接看。du -h记录文件大小,后面排查「文件是不是被截断」时有用。错误码方面,脚本里每个exit N用不同数字,crontab里可以根据返回码发不同告警,比统一返回 1 强。

4. 避坑与排查:那些让脚本半夜挂掉的细节

4.1 现象:脚本在终端跑得好好的,挂到 crontab 就失败

原因通常是环境变量不同。交互式登录会加载.bash_profile,crontab不会,sqlplus可能不在PATH里,ORACLE_HOME、NLS_LANG也没设。解决方式是在脚本开头显式设置,或者用绝对路径调用sqlplus。

export ORACLE_HOME=/u01/app/oracle/product/19c/dbhome_1 export PATH="$ORACLE_HOME/bin:$PATH" export NLS_LANG="AMERICAN_AMERICA.AL32UTF8"

NLS_LANG不设的话,中文可能变成问号,导出文件在别的机器上打开就是乱码。这个坑我踩过不止一次。

4.2 现象:导出文件里字段错位,本来三列变成四列

原因一般是字段内容里包含了分隔符。比如name字段里有个|,用colsep '|'就会多切一刀。解决办法有两个:一是换一个业务数据里绝对不会出现的分隔符,比如\x1f(ASCII 单元分隔符);二是在 SQL 里对文本字段做转义或替换。

select id || '|' || replace(name, '|', '') || '|' || ...

替换会丢数据,更稳妥的是用CHR(31)作为分隔符,肉眼看不见但不会和业务字符冲突。

4.3 现象:DELETE执行很久,最后报ORA-01555 snapshot too old

原因是一次删除的数据量太大,undo 表空间不够回滚。解决方式是分批删,用ROWNUM或主键范围循环。

delete from unload_staging where batch_date < trunc(sysdate) - 7 and rownum <= 10000;

外面套一个 shell 循环,每次删一万行,commit后再删下一批,直到影响行数为 0。这样虽然慢一点,但不会把库拖垮。

4.4 现象:sqlplus登录很慢,或者报连接错误

原因可能很多:监听没起、tnsnames.ora配错、网络不通、密码过期。排查顺序是先tnsping测监听,再sqlplus手动登录看报错。如果报ORA-28001就是密码过期,得先改密码。如果是ORA-12541就是监听没起,去服务器上看lsnrctl status。这些和脚本本身无关,但脚本失败时第一反应应该是手动连一次,别急着改脚本。

4.5 现象:脚本重复执行时,第二次报「文件已存在」或「唯一约束冲突」

原因是导出文件名用了固定名字,或者清理没做幂等。文件名加时间戳能解决第一个,清理用DELETE加条件能解决第二个。如果导出目标是表而不是文件,插入前先DELETE同批次数据,或者用MERGE。别用INSERT硬怼,重复跑必炸。

5. 进阶:把模板做成可配置的多表卸载框架

5.1 用配置文件驱动多张表的导出

单表脚本改一改就能支持多表:把表名、日期字段、输出文件名放进一个清单文件,shell 循环读取,每行调一次导出函数。

# conf/tables.list orders|created_date|orders order_items|created_date|order_items customers|reg_date|customers
# unload_main.sh 片段:循环处理多表 while IFS='|' read -r table_name date_col file_prefix; do [[ -z "$table_name" ]] && continue export_file="${EXPORT_DIR}/${file_prefix}_$(date +%Y%m%d_%H%M%S).dat" log "导出表 ${table_name},日期字段 ${date_col}" sqlplus -S -L "${DB_USER}/${DB_PASS}@${DB_HOST}:${DB_PORT}/${DB_SID}" \ @"sql/export_generic.sql" "$export_file" "$table_name" "$date_col" "$START_DATE" "$END_DATE" \ >> "${LOG_DIR}/unload_$(date +%Y%m%d).log" 2>&1 if [[ ! -s "$export_file" ]]; then log "警告:表 ${table_name} 导出为空" fi done < conf/tables.list

IFS='|' read按分隔符读每一行,continue跳过空行。export_generic.sql里用&2、&3接收表名和日期字段,动态拼 SQL。注意表名不能直接用绑定变量,只能字符串拼接,所以清单文件要严格控制权限,别让人乱改。

5.2 验证卸载结果是否完整

导出完不能只看文件存在,要验证行数和源表一致。简单做法是在导出 SQL 里同时spool一个计数文件,或者导出后单独查一次count(*)。

-- sql/count_check.sql set heading off set feedback off select count(*) from &1 where &2 >= to_date('&3','YYYY-MM-DD') and &2 < to_date('&4','YYYY-MM-DD'); exit
# 对比行数 src_count=$(sqlplus -S -L "${DB_USER}/${DB_PASS}@${DB_HOST}:${DB_PORT}/${DB_SID}" \ @"sql/count_check.sql" "$table_name" "$date_col" "$START_DATE" "$END_DATE" | tr -d ' ') file_count=$(wc -l < "$export_file") if [[ "$src_count" != "$file_count" ]]; then log "行数不一致:源表 ${src_count},文件 ${file_count}" fi

tr -d ' '去掉sqlplus输出里的空格,wc -l统计文件行数。注意如果字段里有换行符,行数会对不上,所以导出前要确保文本字段没有换行,或者用replace(col, chr(10), '')处理掉。

5.3 一个我常用的收尾习惯

每次改完脚本,我会先在一个测试库上跑三遍:第一遍正常跑,第二遍紧接着再跑一次看幂等,第三遍把日期参数改成未来日期看空结果处理。三遍都过了才敢挂到生产。这个习惯帮我挡掉过至少两次「第二次执行删错数据」的事故。脚本这东西,写的时候觉得没问题,跑起来才知道哪里漏了。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询