
简介这是一份面向数据仓库与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-JOBshell 脚本依然可以作为被调度的最小单元不必推翻重来。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/passsid这种命令行明文ps -ef一眼就能看到。常见做法是写一个权限 600 的 env 文件脚本source进来再用sqlplus的登录方式传参。# conf/prod.env export DB_USERunload_user export DB_PASSyour_password export DB_SIDORCLPDB1 export DB_HOST10.0.0.12 export DB_PORT1521 export EXPORT_DIR/data/unload/export export LOG_DIR/data/unload/log export RETENTION_DAYS7# 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_DIRset -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 21 if [[ ! -s $EXPORT_FILE ]]; then echo 导出文件为空: $EXPORT_FILE 2 exit 2 fi1、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 21trunc(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_profilecrontab不会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_LANGAMERICAN_AMERICA.AL32UTF8NLS_LANG不设的话中文可能变成问号导出文件在别的机器上打开就是乱码。这个坑我踩过不止一次。4.2 现象导出文件里字段错位本来三列变成四列原因一般是字段内容里包含了分隔符。比如name字段里有个|用colsep |就会多切一刀。解决办法有两个一是换一个业务数据里绝对不会出现的分隔符比如\x1fASCII 单元分隔符二是在 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 21 if [[ ! -s $export_file ]]; then log 警告表 ${table_name} 导出为空 fi done conf/tables.listIFS| 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} fitr -d 去掉sqlplus输出里的空格wc -l统计文件行数。注意如果字段里有换行符行数会对不上所以导出前要确保文本字段没有换行或者用replace(col, chr(10), )处理掉。5.3 一个我常用的收尾习惯每次改完脚本我会先在一个测试库上跑三遍第一遍正常跑第二遍紧接着再跑一次看幂等第三遍把日期参数改成未来日期看空结果处理。三遍都过了才敢挂到生产。这个习惯帮我挡掉过至少两次「第二次执行删错数据」的事故。脚本这东西写的时候觉得没问题跑起来才知道哪里漏了。希望帮到你。本文还有配套的精品资源点击获取